commit e7e0c9d0df79505709e87c7552d489654617205c Author: Marcel Schneider Date: Tue Feb 25 17:16:52 2020 +0100 Created new project from template diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d94d1ed --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = false +trim_trailing_whitespace = true + +# justfile uses Makefile-like significant indentation +# but doesn't have a problem with blank lines staying un-indented +# within an indented block +[justfile] +indent_style = tab +trim_trailing_whitespace = true + +# If I have a TODO file, I want the outline lists to line up nicely and +# .travis.yml just uses 2-space indentation by convention +[{.travis.yml, TODO}] +indent_style = space +indent_size = 2 + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06dbbc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/callgrind.out.justfile +/dist +/target diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0f87fb7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: rust +rust: + - stable + - beta + - nightly +matrix: + allow_failures: + - rust: nightly +notifications: + email: false diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..8201f99 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,37 @@ +Developer Certificate of Origin +Version 1.1 + +Copyright (C) 2004, 2006 The Linux Foundation and its contributors. +1 Letterman Drive +Suite D4700 +San Francisco, CA, 94129 + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + + +Developer's Certificate of Origin 1.1 + +By making a contribution to this project, I certify that: + +(a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + +(b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + +(c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + +(d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b169b5b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "gitig" +version = "0.1.0" +authors = ["Marcel Schneider "] +edition = "2018" + +[dependencies] +log = "0.4" +stderrlog = "0.4" +structopt = "0.3" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[dependencies.error-chain] +version = "0.12" +default-features = false # disable pulling in backtrace + +[profile.release] +lto = true +codegen-units = 1 +opt-level = "z" + +# Uncomment to sacrifice Drop-on-panic cleanup for 20K space saving +#panic = 'abort' diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..0312280 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,2 @@ +# Version 0.1 +doc-valid-idents = ["MiB", "GiB", "TiB", "PiB", "EiB", "DirectX", "GPLv2", "GPLv3", "GitHub", "IPv4", "IPv6", "JavaScript", "NaN", "OAuth", "OpenGL", "TrueType", "OSes", "node_modules", "exFAT", "eCryptFS"] diff --git a/justfile b/justfile new file mode 100644 index 0000000..7caee50 --- /dev/null +++ b/justfile @@ -0,0 +1,313 @@ +# Version 0.2 +# Copyright 2017-2019, Stephan Sokolow + +# --== Variables to be customized/overridden by the user ==-- + +# The target for `cargo` commands to use and `install-rustup-deps` to install +export CARGO_BUILD_TARGET = "i686-unknown-linux-musl" + +# An easy way to override the `cargo` channel for just this project +channel = "stable" + +# Extra cargo features to enable +features = "" + +# An easy place to modify the build flags used +build_flags = "" + +# Example for OpenPandora cross-compilation +# export CARGO_BUILD_TARGET = "arm-unknown-linux-gnueabi" + +# -- `build-dist` -- + +# Set this to the cross-compiler's `strip` when cross-compiling +strip_bin = "strip" + +# Flags passed to `strip_bin` +strip_flags = "--strip-unneeded" + +# Set this if you need to override it for a cross-compiling `sstrip` +sstrip_bin = "sstrip" + +# Flags passed to [UPX](https://upx.github.io/) +upx_flags = "--ultra-brute" + +# Example for OpenPandora cross-compilation +# strip_bin = `echo $HOME/opt/pandora-dev/arm-2011.09/bin/pandora-strip` + +# -- `kcachegrind` -- + +# Extra arguments to pass to [callgrind](http://valgrind.org/docs/manual/cl-manual.html). +callgrind_args = "" + +# Temporary file used by `just kcachegrind` +callgrind_out_file = "callgrind.out.justfile" + +# Set this to override how `kcachegrind` is called +kcachegrind = "kcachegrind" + +# -- `install` and `uninstall` -- + +# Where to `install` bash completions. +# **You'll need to manually add some lines to source these files in `.bashrc.`** +bash_completion_dir = "~/.bash_completion.d" + +# Where to `install` fish completions. You'll probably never need to change this. +fish_completion_dir = "~/.config/fish/completions" + +# Where to `install` zsh completions. +# **You'll need to add this to your `fpath` manually** +zsh_completion_dir = "~/.zsh/functions" + +# Where to `install` manpages. As long as `~/.cargo/bin` is in your `PATH`, `man` should +# automatically pick up this location. +manpage_dir = "~/.cargo/share/man/man1" + +# --== Code Begins ==-- + +# Internal variables +# TODO: Look up that GitHub issues post on whitespace handling +_cargo_cmd = "cargo" # Used for --dry-run simulation +_cargo = _cargo_cmd + " \"+" + channel + "\"" +_build_flags = "--features=\"" + features + "\" " + build_flags +_doc_flags = "--document-private-items --features=\"" + features + "\"" + +# Parse the value of the "name" key in the [package] section of Cargo.toml +# using only the commands any POSIX-compliant platform should have +# Source: http://stackoverflow.com/a/40778047/435253 +export _pkgname=`sed -nr "/^\[package\]/ { :l /^name[ ]*=/ { s/.*=[ ]*//; p; q;}; n; b l;}" Cargo.toml | sed 's@^"\(.*\)"$@\1@'` +export _rls_bin_path="target/" + CARGO_BUILD_TARGET + "/release/" + _pkgname +export _dbg_bin_path="target/" + CARGO_BUILD_TARGET + "/debug/" + _pkgname + +# Shorthand for `just test` +DEFAULT: test + +# -- Development -- + +# Alias for cargo-edit's `cargo add` which regenerates local API docs afterwards +add +args="": + {{_cargo}} add {{args}} + just doc + +# Alias for `cargo bloat` +bloat +args="": + {{_cargo}} bloat {{_build_flags}} {{args}} + +# Alias for `cargo check` +check +args="": + {{_cargo}} check {{_build_flags}} {{args}} + +# Superset of `cargo clean -v` which deletes other stuff this justfile builds +clean +args="": + {{_cargo}} clean -v {{args}} + export CARGO_TARGET_DIR="target/kcov" && {{_cargo}} clean -v + rm -rf dist + +# Run rustdoc with `--document-private-items` and then run cargo-deadlinks +doc +args="": + {{_cargo}} doc {{_doc_flags}} {{args}} && \ + {{_cargo}} deadlinks --dir target/$CARGO_BUILD_TARGET/doc/{{_pkgname}} + +# Alias for `cargo +nightly fmt -- {{args}}` +fmt +args="": + {{_cargo_cmd}} +nightly fmt -- {{args}} + +# Alias for `cargo +nightly fmt -- --check {{args}}` which un-bloats TODO/FIXME warnings +fmt-check +args="": + cargo +nightly fmt -- --check --color always {{args}} 2>&1 | egrep -v '[0-9]*[ ]*\|' + +# Run a debug build under [callgrind](http://valgrind.org/docs/manual/cl-manual.html), then open the +# profile in [KCachegrind](https://kcachegrind.github.io/) +kcachegrind +args="": + {{_cargo}} build + rm -rf '{{ callgrind_out_file }}' + valgrind --tool=callgrind --callgrind-out-file='{{ callgrind_out_file }}' \ + {{ callgrind_args }} 'target/{{ CARGO_BUILD_TARGET }}/debug/{{ _pkgname }}' \ + '{{ args }}' || true + test -e '{{ callgrind_out_file }}' + {{kcachegrind}} '{{ callgrind_out_file }}' + +# Generate a statement coverage report in `target/cov/` +kcov: + #!/bin/sh + # Adapted from: + # - http://sunjay.ca/2016/07/25/rust-code-coverage + # - https://users.rust-lang.org/t/tutorial-how-to-collect-test-coverages-for-rust-project/650 + # + # As of July 2, 2016, there is no option to make rustdoc generate a runnable + # test executable. That means that documentation tests will not show in your + # coverage data. If you discover a way to run the doctest executable with kcov, + # please open an Issue and we will add that to these instructions. + # -- https://github.com/codecov/example-rust + + # Ensure that kcov can see totally unused functions without clobbering regular builds + # Adapted from: + # - http://stackoverflow.com/a/38371687/435253 + # - https://gist.github.com/dikaiosune/07177baf5cea76c27783efa55e99da89 + export CARGO_TARGET_DIR="target/kcov" + export RUSTFLAGS='-C link-dead-code' + kcov_path="$CARGO_TARGET_DIR/html" + + if [ "$#" -gt 0 ]; then shift; fi # workaround for "can't shift that many" being fatal in dash + cargo test --no-run || exit $? + rm -rf "$kcov_path" + + for file in "$CARGO_TARGET_DIR"/"$CARGO_BUILD_TARGET"/debug/$_pkgname-*; do + if [ -x "$file" ]; then + outpath="$kcov_path/$(basename "$file")" + mkdir -p "$outpath" + kcov --exclude-pattern=/.cargo,/usr/lib --verify "$outpath" "$file" "$@" + elif echo "$file" | grep -F -e '-*'; then + echo "No build files found for coverage!" + exit 1 + fi + done + +# Alias for cargo-edit's `cargo rm` which regenerates local API docs afterwards +rm +args="": + {{_cargo}} rm {{args}} + just doc + +# Convenience alias for opening a crate search on lib.rs in the browser +search +args="": + xdg-open "https://lib.rs/search?q={{args}}" + +# Run all installed static analysis, plus `cargo test` +test: + @echo "============================= Outdated Packages =============================" + @{{_cargo}} outdated + @echo "\n============================= Insecure Packages =============================" + @{{_cargo}} audit -q + @echo "\n=============================== Clippy Lints ================================" + @{{_cargo}} clippy -q {{_build_flags}} + @echo "\n===================== Dead Internal Documentation Links =====================" + @{{_cargo}} doc -q --document-private-items {{_build_flags}} && \ + {{_cargo}} deadlinks --dir target/$CARGO_BUILD_TARGET/doc/{{_pkgname}} + @echo "\n================================ Test Suite =================================" + @{{_cargo}} test -q {{_build_flags}} + @echo "=============================================================================" + +# Alias for cargo-edit's `cargo update` which regenerates local API docs afterwards +update +args="": + {{_cargo}} update {{args}} + just doc + + # TODO: https://users.rust-lang.org/t/howto-sanitize-your-rust-code/9378 + +# -- Local Builds -- + +# Alias for `cargo build` +build: + @echo "\n--== Building with {{channel}} for {{CARGO_BUILD_TARGET}} (features: {{features}}) ==--\n" + {{_cargo}} build {{_build_flags}} + +# Install the un-packed binary, shell completions, and a manpage +install: dist-supplemental + @# Install completions + @# NOTE: bash and zsh completion requires additional setup to source a non-root dir + mkdir -p {{bash_completion_dir}} {{zsh_completion_dir}} {{ fish_completion_dir }} {{ manpage_dir }} + cp dist/{{ _pkgname }}.bash {{ bash_completion_dir }}/{{ _pkgname }} + cp dist/{{ _pkgname }}.zsh {{ zsh_completion_dir }}/_{{ _pkgname }} + cp dist/{{ _pkgname }}.fish {{ fish_completion_dir }}/{{ _pkgname }}.fish + @# Install the manpage + cp dist/{{ _pkgname }}.1.gz {{ manpage_dir }}/{{ _pkgname }}.1.gz || true + @# Install the command to ~/.cargo/bin + {{_cargo}} install --path . --force --features="{{features}}" + +# Alias for `cargo run -- {{args}}` +run +args="": + {{_cargo}} run {{_build_flags}} -- {{args}} + +# Remove any files installed by the `install` task (but leave any parent directories created) +uninstall: + @# TODO: Implement the proper fallback chain from `cargo install` + rm ~/.cargo/bin/{{ _pkgname }} || true + rm {{ manpage_dir }}/{{ _pkgname }}.1.gz || true + rm {{ bash_completion_dir }}/{{ _pkgname }} || true + rm {{ fish_completion_dir }}/{{ _pkgname }}.fish || true + rm {{ zsh_completion_dir }}/_{{ _pkgname }} || true + +# -- Release Builds -- + +# Make a release build and then strip and compress the resulting binary +build-dist: + @echo "\n--== Building with {{channel}} for {{CARGO_BUILD_TARGET}} (features: {{features}}) ==--\n" + {{_cargo}} build --release {{_build_flags}} + + @# Don't modify the original "cargo" output. That confuses cargo somehow. + cp "{{_rls_bin_path}}" "{{_rls_bin_path}}.stripped" + @printf "\n--== Stripping, SStripping, and Compressing With UPX ==--\n" + {{strip_bin}} {{strip_flags}} "{{_rls_bin_path}}.stripped" + @# Allow sstrip to fail because it can't be installed via "just install-deps" + {{sstrip_bin}} "{{_rls_bin_path}}.stripped" || true + @# Allow upx to fail in case the user wants to force no UPXing by leaving it uninstalled + cp "{{_rls_bin_path}}.stripped" "{{_rls_bin_path}}.packed" + upx {{upx_flags}} "{{_rls_bin_path}}.packed" || true + @# Display the resulting file sizes so we can keep an eye on them + @# (Separate `ls` invocations are used to force the display ordering) + @printf "\n--== Final Result ==--\n" + @ls -1sh "{{_rls_bin_path}}" + @ls -1sh "{{_rls_bin_path}}.stripped" + @ls -1sh "{{_rls_bin_path}}.packed" + @printf "\n" + + +# Build the shell completions and a manpage, and put them in `dist/` +dist-supplemental: + mkdir -p dist + @# Generate completions and store them in dist/ + {{_cargo}} run --release {{_build_flags}} -- --dump-completions bash > dist/{{ _pkgname }}.bash + {{_cargo}} run --release {{_build_flags}} -- --dump-completions zsh > dist/{{ _pkgname }}.zsh + {{_cargo}} run --release {{_build_flags}} -- --dump-completions fish > dist/{{ _pkgname }}.fish + {{_cargo}} run --release {{_build_flags}} -- --dump-completions elvish > dist/{{ _pkgname }}.elvish + {{_cargo}} run --release {{_build_flags}} -- --dump-completions powershell > dist/{{ _pkgname }}.powershell + @# Generate manpage and store it gzipped in dist/ + @# (This comes last so the earlier calls to `cargo run` will get the compiler warnings out) + help2man -N '{{_cargo}} run {{_build_flags}} --' \ + | gzip -9 > dist/{{ _pkgname }}.1.gz || true + +# Call `dist-supplemental` and `build-dist` and copy the packed binary to `dist/` +dist: build-dist dist-supplemental + @# Copy the packed command to dist/ + cp "{{ _rls_bin_path }}.packed" dist/{{ _pkgname }} || \ + cp "{{ _rls_bin_path }}.stripped" dist/{{ _pkgname }} + +# -- Dependencies -- + +# Use `apt-get` to install dependencies `cargo` can't (except `kcov` and `sstrip`) +install-apt-deps: + sudo apt-get install binutils help2man kcachegrind upx valgrind + +# `install-rustup-deps` and then `cargo install` tools +install-cargo-deps: install-rustup-deps + @# Prevent "already installed" from causing a failure + {{_cargo}} install cargo-audit || true + {{_cargo}} install cargo-bloat || true + {{_cargo}} install cargo-deadlinks || true + {{_cargo}} install cargo-edit || true + {{_cargo}} install cargo-outdated || true + cargo +nightly install cargo-cov || true + +# Install (don't update) nightly and `channel` toolchains, plus `CARGO_BUILD_TARGET`, clippy, and rustfmt +install-rustup-deps: + @# Prevent this from gleefully doing an unwanted "rustup update" + rustup toolchain list | grep -q '{{channel}}' || rustup toolchain install '{{channel}}' + rustup toolchain list | grep -q nightly || rustup toolchain install nightly + rustup target list | grep -q '{{CARGO_BUILD_TARGET}} (' || rustup target add '{{CARGO_BUILD_TARGET}}' + rustup component list | grep -q 'clippy-\S* (' || rustup component add clippy + rustup component list --toolchain nightly | grep 'rustfmt-\S* (' || rustup component add rustfmt --toolchain nightly + +# Run `install-apt-deps` and `install-cargo-deps`. List what remains. +@install-deps: install-apt-deps install-cargo-deps + echo + echo "-----------------------------------------------------------" + echo "IMPORTANT: You will need to install the following manually:" + echo "-----------------------------------------------------------" + echo " * Rust-compatible kcov (http://sunjay.ca/2016/07/25/rust-code-coverage)" + echo " * sstrip (http://www.muppetlabs.com/%7Ebreadbox/software/elfkickers.html)" + +# Local Variables: +# mode: makefile +# End: + +# vim: set ft=make textwidth=100 colorcolumn=101 noexpandtab sw=8 sts=8 ts=8 : diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..a1a73b9 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,36 @@ +# Version 0.1 + +# Always nice to have a way to gather TODOs and FIXMEs quickly +report_todo = "Always" +report_fixme = "Always" + +# I was one of the proponents in the RFC discussion +use_try_shorthand = true + +# Try to wrestle rustfmt as close as possible to the style I habitually use +# and rigorously enforce by using `git gui` to only commit rustfmt changes +# I approve of. +brace_style = "PreferSameLine" +comment_width = 99 +enum_discrim_align_threshold = 10 +format_strings = true +fn_args_density = "Compressed" +fn_single_line = true +imports_indent = "Visual" +match_block_trailing_comma = true +normalize_doc_attributes = true +overflow_delimited_expr = true +reorder_impl_items = true +struct_field_align_threshold = 10 +use_field_init_shorthand = true +use_small_heuristics = "Max" +where_single_line = true +wrap_comments = true + +# I happen to like /* this */ for multi-line comments, thank you very much +normalize_comments = false + +# ----------------------------------------------------------------------------- + +# Used for debugging rustfmt configurations +#write_mode = "Diff" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..bc94f87 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,87 @@ +/*! Application-specific logic lives here */ +// Parts Copyright 2017-2019, Stephan Sokolow + +// Standard library imports +use std::path::PathBuf; + +// 3rd-party crate imports +use structopt::StructOpt; + +use log::{debug, error, info, trace, warn}; + +// Local Imports +use crate::errors::*; +use crate::helpers::{BoilerplateOpts, HELP_TEMPLATE}; +use crate::validators::path_readable_file; + +/// The verbosity level when no `-q` or `-v` arguments are given, with `0` being `-q` +pub const DEFAULT_VERBOSITY: u64 = 1; + +/// Command-line argument schema +/// +/// ## Relevant Conventions: +/// +/// * Make sure that there is a blank space between the `` `` line and the +/// description text or the `--help` output won't comply with the platform conventions that +/// `help2man` depends on to generate your manpage. +/// (Specifically, it will mistake the ` ` line for part of the description.) +/// * `StructOpt`'s default behaviour of including the author name in the `--help` output is an +/// oddity among Linux commands and, if you don't disable it, you run the risk of people +/// unfamiliar with `StructOpt` assuming that you are an egotistical person who made a conscious +/// choice to add it. +/// +/// The proper standardized location for author information is the `AUTHOR` section which you +/// can read about by typing `man help2man`. +/// +/// ## Cautions: +/// * Subcommands do not inherit `template` and it must be re-specified for each one. +/// ([clap-rs/clap#1184](https://github.com/clap-rs/clap/issues/1184)) +/// * Double-check that your choice of `about` or `long_about` is actually overriding this +/// doc comment. The precedence is affected by things you wouldn't expect, such as the presence +/// or absence of `template` and it's easy to wind up with this doc-comment as your `--help` +/// ([TeXitoi/structopt#173](https://github.com/TeXitoi/structopt/issues/173)) +/// * Do not begin the description text for subcommands with `\n`. It will break the formatting +/// in the top-level help output's list of subcommands. +#[derive(StructOpt, Debug)] +#[structopt(template = HELP_TEMPLATE, + about = "TODO: Replace me with the description text for the command", + global_setting = structopt::clap::AppSettings::ColoredHelp)] +pub struct CliOpts { + #[allow(clippy::missing_docs_in_private_items)] // StructOpt won't let us document this + #[structopt(flatten)] + pub boilerplate: BoilerplateOpts, + + // -- Arguments used by application-specific logic -- + + /// File(s) to use as input + /// + /// **TODO:** Figure out if there's a way to only enforce constraints on this when not asking + /// to dump completions. + #[structopt(parse(from_os_str), + validator_os = path_readable_file)] + inpath: Vec, +} + +/// The actual `main()` +pub fn main(opts: CliOpts) -> Result<()> { + for inpath in opts.inpath { + unimplemented!() + } + + Ok(()) +} + +// Tests go below the code where they'll be out of the way when not the target of attention +#[cfg(test)] +mod tests { + use super::CliOpts; + + // TODO: Unit test to verify that the doc comment on `CliOpts` isn't overriding the intended + // about string. + + #[test] + /// Test something + fn test_something() { + // TODO: Test something + } +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..94fa79f --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,7 @@ +/*! `error-chain` boilerplate and custom `Error` types */ +// Copyright 2020, Marcel Schneider + +use error_chain::*; + +// Create the Error, ErrorKind, ResultExt, and Result types +error_chain! {} diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..02985a3 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,56 @@ +/*! Functions and templates which can be imported by app.rs to save effort */ +// Copyright 2017-2019, Stephan Sokolow + +// FIXME: Report that StructOpt is tripping Clippy's `result_unwrap_used` lint (which I use to push +// for .expect() instead) in my two Option fields and the `allow` gets ignored unless I +// `#![...]` it onto the entire module. +#![allow(clippy::result_unwrap_used)] + +use structopt::{clap, StructOpt}; + +/// Modified version of Clap's default template for proper help2man compatibility +/// +/// Used as a workaround for: +/// 1. Clap's default template interfering with `help2man`'s proper function +/// ([clap-rs/clap/#1432](https://github.com/clap-rs/clap/issues/1432)) +/// 2. Workarounds involving injecting `\n` into the description breaking help output if used +/// on subcommand descriptions. +pub const HELP_TEMPLATE: &str = "{bin} {version} + +{about} + +USAGE: + {usage} + +{all-args} +"; + +/// Options used by boilerplate code +// TODO: Move these into a struct of their own in something like helpers.rs +#[derive(StructOpt, Debug)] +#[structopt(rename_all = "kebab-case")] +pub struct BoilerplateOpts { + + // -- Arguments used by main.rs -- + // TODO: Move these into a struct of their own in something like helpers.rs + + // FIXME: Report that StructOpt trips Clippy's `cast_possible_truncation` lint unless I use + // `u64` for my `from_occurrences` inputs, which is a ridiculous state of things. + + /// Decrease verbosity (-q, -qq, -qqq, etc.) + #[structopt(short, long, parse(from_occurrences))] + pub quiet: u64, + + /// Increase verbosity (-v, -vv, -vvv, etc.) + #[structopt(short, long, parse(from_occurrences))] + pub verbose: u64, + + /// Display timestamps on log messages (sec, ms, ns, none) + #[structopt(short, long, value_name = "resolution")] + pub timestamp: Option, + + /// Write a completion definition for the specified shell to stdout (bash, zsh, etc.) + #[structopt(long, value_name = "shell")] + pub dump_completions: Option, +} + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b8fbdc8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,82 @@ +/*! TODO: Application description here + +This file provided by [rust-cli-boilerplate](https://github.com/ssokolow/rust-cli-boilerplate) +*/ +// Copyright 2017-2019, Stephan Sokolow + +// `error_chain` recursion adjustment +#![recursion_limit = "1024"] + +// Make rustc's built-in lints more strict and set clippy into a whitelist-based configuration so +// we see new lints as they get written (We'll opt back out selectively) +#![warn(warnings, rust_2018_idioms, clippy::all, clippy::complexity, clippy::correctness, + clippy::pedantic, clippy::perf, clippy::style, clippy::restriction)] + +// Opt out of the lints I've seen and don't want +#![allow(clippy::float_arithmetic, clippy::implicit_return)] + +// stdlib imports +use std::io; +use std::convert::TryInto; + +// 3rd-party imports +mod errors; +use structopt::{clap, StructOpt}; +use log::error; + +// Local imports +mod app; +mod helpers; +mod validators; + +/// Boilerplate to parse command-line arguments, set up logging, and handle bubbled-up `Error`s. +/// +/// Based on the `StructOpt` example from stderrlog and the suggested error-chain harness from +/// [quickstart.rs](https://github.com/brson/error-chain/blob/master/examples/quickstart.rs). +/// +/// See `app::main` for the application-specific logic. +/// +/// **TODO:** Consider switching to Failure and look into `impl Termination` as a way to avoid +/// having to put the error message pretty-printing inside main() +fn main() { + // Parse command-line arguments (exiting on parse error, --version, or --help) + let opts = app::CliOpts::from_args(); + + // Configure logging output so that -q is "decrease verbosity" rather than instant silence + let verbosity = opts.boilerplate.verbose + .saturating_add(app::DEFAULT_VERBOSITY) + .saturating_sub(opts.boilerplate.quiet); + stderrlog::new() + .module(module_path!()) + .quiet(verbosity == 0) + .verbosity(verbosity.saturating_sub(1).try_into().expect("should never even come close")) + .timestamp(opts.boilerplate.timestamp.unwrap_or(stderrlog::Timestamp::Off)) + .init() + .expect("initializing logging output"); + + // If requested, generate shell completions and then exit with status of "success" + if let Some(shell) = opts.boilerplate.dump_completions { + app::CliOpts::clap().gen_completions_to( + app::CliOpts::clap().get_bin_name().unwrap_or_else(|| clap::crate_name!()), + shell, + &mut io::stdout()); + std::process::exit(0); + }; + + if let Err(ref e) = app::main(opts) { + // Write the top-level error message, then chained errors, then backtrace if available + error!("error: {}", e); + for e in e.iter().skip(1) { + error!("caused by: {}", e); + } + if let Some(backtrace) = e.backtrace() { + error!("backtrace: {:?}", backtrace); + } + + // Exit with a nonzero exit code + // TODO: Decide how to allow code to set this to something other than 1 + std::process::exit(1); + } +} + +// vim: set sw=4 sts=4 expandtab : diff --git a/src/validators.rs b/src/validators.rs new file mode 100644 index 0000000..8b2a4a5 --- /dev/null +++ b/src/validators.rs @@ -0,0 +1,563 @@ +/*! Validator functions suitable for use with `Clap` and `StructOpt` */ +// Copyright 2017-2019, Stephan Sokolow + +use std::ffi::OsString; +use std::fs::File; +use std::path::{Component, Path}; + +/// Special filenames which cannot be used for real files under Win32 +/// +/// (Unless your app uses the `\\?\` path prefix to bypass legacy Win32 API compatibility +/// limitations) +/// +/// **NOTE:** These are still reserved if you append an extension to them. +/// +/// Source: [Boost Path Name Portability Guide +/// ](https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm) +pub const RESERVED_DOS_FILENAMES: &[&str] = &["AUX", "CON", "NUL", "PRN", // Comments for rustfmt + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", // Serial Ports + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", // Parallel Ports + "CLOCK$" ]; // https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm +// TODO: Add the rest of the disallowed names from +// https://en.wikipedia.org/wiki/Filename#Comparison_of_filename_limitations + +/// Module to contain the unsafety of an `unsafe` call to `access()` +#[cfg(unix)] +mod access { + /// TODO: Make this wrapper portable + /// + /// TODO: Consider making `wrapped_access` typesafe using the `bitflags` + /// crate `clap` pulled in + use libc::{access, c_int, W_OK}; + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; + use std::path::Path; + + /// Lower-level safety wrapper shared by all probably_* functions I define + /// TODO: Unit test **HEAVILY** (Has unsafe block. Here be dragons!) + fn wrapped_access(abs_path: &Path, mode: c_int) -> bool { + // Debug-time check that we're using the API properly + // (Debug-only because relying on it in a release build grants a false + // sense of security and, besides, access() is only really safe to use + // as a way to abort early for convenience on errors that would still + // be safe anyway.) + debug_assert!(abs_path.is_absolute()); + + // Make a null-terminated copy of the path for libc + match CString::new(abs_path.as_os_str().as_bytes()) { + // If we succeed, call access(2), convert the result into bool, and return it + Ok(cstr) => unsafe { access(cstr.as_ptr(), mode) == 0 }, + // If we fail, return false because it can't be an access()ible path + Err(_) => false, + } + } + + /// API suitable for a lightweight "fail early" check for whether a target + /// directory is writable without worry that a fancy filesystem may be + /// configured to allow write but deny deletion for the resulting test file. + /// (It's been seen in the wild) + /// + /// Uses a name which helps to drive home the security hazard in access() + /// abuse and hide the mode flag behind an abstraction so the user can't + /// mess up unsafe{} (eg. On my system, "/" erroneously returns success) + pub fn probably_writable + ?Sized>(path: &P) -> bool { + wrapped_access(path.as_ref(), W_OK) + } + + #[cfg(test)] + mod tests { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; // TODO: Find a better way to produce invalid UTF-8 + use super::probably_writable; + + #[test] + fn probably_writable_basic_functionality() { + assert!(probably_writable(OsStr::new("/tmp"))); // OK Folder + assert!(probably_writable(OsStr::new("/dev/null"))); // OK File + assert!(!probably_writable(OsStr::new("/etc/shadow"))); // Denied File + assert!(!probably_writable(OsStr::new("/etc/ssl/private"))); // Denied Folder + assert!(!probably_writable(OsStr::new("/nonexistant_test_path"))); // Missing Path + assert!(!probably_writable(OsStr::new("/tmp\0with\0null"))); // Bad CString + assert!(!probably_writable(OsStr::from_bytes(b"/not\xffutf8"))); // Bad UTF-8 + assert!(!probably_writable(OsStr::new("/"))); // Root + // TODO: Relative path + // TODO: Non-UTF8 path that actually does exist and is writable + } + } +} + +/// Test that the given path **should** be writable +/// +/// **TODO:** Implement a Windows version of this. +/// +/// Given that every relevant Windows API I can find seems to be a complex mess compared to +/// `access(2)`, I'll probably just want to settle for the compromise I rejected and just try +/// writing and then deleting a test file. +#[cfg(unix)] +pub fn path_output_dir + ?Sized>(value: &P) -> Result<(), OsString> { + let path = value.as_ref(); + + // Test that the path is a directory + // (Check before, not after, as an extra safety guard on the unsafe block) + if !path.is_dir() { + return Err(format!("Not a directory: {}", path.display()).into()); + } + + // TODO: Think about how to code this more elegantly (try! perhaps?) + if let Ok(abs_pathbuf) = path.canonicalize() { + if let Some(abs_path) = abs_pathbuf.to_str() { + if self::access::probably_writable(abs_path) { + return Ok(()); + } + } + } + + Err(format!("Would be unable to write to destination directory: {}", path.display()).into()) +} + +/// The given path is a file that can be opened for reading +/// +/// ## Use For: +/// * Input file paths +/// +/// ## Relevant Conventions: +/// * Commands which read from `stdin` by default should use `-f` to specify the input path. +/// [[1]](http://www.catb.org/esr/writings/taoup/html/ch10s05.html) +/// * Commands which read from files by default should use positional arguments to specify input +/// paths. +/// * Allow an arbitrary number of input paths if feasible. +/// * Interpret a value of `-` to mean "read from `stdin`" if feasible. +/// [[2]](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) +/// +/// **Note:** The following command-lines, which interleave files and `stdin`, are a good test of +/// how the above conventions should interact: +/// +/// data_source | my_utility_a header.dat - footer.dat > output.dat +/// data_source | my_utility_b -f header.dat -f - -f footer.dat > output.dat +/// +/// ## Cautions: +/// * This will momentarily open the given path for reading to verify that it is readable. +/// However, relying on this to remain true will introduce a race condition. This validator is +/// intended only to allow your program to exit as quickly as possible in the case of obviously +/// bad input. +/// * As a more reliable validity check, you are advised to open a handle to the file in question +/// as early in your program's operation as possible, use it for all your interactions with the +/// file, and keep it open until you are finished. This will both verify its validity and +/// minimize the window in which another process could render the path invalid. +pub fn path_readable_file + ?Sized>(value: &P) + -> std::result::Result<(), OsString> { + let path = value.as_ref(); + + if path.is_dir() { + return Err(format!("{}: Input path must be a file, not a directory", + path.display()).into()); + } + + // TODO: Why does this not fail on Linux? I forget what reading a directory actually does. + File::open(path).map(|_| ()).map_err(|e| format!("{}: {}", path.display(), e).into()) +} + +// TODO: Implement path_readable_dir and path_readable for --recurse use-cases + +/// The given path is valid on all major filesystems and OSes +/// +/// ## Use For: +/// * Output file or directory paths +/// +/// ## Relevant Conventions: +/// * Use `-o` to specify the output path. +/// [[1]](http://www.catb.org/esr/writings/taoup/html/ch10s05.html) +/// [[2]](http://tldp.org/LDP/abs/html/standard-options.html) +/// * Interpret a value of `-` to mean "Write output to stdout". +/// [[3]](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html) +/// * Because `-o` does not inherently indicate whether it expects a file or a directory, consider +/// also providing a GNU-style long version with a name like `--outfile` to allow scripts which +/// depend on your tool to be more self-documenting. +/// +/// ## Cautions: +/// * To ensure files can be copied/moved without issue, this validator may impose stricter +/// restrictions on filenames than your filesystem. Do *not* use it for input paths. +/// * Other considerations, such as paths containing symbolic links with longer target names, may +/// still cause your system to reject paths which pass this check. +/// * As a more reliable validity check, you are advised to open a handle to the file in question +/// as early in your program's operation as possible and keep it open until you are finished. +/// This will both verify its validity and minimize the window in which another process could +/// render the path invalid. +/// +/// ## Design Considerations: [[4]](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits) +/// * Many popular Linux filesystems impose no total length limit. +/// * This function imposes a 32,760-character limit for compatibility with flash drives formatted +/// FAT32 or exFAT. +/// * Some POSIX API functions, such as `getcwd()` and `realpath()` rely on the `PATH_MAX` +/// constant, which typically specifies a length of 4096 bytes including terminal `NUL`, but +/// this is not enforced by the filesystem itself. +/// [[4]](https://insanecoding.blogspot.com/2007/11/pathmax-simply-isnt.html) +/// +/// Programs which rely on libc for this functionality but do not attempt to canonicalize paths +/// will usually work if you change the working directory and use relative paths. +/// * The following lengths were considered too limiting to be enforced by this function: +/// * The UDF filesystem used on DVDs imposes a 1023-byte length limit on paths. +/// * When not using the `\\?\` prefix to disable legacy compatibility, Windows paths are +/// limited to 260 characters, which was arrived at as `A:\MAX_FILENAME_LENGTH`. +/// [[5]](https://stackoverflow.com/a/1880453/435253) +/// * ISO 9660 without Joliet or Rock Ridge extensions does not permit periods in directory +/// names, directory trees more than 8 levels deep, or filenames longer than 32 characters. +/// [[6]](https://www.boost.org/doc/libs/1_36_0/libs/filesystem/doc/portability_guide.htm) +/// +/// **TODO:** +/// * Write another function for enforcing the limits imposed by targeting optical media. +pub fn path_valid_portable + ?Sized>(value: &P) -> Result<(), OsString> { + #![allow(clippy::match_same_arms, clippy::decimal_literal_representation)] + let path = value.as_ref(); + + if path.as_os_str().is_empty() { + Err("Path is empty".into()) + } else if path.as_os_str().len() > 32760 { + // Limit length to fit on VFAT/exFAT when using the `\\?\` prefix to disable legacy limits + // Source: https://en.wikipedia.org/wiki/Comparison_of_file_systems + Err(format!("Path is too long ({} chars): {:?}", path.as_os_str().len(), path).into()) + } else { + for component in path.components() { + if let Component::Normal(string) = component { + filename_valid_portable(string)? + } + } + Ok(()) + } +} + +/// The string is a valid file/folder name on all major filesystems and OSes +/// +/// ## Use For: +/// * Output file or directory names within a parent directory specified through other means. +/// +/// ## Relevant Conventions: +/// * Most of the time, you want to let users specify a full path via [`path_valid_portable` +/// ](fn.path_valid_portable.html)instead. +/// +/// ## Cautions: +/// * To ensure files can be copied/moved without issue, this validator may impose stricter +/// restrictions on filenames than your filesystem. Do *not* use it for input filenames. +/// * This validator cannot guarantee that a given filename will be valid once other +/// considerations such as overall path length limits are taken into account. +/// * As a more reliable validity check, you are advised to open a handle to the file in question +/// as early in your program's operation as possible, use it for all your interactions with the +/// file, and keep it open until you are finished. This will both verify its validity and +/// minimize the window in which another process could render the path invalid. +/// +/// ## Design Considerations: [[3]](https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits) +/// * In the interest of not inconveniencing users in the most common case, this validator imposes +/// a 255-character length limit. +/// * The eCryptFS home directory encryption offered by Ubuntu Linux imposes a 143-character +/// length limit when filename encryption is enabled. +/// [[4]](https://bugs.launchpad.net/ecryptfs/+bug/344878) +/// * the Joliet extensions for ISO 9660 are specified to support only 64-character filenames and +/// tested to support either 103 or 110 characters depending whether you ask the mkisofs +/// developers or Microsoft. [[5]](https://en.wikipedia.org/wiki/Joliet_(file_system)) +/// * The [POSIX Portable Filename Character Set +/// ](http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282) +/// is too restrictive to be baked into a general-purpose validator. +/// +/// **TODO:** Consider converting this to a private function that just exists as a helper for the +/// path validator in favour of more specialized validators for filename patterns, prefixes, and/or +/// suffixes, to properly account for how "you can specify a name bu not a path" generally +/// comes about. +pub fn filename_valid_portable + ?Sized>(value: &P) -> Result<(), OsString> { + #![allow(clippy::match_same_arms, clippy::else_if_without_else)] + let path = value.as_ref(); + + // TODO: Should I refuse incorrect Unicode normalization since Finder doesn't like it or just + // advise users to run a normalization pass? + // Source: https://news.ycombinator.com/item?id=16993687 + + // Check that the length is within range + let os_str = path.as_os_str(); + if os_str.len() > 255 { + return Err(format!("File/folder name is too long ({} chars): {:?}", + path.as_os_str().len(), path).into()); + } else if os_str.is_empty() { + return Err("Path component is empty".into()); + } + + // Check for invalid characters + let lossy_str = os_str.to_string_lossy(); + let last_char = lossy_str.chars().last().expect("getting last character"); + if [' ', '.'].iter().any(|&x| x == last_char) { + // The Windows shell and UI don't support component names ending in periods or spaces + // Source: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file + return Err("Windows forbids path components ending with spaces/periods".into()); + } else if lossy_str.as_bytes().iter().any(|c| match c { + // invalid on all APIs which don't use counted strings like inside the NT kernel + b'\0' => true, + // invalid under FAT*, VFAT, exFAT, and NTFS + 0x0..=0x1f | 0x7f | b'"' | b'*' | b'<' | b'>' | b'?' | b'|' => true, + // POSIX path separator (invalid on Unixy platforms like Linux and BSD) + b'/' => true, + // HFS/Carbon path separator (invalid in filenames on MacOS and Mac filesystems) + // DOS/Win32 drive separator (invalid in filenames on Windows and Windows filesystems) + b':' => true, + // DOS/Windows path separator (invalid in filenames on Windows and Windows filesystems) + b'\\' => true, + // let everything else through + _ => false, + }) { + #[allow(clippy::use_debug)] + return Err(format!("Path component contains invalid characters: {:?}", path).into()); + } + + // Reserved DOS filenames that still can't be used on modern Windows for compatibility + if let Some(file_stem) = path.file_stem() { + let stem = file_stem.to_string_lossy().to_uppercase(); + if RESERVED_DOS_FILENAMES.iter().any(|&x| x == stem) { + Err(format!("Filename is reserved on Windows: {:?}", file_stem).into()) + } else { + Ok(()) + } + } else { + Ok(()) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::OsStr; + + #[cfg(unix)] + use std::os::unix::ffi::OsStrExt; + #[cfg(windows)] + use std::os::windows::ffi::OsStringExt; + + #[test] + #[cfg(unix)] + fn path_output_dir_basic_functionality() { + assert!(path_output_dir(OsStr::new("/")).is_err()); // Root + assert!(path_output_dir(OsStr::new("/tmp")).is_ok()); // OK Folder + assert!(path_output_dir(OsStr::new("/dev/null")).is_err()); // OK File + assert!(path_output_dir(OsStr::new("/etc/shadow")).is_err()); // Denied File + assert!(path_output_dir(OsStr::new("/etc/ssl/private")).is_err()); // Denied Folder + assert!(path_output_dir(OsStr::new("/nonexistant_test_path")).is_err()); // Missing Path + assert!(path_output_dir(OsStr::new("/tmp\0with\0null")).is_err()); // Invalid CString + // TODO: is_dir but fails to canonicalize() + // TODO: Not-already-canonicalized paths + + assert!(path_output_dir(OsStr::from_bytes(b"/not\xffutf8")).is_err()); // Invalid UTF-8 + // TODO: Non-UTF8 path that actually does exist and is writable + } + + #[test] + #[cfg(windows)] + fn path_output_dir_basic_functionality() { + unimplemented!("TODO: Implement Windows version of path_output_dir"); + } + + // ---- path_readable_file ---- + + #[cfg(unix)] + #[test] + fn path_readable_file_basic_functionality() { + // Existing paths + assert!(path_readable_file(OsStr::new("/bin/sh")).is_ok()); // OK File + assert!(path_readable_file(OsStr::new("/bin/../etc/.././bin/sh")).is_ok()); // Non-canonic. + assert!(path_readable_file(OsStr::new("/../../../../bin/sh")).is_ok()); // Above root + + // Inaccessible, nonexistent, or invalid paths + assert!(path_readable_file(OsStr::new("")).is_err()); // Empty String + assert!(path_readable_file(OsStr::new("/")).is_err()); // OK Folder + assert!(path_readable_file(OsStr::new("/etc/shadow")).is_err()); // Denied File + assert!(path_readable_file(OsStr::new("/etc/ssl/private")).is_err()); // Denied Foldr + assert!(path_readable_file(OsStr::new("/nonexistant_test_path")).is_err()); // Missing Path + assert!(path_readable_file(OsStr::new("/null\0containing")).is_err()); // Invalid CStr + + } + #[cfg(windows)] + #[test] + fn path_readable_file_basic_functionality() { + unimplemented!("TODO: Pick some appropriate equivalent test paths for Windows"); + } + + #[cfg(unix)] + #[test] + fn path_readable_file_invalid_utf8() { + assert!(path_readable_file(OsStr::from_bytes(b"/not\xffutf8")).is_err()); // Invalid UTF-8 + // TODO: Non-UTF8 path that actually IS valid + } + #[cfg(windows)] + #[test] + fn path_readable_file_unpaired_surrogates() { + assert!(path_readable_file(&OsString::from_wide( + &['C' as u16, ':' as u16, '\\' as u16, 0xd800])).is_err()); + // TODO: Unpaired surrogate path that actually IS valid + } + + // ---- filename_valid_portable ---- + + const VALID_FILENAMES: &[&str] = &[ + // regular, space, and leading period + "test1", "te st", ".test", + // Stuff which would break if the DOS reserved names check is doing dumb pattern matching + "lpt", "lpt0", "lpt10", + ]; + + // Paths which should pass because std::path::Path will recognize the separators + // TODO: Actually run the tests on Windows to make sure they work + #[cfg(windows)] + const PATHS_WITH_NATIVE_SEPARATORS: &[&str] = &[ + "re/lative", "/ab/solute", "re\\lative", "\\ab\\solute"]; + #[cfg(unix)] + const PATHS_WITH_NATIVE_SEPARATORS: &[&str] = &["re/lative", "/ab/solute"]; + + // Paths which should fail because std::path::Path won't recognize the separators and we don't + // want them showing up in the components. + #[cfg(windows)] + const PATHS_WITH_FOREIGN_SEPARATORS: &[&str] = &["Classic Mac HD:Folder Name:File"]; + #[cfg(unix)] + const PATHS_WITH_FOREIGN_SEPARATORS: &[&str] = &[ + "relative\\win32", + "C:\\absolute\\win32", + "\\drive\\relative\\win32", + "\\\\unc\\path\\for\\win32", + "Classic Mac HD:Folder Name:File", + ]; + + // Source: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file + const INVALID_PORTABLE_FILENAMES: &[&str] = &[ + "test\x03", "test\x07", "test\x08", "test\x0B", "test\x7f", // Control characters (VFAT) + "\"test\"", "", "testsss|", "testsss*", "testsss?", "?estsss", // VFAT + "ends with space ", "ends_with_period.", // DOS/Win32 + "CON", "Con", "coN", "cOn", "CoN", "con", "lpt1", "com9", // Reserved names (DOS/Win32) + "con.txt", "lpt1.dat", // DOS/Win32 API (Reserved names are extension agnostic) + "", "\0"]; // POSIX + + #[test] + fn filename_valid_portable_accepts_valid_names() { + for path in VALID_FILENAMES { + assert!(filename_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path); + } + } + + #[test] + fn filename_valid_portable_refuses_path_separators() { + for path in PATHS_WITH_NATIVE_SEPARATORS { + assert!(filename_valid_portable(OsStr::new(path)).is_err(), "{:?}", path); + } + for path in PATHS_WITH_FOREIGN_SEPARATORS { + assert!(filename_valid_portable(OsStr::new(path)).is_err(), "{:?}", path); + } + } + + #[test] + fn filename_valid_portable_refuses_invalid_characters() { + for fname in INVALID_PORTABLE_FILENAMES { + assert!(filename_valid_portable(OsStr::new(fname)).is_err(), "{:?}", fname); + } + } + + #[test] + fn filename_valid_portable_refuses_empty_strings() { + assert!(filename_valid_portable(OsStr::new("")).is_err()); + } + + #[test] + fn filename_valid_portable_enforces_length_limits() { + // 256 characters + let mut test_str = std::str::from_utf8(&[b'X'; 256]).expect("parsing constant"); + assert!(filename_valid_portable(OsStr::new(test_str)).is_err()); + + // 255 characters (maximum for NTFS, ext2/3/4, and a lot of others) + test_str = std::str::from_utf8(&[b'X'; 255]).expect("parsing constant"); + assert!(filename_valid_portable(OsStr::new(test_str)).is_ok()); + } + + #[cfg(unix)] + #[test] + fn filename_valid_portable_accepts_non_utf8_bytes() { + // Ensure that we don't refuse invalid UTF-8 that "bag of bytes" POSIX allows + assert!(filename_valid_portable(OsStr::from_bytes(b"\xff")).is_ok()); + } + #[cfg(windows)] + #[test] + fn filename_valid_portable_accepts_unpaired_surrogates() { + assert!(path_valid_portable(&OsString::from_wide(&[0xd800])).is_ok()); + } + + // ---- path_valid_portable ---- + + #[test] + fn path_valid_portable_accepts_valid_names() { + for path in VALID_FILENAMES { + assert!(path_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path); + } + + // No filename (.file_stem() returns None) + assert!(path_valid_portable(OsStr::new("foo/..")).is_ok()); + } + + #[test] + fn path_valid_portable_accepts_native_path_separators() { + for path in PATHS_WITH_NATIVE_SEPARATORS { + assert!(path_valid_portable(OsStr::new(path)).is_ok(), "{:?}", path); + } + + // Verify that repeated separators are getting collapsed before filename_valid_portable + // sees them. + // TODO: Make this conditional on platform and also test repeated backslashes on Windows + assert!(path_valid_portable(OsStr::new("/path//with/repeated//separators")).is_ok()); + } + + #[test] + fn path_valid_portable_refuses_foreign_path_separators() { + for path in PATHS_WITH_FOREIGN_SEPARATORS { + assert!(path_valid_portable(OsStr::new(path)).is_err(), "{:?}", path); + } + } + + #[test] + fn path_valid_portable_refuses_invalid_characters() { + for fname in INVALID_PORTABLE_FILENAMES { + assert!(path_valid_portable(OsStr::new(fname)).is_err(), "{:?}", fname); + } + } + + #[test] + fn path_valid_portable_enforces_length_limits() { + let mut test_string = String::with_capacity(255 * 130); + #[allow(clippy::decimal_literal_representation)] + while test_string.len() < 32761 { + test_string.push_str(std::str::from_utf8(&[b'X'; 255]).expect("utf8 from literal")); + test_string.push('/'); + } + + // >32760 characters + assert!(path_valid_portable(OsStr::new(&test_string)).is_err()); + + // 32760 characters (maximum for FAT32/VFAT/exFAT) + #[allow(clippy::decimal_literal_representation)] + test_string.truncate(32760); + assert!(path_valid_portable(OsStr::new(&test_string)).is_ok()); + + // 256 characters with no path separators + test_string.truncate(255); + test_string.push('X'); + assert!(path_valid_portable(OsStr::new(&test_string)).is_err()); + + // 255 characters with no path separators + test_string.truncate(255); + assert!(path_valid_portable(OsStr::new(&test_string)).is_ok()); + } + + #[cfg(unix)] + #[test] + fn path_valid_portable_accepts_non_utf8_bytes() { + // Ensure that we don't refuse invalid UTF-8 that "bag of bytes" POSIX allows + assert!(path_valid_portable(OsStr::from_bytes(b"/\xff/foo")).is_ok()); + } + #[cfg(windows)] + #[test] + fn path_valid_portable_accepts_unpaired_surrogates() { + assert!(path_valid_portable(&OsString::from_wide( + &['C' as u16, ':' as u16, '\\' as u16, 0xd800])).is_ok()); + } + +}