summaryrefslogtreecommitdiff
path: root/support/pkgcheck
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
committerNorbert Preining <norbert@preining.info>2019-09-02 13:46:59 +0900
commite0c6872cf40896c7be36b11dcc744620f10adf1d (patch)
tree60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/pkgcheck
Initial commit
Diffstat (limited to 'support/pkgcheck')
-rw-r--r--support/pkgcheck/CHANGES.md62
-rw-r--r--support/pkgcheck/Cargo.toml35
-rw-r--r--support/pkgcheck/LICENSE-APACHE201
-rw-r--r--support/pkgcheck/LICENSE-MIT21
-rw-r--r--support/pkgcheck/README.md66
-rwxr-xr-xsupport/pkgcheck/bin/pkgcheckbin0 -> 7436104 bytes
-rw-r--r--support/pkgcheck/docs/errorsd.tex332
-rw-r--r--support/pkgcheck/docs/fatald.tex39
-rw-r--r--support/pkgcheck/docs/informationd.tex51
-rw-r--r--support/pkgcheck/docs/pkgcheck.pdfbin0 -> 62946 bytes
-rw-r--r--support/pkgcheck/docs/pkgcheck.tex375
-rw-r--r--support/pkgcheck/docs/title.tex1
-rw-r--r--support/pkgcheck/docs/warningsd.tex39
-rw-r--r--support/pkgcheck/quick_intro.txt24
-rw-r--r--support/pkgcheck/src/filemagic.rs422
-rw-r--r--support/pkgcheck/src/generate.pest34
-rw-r--r--support/pkgcheck/src/generate.pest.md15
-rw-r--r--support/pkgcheck/src/gparser.rs46
-rw-r--r--support/pkgcheck/src/linkcheck.rs259
-rw-r--r--support/pkgcheck/src/main.rs1287
-rw-r--r--support/pkgcheck/src/messages/errorsd.rs445
-rw-r--r--support/pkgcheck/src/messages/fatald.rs66
-rw-r--r--support/pkgcheck/src/messages/informationd.rs77
-rw-r--r--support/pkgcheck/src/messages/mod.rs657
-rw-r--r--support/pkgcheck/src/messages/warningsd.rs58
-rw-r--r--support/pkgcheck/src/recode.rs62
-rw-r--r--support/pkgcheck/src/utils.rs396
27 files changed, 5070 insertions, 0 deletions
diff --git a/support/pkgcheck/CHANGES.md b/support/pkgcheck/CHANGES.md
new file mode 100644
index 0000000000..912a2e8aaf
--- /dev/null
+++ b/support/pkgcheck/CHANGES.md
@@ -0,0 +1,62 @@
+# General remarks
+
+- the x.y.z is the version of the Rust source code which follows semantic versioning
+- the combination of version and version date designates the version of the package as it is uploaded to CTAN
+
+
+# Changes
+
+2018-12-09 (1.0.0)
+ - 1.0.0 First stable version
+
+2018-12-16 (1.0.0)
+ - quick_intro.txt: improve wording
+ - build_ctan_zip.p6: add source files to be included into CTAN zip archive
+ - devnotes.md: add documentation how to build your own binary
+2018-12-26 (1.1.0)
+ - enhance check for generated files which now could reside in any
+ subdirectory in the package tree. This changes the format of the
+ e0019 message slightly where now the path of a generated file will be
+ displayed in the e0019 message
+ - change ordering of the sections in the pkgcheck.pdf document. First come the
+ informational messages, then warnings and error messages and finally fatal messages.
+ - add a short info to the PDF documention about how to install pkgcheck
+2019-01-02 (1.2.0)
+ - some code simplification
+ - **New feature**: checks that path names in the TDS zip archive contain the package name
+ (only exception is a man page path)
+ - add test cases for messages e0026, e0028
+2019-03-09 (1.3.0)
+ - recompile with newest http library
+ - checking URLs
+ - when checking URLs we try to get the headers first. If this fails we try to get the web page.
+ - when checking URLs and a redirect has an invalid location containing 127.0.0.1
+ then we regard the url as ok
+ - ignoring .tfm files when checking for duplicates
+ - now checking for UTF BOMs. If a README file contains a BOM we issue an error message,
+ if other files contain BOMs we issue a warning message
+2019-03-11 (1.4.0)
+ - recognizing generated files in a .dtx file when they are included using a
+ filecontents resp. filecontents* environment.
+2019-03-14 (1.5.0)
+ - a README can be a symlink which will now be detected properly
+ - if a symlink points to a file object outside of the package directory tree error message
+ e0030 will be issued
+2019-03-29 (1.6.0)
+ - new error message e0031 when a filename contains invalid UTF-8 characters
+2019-06-30 (1.7.0)
+ - when symlinks occur in e0025 they are reported as files
+ - improved error message e0025 now displaying the paths of the affected
+ files/directories/symlinks
+ - if a found http(s) link ends with "`" then the trailing "`" will be discarded
+ - compiled with rust edition 2018
+2019-07-20 (1.8.0)
+ - add a check to detect temporary files in the TDS zip archive
+2019-07-21 (1.8.1)
+ - correct typo in e0008 message text
+2019-08-07 (1.8.2)
+ - updating crates, requiring slight code changes in integration.rs
+ - ignore windows files .nsh and .reg when checking for CRLF line endings
+ - new error message e0034 for unwanted files in top level directory of a TDS zip archive
+ - e0020 now reporting only unwanted directory in top level directory of a TDS zip archive
+
diff --git a/support/pkgcheck/Cargo.toml b/support/pkgcheck/Cargo.toml
new file mode 100644
index 0000000000..74b14f05cc
--- /dev/null
+++ b/support/pkgcheck/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "pkgcheck"
+version = "1.8.2"
+authors = ["Manfred Lotz <manfred@ctan.org>"]
+description = "Command-line tool to check packages uploaded to CTAN."
+license = "MIT/Apache-2.0"
+edition = "2018"
+
+[dependencies]
+structopt = "0.2"
+walkdir = "2.2"
+scoped_threadpool = "0.1"
+num_cpus = "1"
+blake2 = "0.8"
+fnv = "1.0.6"
+unbytify = "0.2"
+regex = "1"
+glob = "0.3"
+colored = "1"
+linkify = "0.4"
+reqwest = "0.9"
+lazy_static = "1"
+tempdir = "0.3"
+assert_cmd = "0.11"
+predicates = "1.0"
+openssl-probe = "0.1.2"
+pest = "^2.1"
+pest_derive = "2.1"
+escargot = "0.5"
+
+threadpool = "1.7.1"
+unicode-bom = "1"
+
+
+url = "2.1"
diff --git a/support/pkgcheck/LICENSE-APACHE b/support/pkgcheck/LICENSE-APACHE
new file mode 100644
index 0000000000..261eeb9e9f
--- /dev/null
+++ b/support/pkgcheck/LICENSE-APACHE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/support/pkgcheck/LICENSE-MIT b/support/pkgcheck/LICENSE-MIT
new file mode 100644
index 0000000000..c0f44fc630
--- /dev/null
+++ b/support/pkgcheck/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-2019 Manfred Lotz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/support/pkgcheck/README.md b/support/pkgcheck/README.md
new file mode 100644
index 0000000000..96d6b16716
--- /dev/null
+++ b/support/pkgcheck/README.md
@@ -0,0 +1,66 @@
+---
+pkgcheck utility
+
+Author: Manfred Lotz, <manfred@ctan.org>
+
+License: Apache License, Version 2.0 or MIT License
+
+---
+
+
+# Overview
+
+`pkgcheck` is a utility which the author uses to check uploaded packages to CTAN before
+installing them. It is a binary running on Linux only.
+
+There is no Windows version planned.
+
+
+# Dependencies
+
+The `pkgcheck` binary is a 64-bit statically linked binary, and thus it should run also on
+older Linux versions.
+
+It uses the following external programs:
+
+- `pdfinfo` for checking pdf documents
+- `unzip` for temporarily unpacking a TDS zip archive
+
+# Installing the binary
+
+Copy the binary from `bin/pkgcheck` to a suitable location on your hard disk, and
+(recommended) make sure the directory is in the `PATH` or call `pkgcheck` using an
+absolute path name.
+
+# Documentation
+
+The documentation is `docs/pkgcheck.pdf`. It contains
+a description of all fatal, error, warning and information messages.
+
+
+# Build the documentation
+
+Run either `xelatex` or `lualatex`. Note that `-shell-escape` is required.
+
+
+```
+cd docs
+lualatex -shell-escape pkgcheck.tex
+```
+
+
+
+# License
+
+Licensed under either of
+
+- Apache License, Version 2.0
+
+ - See file LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0
+
+- MIT license
+
+ - See file LICENSE-MIT or http://opensource.org/licenses/MIT
+
+at your option.
+
diff --git a/support/pkgcheck/bin/pkgcheck b/support/pkgcheck/bin/pkgcheck
new file mode 100755
index 0000000000..aedf999a10
--- /dev/null
+++ b/support/pkgcheck/bin/pkgcheck
Binary files differ
diff --git a/support/pkgcheck/docs/errorsd.tex b/support/pkgcheck/docs/errorsd.tex
new file mode 100644
index 0000000000..1557460ea1
--- /dev/null
+++ b/support/pkgcheck/docs/errorsd.tex
@@ -0,0 +1,332 @@
+\hypertarget{e0001----bad-characters-in-file-name}{%
+\subsection{E0001 -\/- Bad characters in file
+name}\label{e0001----bad-characters-in-file-name}}
+
+File name should not contain non-ascii characters. Additionally, file
+names should not contain control characters or other characters which
+may have a special meaning for UNIX shells.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#nounixspecialcharacters}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#nounixspecialcharacters}
+
+\hypertarget{e0002----file-permissions}{%
+\subsection{E0002 -\/- File
+Permissions}\label{e0002----file-permissions}}
+
+Files submitted to CTAN should be world readable.
+
+Only files that are truly executable (like scripts and binaries) should
+be marked as such.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filepermissions}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filepermissions}
+
+\hypertarget{e0003----readme-is-not-a-text-file}{%
+\subsection{E0003 -\/- README is not a text
+file}\label{e0003----readme-is-not-a-text-file}}
+
+The \texttt{README} file specified in the error message must be a text
+file but it isn't.
+
+\hypertarget{e0004----empty-directory-not-allowed}{%
+\subsection{E0004 -\/- Empty directory not
+allowed}\label{e0004----empty-directory-not-allowed}}
+
+Empty directories are considered as rubbish, and are usually not
+accepted as part of a package.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noemptyfiles}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noemptyfiles}
+
+\hypertarget{e0005----empty-files-not-allowed}{%
+\subsection{E0005 -\/- Empty files not
+allowed}\label{e0005----empty-files-not-allowed}}
+
+Empty files are considered as rubbish, and are usually not accepted as
+part of a package.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noemptyfiles}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noemptyfiles}
+
+\hypertarget{e0006----hidden-directories-not-allowed}{%
+\subsection{E0006 -\/- Hidden directories not
+allowed}\label{e0006----hidden-directories-not-allowed}}
+
+A package should not contain hidden directories.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noauxfiles}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noauxfiles}
+
+\hypertarget{e0007----hidden-files-not-allowed}{%
+\subsection{E0007 -\/- Hidden files not
+allowed}\label{e0007----hidden-files-not-allowed}}
+
+A package should not contain hidden files.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noauxfiles}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noauxfiles}
+
+\hypertarget{e0008----temporary-file-detected}{%
+\subsection{E0008 -\/- Temporary file
+detected}\label{e0008----temporary-file-detected}}
+
+A temporary file was detected. These are typically files created by TeX
+\& friends and should not be part of a package.
+
+Temporary files will also be detected in a TDS zip archive.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noauxfiles}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noauxfiles}
+
+\hypertarget{e0009----package-doesnt-contain-a-readme-file}{%
+\subsection{E0009 -\/- Package doesn't contain a README
+file}\label{e0009----package-doesnt-contain-a-readme-file}}
+
+A package must contain at least one of \texttt{README},
+\texttt{README.md} or \texttt{README.txt} file.
+
+For more details refer to:
+\href{http://mirrors.ibiblio.org/CTAN/help/ctan/CTAN-upload-addendum.html\#readme}{http://mirrors.ibiblio.org/CTAN/help/ctan/CTAN-upload-addendum.html\#readme}
+
+\hypertarget{e0010----broken-symlink-detected}{%
+\subsection{E0010 -\/- Broken symlink
+detected}\label{e0010----broken-symlink-detected}}
+
+A broken symlink was detected.
+
+\hypertarget{e0011----wrong-permission-for-directory}{%
+\subsection{E0011 -\/- Wrong permission for
+directory}\label{e0011----wrong-permission-for-directory}}
+
+Directories should have rwx for the owner and at least \texttt{r-x} for
+others (i.e. world readable).
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filepermissions}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filepermissions}
+
+\hypertarget{e0012----crlf-line-endings-detected}{%
+\subsection{E0012 -\/- CRLF line endings
+detected}\label{e0012----crlf-line-endings-detected}}
+
+The file specified in the error message contains CRLF line endings. Text
+files should have UNIX style line endings.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#crlf}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#crlf}
+
+\hypertarget{e0013----socket-special-fie-detected}{%
+\subsection{E0013 -\/- Socket special fie
+detected}\label{e0013----socket-special-fie-detected}}
+
+The file specified in the error message is a socket special file which
+is not allowed.
+
+\hypertarget{e0014----fifo-special-file-detected}{%
+\subsection{E0014 -\/- Fifo special file
+detected}\label{e0014----fifo-special-file-detected}}
+
+The file specified in the error message is a fifo special file which is
+not allowed.
+
+\hypertarget{e0015----bloch-device-file-detected}{%
+\subsection{E0015 -\/- Bloch device file
+detected}\label{e0015----bloch-device-file-detected}}
+
+The file specified in the error message is a block device file which is
+not allowed.
+
+\hypertarget{e0016----character-device-file-detected}{%
+\subsection{E0016 -\/- Character device file
+detected}\label{e0016----character-device-file-detected}}
+
+The file specified in the error message is a character device file which
+is not allowed.
+
+\hypertarget{e0017----pdf-document-is-in-error}{%
+\subsection{E0017 -\/- PDF document is in
+error}\label{e0017----pdf-document-is-in-error}}
+
+The PDF document mentioned in the message is in error.
+
+\texttt{pdfinfo} will be run to check if a PDF document can be read.
+Message \texttt{E0017} will be followed by the error messages from
+\texttt{pdfinfo}.
+
+Example:
+
+\begin{verbatim}
+I0002 Checking package files in directory somepkg
+E0017 PDF error detected in somepkg/sompkg.pdf
+Syntax Error (1293042): Illegal character ')'
+Syntax Error: Couldn't find trailer dictionary
+Syntax Error (1293042): Illegal character ')'
+Syntax Error: Couldn't find trailer dictionary
+Syntax Error: Couldn't read xref table
+\end{verbatim}
+
+\hypertarget{e0018----unwanted-directory-detected}{%
+\subsection{E0018 -\/- Unwanted directory
+detected}\label{e0018----unwanted-directory-detected}}
+
+A directory was detected which should not be part of a package. Example:
+\texttt{\_\_MACOSX}
+
+\hypertarget{e0019----generated-file-detected}{%
+\subsection{E0019 -\/- Generated file
+detected}\label{e0019----generated-file-detected}}
+
+In order to avoid redundancy we don't want to have included files in a
+package which easily can be generated from other files in the
+submission.
+
+Exceptions are the \texttt{README} files of the package, i.e.
+\texttt{README}, \texttt{README.md} or \texttt{README.txt}.
+
+Starting with version 1.1.0 \texttt{pkgcheck} also detects generated
+files if they are in a different directory in the package.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#nogeneratedfiles}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#nogeneratedfiles}
+
+\hypertarget{e0020----unwanted-directory-detected-in-the-top-level-directory-in-tds-zip-archive}{%
+\subsection{E0020 -\/- Unwanted directory detected in the top level
+directory in TDS zip
+archive}\label{e0020----unwanted-directory-detected-in-the-top-level-directory-in-tds-zip-archive}}
+
+A top level directory of a TDS archive should only contain all or some
+of the following directories:
+
+\begin{itemize}
+\tightlist
+\item
+ \texttt{tex}
+\item
+ \texttt{fonts}
+\item
+ \texttt{metafont}
+\item
+ \texttt{metapost}
+\item
+ \texttt{bibtex}
+\item
+ \texttt{scripts}
+\item
+ \texttt{doc}
+\item
+ \texttt{source}
+\end{itemize}
+
+\hypertarget{e0021----error-when-reading-a-file}{%
+\subsection{E0021 -\/- Error when reading a
+file}\label{e0021----error-when-reading-a-file}}
+
+An error was encountered when reading the file specified in the message.
+
+\hypertarget{e0022----check-of-an-url-in-a-readme-file-failed}{%
+\subsection{E0022 -\/- Check of an URL in a README file
+failed}\label{e0022----check-of-an-url-in-a-readme-file-failed}}
+
+URL checking is in effect. An error occcured when trying to retrieve an
+URL which was found in the specified \texttt{README} file.
+
+\hypertarget{e0023----follow-up-error-when-trying-to-read-a-directory-with-insufficient-permissions}{%
+\subsection{E0023 -\/- Follow up error when trying to read a directory
+with insufficient
+permissions}\label{e0023----follow-up-error-when-trying-to-read-a-directory-with-insufficient-permissions}}
+
+Error which is a follow-up error. For instance, when a directory could
+not be read.
+
+\hypertarget{e0024----tds-zip-archive-has-wrong-permissions}{%
+\subsection{E0024 -\/- TDS zip archive has wrong
+permissions}\label{e0024----tds-zip-archive-has-wrong-permissions}}
+
+The TDS zip archive should have at least \texttt{r-\/-} for the owner
+and at least \texttt{r-\/-} for others (i.e. world readable).
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filepermissions}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filepermissions}
+
+\hypertarget{e0025----duplicate-names-when-ignoring-letter-case-for-files-or-directories}{%
+\subsection{E0025 -\/- Duplicate names when ignoring letter case for
+files or
+directories}\label{e0025----duplicate-names-when-ignoring-letter-case-for-files-or-directories}}
+
+As there are operating systems which do not distinguish between
+\texttt{myfile} and \texttt{MYFILE} we don't want to have file names in
+a directory which are the same after converting to lower case.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filenames}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filenames}
+
+\hypertarget{e0026----files-not-in-tds-or-different-in-tds-and-non-install-tree}{%
+\subsection{E0026 -\/- Files not in TDS or different in TDS and
+non-install
+tree}\label{e0026----files-not-in-tds-or-different-in-tds-and-non-install-tree}}
+
+The file mentioned in the error message is either not existing in the
+TDS zip archive, or it is different to the one in the non-install tree
+
+\hypertarget{e0027----an-io-error-occured}{%
+\subsection{E0027 -\/- An I/O error
+occured}\label{e0027----an-io-error-occured}}
+
+Some kind of I/O error occured. If you believe there is an error in
+\texttt{pkgcheck} please contact the author.
+
+\hypertarget{e0028----a-path-name-in-a-tds-zip-archive-must-contain-the-package-name}{%
+\subsection{E0028 -\/- A path name in a TDS zip archive must contain the
+package
+name}\label{e0028----a-path-name-in-a-tds-zip-archive-must-contain-the-package-name}}
+
+The path names in a TDS zip archive must contain the package name.
+
+\textbf{Example:} Assume a package \texttt{somepkg}. Then path names
+should look like follows:
+
+\begin{verbatim}
+tex/latex/somepkg/somepkg.cls
+doc/latex/somepkg/README
+source/latex/somepkg/somepkg.dtx
+...
+\end{verbatim}
+
+\hypertarget{e0029----readme-file--encoding-with-bom-detected}{%
+\subsection{\texorpdfstring{E0029 -\/- README file: encoding with BOM
+detected}{E0029 -\/- README file: encoding with BOM detected}}\label{e0029----readme-file--encoding-with-bom-detected}}
+
+A README file should be either ASCII or UTF-8 without BOM(byte order
+mark)
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#readme}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#readme}
+
+\hypertarget{e0030----a-symlink-was-found-which-points-outside-of-the-package-directory-tree}{%
+\subsection{E0030 -\/- A symlink was found which points outside of the
+package directory
+tree}\label{e0030----a-symlink-was-found-which-points-outside-of-the-package-directory-tree}}
+
+A symlink must not point to a file or directory outside of the package
+directory tree.
+
+\hypertarget{e0031----file-name-contains-invalid-utf-8-characters}{%
+\subsection{E0031 -\/- File name contains invalid UTF-8
+character(s)}\label{e0031----file-name-contains-invalid-utf-8-characters}}
+
+A file name contains invald UTF-8 character(s).
+
+\hypertarget{e0033----error-when-unpacking-tds-archive}{%
+\subsection{E0033 -\/- Error when unpacking tds
+archive}\label{e0033----error-when-unpacking-tds-archive}}
+
+In order to investigate the contents of the TDS zip archive
+\texttt{pkgcheck} unpacks the TDS zip archive to a temporary location
+which failed for the reason given in the error message.
+
+\hypertarget{e0034----unwanted-file-detected-in-the-top-level-directory-in-tds-zip-archive}{%
+\subsection{E0034 -\/- Unwanted file detected in the top level directory
+in TDS zip
+archive}\label{e0034----unwanted-file-detected-in-the-top-level-directory-in-tds-zip-archive}}
+
+A top level directory of a TDS archive should only contain certain
+directories but no files.
diff --git a/support/pkgcheck/docs/fatald.tex b/support/pkgcheck/docs/fatald.tex
new file mode 100644
index 0000000000..b4c175a7c0
--- /dev/null
+++ b/support/pkgcheck/docs/fatald.tex
@@ -0,0 +1,39 @@
+\hypertarget{f0001----specify-a-directory-to-check-use-option--d}{%
+\subsection{F0001 -\/- Specify a directory to check (use option
+-d)}\label{f0001----specify-a-directory-to-check-use-option--d}}
+
+\texttt{pkgcheck} was called without any options. Use option \texttt{-d}
+to check a directory
+
+\hypertarget{f0002----specified-directory-does-not-exist-exiting}{%
+\subsection{F0002 -\/- Specified directory does not exist.
+Exiting...}\label{f0002----specified-directory-does-not-exist-exiting}}
+
+The directory specified at the command line does not exit.
+
+\hypertarget{f0003----specified-tds-archive-does-not-exist-or-is-no-file}{%
+\subsection{F0003 -\/- Specified TDS archive does not exist or is no
+file}\label{f0003----specified-tds-archive-does-not-exist-or-is-no-file}}
+
+Specify a valid TDS zip archive when calling \texttt{pkgcheck}
+
+\hypertarget{f0004----the-file-specified-as-tds-archive-is-no-zip-archive}{%
+\subsection{F0004 -\/- The file specified as TDS archive is no zip
+archive}\label{f0004----the-file-specified-as-tds-archive-is-no-zip-archive}}
+
+Specify a valid TDS zip archive when calling \texttt{pkgcheck}
+
+\hypertarget{f0005----bad-file-name-for-the-zip-archive}{%
+\subsection{F0005 -\/- Bad file name for the zip
+archive}\label{f0005----bad-file-name-for-the-zip-archive}}
+
+pkgcheck detected that the file name of the TDS zip archive doesn't end
+with \texttt{.tds.zip}
+
+\hypertarget{f0006----unknown-error-code-specified-with-option--e-resp----explain-exiting}{%
+\subsection{F0006 -\/- Unknown error code specified with option -e resp.
+-\/-\/-explain.
+Exiting...}\label{f0006----unknown-error-code-specified-with-option--e-resp----explain-exiting}}
+
+\texttt{pkgcheck} was called with option \texttt{-e} resp.
+\texttt{-\/-explain}, and an unknown error code was specified.
diff --git a/support/pkgcheck/docs/informationd.tex b/support/pkgcheck/docs/informationd.tex
new file mode 100644
index 0000000000..88a2305f48
--- /dev/null
+++ b/support/pkgcheck/docs/informationd.tex
@@ -0,0 +1,51 @@
+\hypertarget{i0001----successfully-converted-from-crlf-to-lf}{%
+\subsection{I0001 -\/- Successfully converted from CRLF to
+LF}\label{i0001----successfully-converted-from-crlf-to-lf}}
+
+Just an information that \texttt{pkgcheck} has successfully converted a
+file from CRLF to LF line endings
+
+\hypertarget{i0002----checking-package-files-in-directory}{%
+\subsection{I0002 -\/- Checking package files in
+directory}\label{i0002----checking-package-files-in-directory}}
+
+Just an information that \texttt{pkgcheck} starts checking the package
+files in the unzipped directory trees
+
+\hypertarget{i0003----checking-tds-zip-archive}{%
+\subsection{I0003 -\/- Checking TDS zip
+archive}\label{i0003----checking-tds-zip-archive}}
+
+Just an information that \texttt{pkgcheck} starts checking the TDS zip
+archive
+
+\hypertarget{i0004----correcting-line-endings-for-file}{%
+\subsection{I0004 -\/- Correcting line endings for
+file}\label{i0004----correcting-line-endings-for-file}}
+
+The file had CRLF line ending and will be corrected to have LF (Unix
+like) line endings.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#crlf}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#crlf}
+
+\hypertarget{i0005----corrections-permissions-for-file-or-directory}{%
+\subsection{I0005 -\/- Corrections permissions for file or
+directory}\label{i0005----corrections-permissions-for-file-or-directory}}
+
+\texttt{pkgcheck} corrects wrong permsissions for package files and
+directories. It runs the \texttt{chmod} command in verbose mode.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filepermissions}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#filepermissions}
+
+\hypertarget{i0006----files-having-one-of-the-following-file-name-endings-are-regarded-as-temporary}{%
+\subsection{I0006 -\/- Files having one of the following file name
+endings are regarded as
+temporary}\label{i0006----files-having-one-of-the-following-file-name-endings-are-regarded-as-temporary}}
+
+Option -\/-show-temp-endings was used, and pkgcheck prints a list of
+temporary file endings and their meanings.
+
+For more details refer to:
+\href{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noauxfiles}{http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html\#noauxfiles}
diff --git a/support/pkgcheck/docs/pkgcheck.pdf b/support/pkgcheck/docs/pkgcheck.pdf
new file mode 100644
index 0000000000..23e52fafe3
--- /dev/null
+++ b/support/pkgcheck/docs/pkgcheck.pdf
Binary files differ
diff --git a/support/pkgcheck/docs/pkgcheck.tex b/support/pkgcheck/docs/pkgcheck.tex
new file mode 100644
index 0000000000..ab96cc390a
--- /dev/null
+++ b/support/pkgcheck/docs/pkgcheck.tex
@@ -0,0 +1,375 @@
+\listfiles
+\documentclass[fontsize=10pt,paper=a4,abstract=on]{scrartcl}
+
+\DeclareTOCStyleEntry[dynnumwidth=true]{default}{subsection}
+
+\usepackage{unicode-math}
+
+\usepackage[RM={StylisticSet=3}]{plex-otf}
+
+\usepackage[english]{babel}
+\usepackage[autostyle]{csquotes}
+
+\usepackage{array,multido}
+\usepackage{booktabs} % for examples
+%\usepackage{xltabular} % for examples
+\usepackage{listings}
+\lstset{columns=fixed,basicstyle=\ttfamily\small}
+\usepackage[table]{xcolor}
+\usepackage{xspace}
+
+\usepackage{longtable}
+
+\usepackage{minted}
+\usemintedstyle{emacs}
+
+\pagestyle{headings}
+
+\usepackage[colorlinks, pdfusetitle, hyperfootnotes=false]{hyperref}
+% define \code for url-like breaking of typewriter fragments.
+\ifx\nolinkurl\undefined \let\code\url \else \let\code\nolinkurl \fi
+
+% Define \cs to prepend a backslash, and be unbreakable:
+\DeclareRobustCommand\cs[1]{\mbox{\texttt{\char`\\#1}}}
+
+
+\newcommand{\pkgcheck}{\texttt{pkgcheck}\xspace}
+\newcommand{\msg}[1]{\texttt{#1}\xspace}
+
+\providecommand{\tightlist}{%
+ \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
+
+% \pdfinfo{
+% /Author (Manfred Lotz)
+% /Title pkgcheck Utility
+% }
+
+%\title{pkgcheck utility}
+\include{title}
+\author{Manfred Lotz}
+
+
+\begin{document}
+\maketitle
+\tableofcontents
+
+\begin{abstract}
+ This document describes the \pkgcheck command line utility which is used by the
+ author when checking uploaded packages to CTAN.
+\end{abstract}
+
+
+\section{Introduction}
+
+Uploaded packages to CTAN must satisfy various requirements in order to get
+installed on CTAN.
+
+A first introduction is given here \url{https://ctan.org/help/upload-pkg}.
+
+Even more details are to be found in the excellent CTAN-upload addendum
+\url{https://ctan.org/file/help/ctan/CTAN-upload-addendum} written by Petra Rübe-Pugliese.
+
+The \pkgcheck utility which runs on Linux systems only checks those requirements
+which can be checked by a program.
+
+\section{pkgcheck utility}
+
+The \pkgcheck utility is a compiled program written in the Rust programming
+language. It runs in a Linux environment. Currently, Windows is not supported.
+Simply, because the author doesn't use Windows at all.
+
+It will be invoked from the command line, and any error or
+warning message has a certain message id. \pkgcheck offers an option to get more
+information for a certain error.
+
+\section{Requirements}
+
+\pkgcheck doesn't have any special runtime requirements.
+
+The \pkgcheck is a 64-bit statically linked binary, and should work an any 64-bit Linux.
+It is available in the repository in directory \verb|bin/|.
+
+Currently, the only external programs required are:
+\begin{itemize}
+\item \verb|/usr/bin/unzip|
+
+Used only, when a TDS zip archive will be extracted.
+
+\item \verb|/usr/bin/pdfinfo|
+
+Used only, when a PDF document will be checked.
+
+\end{itemize}
+
+\section{Installation}
+
+Copy the binary from \verb|bin/pkgcheck| to a suitable location on your hard disk, and
+(recommended) make sure the directory is in the \verb|PATH| or call \verb|pkgcheck| using an
+absolute path name.
+
+\section{Utility usage}
+
+
+\subsection{Help option}
+
+Running \verb|pkgcheck --help| shows the available command line options.
+
+Here a sample output:
+
+\begin{verbatim}
+pkgcheck 1.0.0
+Manfred Lotz <manfred@ctan.org>
+A checker for uploaded packages to CTAN.
+
+USAGE:
+ pkgcheck [FLAGS] [OPTIONS]
+
+FLAGS:
+ -L, --correct-crlf Correct CRLF line endings
+ -C, --correct-perms Correct permissions
+ --explain-all Explains all error or warning messages
+ -h, --help Prints help information
+ -I, --ignore-dupes Ignore dupes
+ --no-colors Don't display messages in color
+ --show-temp-endings Show file endings for temporary files
+ --urlcheck Check URLs found in README files
+ -V, --version Prints version information
+ -v, --verbose Verbose operation?
+
+OPTIONS:
+ -e, --explain <explain> Explain error or warning message
+ -d, --package-dir <pkg_dir> Package directory
+ -T, --tds-zip <tds_zip> tds zip archive
+
+\end{verbatim}
+
+
+\subsection{Check a package}
+
+A package for CTAN is supposed to be uploaded as a ZIP or a g-zipped
+tar archive. The package must have a top level directory.
+
+After unpacking the archive of a package \texttt{mypkg} into directory
+\texttt{mypkg/} it can be checked by running \pkgcheck with
+option verb|--package-dir| or shorter \verb|-d|.
+
+
+\begin{minted}[linenos=true,frame=single,fontsize=\footnotesize]{sh}
+ pkgcheck -d mypkg
+\end{minted}
+
+\pkgcheck returns 1 if there are any errors, otherwise 0.
+
+
+
+\subsection{Check a package which has a TDS archive}
+
+If a package contains a TDS ZIP archive it is supposed to be in the
+top level directory of a package.
+
+In order to check the TDS ZIP archive the option
+\verb|-T <tds_zip>| or \verb|--tds-zip <tds_zip>| can be used.
+
+Please note that a TDS ZIP archive will always be checked together with
+the non-install tree of the package which means that
+\verb|--tds-zip| requires option \verb|--package-dir| as well.
+
+Checking package \texttt{mypkg} \pkgcheck will be invoked like follows:
+
+\begin{minted}[linenos=true,frame=single,fontsize=\footnotesize]{sh}
+ pkgcheck -d mypkg -T mypkg.tds.zip
+\end{minted}
+
+
+As before \pkgcheck returns 1 if there are any errors, otherwise 0.
+
+
+\subsection{Pkgcheck messages}
+
+\pkgcheck issues three kind of messages
+
+\begin{itemize}
+\item Information messages
+\item Warning messages
+\item Error messages
+\end{itemize}
+
+Messages have unique ids and the detailed explanation of a message can be either
+looked up in this document, or it can be displayed by using command line option
+\verb|--explain| or \verb|-e|.
+
+
+\begin{minted}[linenos=true,label=Example,frame=single,fontsize=\footnotesize]{sh}
+ pkgcheck --explain e0012
+\end{minted}
+
+
+
+\subsection{Duplicate files}
+
+By default, \pkgcheck detects duplicate files in a package. This could be
+disabled by using command line switch \verb|--ignore-dups| or shorter \verb|-I|.
+
+\subsection{Permissions}
+
+\pkgcheck offers the option \verb|--correct-crlf| or shorter \verb|-L| to
+correct wrong permissions in a package.
+
+
+\subsection{CRLF line endings}
+
+\pkgcheck detects CRLF line endings in text files as good as it can.
+It reads up to 1 MB to check for CRLF line endings.
+
+Option \verb|--correct-crlf| or for short \verb|-L| can be used to convert a
+file from CRLF to LF line endings.
+
+\subsection{Help options}
+
+\begin{itemize}
+\item \verb|-V|
+
+ Outputs \pkgcheck's version number.
+
+\item \verb|--help|
+
+ \verb|--help| shows the available command line options.
+
+\begin{verbatim}
+pkgcheck 1.0.0
+Manfred Lotz <manfred@ctan.org>
+A checker for uploaded packages to CTAN.
+
+USAGE:
+ pkgcheck [FLAGS] [OPTIONS]
+
+FLAGS:
+ -L, --correct-crlf Correct CRLF line endings
+ -C, --correct-perms Correct permissions
+ --explain-all Explains all error or warning messages
+ -h, --help Prints help information
+ -I, --ignore-dupes Ignore dupes
+ --no-colors Don't display messages in color
+ --show-temp-endings Show file endings for temporary files
+ --urlcheck Check URLs found in README files
+ -V, --version Prints version information
+ -v, --verbose Verbose operation?
+
+OPTIONS:
+ -e, --explain <explain> Explain error or warning message
+ -d, --package-dir <pkg_dir> Package directory
+ -T, --tds-zip <tds_zip> tds zip archive
+
+\end{verbatim}
+
+\item \verb|--explain <explain|
+
+ This option explains an error message in more detail.
+
+ Example:
+
+\begin{minted}[linenos=true,frame=single,fontsize=\footnotesize]{sh}
+ pkgcheck -e e0012
+\end{minted}
+
+
+\item \verb|--explain-all|
+
+Outputs a list of explanations of all messages.
+
+\item \verb|--show-temp-endings|
+
+ Outputs a list of all file name endings which \pkgcheck uses to detect
+ temporary files.
+\end{itemize}
+
+%\subsection{Other options}
+
+\section{About checking file types}
+
+\pkgcheck determines, similar to the UNIX \verb|file| command, the type of file.
+This is required before for example checking permissions or complaining that a
+text file has CRLF line endings.
+
+It is very important to note that determining file types is not bullet proof.
+So, it might happen in some cases that \pkgcheck makes mistakes when determining
+a file type. This could lead to a subseqent mistake when complaining about an
+x-bit, or complaining about CRF line ending.
+
+
+
+
+
+
+\section{About permissions checking}
+
+From an installation point of view the files and directories of a package
+\begin{itemize}
+\item must be at least world readable
+\item must be writable by owner or group
+\item must not have the x-bit on for the owner if the file isn't an executable,
+ i.e. a script or binary
+\end{itemize}
+
+The reason for this minimal requirement is that the installation utility used by
+the CTAN team (which by the way was written by Rainer Schöpf a long time ago)
+sets permissions correctly if the owner permission is set correctly.
+
+Examples:
+
+\begin{itemize}
+\item \verb|README.md| with \texttt{666} is ok because the installation utility converts
+ the permission to 664
+\item \verb|README.md| with \texttt{660} is wrong because the installation utility
+ wouldn't have access to the file
+\item \verb|some.pdf| with \texttt{744} would be wrong because a PDF document must not
+ have the x-bit on for the owner
+\end{itemize}
+
+Because of the smartness of the installation utility \pkgcheck does check
+minimal requirements only, i.e some weird looking permissions like the \texttt{666} above
+are accepted.
+
+\section{Different kind of messages}
+
+\begin{description}
+\item[Innnn] Informational messages
+
+ These are message which announce \pkgcheck actions.
+
+\item[Fnnnn] Fatal messages
+
+ Fatal messages report unrecoverable errors. In this case \pkgcheck's only
+ option is to terminate. If for example, the package directory specified at
+ the command line doesn't exist then the only option is to terminate.
+
+\item[Ennnn] Error messages
+
+ Error messages report errors which must be fixed before installing a package.
+
+\item[Wnnnn] Warning messages
+
+ Warning messages denote possible errors depending upon the situation.
+
+ For example, for a font package having many duplicate files might be ok. For
+ another package it could be regarded as an error.
+
+
+
+
+\end{description}
+\section{Informational messages}
+\input{informationd}
+
+\section{Warning messages}
+\input{warningsd}
+
+\section{Error messages}
+\input{errorsd}
+
+\section{Fatal messages}
+\input{fatald}
+
+\end{document}
+
+
diff --git a/support/pkgcheck/docs/title.tex b/support/pkgcheck/docs/title.tex
new file mode 100644
index 0000000000..f13bf6193c
--- /dev/null
+++ b/support/pkgcheck/docs/title.tex
@@ -0,0 +1 @@
+\title{pkgcheck Utility, v1.8.2}
diff --git a/support/pkgcheck/docs/warningsd.tex b/support/pkgcheck/docs/warningsd.tex
new file mode 100644
index 0000000000..7b8c676e23
--- /dev/null
+++ b/support/pkgcheck/docs/warningsd.tex
@@ -0,0 +1,39 @@
+\hypertarget{w0001----archive-as-package-file-detected}{%
+\subsection{W0001 -\/- Archive as package file
+detected}\label{w0001----archive-as-package-file-detected}}
+
+Usually a CTAN package should not contain archives. An exception are
+situations where, for example, the source code of a package is kept in a
+separate zip archive.
+
+\hypertarget{w0002----duplicate-files-detected}{%
+\subsection{W0002 -\/- Duplicate files
+detected}\label{w0002----duplicate-files-detected}}
+
+Duplicate files were detected which are listed right after this message.
+
+The message is a warning message as something like this could not be
+seen as an error in general.
+
+\hypertarget{w0003----same-named-files-detected-in-the-package-tree}{%
+\subsection{W0003 -\/- Same named files detected in the package
+tree}\label{w0003----same-named-files-detected-in-the-package-tree}}
+
+We like to have unique file names over the whole package directory tree.
+When we discover same named files we report it as a warning. Common
+names like \texttt{README}, \texttt{README.txt}, \texttt{README.md},
+\texttt{Makefile}, \texttt{Makefile.in}, \texttt{Makefile.am} and
+\texttt{makefile} are ignored when checking.
+
+For more details refer to:
+\href{http://mirror.utexas.edu/ctan/help/ctan/CTAN-upload-addendum.html\#uniquefilenames}{http://mirror.utexas.edu/ctan/help/ctan/CTAN-upload-addendum.html\#uniquefilenames}
+
+\hypertarget{w0004-----encoding-with-bom-detected}{%
+\subsection{\texorpdfstring{W0004 -\/- encoding with BOM
+detected}{W0004 -\/- encoding with BOM detected}}\label{w0004-----encoding-with-bom-detected}}
+
+A UTF encoded package file contains a BOM (byte order mark). Currently,
+we issues a warning.
+
+Nevertheless, the CTAN team discourages uses of BOM. Please be aware,
+that in some future time this could be reagarded as an error.
diff --git a/support/pkgcheck/quick_intro.txt b/support/pkgcheck/quick_intro.txt
new file mode 100644
index 0000000000..ae2d1a4dea
--- /dev/null
+++ b/support/pkgcheck/quick_intro.txt
@@ -0,0 +1,24 @@
+
+You want to check your package before uploading to CTAN?
+
+
+1. Build the zip archive
+
+2. Make sure your umask is set to 022
+
+3. Assuming your package's name is mypkg and your zip archive is mypkg.zip
+
+ Unpack mypkg.zip (which should contain a top level directory mypkg/),
+ so that you obtain again a directory mypkg/ containing the files of your package.
+
+ unzip mypkg.zip
+
+ Now run `pkgcheck`
+
+ `pkgcheck -d mypkg`
+
+
+If you want to check URLs in README file run
+
+ `pkgcheck --urlcheck -d mypkg`
+
diff --git a/support/pkgcheck/src/filemagic.rs b/support/pkgcheck/src/filemagic.rs
new file mode 100644
index 0000000000..40edd18351
--- /dev/null
+++ b/support/pkgcheck/src/filemagic.rs
@@ -0,0 +1,422 @@
+#![allow(clippy::trivial_regex)]
+
+use regex::bytes::Regex;
+use regex::bytes::RegexSet;
+
+use std::fs;
+use std::fs::File;
+use std::io;
+use std::io::prelude::*;
+use std::path::Path;
+
+use std::os::unix::fs::FileTypeExt;
+
+use unicode_bom::Bom;
+
+//
+// File signatures links
+// - https://asecuritysite.com/forensics/magic
+// - https://filesignatures.net/
+// - https://github.com/7h3rAm/cigma/blob/master/cigma/magicbytes.json
+
+#[derive(Debug, PartialEq)]
+pub enum Mimetype {
+ Binary,
+ Script,
+ ScriptCRLF,
+ Pdf,
+ Archive,
+ Zip,
+ Text,
+ TextCRLF,
+ Data,
+ Unknown,
+ BlockDevice,
+ CharDevice,
+ Directory,
+ Symlink,
+ Fifo,
+ Socket,
+ Zerofile,
+ VeryShort,
+ BOM(Bom),
+}
+
+pub struct Filetype {
+ buffer: Vec<u8>,
+}
+
+fn is_binary_data(vec: &[u8], len: usize) -> bool {
+ for v in vec.iter().take(len) {
+ if *v <= 8 {
+ return true;
+ }
+ }
+
+ false
+}
+
+fn _is_crlf(buffer: &[u8], len: usize) -> bool {
+ let mut cr = 0;
+ let mut lf = 0;
+
+ const CR: u8 = 0x0d; // 13
+ const LF: u8 = 0x0a; // 10
+
+ for c in buffer.iter().take(len) {
+ if *c == LF {
+ lf += 1;
+ } else if *c == CR {
+ cr += 1;
+ }
+ }
+
+ let diff: i32 = cr - lf;
+ if cr > 0 && diff == 0 {
+ return true;
+ }
+
+ //println!("cr: {}, lf: {}", cr, lf);
+ // Heuristics: we accept if only a few lines are not CRLF
+ match (cr, lf) {
+ (0, _lf) => return false,
+ (_cr, 0) => return true,
+ (cr, _lf) => {
+ if cr > 500 && diff.abs() < 3 {
+ return true;
+ }
+ }
+ }
+
+ false
+}
+
+fn is_crlf(buffer: &[u8], len: usize) -> bool {
+ let mut seen_cr = false;
+ let mut n_crlf = 0;
+ let mut n_lf = 0;
+ let mut n_cr = 0;
+
+ const CR: u8 = 0x0d; // CR 0x0D 13 \r
+ const LF: u8 = 0x0a; // LF 0x0A 10 \n
+
+ for c in buffer.iter().take(len) {
+ if *c == LF {
+ if seen_cr {
+ n_crlf += 1;
+ } else {
+ n_lf += 1;
+ }
+ } else if seen_cr {
+ n_cr += 1;
+ }
+
+ seen_cr = *c == CR;
+ }
+
+ // println!("cr: {}, lf: {}, crlf: {}", n_cr, n_lf, n_crlf);
+ // if (n_crlf == 0 && n_cr == 0 && n_nel == 0 && n_lf == 0)
+ // --> no line terminators
+ if n_crlf == 0 && n_cr == 0 && n_lf == 0 {
+ return false;
+ }
+
+ if n_crlf > 0 || n_cr > 0 {
+ return true;
+ }
+
+ false
+}
+
+impl Filetype {
+ pub fn new() -> Self {
+ Filetype {
+ buffer: vec![0; 1024 * 1024],
+ }
+ }
+
+ pub fn analyze(&mut self, fname: &str) -> Result<Mimetype, io::Error> {
+ // Result<Err,Mimetype> {
+ let path = Path::new(fname);
+
+ if let Some(ft) = get_filetype(path) {
+ return Ok(ft);
+ }
+
+ let metadata = fs::symlink_metadata(fname)?;
+ let file_length: usize = metadata.len() as usize;
+
+ if file_length == 0 {
+ return Ok(Mimetype::Zerofile);
+ }
+
+ if metadata.len() == 1 {
+ return Ok(Mimetype::VeryShort);
+ }
+
+ let mut hdl_in = File::open(path)?;
+
+ let mut bytes_read: usize = hdl_in.read(&mut self.buffer[0..262])?;
+
+ // PostScript signatures
+ // - %!PS-Adobe-1.0, %!PS-Adobe-2.0, %!PS-Adobe-3.0, %!PS-Adobe-3.1
+ // - %! and a line feed
+ if bytes_read >= 4 && &self.buffer[0..4] == b"%!PS" {
+ return Ok(Mimetype::Data);
+ }
+
+ // - %!\r\n%%BoundingBox:
+ let re: Regex = Regex::new(r"^(?-u)%!(\x0d\x0a|\x0A)%%BoundingBox").unwrap();
+ if bytes_read >= 20 && re.is_match(&self.buffer) {
+ return Ok(Mimetype::Data);
+ }
+
+ if bytes_read >= 4 && &self.buffer[0..4] == b"%PDF" {
+ return Ok(Mimetype::Pdf);
+ }
+
+ if bytes_read >= 5 && &self.buffer[0..5] == b"<?php" {
+ return Ok(Mimetype::Script);
+ }
+
+ // rtf document
+ if bytes_read >= 6 && &self.buffer[0..6] == b"\x7B\x5C\x72\x74\x66\x31" {
+ return Ok(Mimetype::Data);
+ }
+
+ // ZOO archive http://fileformats.archiveteam.org/wiki/ZOO
+ if bytes_read >= 60 && &self.buffer[20..24] == b"\xDC\xA7\xC4\xFD" {
+ return Ok(Mimetype::Archive);
+ }
+
+ let bom: Bom = Bom::from(&self.buffer[0..]);
+
+ if bom != Bom::Null {
+ return Ok(Mimetype::BOM(bom));
+ }
+
+ if is_binary_data(&self.buffer, bytes_read) {
+ match analyze_binary(&self.buffer) {
+ Some(Mimetype::Zip) => {
+ if fname.ends_with(".cdy") {
+ return Ok(Mimetype::Data);
+ } else {
+ return Ok(Mimetype::Zip);
+ }
+ }
+ Some(mt) => return Ok(mt),
+ None => return Ok(Mimetype::Unknown),
+ }
+ }
+
+ // https://en.wikipedia.org/wiki/BinHex
+ if bytes_read >= 200
+ && self
+ .buffer
+ .starts_with(b"(This file must be converted with BinHex 4.0)")
+ {
+ return Ok(Mimetype::Binary);
+ }
+
+ if bytes_read < file_length {
+ if let Ok(rb) = hdl_in.read(&mut self.buffer[262..]) {
+ bytes_read += rb
+ }
+ }
+
+ let crlf = is_crlf(&self.buffer, bytes_read);
+
+ if bytes_read >= 5 && (self.buffer.starts_with(b"#!") || self.buffer.starts_with(b"<?php"))
+ {
+ if crlf {
+ return Ok(Mimetype::ScriptCRLF);
+ } else {
+ return Ok(Mimetype::Script);
+ }
+ }
+
+ let is_script = Regex::new(r"\.(csh|ksh|py|pl|sh|tcsh)$")
+ .unwrap()
+ .is_match(fname.as_bytes());
+
+ match (crlf, is_script) {
+ (false, false) => Ok(Mimetype::Text),
+ (true, false) => Ok(Mimetype::TextCRLF),
+ (false, true) => Ok(Mimetype::Script),
+ (true, true) => Ok(Mimetype::ScriptCRLF),
+ }
+ }
+}
+
+// https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
+fn is_binary(vec: &[u8]) -> Option<Mimetype> {
+ let binary_re: RegexSet = RegexSet::new(&[
+ r"^(?-u)\x7FELF[\x01\x02][\x01\x02]\x01[\x00-\x11]", // Executable and Linkable Format (ELF)
+ r"^(?-u)\x00\x00\x03\xF3", // AmigaOS loadseg()ble executable/binary
+ r"^(?-u)MZ", // DOS MZ executable file format and its descendants (including NE and PE)
+ r"^(?-u)\x64 \x65\x78\x0A\x30\x33\x35\x00", // Dalvik's executable
+ r"^(?-u)#[!]", // script executable
+ r"^(?-u)(\xCE|\xCF)\xFA\xED\xFE", // Mach-O binary
+ r"^(?-u)\x1B\x4C\x75\x61", // Lua bytecode
+ ])
+ .unwrap();
+
+ if binary_re.is_match(vec) {
+ return Some(Mimetype::Binary);
+ }
+ None
+}
+
+// https://github.com/7h3rAm/cigma/blob/master/cigma/magicbytes.json
+// https://en.wikipedia.org/wiki/List_of_file_signatures
+fn is_archive(vec: &[u8]) -> Option<Mimetype> {
+ // we first have to catch zip files with mimetype formats
+ // - opendocument formats
+ // - Word Open XML
+ // Those we do not regard as archives
+ let special_zip: RegexSet = RegexSet::new(&[
+ r"^(?-u)PK\x03\x04.{20,}\x08\x00\x00\x00mimetypeapplication",
+ r"^(?-u)PK\x03\x04\x14\x00\x06\x00", // Word Open XML (.docx)
+ r"^(?-u)PK\x03\x04\x14\x00\x08\x00", // Java Jar file
+ r"^(?-u)PK\x03\x04\x14\x00\x08\x08", // Java Jar file
+ r"^(?-u)PK\x03\x04\x0A.*?META-INF", // Java Jar file
+ r"^(?-u)PK\x03\x04.*?META-INF", // Java Jar file
+ r"^(?-u)PK\x03\x04\x0A.*?\x56\x92\x48\x4F\xEF", // Java Jar file
+ ])
+ .unwrap();
+
+ if special_zip.is_match(vec) {
+ return Some(Mimetype::Data);
+ }
+
+ let archive_re: RegexSet = RegexSet::new(&[
+ r"^(?-u)\x37\x7A\xBC\xAF\x27\x1C", // 7zip
+ r"^(?-u)\x1f\x8B", // gzip (.gz)
+ r"^(?-u)\x1f\x9D", // LZW (.tar.Z)
+ r"^(?-u)\x1f\xA0", // LZH (.tar.Z)
+ r"^(?-u)\xFD\x37\x7A\x58\x5A\x00\x00", // XZ comp. utility using LZMA2 compression (.xz)
+ r"^(?-u)\x4D\x53\x43\x46", // Microsoft cabinet (.cab)
+ r"^(?-u)\x42\x5A\x68", // bzip2
+ r"^(?-u)\x5A\x57\x53", // lzma
+ r"^(?-u)\x5D\x00\x00(\x01|\x02|\x04|\x08|\x10|\x20|\x40|\x80)\x00", // lzma
+ r"^(?-u)\x5D\x00\x00\x00\x01", // lzma
+ r"^(?-u)(SIT!|SITD|STi0|StuffIt)", // SIT / stuffit (macintosh related)
+ r"^(?-u)\x4D\x5A", // DOS MZ executable format, but found in zip archives
+ r"^(?-u)\x52\x61\x72\x21\x1A\x07\x00", // RAR archive version 1.50 onwards
+ r"^(?-u)\x52\x61\x72\x21\x1A\x07\x01\x00", // RAR archive version 5.0 onwards
+ // https://en.wikipedia.org/wiki/LHA_(file_format)
+ r"^(?-u)..-lh[0124567d]", // LHarc (canonical LZH)
+ r"^(?-u)..-lh[89abce]", // LHarc (Joe Jared extensions)
+ r"^(?-u)..-lhx", // LHarc (UNLHA32 extensions)
+ r"^(?-u)..-(pc1|pm0|pm1|pm2|pms)", // LHarc (PMarc extensions)
+ r"^(?-u)..-lz[s234578]", // LHarc (LArc extensions)
+ r"^(?-u)\x53\x5a\x44\x44\x88\xf0\x27\x33", // RAR archive version 5.0 onwards
+ ])
+ .unwrap();
+
+ if archive_re.is_match(vec) {
+ return Some(Mimetype::Archive);
+ }
+
+ let archive_re: RegexSet = RegexSet::new(&[
+ r"^(?-u)PK(\x03\x04|\x4c\x49\x54\x45|\x30\x30\x50|\x05\x06|\x07\x08)", // zip archive
+ ])
+ .unwrap();
+ if archive_re.is_match(vec) {
+ return Some(Mimetype::Zip);
+ }
+
+ None
+}
+
+fn analyze_binary(vec: &[u8]) -> Option<Mimetype> {
+ let rc = is_binary(vec);
+ if rc.is_some() {
+ return rc;
+ }
+
+ let rc = is_archive(vec);
+ if rc.is_some() {
+ return rc;
+ }
+
+ Some(Mimetype::Data)
+}
+
+fn get_filetype(entry: &Path) -> Option<Mimetype> {
+ match entry.symlink_metadata() {
+ Ok(mt) => {
+ let ft = mt.file_type();
+ if ft.is_symlink() {
+ return Some(Mimetype::Symlink);
+ }
+ if ft.is_dir() {
+ return Some(Mimetype::Directory);
+ }
+ if ft.is_block_device() {
+ return Some(Mimetype::BlockDevice);
+ }
+ if ft.is_char_device() {
+ return Some(Mimetype::CharDevice);
+ }
+ if ft.is_fifo() {
+ return Some(Mimetype::Fifo);
+ }
+ if ft.is_socket() {
+ return Some(Mimetype::Socket);
+ }
+ None
+ }
+ Err(_e) => None,
+ }
+}
+
+#[test]
+fn test_filetype() {
+ let mut ft = Filetype::new();
+
+ assert!(ft.analyze("tests_filemagic/zerofile").ok() == Some(Mimetype::Zerofile));
+ assert!(ft.analyze("tests_filemagic/a_small_file").ok() == Some(Mimetype::VeryShort));
+ assert!(ft.analyze("/dev/null").ok() == Some(Mimetype::CharDevice));
+ assert!(ft.analyze("tests_filemagic/").ok() == Some(Mimetype::Directory));
+ assert!(ft.analyze("tests_filemagic/zerofile_symlink").ok() == Some(Mimetype::Symlink));
+
+ assert!(ft.analyze("tests_filemagic/some.pdf").ok() == Some(Mimetype::Pdf));
+
+ // This file is a pdf but has lines starting with % before the pdf signature shows up
+ // The unix `file` command) says: data
+ // analyze() says TextCRLF
+ //assert!(ft.analyze("tests_filemagic/musterlogo.pdf").ok() == Some(Mimetype::Script));
+
+ assert!(ft.analyze("tests_filemagic/x.pl").ok() == Some(Mimetype::Script));
+ assert!(ft.analyze("tests_filemagic/main.php").ok() == Some(Mimetype::Script));
+
+ assert!(ft.analyze("tests_filemagic/test.7z").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/x.tgz").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/test.pdf.xz").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/swebib.cab").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/test.tar.bz2").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/PIE.rar").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/infozip-os390.tar.Z").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/bla.lha").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/dvi.zoo").ok() == Some(Mimetype::Archive));
+ assert!(ft.analyze("tests_filemagic/rsfs-oztex.sit").ok() == Some(Mimetype::Archive));
+
+ assert!(ft.analyze("tests_filemagic/empty.zip").ok() == Some(Mimetype::Zip));
+
+ assert!(ft.analyze("tests_filemagic/README").ok() == Some(Mimetype::Text));
+ // assert!(ft.analyze("tests_filemagic/README1").ok() == Some(Mimetype::Text));
+
+ assert!(ft.analyze("tests_filemagic/cp").ok() == Some(Mimetype::Binary));
+ assert!(ft.analyze("tests_filemagic/cheq-f.sit-hqx").ok() == Some(Mimetype::Binary));
+ assert!(ft.analyze("tests_filemagic/MuchMore").ok() == Some(Mimetype::Binary));
+
+ assert!(ft.analyze("tests_filemagic/support.ps").ok() == Some(Mimetype::Data));
+ assert!(ft.analyze("tests_filemagic/rosette.eps").ok() == Some(Mimetype::Data));
+ assert!(ft.analyze("tests_filemagic/eutest.ps").ok() == Some(Mimetype::Data));
+ // assert!(ft.analyze("tests_filemagic/NORMAL.PS").ok() == Some(Mimetype::Data));
+ assert!(ft.analyze("tests_filemagic/chap5.rtf").ok() == Some(Mimetype::Data));
+ assert!(ft.analyze("tests_filemagic/commons-math.jar").ok() == Some(Mimetype::Data));
+
+ assert!(ft.analyze("tests_filemagic/8stbu11h.htm").ok() == Some(Mimetype::TextCRLF));
+}
diff --git a/support/pkgcheck/src/generate.pest b/support/pkgcheck/src/generate.pest
new file mode 100644
index 0000000000..749cb60e5e
--- /dev/null
+++ b/support/pkgcheck/src/generate.pest
@@ -0,0 +1,34 @@
+file = { SOI ~ ( generatefile_stmt | generate_stmt | filecontents | other)* ~ EOI }
+
+other = _{ (!NEWLINE ~ ANY) }
+
+generatefile_stmt = _{ "\\generateFile" ~ LPAREN ~ filename ~ RPAREN }
+
+generate_stmt = _{ "\\generate" ~ LPAREN ~ (file_stmt | cmd )+ ~ RPAREN }
+file_stmt = _{ "\\file" ~ LPAREN ~ filename ~ RPAREN ~
+ LPAREN ~ (from | cmd) + ~RPAREN }
+
+filecontents = _{ "\\begin" ~ LPAREN ~ filecontents_lit ~ RPAREN ~
+ LPAREN ~ filename ~ RPAREN }
+
+filecontents_lit = _{ "filecontents*" | "filecontents" }
+
+filename = { (!RPAREN ~ ANY)* }
+
+from = _{ "\\from" ~ LPAREN ~ anyx ~ RPAREN ~
+ LPAREN ~ anyx ~ RPAREN }
+
+cmd = _{ "\\" ~ ident ~ (LPAREN ~ anyx ~ RPAREN)* }
+
+ident_char = _{ 'a'..'z' | 'A'..'Z' | '0'..'9' | "_" }
+ident = _{
+ ('a'..'z' | 'A'..'Z') ~ ident_char* |
+ "_" ~ ident_char+ }
+
+
+LPAREN = _{ "{" }
+RPAREN = _{ "}" }
+
+anyx = _{ (!RPAREN ~ ANY)* }
+WHITESPACE = _{ " " | "\t" | NEWLINE }
+COMMENT = _{ "%" ~ (!NEWLINE ~ ANY)* } \ No newline at end of file
diff --git a/support/pkgcheck/src/generate.pest.md b/support/pkgcheck/src/generate.pest.md
new file mode 100644
index 0000000000..643fe57750
--- /dev/null
+++ b/support/pkgcheck/src/generate.pest.md
@@ -0,0 +1,15 @@
+# Some notes about gparser.pest
+
+## COMMENT and WHITESPACE
+
+The Pest book at https://pest.rs/book/grammars/syntax.html says:
+
+> Many languages and text formats allow arbitrary whitespace and comments between logical tokens.
+> For instance, Rust considers 4+5 equivalent to 4 + 5 and 4 /* comment */ + 5.
+
+> The optional rules WHITESPACE and COMMENT implement this behaviour. If either (or both) are defined,
+> they will be implicitly inserted at every sequence and between every repetition (except in atomic rules).
+
+
+Therefore COMMENT and WHITESPACE are defined in `gparser.pest`.
+
diff --git a/support/pkgcheck/src/gparser.rs b/support/pkgcheck/src/gparser.rs
new file mode 100644
index 0000000000..6f9d7f393a
--- /dev/null
+++ b/support/pkgcheck/src/gparser.rs
@@ -0,0 +1,46 @@
+use pest::Parser;
+
+#[cfg(debug_assertions)]
+const _GRAMMAR: &'static str = include_str!("generate.pest");
+
+#[derive(Parser)]
+#[grammar = "generate.pest"]
+pub struct GenerateParser;
+
+pub fn parse_generate(s: &str) -> Option<Vec<String>> {
+ let pairs = GenerateParser::parse(Rule::file, s);
+
+ if pairs.is_err() {
+ return None;
+ }
+
+ let mut found = Vec::new();
+
+ for pair in pairs.unwrap() {
+ // A pair is a combination of the rule which matched and a span of input
+
+ // A pair can be converted to an iterator of the tokens which make it up:
+ for inner_pair in pair.into_inner() {
+ let inner_span = inner_pair.clone().as_span();
+ match inner_pair.as_rule() {
+ Rule::filename => {
+ //println!("fname: {}", inner_span.as_str());
+ // https://rust-lang-nursery.github.io/rust-clippy/v0.0.212/index.html#clone_double_ref
+ let s: String = inner_span.as_str().to_string();
+ //println!("filename: {}", s);
+ found.push(s);
+ }
+ Rule::EOI => (),
+ e => {
+ println!("unreachable: {:?}", e);
+ unreachable!()
+ }
+ };
+ }
+ }
+ if found.is_empty() {
+ None
+ } else {
+ Some(found)
+ }
+}
diff --git a/support/pkgcheck/src/linkcheck.rs b/support/pkgcheck/src/linkcheck.rs
new file mode 100644
index 0000000000..60b993e960
--- /dev/null
+++ b/support/pkgcheck/src/linkcheck.rs
@@ -0,0 +1,259 @@
+use threadpool::ThreadPool;
+
+use colored::*;
+use linkify::{LinkFinder, LinkKind};
+
+use std::fs::File;
+use std::io::prelude::*;
+
+//use reqwest::Client;
+use openssl_probe;
+use reqwest;
+use reqwest::header;
+use std::time::Duration;
+//use std::thread;
+use std::sync::Arc;
+use std::sync::Mutex;
+
+use std::sync::atomic::Ordering;
+
+use std::collections::HashMap;
+use std::collections::HashSet;
+
+enum UrlStatus {
+ Unknown,
+ UrlOk,
+ UrlError(String),
+}
+
+struct HashVal {
+ paths: HashSet<String>,
+ status: UrlStatus,
+}
+
+type UrlHash = HashMap<String, HashVal>;
+
+pub struct LinkCheck {
+ pool: Mutex<ThreadPool>,
+ urlhash: Arc<Mutex<UrlHash>>,
+ print_all: bool,
+}
+
+impl LinkCheck {
+ pub fn new(num_threads: usize, print_all: bool) -> LinkCheck {
+ openssl_probe::init_ssl_cert_env_vars();
+ let pool = Mutex::new(ThreadPool::new(num_threads));
+ LinkCheck {
+ pool,
+ urlhash: Arc::new(Mutex::new(HashMap::default())),
+ print_all,
+ }
+ }
+
+ pub fn check_urls(&self, fname: &str) {
+ let print_all = self.print_all;
+ if let Some(links) = get_links(fname) {
+ for l in links {
+ let urlhash = self.urlhash.clone();
+ let fname_s = String::from(fname);
+ self.pool.lock().unwrap().execute(move || {
+ check_link(&l, &fname_s, &urlhash, print_all);
+ });
+ }
+ }
+ }
+}
+
+impl Drop for LinkCheck {
+ fn drop(&mut self) {
+ //println!("Now dropping ...");
+ let pool = self.pool.lock().unwrap();
+ pool.join();
+ }
+}
+
+fn check_link(url: &str, fname: &str, urlhash: &Arc<Mutex<UrlHash>>, print_all: bool) {
+ let url = String::from(url);
+
+ let mut run_check_link = false;
+
+ // It is very important to keep the lock for the urlhash
+ // only for a short period of time
+ //
+ // If we don't find the url in the urlhash then
+ // we set `run_check_link` to `true` so that we will
+ // check the url
+ {
+ let f = String::from(fname);
+
+ let mut urlhash = urlhash.lock().unwrap();
+ if !urlhash.contains_key(&url) {
+ let mut hs = HashSet::default();
+ hs.insert(f.clone());
+ let url1 = url.clone();
+
+ urlhash.insert(
+ url1,
+ HashVal {
+ status: UrlStatus::Unknown,
+ paths: hs,
+ },
+ );
+ run_check_link = true;
+ } else if let Some(hs) = urlhash.get_mut(&url) {
+ match &hs.status {
+ UrlStatus::Unknown => {
+ hs.paths.insert(f);
+ }
+ UrlStatus::UrlOk => {
+ if print_all {
+ print_ok(no_colors!(), &url, &f);
+ };
+ }
+ UrlStatus::UrlError(e) => {
+ e0022!(f, e);
+ }
+ }
+ }
+ }
+
+ //
+ if run_check_link {
+ match check_link_inner(&url, true) {
+ UrlStatus::UrlOk => {
+ let mut urlhash = urlhash.lock().unwrap();
+ if let Some(mut hs) = urlhash.get_mut(&url) {
+ if print_all {
+ for p in hs.paths.iter() {
+ print_ok(no_colors!(), &url, p);
+ }
+ }
+ hs.status = UrlStatus::UrlOk;
+ }
+ }
+ UrlStatus::UrlError(e) => {
+ let mut urlhash = urlhash.lock().unwrap();
+ if let Some(mut hs) = urlhash.get_mut(&url) {
+ for p in hs.paths.iter() {
+ e0022!(p, e);
+ }
+ hs.status = UrlStatus::UrlError(e);
+ }
+ }
+ _ => (),
+ }
+ }
+}
+
+fn get_links(fname: &str) -> Option<Vec<String>> {
+ let fhdl = File::open(fname);
+ match fhdl {
+ Ok(mut f) => {
+ let mut buf = Vec::new();
+
+ match f.read_to_end(&mut buf) {
+ Ok(_bytes_read) => {
+ return get_links_inner(&String::from_utf8_lossy(&buf));
+ }
+ Err(e) => println!("Error reading file {}: {:?}", fname, e),
+ }
+ }
+ Err(e) => println!("Error opening file {}: {}", fname, e),
+ }
+
+ None
+}
+
+// retrieves links in a string and then checks those links
+fn get_links_inner(s: &str) -> Option<Vec<String>> {
+ let mut finder = LinkFinder::new();
+ finder.kinds(&[LinkKind::Url]);
+ let links: Vec<_> = finder.links(s).collect();
+ let result: Vec<&str> = links.iter().map(|e| e.as_str()).collect();
+
+ let mut links = vec![];
+ for r in result {
+ if !r.starts_with("http://") && !r.starts_with("https://") && !r.starts_with("ftp://") {
+ continue;
+ }
+ // This is a workaround to prevent URLs ending with `
+ if r.ends_with('`') {
+ links.push(String::from(&r[..r.len()-1]));
+ } else {
+ links.push(String::from(r));
+ }
+
+ }
+ if !links.is_empty() {
+ Some(links)
+ } else {
+ None
+ }
+}
+
+fn check_link_inner(l: &str, head: bool) -> UrlStatus {
+ let mut headers = header::HeaderMap::new();
+ headers.insert(
+ header::USER_AGENT,
+ header::HeaderValue::from_static(
+ "Mozilla/5.0 (X11; Linux i686; rv:64.0) Gecko/20100101 Firefox/64.0",
+ ),
+ );
+
+ let default_policy = reqwest::RedirectPolicy::default();
+ let policy = reqwest::RedirectPolicy::custom(move |attempt| {
+ if attempt.url().host_str() == Some("127.0.0.1") {
+ attempt.stop()
+ } else {
+ default_policy.redirect(attempt)
+ }
+ });
+
+ let cb = reqwest::Client::builder()
+ .gzip(true)
+ .redirect(policy)
+ .default_headers(headers)
+ .timeout(Duration::from_secs(7))
+ .build()
+ .unwrap();
+ // let url: Url =
+ // match l.parse() {
+ // Ok(url) => url,
+ // Err(e) => { println!("Error: {:?}", e); panic!("Scheiss"); }
+ // };
+ let resp = if head {
+ cb.head(l).send()
+ } else {
+ cb.get(l).send()
+ };
+ match resp {
+ Ok(s) => {
+ if s.status().is_informational()
+ || s.status().is_success()
+ || s.status().is_redirection()
+ {
+ return UrlStatus::UrlOk;
+ }
+
+ if head {
+ check_link_inner(l, false)
+ } else {
+ let e = format!("{}: {}", l, s.status());
+ UrlStatus::UrlError(e)
+ }
+ }
+ Err(e) => {
+ let e = format!("{}", e);
+ UrlStatus::UrlError(e)
+ }
+ }
+}
+
+fn print_ok(no_colors: bool, url: &str, f: &str) {
+ if no_colors {
+ println!("✔ {} in {}", &url, f);
+ } else {
+ // println!("✔ {} in {}", &url, f);
+ println!("{} {} in {}", "✔".bright_green().bold(), url, f);
+ }
+}
diff --git a/support/pkgcheck/src/main.rs b/support/pkgcheck/src/main.rs
new file mode 100644
index 0000000000..a7be277330
--- /dev/null
+++ b/support/pkgcheck/src/main.rs
@@ -0,0 +1,1287 @@
+extern crate pest;
+#[macro_use]
+extern crate pest_derive;
+
+mod gparser;
+
+#[macro_use]
+mod messages;
+use messages::{explains, explains_all};
+
+#[macro_use]
+mod utils;
+mod linkcheck;
+
+mod recode;
+use recode::crlf2lf;
+
+mod filemagic;
+
+use linkcheck::LinkCheck;
+use std::fmt;
+use std::fmt::Display;
+use std::str;
+use utils::*;
+
+use std::os::unix::fs::MetadataExt;
+
+use scoped_threadpool::Pool;
+
+
+use tempdir::TempDir;
+#[macro_use]
+extern crate lazy_static;
+use colored::*;
+
+use regex::Regex;
+
+use std::fs::File;
+use std::fs::Metadata;
+use std::io::prelude::*;
+use std::io::BufReader;
+use std::os::unix::fs::FileTypeExt;
+use std::{fs, process};
+// use std::os::unix::fs::PermissionsExt;
+
+use std::ffi::OsStr;
+use std::io::{self, Read};
+use std::path::Path;
+use std::path::PathBuf;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use blake2::{Blake2b, Digest};
+use fnv::{FnvHashMap as HashMap, FnvHashSet as HashSet};
+use std::sync::mpsc::{channel, Sender};
+use structopt::StructOpt;
+#[cfg(unix)]
+use walkdir::{DirEntry, WalkDir};
+
+fn err(path: &PathBuf, err: &io::Error) {
+ e0027!(path.display(), err);
+}
+
+type HashSender = Sender<(u64, PathBuf, Vec<u8>)>;
+//type DupeSender = Sender<(u64, Vec<PathBuf>)>;
+
+const BLOCKSIZE: usize = 4096;
+
+fn hash_file_inner(path: &PathBuf) -> io::Result<Vec<u8>> {
+ let mut buf = [0u8; BLOCKSIZE];
+ let mut fp = File::open(&path)?;
+ let mut digest = Blake2b::default();
+ // When we compare byte-by-byte, we don't need to hash the whole file.
+ // Instead, hash a block of 4kB, skipping 100kB.
+ loop {
+ match fp.read(&mut buf)? {
+ 0 => break,
+ n => digest.input(&buf[..n]),
+ }
+ }
+ Ok(digest.result().to_vec())
+}
+
+fn hash_file(fsize: u64, path: PathBuf, tx: &HashSender) {
+ match hash_file_inner(&path) {
+ Ok(hash) => tx.send((fsize, path, hash)).unwrap(),
+ Err(e) => err(&path, &e),
+ }
+}
+
+fn fix_perms(entry: &str) {
+ i0005!(entry);
+ let rc = run_cmd("/bin/chmod", &["-v", "ug=rwX,o=rX", entry]);
+
+ if rc.status {
+ if let Some(op) = rc.output {
+ print!("{}", op);
+ }
+ }
+}
+
+fn filename_has_bad_chars(entry: &DirEntry) -> Option<(usize, char)> {
+ let s = entry.file_name().to_str().unwrap();
+
+ for (i, c) in s.char_indices() {
+ match c {
+ '\x00'...'\x1f' => return Some((i, c)),
+ ' ' | '!' | '"' | '#' | '$' | '%' | '&' | '\'' => return Some((i, c)),
+ '(' | ')' | '*' => return Some((i, c)),
+ '+' | ',' | '-' | '.' => (),
+ '0'...'9' => (),
+ ':' | ';' | '<' | '=' | '>' | '?' | '@' => return Some((i, c)),
+ 'A'...'Z' => (),
+ '[' | '\\' | ']' | '^' => return Some((i, c)),
+ '_' => (),
+ '`' => return Some((i, c)),
+ 'a'...'z' => (),
+ '{' | '|' | '}' | '~' => return Some((i, c)),
+ '/' => return None,
+ _ => return Some((i, c)),
+ };
+ }
+ None
+}
+
+// returns false if CRLF, otherwise true
+fn fix_crlf(fname: &str) -> bool {
+ i0004!(fname);
+ //run_cmd("/usr/bin/fromdos", &["-p", "-e", "-v", fname]).status
+ match crlf2lf(fname) {
+ Ok(_) => {
+ i0007!(fname);
+ true
+ }
+ Err(e) => {
+ e0027!(fname, e);
+ false
+ }
+ }
+}
+
+fn check_readme(dir_entry: &str, is_readme: &ReadmeKind, ft: &filemagic::Mimetype) -> bool {
+ let msg_name = if let ReadmeKind::Symlink(s) = is_readme {
+ format!("{} (symlinked from {})", dir_entry, &s)
+ } else {
+ dir_entry.to_string()
+ };
+
+ match ft {
+ filemagic::Mimetype::Pdf
+ | filemagic::Mimetype::Binary
+ | filemagic::Mimetype::Archive
+ | filemagic::Mimetype::Zip => {
+ e0003!(msg_name);
+ return false;
+ }
+ filemagic::Mimetype::BOM(b) => {
+ e0029!(msg_name, b.as_ref());
+ return false;
+ }
+ filemagic::Mimetype::Text | filemagic::Mimetype::TextCRLF => match File::open(dir_entry) {
+ Ok(f) => {
+ if !check_readme_inner(&msg_name, &f) {
+ return false;
+ }
+ }
+ Err(e) => {
+ e0027!(msg_name, e);
+ return false;
+ }
+ },
+ _ => (),
+ }
+ true
+}
+
+fn check_readme_inner(fname: &str, f: &std::fs::File) -> bool {
+ let reader = BufReader::new(f);
+
+ let lines = reader.split(b'\n').map(|l| l.unwrap());
+ let mut result = true;
+
+ for (lineno, line) in lines.enumerate() {
+ if let Err(e) = String::from_utf8(line.clone()) {
+ e0021!(fname, lineno + 1, e);
+ result = false;
+ }
+ }
+ result
+}
+
+fn is_readme(entry: &DirEntry) -> bool {
+ let r = entry.file_name().to_str().unwrap();
+
+ match r {
+ "README" | "README.txt" | "README.md" => true,
+ _ => false,
+ }
+}
+
+fn get_devno(meta: &Metadata) -> u64 {
+ meta.dev()
+}
+
+fn _get_devno(entry: &DirEntry) -> u64 {
+ let meta = fs::metadata(entry.path().to_str().unwrap());
+ match meta {
+ Ok(m) => m.dev(),
+ _ => 0,
+ }
+}
+
+#[derive(StructOpt, Debug)]
+#[structopt(about = "A checker for uploaded packages to CTAN.")]
+struct Args {
+ #[structopt(short = "I", long = "ignore-dupes", help = "Ignore dupes")]
+ ignore_dupes: bool,
+ #[structopt(short = "v", long = "verbose", help = "Verbose operation?")]
+ verbose: bool,
+ #[structopt(short = "L", long = "correct-crlf", help = "Correct CRLF line endings")]
+ correct_crlf: bool,
+ #[structopt(short = "C", long = "correct-perms", help = "Correct permissions")]
+ correct_perms: bool,
+ #[structopt(long = "no-colors", help = "Don't display messages in color")]
+ no_colors: bool,
+ #[structopt(long = "urlcheck", help = "Check URLs found in README files")]
+ urlcheck: bool,
+ #[structopt(short = "T", long = "tds-zip", help = "tds zip archive", group = "tds")]
+ tds_zip: Option<String>,
+ #[structopt(
+ short = "e",
+ long = "explain",
+ help = "Explain error or warning message",
+ group = "only_one"
+ )]
+ explain: Option<String>,
+ #[structopt(
+ long = "explain-all",
+ help = "Explains all error or warning messages",
+ group = "only_one"
+ )]
+ explain_all: bool,
+ #[structopt(
+ long = "show-temp-endings",
+ help = "Show file endings for temporary files",
+ group = "only_one"
+ )]
+ show_tmp_endings: bool,
+ #[structopt(short = "d", long = "package-dir", help = "Package directory")]
+ pkg_dir: Option<String>,
+}
+
+// We take care to avoid visiting a single inode twice,
+// which takes care of (false positive) hardlinks.
+#[cfg(unix)]
+fn check_inode(set: &mut HashSet<(u64, u64)>, meta: &Metadata) -> bool {
+ set.insert((get_devno(meta), meta.ino()))
+}
+
+#[cfg(not(unix))]
+fn check_inode(_: &mut HashSet<u64>, _: &Metadata) -> bool {
+ true
+}
+
+lazy_static! {
+ static ref ARGS: Args = Args::from_args();
+ static ref ERROR_OCCURED: AtomicBool = AtomicBool::new(false);
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DPath {
+ Both(PathBuf),
+ Tds(PathBuf),
+}
+
+impl fmt::Display for DPath {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ DPath::Both(p) => write!(f, "{}", p.display()),
+ DPath::Tds(p) => write!(f, "{}", p.display()),
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct DupPath {
+ len: usize,
+ plen: usize,
+ dupes: Vec<DPath>,
+}
+
+impl DupPath {
+ pub fn new() -> DupPath {
+ DupPath {
+ len: 0,
+ plen: 0,
+ dupes: Vec::new(),
+ }
+ }
+
+ pub fn push(&mut self, pb: PathBuf) {
+ let pname = pb.to_string_lossy();
+ self.len += 1;
+ if pname.ends_with(".tfm") {
+ self.dupes.push(DPath::Tds(pb.clone()));
+ } else {
+ self.plen += 1;
+ self.dupes.push(DPath::Both(pb.clone()));
+ }
+ }
+}
+
+type Hashes = HashMap<(u64, Vec<u8>), DupPath>;
+
+fn main() {
+ match &ARGS.explain {
+ None => (),
+ Some(e) => {
+ explains(&e);
+ process::exit(0);
+ }
+ }
+
+ if ARGS.explain_all {
+ explains_all();
+ process::exit(0);
+ }
+
+ if ARGS.show_tmp_endings {
+ show_tmp_endings();
+ process::exit(0);
+ }
+
+ let pkg_dir = match &ARGS.pkg_dir {
+ None => {
+ f0001!();
+ process::exit(1);
+ }
+ Some(d) => {
+ if !exists_dir(&d) {
+ f0002!(d);
+ process::exit(1);
+ };
+ d
+ }
+ };
+
+ let tds_zip = &ARGS.tds_zip;
+ let pkg_name = match tds_zip {
+ None => None,
+ Some(tz) => {
+ let pn = check_tds_archive_name(tds_zip);
+ if !exists_file(&tz) {
+ f0003!(&tz);
+ process::exit(1);
+ }
+ let mut fmagic = filemagic::Filetype::new();
+ match fmagic.analyze(&tz) {
+ Ok(filemagic::Mimetype::Zip) => (),
+ _ => {
+ f0004!(&tz);
+ process::exit(1)
+ }
+ };
+ pn
+ }
+ };
+
+ if let Some(hashes) = check_package(pkg_dir, tds_zip) {
+ if tds_zip.is_some() {
+ if let Some(pn) = pkg_name {
+ if let Some(s) = ARGS.tds_zip.as_ref() {
+ match check_tds_archive(&pn, s, &hashes) {
+ Ok(_) => (),
+ Err(e) => {
+ e0027!(s, e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if ERROR_OCCURED.load(Ordering::Relaxed) {
+ process::exit(1);
+ } else {
+ process::exit(0);
+ }
+}
+
+fn print_duplicates(hashes: &Hashes) {
+ let mut total_dupes = 0;
+ let mut total_files = 0;
+ let mut total_size = 0;
+
+ let mut header_printed = false;
+ for (k, paths) in hashes.iter() {
+ let (sz, _hash) = k;
+
+ if paths.plen <= 1 {
+ total_files += 1;
+ total_size += sz;
+ continue;
+ } else if !header_printed {
+ w0002!();
+ header_printed = true;
+ }
+
+ total_files += paths.plen;
+ total_size += sz * (paths.plen - 1) as u64;
+ total_dupes += (paths.plen - 1) as u64;
+
+ println!("Size: {}", sz);
+ for p in &paths.dupes {
+ if let DPath::Both(p) = p {
+ let ps = p.as_path().to_str().unwrap();
+ println!(" >>> {}", ps);
+ }
+ }
+ println!();
+ }
+
+ if ARGS.verbose && total_dupes > 0 {
+ println!("Duplicate statistics");
+ println!(" Found {} duplicate files", total_files);
+ println!(" Size of duplicate files: {}", total_size);
+ }
+}
+
+//#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq)]
+pub enum FType {
+ Regular,
+ Directory,
+ Symlink,
+ BlockDevice,
+ CharDevice,
+ Fifo,
+ Socket,
+ Error(String),
+}
+
+fn get_filetype(entry: &DirEntry) -> FType {
+ match entry.metadata() {
+ Ok(mt) => {
+ let ft = mt.file_type();
+ if ft.is_symlink() {
+ return FType::Symlink;
+ }
+ if ft.is_dir() {
+ return FType::Directory;
+ }
+ if ft.is_block_device() {
+ return FType::BlockDevice;
+ }
+ if ft.is_char_device() {
+ return FType::CharDevice;
+ }
+ if ft.is_fifo() {
+ return FType::Fifo;
+ }
+ if ft.is_socket() {
+ return FType::Socket;
+ }
+ FType::Regular
+ }
+ Err(e) => FType::Error(format!("{}", e)),
+ }
+}
+
+//
+// read file into buffer[u8]
+// then convert into string
+//
+fn check_generated_files(entry: &str, generated: &mut HashMap<PathBuf, PathBuf>) {
+ // unwrap() is ok here as we only call this function for files,
+ // specifically .ins or .dtx files
+ let entry_fname = filename(entry).unwrap().to_string();
+ let entry_base = &entry_fname[0..entry_fname.len() - 4];
+
+ let fhdl = File::open(&entry);
+ match fhdl {
+ Ok(mut f) => {
+ let mut buf = Vec::new();
+
+ match f.read_to_end(&mut buf) {
+ Ok(_bytes_read) => {
+ if let Some(found) =
+ gparser::parse_generate(&String::from_utf8_lossy(&buf.clone()))
+ {
+ for fname in found {
+ // As we request a README in the top level directory of
+ // a package we ignore if a README was generated by an
+ // .ins or .dtx file
+ // CAVEAT: If this happens in a subdirectory it could be an error!!!!
+ if fname == "README" || fname == "README.txt" || fname == "README.md" {
+ continue;
+ }
+
+ // If the filename in the 'file{} statement is jobname.<ext>
+ // we replace jobname with the filename before we investigate further
+ let fname_x = if fname.starts_with("\\jobname") {
+ let ext = match get_extension_from_filename(&fname) {
+ Some(e) => e,
+ None => "",
+ };
+ format!("{}.{}", entry_base, ext)
+ } else {
+ fname.to_string()
+ };
+
+ let pb = PathBuf::from(fname_x);
+
+ generated.entry(pb).or_insert_with(|| PathBuf::from(entry));
+ }
+ };
+ }
+ Err(e) => println!("Error reading file {}: {:?}", entry, e),
+ }
+ }
+ Err(e) => println!("Error opening file {}: {:?}", entry, e),
+ }
+ return;
+}
+
+fn x_bit_set(p: u32) -> bool {
+ let p1 = p & 0o7777;
+ p1 & 0o111 != 0
+}
+
+// checks that archive name is in the format x.tds.zip and returns:
+// None if not
+// Option<x> if x.tds.zip
+// Caveat: we currently don't know if x is really the package name.
+fn check_tds_archive_name(tds_zip: &Option<String>) -> Option<String> {
+ match tds_zip {
+ None => None,
+ Some(tz) => {
+ // 8 for ".tds.zip", 1 means a package name is at least a character
+ if tz.len() < 8 + 1 || !tz.ends_with(".tds.zip") {
+ f0005!(tz);
+ process::exit(1);
+ }
+ let mut pname = String::from(utils::basename(&tz));
+ let plen = pname.len();
+ pname.truncate(plen - 8);
+ Some(pname.to_string())
+ }
+ }
+}
+
+fn unzip_tds_archive(tds_zip: &str, tmp_dir: &str) {
+ match run_cmd("/usr/bin/unzip", &["-q", "-d", tmp_dir, &tds_zip]) {
+ CmdReturn { status: true, .. } => (),
+ CmdReturn {
+ status: false,
+ output: out,
+ } => {
+ if let Some(o) = out {
+ e0033!(&tds_zip, o);
+ } else {
+ e0033!(&tds_zip, "<no error given");
+ }
+ process::exit(1);
+ }
+ }
+}
+
+fn check_tds_archive(
+ pkg_name: &str,
+ tds_zip: &str,
+ hashes: &HashMap<(u64, Vec<u8>), DupPath>,
+) -> Result<(), io::Error> {
+ i0003!(tds_zip);
+
+ let dir_entry = Path::new(tds_zip);
+ let p = get_perms(&dir_entry);
+ if !owner_has(p, 4) || !others_have(p, 4) || x_bit_set(p) {
+ e0024!(tds_zip, perms_to_string(p));
+ if ARGS.correct_perms {
+ i0005!(&tds_zip);
+ set_perms(&tds_zip, 0o664);
+ }
+ };
+
+ let tmp_dir = TempDir::new("pkgcheck")?;
+ let tmp_dir_offset = tmp_dir.path().to_str().unwrap().len() + 1;
+ unzip_tds_archive(tds_zip, tmp_dir.path().to_str().unwrap());
+
+ let tds_toplevel_dirs: HashSet<String> = [
+ "tex", "fonts", "metafont", "metapost", "bibtex", "scripts", "doc", "source",
+ ]
+ .iter()
+ .map(|&s| s.to_string())
+ .collect();
+
+ for entry in fs::read_dir(tmp_dir.path())? {
+ let entry = entry?;
+ let path = entry.path();
+
+ match path.file_stem() {
+ None => (),
+ Some(file_stem) => {
+ let fs = file_stem.to_str().expect("bad file name");
+ if !tds_toplevel_dirs.contains(fs) {
+ if path.is_dir() {
+ e0020!(fs);
+ } else {
+ e0034!(fs);
+ }
+ }
+ }
+ }
+
+ // if path.is_dir() {
+ // match path.file_stem() {
+ // None => (),
+ // Some(file_stem) => {
+ // let fs = file_stem.to_str().expect("bad file name");
+ // if !tds_toplevel_dirs.contains(fs) {
+ // e0020!(fs);
+ // }
+ // }
+ // }
+ // } else {
+ // println!("mist");
+ // e0020!(path.display());
+ // }
+ }
+
+ // in order to compare the package files with the content of the
+ // tds zip archive we need to checksum the files in the tds zip
+ // archive.
+
+ let mut sizes = HashMap::default();
+ let mut pool = Pool::new(num_cpus::get() as u32 + 1);
+ {
+ // Processing a single file entry, with the "sizes" hashmap collecting
+ // same-size files. Entries are either Found::One or Found::Multiple,
+ // so that we can submit the first file's path as a hashing job when the
+ // first duplicate is found. Hashing each file is submitted as a job to
+ // the pool.
+ let mut process = |fsize, dir_entry: &DirEntry| {
+ let path = dir_entry.path().to_path_buf();
+ let sizeref = &mut sizes;
+
+ sizeref.entry(fsize).or_insert_with(Vec::new).push(path);
+ };
+
+ let mut map_files_found = false;
+ let mut map_dvips_found = false;
+
+ for dir_entry in WalkDir::new(tmp_dir.path().to_str().unwrap()).follow_links(false) {
+ match dir_entry {
+ Ok(dir_entry) => {
+ //let dir_entry_str = dir_entry.path().to_str().unwrap();
+
+ let ft = get_filetype(&dir_entry);
+
+ if ft == FType::Directory {
+ continue;
+ }
+
+ if let FType::Error(e) = ft {
+ e0023!(e);
+ continue;
+ }
+
+ match dir_entry.metadata() {
+ Ok(meta) => {
+ let fsize = meta.len();
+ process(fsize, &dir_entry);
+
+ let dir_entry_str = match dir_entry.path().to_str() {
+ Some(d) => d,
+ None => {
+ e0031!(dir_entry.path().to_string_lossy());
+ continue;
+ }
+ };
+ let dir_entry_display = &dir_entry_str[tmp_dir_offset..];
+
+ if is_temporary_file(&dir_entry_str) {
+ e0008!(dir_entry_display);
+ }
+
+ // if the path doesn't contain a man page...
+ if dir_entry_str.find("/man/").is_none() {
+ let pkg_name_s = format!("/{}/", pkg_name);
+ // ...then we want to have the package name in the path
+ if dir_entry_str.find(&pkg_name_s).is_none() {
+ e0028!(pkg_name, dir_entry_display);
+ }
+ }
+
+ if dir_entry_str.ends_with(".map") {
+ map_files_found = true;
+ let re: Regex = Regex::new(r"fonts[/]map[/]dvips[/]").unwrap();
+ if re.is_match(dir_entry_str) {
+ map_dvips_found = true;
+ }
+
+ // if ( $fname =~ m{ fonts [/] map [/] dvips [/] }xms ) {
+ // $map_dvips_found = 1;
+ // }
+ }
+ }
+ Err(e) => {
+ e0027!(dir_entry.path().display(), e);
+ }
+ }
+ }
+ Err(e) => {
+ eprintln!("{}", e);
+ }
+ }
+ }
+
+ if map_files_found && !map_dvips_found {
+ println!( "Map files found for package {} but none of them is in a path starting with fonts/map/dvips", pkg_name);
+ }
+ };
+
+ let mut tds_hashes: HashMap<(u64, Vec<u8>), Vec<PathBuf>> = HashMap::default();
+ pool.scoped(|scope| {
+ let (tx, rx) = channel();
+
+ let hashref = &mut tds_hashes;
+ scope.execute(move || {
+ for (size, path, hash) in rx.iter() {
+ hashref
+ .entry((size, hash))
+ .or_insert_with(Vec::new)
+ .push(path);
+ }
+ });
+
+ for size in sizes.keys() {
+ for p in &sizes[size] {
+ let txc = tx.clone();
+ scope.execute(move || hash_file(*size, p.to_path_buf(), &txc));
+ }
+ }
+ });
+
+ tmp_dir.close()?;
+ // now check if each package file is in the tds archive
+ for (k, paths) in hashes.iter() {
+ if !tds_hashes.contains_key(k) {
+ let p = &paths.dupes[0];
+ e0026!(p);
+ }
+ }
+
+ Ok(())
+}
+
+fn get_extension_from_filename(filename: &str) -> Option<&str> {
+ Path::new(filename).extension().and_then(OsStr::to_str)
+}
+
+fn found_unwanted_filetype(fname: &str, ft: &FType) -> bool {
+ match ft {
+ FType::Socket => {
+ e0013!(fname);
+ true
+ }
+ FType::Fifo => {
+ e0014!(fname);
+ true
+ }
+ FType::BlockDevice => {
+ e0015!(fname);
+ true
+ }
+ FType::CharDevice => {
+ e0016!(fname);
+ true
+ }
+ FType::Error(e) => {
+ e0023!(e);
+ true
+ }
+ _ => false,
+ }
+}
+
+fn check_and_correct_perms(dir_entry: &str, p: u32) {
+ if owner_has(p, 5) || !others_have(p, 4) {
+ e0002!(dir_entry, perms_to_string(p));
+ if ARGS.correct_perms {
+ i0005!(&dir_entry);
+ set_perms(&dir_entry, 0o664);
+ }
+ };
+}
+
+
+#[derive(Debug, Clone, PartialEq)]
+enum FileKind {
+ File,
+ Directory,
+ Symlink(String),
+}
+
+
+impl Display for FileKind {
+ fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
+ match *self {
+ FileKind::File => f.write_str("file"),
+ FileKind::Directory => f.write_str("directory"),
+ FileKind::Symlink(_) => f.write_str("symlink"),
+ }
+ }
+}
+
+
+
+#[derive(Debug, Clone, PartialEq)]
+enum ReadmeKind {
+ No,
+ Yes,
+ Symlink(String),
+}
+
+fn check_package(root: &str, tds_zip: &Option<String>) -> Option<Hashes> {
+ //let mut urls = HashMap::default(); //: <String,UrlStatus>
+
+ let mut lcnames: HashMap<PathBuf, Vec<(PathBuf, FileKind)>> = HashMap::default();
+
+ let mut doublenames: HashMap<PathBuf, Vec<PathBuf>> = HashMap::default();
+
+ let mut inodes = HashSet::default();
+
+ i0002!(root);
+ // This hash contains all package file names.
+ //
+ // PathBuf: the full path starting at the directory specified at the command line
+ // Metadata: the meta data of the file
+ // String: the file name without any directory part
+ // ReadmeKind: is it a certain README, file or symlink?
+ // A special case of a README file is a file with has a different name but
+ // was poinded to by a symlink. Example: README --> README.rst
+ let mut file_names: HashMap<PathBuf, (Metadata, String, ReadmeKind)> = HashMap::default();
+ {
+ let mut readme_found = false;
+
+ let root_absolute = PathBuf::from(root)
+ .canonicalize()
+ .unwrap()
+ .to_string_lossy()
+ .to_string();
+ for dir_entry in WalkDir::new(root).follow_links(false) {
+ match dir_entry {
+ Ok(dir_entry) => {
+ let dir_entry_str = match dir_entry.path().to_str() {
+ Some(d) => d,
+ None => {
+ e0031!(dir_entry.path().to_string_lossy());
+ continue;
+ }
+ };
+ let meta = match dir_entry.metadata() {
+ Ok(meta) => meta,
+ Err(e) => {
+ e0023!(e);
+ continue;
+ }
+ };
+
+ if !check_inode(&mut inodes, &meta) {
+ continue;
+ }
+
+ // this is the file_name without the directory part
+ // unwrap() is ok here as we covered potential UTF-8 related errors
+ // above in the definition of dir_entry_str
+ let file_name = dir_entry.file_name().to_str().unwrap().to_string();
+
+ let ft = get_filetype(&dir_entry);
+
+ if found_unwanted_filetype(dir_entry_str, &ft) {
+ continue;
+ }
+
+ if let Some((offset, ch)) = filename_has_bad_chars(&dir_entry) {
+ e0001!(ch, dir_entry_str, offset);
+ }
+
+ // 1. dealing with symlinks
+ if ft == FType::Symlink {
+ match get_symlink(&dir_entry) {
+ Ok(None) => {
+ e0010!(dir_entry_str);
+ continue;
+ }
+ Err(e) => {
+ e0027!(dir_entry_str, e);
+ continue;
+ }
+ Ok(Some(p)) => {
+ let pd: String =
+ p.canonicalize().unwrap().to_string_lossy().to_string();
+ if !pd.starts_with(&root_absolute) {
+ e0030!(dir_entry_str, p.display());
+ continue;
+ }
+
+ let lc_dir_entry_str = dir_entry_str.to_lowercase();
+ if let Some(_dir_name) = filename(dir_entry_str) {
+ let lcnref = &mut lcnames;
+ lcnref
+ .entry(PathBuf::from(lc_dir_entry_str))
+ .or_insert_with(Vec::new)
+ .push((
+ PathBuf::from(dir_entry_str),
+ //FileKind::Symlink(dir_entry_str.into()),
+ FileKind::Symlink(pd.clone()),
+ ));
+ }
+ if is_readme(&dir_entry) {
+ readme_found = true;
+ file_names.insert(
+ p,
+ (
+ meta,
+ file_name,
+ ReadmeKind::Symlink(dir_entry_str.to_string()),
+ ),
+ );
+ }
+ continue;
+ }
+ }
+ }
+
+ let p = get_perms(&dir_entry.path());
+
+ // 2. dealing with directories
+ if ft == FType::Directory {
+ let lc_dir_entry_str = dir_entry_str.to_lowercase();
+ if let Some(_dir_name) = filename(dir_entry_str) {
+ let lcnref = &mut lcnames;
+
+ lcnref
+ .entry(PathBuf::from(lc_dir_entry_str))
+ .or_insert_with(Vec::new)
+ .push((PathBuf::from(dir_entry_str), FileKind::Directory));
+ }
+
+ if !owner_has(p, 5) || !others_have(p, 5) {
+ e0011!(dir_entry_str, perms_to_string(p));
+ if ARGS.correct_perms {
+ i0005!(&dir_entry_str);
+ set_perms(&dir_entry_str, 0o775);
+ }
+ }
+
+ match is_empty_directory(&dir_entry) {
+ Ok(true) => {
+ e0004!(dir_entry_str);
+ }
+ Err(e) => {
+ e0027!(dir_entry_str, e);
+ }
+ Ok(false) => (),
+ }
+ if is_hidden_directory(&dir_entry) {
+ e0006!(dir_entry_str);
+ }
+
+ if is_unwanted_directory(&dir_entry.file_name().to_str().unwrap()) {
+ e0018!(dir_entry_str);
+ }
+ continue;
+ }
+
+ // 3. dealing with regular files
+ if is_hidden(&file_name) {
+ e0007!(dir_entry_str);
+ }
+ if is_temporary_file(&dir_entry_str) {
+ e0008!(dir_entry_str);
+ }
+
+
+ if let Some(file_name) = filename(dir_entry_str) {
+ let doubleref = &mut doublenames;
+
+ doubleref
+ .entry(PathBuf::from(file_name))
+ .or_insert_with(Vec::new)
+ .push(PathBuf::from(dir_entry_str));
+ }
+
+
+ if is_readme(&dir_entry) {
+ // We want to deal with README files only if they are
+ // in the root directory of the package.
+ let f = format!(
+ "{}{}{}",
+ root,
+ // we have to pay attention if `root` ends already with '/'
+ if root.ends_with('/') { "" } else { "/" },
+ &file_name
+ );
+
+ if dir_entry_str == f {
+ readme_found = true;
+ file_names.insert(
+ dir_entry.path().to_path_buf(),
+ (meta, file_name.clone(), ReadmeKind::Yes),
+ );
+ } else {
+ file_names.entry(dir_entry.path().to_path_buf()).or_insert((
+ meta,
+ file_name.clone(),
+ ReadmeKind::No,
+ ));
+ }
+ } else {
+ file_names.entry(dir_entry.path().to_path_buf()).or_insert((
+ meta,
+ file_name.clone(),
+ ReadmeKind::No,
+ ));
+ }
+
+ let lc_dir_entry_str = dir_entry_str.to_lowercase();
+
+ let lcnref = &mut lcnames;
+
+ lcnref
+ .entry(PathBuf::from(lc_dir_entry_str))
+ .or_insert_with(Vec::new)
+ .push((PathBuf::from(dir_entry_str), FileKind::File));
+
+ }
+
+ Err(e) => {
+ eprintln!("{}", e);
+ }
+ }
+ }
+
+ if !readme_found {
+ e0009!();
+ }
+ }
+
+ let lc = LinkCheck::new(4, false);
+ let mut detective = filemagic::Filetype::new();
+
+ let mut sizes = HashMap::default();
+ let mut generated: HashMap<PathBuf, PathBuf> = HashMap::default();
+
+ {
+ // Processing a single file entry, with the "sizes" hashmap collecting
+ // same-size files. Entries are either Found::One or Found::Multiple,
+ // so that we can submit the first file's path as a hashing job when the
+ // first duplicate is found. Hashing each file is submitted as a job to
+ // the pool.
+ let mut process = |fsize, path: &PathBuf| {
+ let sizeref = &mut sizes;
+
+ let path = path.clone();
+ sizeref.entry(fsize).or_insert_with(Vec::new).push(path);
+ };
+ for (path, (meta, _file_name, is_readme)) in file_names.iter() {
+
+ //println!("DEBUG: path {:?}, file_name {:?}, is_readme: {} ", &path, &file_name, is_readme);
+ let dir_entry_str = match path.to_str() {
+ Some(d) => d,
+ None => {
+ e0031!(&path.to_string_lossy());
+ continue;
+ }
+ };
+
+ let fsize = meta.len();
+
+ // if it is an empty file we don't need
+ // to check further
+ if fsize == 0 {
+ e0005!(dir_entry_str);
+ continue;
+ }
+
+ let p = get_perms(&path);
+ if !owner_has(p, 4) {
+ e0002!(dir_entry_str, perms_to_string(p));
+ if ARGS.correct_perms {
+ fix_perms(&dir_entry_str);
+ }
+ continue;
+ }
+ let ftr = detective.analyze(&dir_entry_str);
+ //println!(">>> {:?}", ftr);
+ // we ignore errors from filetype recognition
+ if ftr.is_err() {
+ continue;
+ }
+
+ let ft = ftr.unwrap();
+
+ // DEBUG !readme_symlinked.contains(dir_entry_str)
+ if ReadmeKind::No != *is_readme {
+ if !check_readme(dir_entry_str, is_readme, &ft) {
+ continue;
+ }
+ if ARGS.urlcheck {
+ lc.check_urls(dir_entry_str);
+ }
+ }
+
+ match ft {
+ filemagic::Mimetype::Text | filemagic::Mimetype::TextCRLF => {
+ check_and_correct_perms(dir_entry_str, p);
+ let fext = get_extension_from_filename(&dir_entry_str);
+ if fext == Some("ins") || fext == Some("dtx") {
+ check_generated_files(&dir_entry_str, &mut generated);
+ }
+ match fext {
+ Some("bat") | Some("cmd") |
+ Some("nsh") | Some("reg") => (),
+ Some(_) | None => {
+ if ft == filemagic::Mimetype::TextCRLF {
+ e0012!(dir_entry_str);
+ if ARGS.correct_crlf {
+ fix_crlf(dir_entry_str);
+ }
+ }
+ }
+ };
+ }
+
+ filemagic::Mimetype::BOM(b) => {
+ //println!("{}: {} with BOM detected", dir_entry_str, b.as_ref());
+ w0004!(dir_entry_str, b.as_ref());
+ check_and_correct_perms(dir_entry_str, p);
+ }
+ filemagic::Mimetype::Binary
+ | filemagic::Mimetype::Script
+ | filemagic::Mimetype::ScriptCRLF => {
+ if !owner_has(p, 4) || !others_have(p, 4) {
+ e0002!(dir_entry_str, perms_to_string(p));
+ };
+ if ARGS.correct_perms {
+ fix_perms(&dir_entry_str);
+ }
+ }
+ filemagic::Mimetype::Pdf => {
+ check_and_correct_perms(dir_entry_str, p);
+ let ret = is_pdf_ok(&dir_entry_str);
+ if !ret.status {
+ e0017!(dir_entry_str);
+ if let Some(output) = ret.output {
+ println!("{}", &output);
+ };
+ }
+ }
+
+ filemagic::Mimetype::Archive | filemagic::Mimetype::Zip => {
+ w0001!(dir_entry_str);
+ check_and_correct_perms(dir_entry_str, p);
+ }
+
+ filemagic::Mimetype::Data => check_and_correct_perms(dir_entry_str, p),
+ _ => continue,
+ }
+ if others_match(p, 0) {
+ e0002!(dir_entry_str, perms_to_string(p));
+ if ARGS.correct_perms {
+ i0005!(&dir_entry_str);
+ set_perms(&dir_entry_str, 0o664);
+ }
+ }
+ if !(ARGS.ignore_dupes && tds_zip.is_none()) {
+ process(fsize, &path);
+ }
+ }
+ }
+
+ print_casefolding(&lcnames);
+ print_generated(&doublenames, &generated);
+ print_doublenames(&doublenames);
+
+ if ARGS.ignore_dupes && tds_zip.is_none() {
+ return None;
+ }
+
+ // Set up thread pool for the task to hash a file. Number of CPUs + 1 has been
+ // found to be a good pool size, likely since the walker thread should be
+ // doing mostly IO.
+ let mut pool = Pool::new(num_cpus::get() as u32 + 1);
+
+ let mut hashes: HashMap<(u64, Vec<u8>), DupPath> = HashMap::default();
+ pool.scoped(|scope| {
+ let (tx, rx) = channel();
+
+ let hashref = &mut hashes;
+ scope.execute(move || {
+ for (size, path, hash) in rx.iter() {
+ hashref
+ .entry((size, hash))
+ .or_insert_with(DupPath::new)
+ .push(path);
+ }
+ });
+
+ for size in sizes.keys() {
+ let paths = &sizes[size];
+ if paths.len() == 1 && tds_zip.is_none() {
+ continue;
+ };
+
+ for p in &sizes[size] {
+ let txc = tx.clone();
+ scope.execute(move || hash_file(*size, p.to_path_buf(), &txc));
+ }
+ }
+ });
+
+ if !ARGS.ignore_dupes {
+ print_duplicates(&hashes);
+ }
+ Some(hashes)
+}
+
+fn print_casefolding(hashes: &HashMap<PathBuf, Vec<(PathBuf, FileKind)>>) {
+
+ for (k, eles) in hashes.iter() {
+ //println!("pcf: {:?}, {:?}", k, &eles);
+ if eles.len() == 1 {
+ continue;
+ }
+
+ e0025!(k.display());
+
+ for (p, ty) in eles {
+ println!(" >>> {} ({})", p.display(), ty);
+ }
+ }
+}
+
+fn print_generated(hashes: &HashMap<PathBuf, Vec<PathBuf>>, generated: &HashMap<PathBuf, PathBuf>) {
+ for (k, gen) in generated.iter() {
+ if hashes.contains_key(k) {
+ let v = &hashes[k];
+ for fname in v {
+ e0019!(fname.to_str().unwrap(), gen.to_str().unwrap());
+ }
+ }
+ }
+}
+
+fn print_doublenames(hashes: &HashMap<PathBuf, Vec<PathBuf>>) {
+ for (k, paths) in hashes.iter() {
+ if paths.len() == 1 {
+ continue;
+ }
+ let ks = k.to_str().unwrap();
+ if ks == "README"
+ || ks == "README.txt"
+ || ks == "README.md"
+ || ks == "Makefile"
+ || ks == "Makefile.am"
+ || ks == "Makefile.in"
+ || ks == "makefile"
+ {
+ continue;
+ }
+
+ w0003!(k.to_str().unwrap());
+ // println!(":: {}", k.display());
+
+ for p in paths {
+ println!(" >>> {}", p.display());
+ }
+ }
+}
+
+fn show_tmp_endings() {
+ i0006!();
+ for (t, c) in temp_file_endings() {
+ println!("{:23} {}", t, c);
+ }
+}
diff --git a/support/pkgcheck/src/messages/errorsd.rs b/support/pkgcheck/src/messages/errorsd.rs
new file mode 100644
index 0000000000..bd726754c0
--- /dev/null
+++ b/support/pkgcheck/src/messages/errorsd.rs
@@ -0,0 +1,445 @@
+// This file is generated by a Perl script. The source is
+// in the docs/ directory of the repository.
+
+pub fn e0001d() {
+ println!(
+ r#"
+E0001 -- Bad characters in file name
+
+File name should not contain non-ascii characters. Additionally, file
+names should not contain control characters or other characters which
+may have a special meaning for UNIX shells.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#nounixspecialcharacters
+"#
+ )
+}
+
+pub fn e0002d() {
+ println!(
+ r#"
+E0002 -- File Permissions
+
+Files submitted to CTAN should be world readable.
+
+Only files that are truly executable (like scripts and binaries) should
+be marked as such.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#filepermissions
+"#
+ )
+}
+
+pub fn e0003d() {
+ println!(
+ r#"
+E0003 -- README is not a text file
+
+The README file specified in the error message must be a text file but
+it isn't.
+"#
+ )
+}
+
+pub fn e0004d() {
+ println!(
+ r#"
+E0004 -- Empty directory not allowed
+
+Empty directories are considered as rubbish, and are usually not
+accepted as part of a package.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#noemptyfiles
+"#
+ )
+}
+
+pub fn e0005d() {
+ println!(
+ r#"
+E0005 -- Empty files not allowed
+
+Empty files are considered as rubbish, and are usually not accepted as
+part of a package.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#noemptyfiles
+"#
+ )
+}
+
+pub fn e0006d() {
+ println!(
+ r#"
+E0006 -- Hidden directories not allowed
+
+A package should not contain hidden directories.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#noauxfiles
+"#
+ )
+}
+
+pub fn e0007d() {
+ println!(
+ r#"
+E0007 -- Hidden files not allowed
+
+A package should not contain hidden files.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#noauxfiles
+"#
+ )
+}
+
+pub fn e0008d() {
+ println!(
+ r#"
+E0008 -- Temporary file detected
+
+A temporary file was detected. These are typically files created by TeX
+& friends and should not be part of a package.
+
+Temporary files will also be detected in a TDS zip archive.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#noauxfiles
+"#
+ )
+}
+
+pub fn e0009d() {
+ println!(
+ r#"
+E0009 -- Package doesn't contain a README file
+
+A package must contain at least one of README, README.md or README.txt
+file.
+
+For more details refer to:
+http://mirrors.ibiblio.org/CTAN/help/ctan/CTAN-upload-addendum.html#readme
+"#
+ )
+}
+
+pub fn e0010d() {
+ println!(
+ r#"
+E0010 -- Broken symlink detected
+
+A broken symlink was detected.
+"#
+ )
+}
+
+pub fn e0011d() {
+ println!(
+ r#"
+E0011 -- Wrong permission for directory
+
+Directories should have rwx for the owner and at least r-x for others
+(i.e. world readable).
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#filepermissions
+"#
+ )
+}
+
+pub fn e0012d() {
+ println!(
+ r#"
+E0012 -- CRLF line endings detected
+
+The file specified in the error message contains CRLF line endings. Text
+files should have UNIX style line endings.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#crlf
+"#
+ )
+}
+
+pub fn e0013d() {
+ println!(
+ r#"
+E0013 -- Socket special fie detected
+
+The file specified in the error message is a socket special file which
+is not allowed.
+"#
+ )
+}
+
+pub fn e0014d() {
+ println!(
+ r#"
+E0014 -- Fifo special file detected
+
+The file specified in the error message is a fifo special file which is
+not allowed.
+"#
+ )
+}
+
+pub fn e0015d() {
+ println!(
+ r#"
+E0015 -- Bloch device file detected
+
+The file specified in the error message is a block device file which is
+not allowed.
+"#
+ )
+}
+
+pub fn e0016d() {
+ println!(
+ r#"
+E0016 -- Character device file detected
+
+The file specified in the error message is a character device file which
+is not allowed.
+"#
+ )
+}
+
+pub fn e0017d() {
+ println!(
+ r#"
+E0017 -- PDF document is in error
+
+The PDF document mentioned in the message is in error.
+
+pdfinfo will be run to check if a PDF document can be read. Message
+E0017 will be followed by the error messages from pdfinfo.
+
+Example:
+
+ I0002 Checking package files in directory somepkg
+ E0017 PDF error detected in somepkg/sompkg.pdf
+ Syntax Error (1293042): Illegal character ')'
+ Syntax Error: Couldn't find trailer dictionary
+ Syntax Error (1293042): Illegal character ')'
+ Syntax Error: Couldn't find trailer dictionary
+ Syntax Error: Couldn't read xref table
+"#
+ )
+}
+
+pub fn e0018d() {
+ println!(
+ r#"
+E0018 -- Unwanted directory detected
+
+A directory was detected which should not be part of a package. Example:
+__MACOSX
+"#
+ )
+}
+
+pub fn e0019d() {
+ println!(
+ r#"
+E0019 -- Generated file detected
+
+In order to avoid redundancy we don't want to have included files in a
+package which easily can be generated from other files in the
+submission.
+
+Exceptions are the README files of the package, i.e. README, README.md
+or README.txt.
+
+Starting with version 1.1.0 pkgcheck also detects generated files if
+they are in a different directory in the package.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#nogeneratedfiles
+"#
+ )
+}
+
+pub fn e0020d() {
+ println!(
+ r#"
+E0020 -- Unwanted directory detected in the top level directory in TDS zip archive
+
+A top level directory of a TDS archive should only contain all or some
+of the following directories:
+
+- tex
+- fonts
+- metafont
+- metapost
+- bibtex
+- scripts
+- doc
+- source
+"#
+ )
+}
+
+pub fn e0021d() {
+ println!(
+ r#"
+E0021 -- Error when reading a file
+
+An error was encountered when reading the file specified in the message.
+"#
+ )
+}
+
+pub fn e0022d() {
+ println!(
+ r#"
+E0022 -- Check of an URL in a README file failed
+
+URL checking is in effect. An error occcured when trying to retrieve an
+URL which was found in the specified README file.
+"#
+ )
+}
+
+pub fn e0023d() {
+ println!(
+ r#"
+E0023 -- Follow up error when trying to read a directory with insufficient permissions
+
+Error which is a follow-up error. For instance, when a directory could
+not be read.
+"#
+ )
+}
+
+pub fn e0024d() {
+ println!(
+ r#"
+E0024 -- TDS zip archive has wrong permissions
+
+The TDS zip archive should have at least r-- for the owner and at least
+r-- for others (i.e. world readable).
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#filepermissions
+"#
+ )
+}
+
+pub fn e0025d() {
+ println!(
+ r#"
+E0025 -- Duplicate names when ignoring letter case for files or directories
+
+As there are operating systems which do not distinguish between myfile
+and MYFILE we don't want to have file names in a directory which are the
+same after converting to lower case.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#filenames
+"#
+ )
+}
+
+pub fn e0026d() {
+ println!(
+ r#"
+E0026 -- Files not in TDS or different in TDS and non-install tree
+
+The file mentioned in the error message is either not existing in the
+TDS zip archive, or it is different to the one in the non-install tree
+"#
+ )
+}
+
+pub fn e0027d() {
+ println!(
+ r#"
+E0027 -- An I/O error occured
+
+Some kind of I/O error occured. If you believe there is an error in
+pkgcheck please contact the author.
+"#
+ )
+}
+
+pub fn e0028d() {
+ println!(
+ r#"
+E0028 -- A path name in a TDS zip archive must contain the package name
+
+The path names in a TDS zip archive must contain the package name.
+
+EXAMPLE: Assume a package somepkg. Then path names should look like
+follows:
+
+ tex/latex/somepkg/somepkg.cls
+ doc/latex/somepkg/README
+ source/latex/somepkg/somepkg.dtx
+ ...
+"#
+ )
+}
+
+pub fn e0029d() {
+ println!(
+ r#"
+E0029 -- README file: encoding with BOM detected
+
+A README file should be either ASCII or UTF-8 without BOM(byte order
+mark)
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#readme
+"#
+ )
+}
+
+pub fn e0030d() {
+ println!(
+ r#"
+E0030 -- A symlink was found which points outside of the package directory tree
+
+A symlink must not point to a file or directory outside of the package
+directory tree.
+"#
+ )
+}
+
+pub fn e0031d() {
+ println!(
+ r#"
+E0031 -- File name contains invalid UTF-8 character(s)
+
+A file name contains invald UTF-8 character(s).
+"#
+ )
+}
+
+pub fn e0033d() {
+ println!(
+ r#"
+E0033 -- Error when unpacking tds archive
+
+In order to investigate the contents of the TDS zip archive pkgcheck
+unpacks the TDS zip archive to a temporary location which failed for the
+reason given in the error message.
+"#
+ )
+}
+
+pub fn e0034d() {
+ println!(
+ r#"
+E0034 -- Unwanted file detected in the top level directory in TDS zip archive
+
+A top level directory of a TDS archive should only contain certain
+directories but no files.
+"#
+ )
+}
+
diff --git a/support/pkgcheck/src/messages/fatald.rs b/support/pkgcheck/src/messages/fatald.rs
new file mode 100644
index 0000000000..b41cab83b1
--- /dev/null
+++ b/support/pkgcheck/src/messages/fatald.rs
@@ -0,0 +1,66 @@
+// This file is generated by a Perl script. The source is
+// in the docs/ directory of the repository.
+
+pub fn f0001d() {
+ println!(
+ r#"
+F0001 -- Specify a directory to check (use option -d)
+
+pkgcheck was called without any options. Use option -d to check a
+directory
+"#
+ )
+}
+
+pub fn f0002d() {
+ println!(
+ r#"
+F0002 -- Specified directory does not exist. Exiting...
+
+The directory specified at the command line does not exit.
+"#
+ )
+}
+
+pub fn f0003d() {
+ println!(
+ r#"
+F0003 -- Specified TDS archive does not exist or is no file
+
+Specify a valid TDS zip archive when calling pkgcheck
+"#
+ )
+}
+
+pub fn f0004d() {
+ println!(
+ r#"
+F0004 -- The file specified as TDS archive is no zip archive
+
+Specify a valid TDS zip archive when calling pkgcheck
+"#
+ )
+}
+
+pub fn f0005d() {
+ println!(
+ r#"
+F0005 -- Bad file name for the zip archive
+
+pkgcheck detected that the file name of the TDS zip archive doesn't end
+with .tds.zip
+"#
+ )
+}
+
+pub fn f0006d() {
+ println!(
+ r#"
+F0006 -- Unknown error code specified with option -e resp. ---explain. Exiting...
+
+pkgcheck was called with option -e resp. --explain, and an unknown error
+code was specified.
+"#
+ )
+}
+
diff --git a/support/pkgcheck/src/messages/informationd.rs b/support/pkgcheck/src/messages/informationd.rs
new file mode 100644
index 0000000000..d4e3ea43b3
--- /dev/null
+++ b/support/pkgcheck/src/messages/informationd.rs
@@ -0,0 +1,77 @@
+// This file is generated by a Perl script. The source is
+// in the docs/ directory of the repository.
+
+pub fn i0001d() {
+ println!(
+ r#"
+I0001 -- Successfully converted from CRLF to LF
+
+Just an information that pkgcheck has successfully converted a file from
+CRLF to LF line endings
+"#
+ )
+}
+
+pub fn i0002d() {
+ println!(
+ r#"
+I0002 -- Checking package files in directory
+
+Just an information that pkgcheck starts checking the package files in
+the unzipped directory trees
+"#
+ )
+}
+
+pub fn i0003d() {
+ println!(
+ r#"
+I0003 -- Checking TDS zip archive
+
+Just an information that pkgcheck starts checking the TDS zip archive
+"#
+ )
+}
+
+pub fn i0004d() {
+ println!(
+ r#"
+I0004 -- Correcting line endings for file
+
+The file had CRLF line ending and will be corrected to have LF (Unix
+like) line endings.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#crlf
+"#
+ )
+}
+
+pub fn i0005d() {
+ println!(
+ r#"
+I0005 -- Corrections permissions for file or directory
+
+pkgcheck corrects wrong permsissions for package files and directories.
+It runs the chmod command in verbose mode.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#filepermissions
+"#
+ )
+}
+
+pub fn i0006d() {
+ println!(
+ r#"
+I0006 -- Files having one of the following file name endings are regarded as temporary
+
+Option --show-temp-endings was used, and pkgcheck prints a list of
+temporary file endings and their meanings.
+
+For more details refer to:
+http://mirror.ctan.org/help/ctan/CTAN-upload-addendum.html#noauxfiles
+"#
+ )
+}
+
diff --git a/support/pkgcheck/src/messages/mod.rs b/support/pkgcheck/src/messages/mod.rs
new file mode 100644
index 0000000000..c63b6e2cec
--- /dev/null
+++ b/support/pkgcheck/src/messages/mod.rs
@@ -0,0 +1,657 @@
+mod errorsd;
+mod fatald;
+mod informationd;
+mod warningsd;
+
+use self::errorsd::*;
+use self::fatald::*;
+use self::informationd::*;
+use self::warningsd::*;
+
+macro_rules! no_colors {
+ () => {
+ $crate::ARGS.no_colors
+ };
+}
+
+macro_rules! error_occured {
+ () => {
+ $crate::ERROR_OCCURED.store(true, Ordering::Relaxed);
+ };
+}
+macro_rules! yellow {
+ ($fmt:expr) => {
+ if no_colors!() {
+ $fmt.clear()
+ } else {
+ $fmt.bright_yellow()
+ }
+ };
+}
+
+// macro_rules! red {
+// ($fmt:expr) => {
+// if no_colors!() {
+// $fmt.clear()
+// } else {
+// $fmt.bright_red()
+// }
+// };
+// }
+
+macro_rules! msgid {
+ ($fmt:expr) => {
+ match (no_colors!(), &$fmt[..1]) {
+ (true, _) => $fmt.clear(),
+ (false, "E") => $fmt.bright_red().bold(),
+ (false, "I") => $fmt.bright_yellow(),
+ (false, "W") => $fmt.bright_red(),
+ (false, "F") => $fmt.bright_red().bold(),
+ (_, _) => $fmt.clear(),
+ }
+ };
+}
+
+macro_rules! f0001 {
+ () => {
+ error_occured!();
+ print!(
+ "{} Specify a directory to check (use option -d)\n",
+ msgid!("F0001")
+ );
+ };
+}
+
+macro_rules! f0002 {
+ ($fmt1:expr) => {
+ error_occured!();
+ print!(
+ "{} Specified directory {} does not exist. Exiting...\n",
+ msgid!("F0002"),
+ $fmt1
+ );
+ };
+}
+
+macro_rules! f0003 {
+ ($fmt1:expr) => {
+ error_occured!();
+ print!(
+ "{} Specified TDS archive {} does not exist or is no file. Exiting...\n",
+ msgid!("F0003"),
+ $fmt1
+ );
+ };
+}
+
+macro_rules! f0004 {
+ ($fmt1:expr) => {
+ error_occured!();
+ print!(
+ "{} File {} specified as TDS archive is no zip archive. Exiting...\n",
+ msgid!("F0004"),
+ $fmt1
+ );
+ };
+}
+
+macro_rules! f0005 {
+ ($fmt1:expr) => {
+ error_occured!();
+ print!(
+ "{} Bad filename {} for the tds.zip archive. Exiting...\n",
+ msgid!("F0005"),
+ $fmt1
+ );
+ };
+}
+
+// macro_rules! f0006 {
+// ($fmt1:expr) => {
+// error_occured!();
+// print!(
+// "{} Unknown error code `{}` specified with option -e resp. ---explain. Exiting...\n",
+// msgid!("F0006"),
+// $fmt1
+// );
+// };
+// }
+
+macro_rules! e0001 {
+ ($fmt1:expr, $fmt2:expr, $fmt3:expr) => {
+ error_occured!();
+ print!(
+ "{} Bad character {} ({:#02x}) found in file name {} at offset {}\n",
+ msgid!("E0001"),
+ $fmt1,
+ $fmt1 as usize,
+ $fmt2,
+ $fmt3
+ );
+ };
+}
+
+macro_rules! e0002 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} File {} has bad permissions: {}\n",
+ msgid!("E0002"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! e0003 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} {} is no text file\n", msgid!("E0003"), $fmt);
+ };
+}
+
+macro_rules! e0004 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} Empty directory {} detected\n", msgid!("E0004"), $fmt);
+ };
+}
+
+macro_rules! e0005 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} Empty file {} detected\n", msgid!("E0005"), $fmt);
+ };
+}
+
+macro_rules! e0006 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} Hidden directory {} detected\n", msgid!("E0006"), $fmt);
+ };
+}
+
+macro_rules! e0007 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} Hidden file {} detected\n", msgid!("E0007"), $fmt);
+ };
+}
+
+macro_rules! e0008 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} Temporary file {} detected\n", msgid!("E0008"), $fmt);
+ };
+}
+
+macro_rules! e0009 {
+ () => {
+ error_occured!();
+ print!(
+ "{} One of README/README.md/README.txt must exist\n",
+ msgid!("E0009")
+ );
+ };
+}
+
+macro_rules! e0010 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} {} is a broken symlink\n", msgid!("E0010"), $fmt);
+ };
+}
+
+macro_rules! e0011 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} Directory {} has bad permissions: {}\n",
+ msgid!("E0011"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! e0012 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} CRLF detected: {}\n", msgid!("E0012"), $fmt);
+ };
+}
+
+macro_rules! e0013 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} {} is a socket special file\n", msgid!("E0013"), $fmt);
+ };
+}
+
+macro_rules! e0014 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} {} is a fifo file\n", msgid!("E0014"), $fmt);
+ };
+}
+
+macro_rules! e0015 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} {} is a block device file\n", msgid!("E0015"), $fmt);
+ };
+}
+
+macro_rules! e0016 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!(
+ "{} {} is a character device file\n",
+ msgid!("E0016"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! e0017 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} PDF error detected in {}\n", msgid!("E0017"), $fmt);
+ };
+}
+
+macro_rules! e0018 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!(
+ "{} Unwanted directory {} detected\n",
+ msgid!("E0018"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! e0019 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} {} generated by {} exists\n",
+ msgid!("E0019"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! e0020 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!(
+ "{} Unwanted directory `{}` detected in the top level directory of a TDS archive\n",
+ msgid!("E0020"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! e0021 {
+ ($fmt1:expr, $fmt2:expr, $fmt3:expr) => {
+ error_occured!();
+ print!(
+ "{} File {} : Error reading in line {}: {}\n",
+ msgid!("E0021"),
+ $fmt1,
+ $fmt2,
+ $fmt3
+ );
+ };
+}
+
+macro_rules! e0022 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!("{} {}: `{}`\n", msgid!("E0022"), $fmt1, $fmt2);
+ };
+}
+
+macro_rules! e0023 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!("{} {}\n", msgid!("E0023"), $fmt);
+ };
+}
+
+macro_rules! e0024 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} The TDS zip archive {} has bad permissions: {}\n",
+ msgid!("E0024"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! e0025 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!(
+ "{} Duplicate names when ignoring letter case for: {}\n",
+ msgid!("E0025"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! e0026 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!(
+ "{} {} : file not in TDS or different in TDS and non-install tree\n",
+ msgid!("E0026"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! e0027 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} {}: An I/O error occured -> {}\n",
+ msgid!("E0027"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! e0028 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} No directory {} (= package name) found in path {}\n",
+ msgid!("E0028"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! e0029 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} {}: {} encoding with BOM detected\n",
+ msgid!("E0029"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! e0030 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} Symlink {} points to {} which is outside of the package directory tree\n",
+ msgid!("E0030"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! e0031 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!(
+ "{} File name {} contains invalid UTF-8 character(s)\n",
+ msgid!("E0031"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! e0033 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} Error `{}` when unpacking tds archive {}. Exiting...\n",
+ msgid!("E0033"),
+ $fmt2,
+ $fmt1
+ );
+ };
+}
+
+macro_rules! e0034 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!(
+ "{} Unwanted file `{}` detected in the top level directory of a TDS archive\n",
+ msgid!("E0034"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! w0001 {
+ ($fmt:expr) => {
+ print!(
+ "{} Archive as package file detected: {}\n",
+ msgid!("W0001"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! w0002 {
+ () => {
+ print!("{} Duplicate files detected\n", msgid!("W0002"),);
+ };
+}
+
+macro_rules! w0003 {
+ ($fmt:expr) => {
+ error_occured!();
+ print!(
+ "{} Same named files detected in the package directory tree: {}\n",
+ msgid!("W0003"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! w0004 {
+ ($fmt1:expr, $fmt2:expr) => {
+ error_occured!();
+ print!(
+ "{} {}: {} encoding with BOM detected\n",
+ msgid!("W0004"),
+ $fmt1,
+ $fmt2
+ );
+ };
+}
+
+macro_rules! i0002 {
+ ($fmt:expr) => {
+ print!(
+ "{} {} {}\n",
+ msgid!("I0002"),
+ yellow!("Checking package files in directory"),
+ yellow!($fmt)
+ );
+ };
+}
+
+macro_rules! i0003 {
+ ($fmt:expr) => {
+ print!(
+ "{} {} {}\n",
+ msgid!("I0003"),
+ yellow!("Checking TDS zip archive"),
+ yellow!($fmt)
+ );
+ };
+}
+
+macro_rules! i0004 {
+ ($fmt:expr) => {
+ print!(
+ "{} Correcting line endings for file {}\n",
+ msgid!("I0004"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! i0005 {
+ ($fmt:expr) => {
+ print!(
+ "{} Correcting permissions for {}\n",
+ msgid!("I0005"),
+ $fmt
+ );
+ };
+}
+
+macro_rules! i0006 {
+ () => {
+ print!(
+ "{} Files having one of the following file name endings are regarded as temporary\n",
+ msgid!("I0006")
+ );
+ };
+}
+
+macro_rules! i0007 {
+ ($fmt:expr) => {
+ print!(
+ "{} {}: Successfully converted from CRLF to LF\n",
+ msgid!("I0007"),
+ $fmt
+ );
+ };
+}
+
+pub fn explains(err: &str) {
+ let err_upp = err.to_ascii_uppercase();
+ match err_upp.as_str() {
+ "F0001" => f0001d(),
+ "F0002" => f0002d(),
+ "F0003" => f0003d(),
+ "F0004" => f0004d(),
+ "F0005" => f0005d(),
+ "F0006" => f0006d(),
+
+ "E0001" => e0001d(),
+ "E0002" => e0002d(),
+ "E0003" => e0003d(),
+ "E0004" => e0004d(),
+ "E0005" => e0005d(),
+ "E0006" => e0006d(),
+ "E0007" => e0007d(),
+ "E0008" => e0008d(),
+ "E0009" => e0009d(),
+ "E0010" => e0010d(),
+ "E0011" => e0011d(),
+ "E0012" => e0012d(),
+ "E0013" => e0013d(),
+ "E0014" => e0014d(),
+ "E0015" => e0015d(),
+ "E0016" => e0016d(),
+ "E0017" => e0017d(),
+ "E0018" => e0018d(),
+ "E0019" => e0019d(),
+ "E0020" => e0020d(),
+ "E0021" => e0021d(),
+ "E0022" => e0022d(),
+ "E0023" => e0023d(),
+ "E0024" => e0024d(),
+ "E0025" => e0025d(),
+ "E0026" => e0026d(),
+ "E0027" => e0027d(),
+ "E0028" => e0028d(),
+ "E0029" => e0029d(),
+ "E0030" => e0030d(),
+ "E0031" => e0031d(),
+ "E0033" => e0033d(),
+ "E0034" => e0034d(),
+
+ // "I0001" => i0001d!(),
+ "I0001" => i0001d(),
+ "I0002" => i0002d(),
+ "I0003" => i0003d(),
+ "I0004" => i0004d(),
+ "I0005" => i0005d(),
+ "I0006" => i0006d(),
+
+ "W0001" => w0001d(),
+ "W0002" => w0002d(),
+ "W0003" => w0003d(),
+ "W0004" => w0004d(),
+
+ e => println!(
+ "F0006 Unknown error code `{}` specified with option -e resp. --explain. Exiting...",
+ e
+ ),
+ }
+}
+
+pub fn explains_all() {
+ explains("F0001");
+ explains("F0002");
+ explains("F0003");
+ explains("F0004");
+ explains("F0005");
+ explains("F0006");
+
+ explains("E0001");
+ explains("E0002");
+ explains("E0003");
+ explains("E0004");
+ explains("E0005");
+ explains("E0006");
+ explains("E0007");
+ explains("E0008");
+ explains("E0009");
+ explains("E0010");
+ explains("E0011");
+ explains("E0012");
+ explains("E0013");
+ explains("E0014");
+ explains("E0015");
+ explains("E0016");
+ explains("E0017");
+ explains("E0018");
+ explains("E0019");
+ explains("E0020");
+ explains("E0021");
+ explains("E0022");
+ explains("E0023");
+ explains("E0024");
+ explains("E0025");
+ explains("E0026");
+ explains("E0027");
+ explains("E0028");
+ explains("E0029");
+ explains("E0030");
+ explains("E0031");
+ explains("E0033");
+ explains("E0034");
+
+ explains("I0001");
+ explains("I0002");
+ explains("I0003");
+ explains("I0004");
+ explains("I0005");
+ explains("I0006");
+
+ explains("W0001");
+ explains("W0002");
+ explains("W0003");
+ explains("W0004");
+}
diff --git a/support/pkgcheck/src/messages/warningsd.rs b/support/pkgcheck/src/messages/warningsd.rs
new file mode 100644
index 0000000000..588144968d
--- /dev/null
+++ b/support/pkgcheck/src/messages/warningsd.rs
@@ -0,0 +1,58 @@
+// This file is generated by a Perl script. The source is
+// in the docs/ directory of the repository.
+
+pub fn w0001d() {
+ println!(
+ r#"
+W0001 -- Archive as package file detected
+
+Usually a CTAN package should not contain archives. An exception are
+situations where, for example, the source code of a package is kept in a
+separate zip archive.
+"#
+ )
+}
+
+pub fn w0002d() {
+ println!(
+ r#"
+W0002 -- Duplicate files detected
+
+Duplicate files were detected which are listed right after this message.
+
+The message is a warning message as something like this could not be
+seen as an error in general.
+"#
+ )
+}
+
+pub fn w0003d() {
+ println!(
+ r#"
+W0003 -- Same named files detected in the package tree
+
+We like to have unique file names over the whole package directory tree.
+When we discover same named files we report it as a warning. Common
+names like README, README.txt, README.md, Makefile, Makefile.in,
+Makefile.am and makefile are ignored when checking.
+
+For more details refer to:
+http://mirror.utexas.edu/ctan/help/ctan/CTAN-upload-addendum.html#uniquefilenames
+"#
+ )
+}
+
+pub fn w0004d() {
+ println!(
+ r#"
+W0004 -- encoding with BOM detected
+
+A UTF encoded package file contains a BOM (byte order mark). Currently,
+we issues a warning.
+
+Nevertheless, the CTAN team discourages uses of BOM. Please be aware,
+that in some future time this could be reagarded as an error.
+"#
+ )
+}
+
diff --git a/support/pkgcheck/src/recode.rs b/support/pkgcheck/src/recode.rs
new file mode 100644
index 0000000000..39a85bacd2
--- /dev/null
+++ b/support/pkgcheck/src/recode.rs
@@ -0,0 +1,62 @@
+// the code here is taken (and slightly modified) from https://github.com/XadillaX/crlf2lf
+
+//
+// convert CRLF (0D 0A) to LF ( 0A )
+//
+
+use std::fs::File;
+use std::io::prelude::*;
+use std::io::{self, Read};
+use std::path::Path;
+
+// The caller has to make sure that crlf2lf is only
+// used for text files
+pub fn crlf2lf(fname: &str) -> Result<(), io::Error> {
+ let path = Path::new(fname);
+
+ let mut hdl_in;
+ match File::open(path) {
+ Ok(f) => {
+ hdl_in = f;
+ }
+ Err(e) => return Err(e),
+ };
+ let mut buffer: Vec<u8> = Vec::new();
+
+ // read the whole file
+ if let Err(e) = hdl_in.read_to_end(&mut buffer) {
+ return Err(e);
+ };
+
+ // convert \r\n to \n
+ let mut another_vec: Vec<u8> = Vec::new();
+ const CR: u8 = 0x0d; // 13
+ const LF: u8 = 0x0a; // 10
+
+ for i in 0..buffer.len() {
+ if buffer[i] == CR {
+ if i < buffer.len() - 1 && buffer[i + 1] == LF {
+ continue;
+ }
+
+ if i > 0 && buffer[i - 1] == LF {
+ continue;
+ }
+ }
+ another_vec.push(buffer[i]);
+ }
+
+ let mut hdl_out;
+ match File::create(path) {
+ Ok(f) => {
+ hdl_out = f;
+ }
+ Err(e) => return Err(e),
+ };
+
+ // write back
+ match hdl_out.write(&another_vec) {
+ Ok(_) => Ok(()),
+ Err(e) => Err(e),
+ }
+}
diff --git a/support/pkgcheck/src/utils.rs b/support/pkgcheck/src/utils.rs
new file mode 100644
index 0000000000..6b2d0a8753
--- /dev/null
+++ b/support/pkgcheck/src/utils.rs
@@ -0,0 +1,396 @@
+use std::fs::read_dir;
+use std::fs::read_link;
+use std::os::unix::fs::PermissionsExt;
+use std::path::Path;
+use std::path::PathBuf;
+use walkdir::DirEntry;
+
+use regex::Regex;
+use std::borrow::Cow;
+use std::io;
+use std::process::Command;
+
+
+use std::fs;
+
+pub fn temp_file_endings() -> Vec<(&'static str, &'static str)> {
+ // https://github.com/github/gitignore/blob/master/TeX.gitignore
+ // http://hopf.math.purdue.edu/doc/html/suffixes.html
+ vec![
+ ("-blx.aux", "bibliography auxiliary file"),
+ ("-blx.bib", "bibliography auxiliary file"),
+ (".4ct", "htlatex related"),
+ (".4tc", "htlatex related"),
+ (".DS_Store", "Mac OS custom attribute file"),
+ (".acn", "glossaries related"),
+ (".acr", "glossaries related"),
+ (".alg", ""),
+ (".aux", "core latex auxiliary file"),
+ (".backup", "backup file"),
+ (".bak", "backup file"),
+ (".bbl", "bibliography auxiliary file"),
+ (".bcf", "bibliography auxiliary file"),
+ (".blg", "bibliography log file"),
+ (".brf", "hyperref related"),
+ (".cb", "core latex auxiliary file"),
+ (".cb2", "core latex auxiliary file"),
+ (".cpt", "cprotect related"),
+ (".dvi", "intermediate document"),
+ (".ent", ""),
+ (".fdb_latexmk", "latexmk related"),
+ (".fff", "endfloat related"),
+ (".fls", ""),
+ (".fmt", "core latex auxiliary file"),
+ (".fot", "core latex auxiliary file"),
+ (".gaux", "generated by gregoriotex"),
+ (".glg", "glossary related"),
+ (".glo", "glossary related"),
+ (".glog", "generated by gregoriotex"),
+ (".gls", "glossary related"),
+ (".glsdefs", "glossaries related"),
+ (".gtex", "generated by gregoriotex"),
+ (".idv", "htlatex related"),
+ (".idx", "makeidx related"),
+ (".ilg", "makeidx related"),
+ (".ind", "makeidx related"),
+ (".lg", "htlatex related"),
+ (".loa", "core latex auxiliary file (list of algorithms)"),
+ (".lod", "generated by easy-todo"),
+ (".lof", "core latex auxiliary file (list of figures)"),
+ (".log", "a log file for any flavor of TeX"),
+ (".lol", "core latex auxiliary file (list of listings)"),
+ (".los", "list of slides"),
+ (".lot", "core latex auxiliary file"),
+ (".lox", ""),
+ (".lyx#", "LyX related autosave file"),
+ (".maf", "generated by minitoc"),
+ (".mlc", "generated by minitoc"),
+ (".mlf", "generated by minitoc"),
+ (".mlt", "generated by minitoc"),
+ (".nav", "beamer related"),
+ (".nlg", ""),
+ (".nlo", ""),
+ (".nls", ""),
+ (".o", "C object file"),
+ (".out", "Core latex auxiliary file"),
+ (".pdfsync", "pdfsync related"),
+ (".pre", "beamer related"),
+ (".pyg", ""),
+ (".run.xml", ""),
+ (".sav", "used for saved data"),
+ (".snm", "beamer related"),
+ (".soc", ""),
+ (".spl", "elsarticle related"),
+ (".sta", "generated by standalone package"),
+ (".swp", "vim swap file"),
+ (".synctex", "synctex related"),
+ (".synctex(busy)", "synctex related"),
+ (".synctex.gz", "synctex related"),
+ (".synctex.gz(busy)", "synctex related"),
+ (".tdo", "generated by todonotes (list of todos)"),
+ (".thm", "amsthm related"),
+ (".tmb", "generated by thumbs package"),
+ (".tmp", "indicates a temporary file"),
+ (".toc", "core latex auxiliary file (table of contents)"),
+ (".trc", "htlatex related"),
+ (".ttt", "endfloat related"),
+ (".tuc", ""),
+ (".upa", "generated by the soulpos package"),
+ (".upb", "generated by the soulpos package"),
+ (".url", "generated by jurabib"),
+ (".vrb", "beamer related"),
+ (".w18", "temporary file for the ifplatform package"),
+ (".xdv", "intermediate document"),
+ (".xref", "htlatex related"),
+ ("~", "a file name ending with ~ (tilde) is temporary anyway"),
+ // ( ".lyx~", "LyX related backup file" ),
+ ]
+}
+
+pub fn regex_temporary_file_endings() -> Regex {
+ let mut rv = String::new();
+ let mut first_time = true;
+ for (p, _) in temp_file_endings() {
+ if first_time {
+ rv.push('(');
+ first_time = false;
+ } else {
+ rv.push('|');
+ }
+ let px = str::replace(p, ".", "\\.");
+
+ rv.push_str(&px);
+ }
+ rv.push_str(")$");
+ Regex::new(&rv).unwrap()
+}
+
+pub fn is_temporary_file(entry: &str) -> bool {
+ lazy_static! {
+ static ref RE: Regex = regex_temporary_file_endings();
+ }
+
+ RE.is_match(entry)
+}
+
+pub fn is_empty_directory(entry: &DirEntry) -> Result<bool, io::Error> {
+ let s = entry.path().to_str().unwrap();
+ match read_dir(s) {
+ // try to read the directory specified
+ Ok(contents) => Ok(contents.count() == 0),
+ Err(e) => Err(e),
+ }
+}
+
+pub fn is_unwanted_directory(entry: &str) -> bool {
+ let names = ["__MACOSX"];
+ for n in &names {
+ if *n == entry {
+ return true;
+ };
+ }
+
+ false
+}
+
+// is_hidden checks for files or directories starting with a dot
+pub fn is_hidden(entry: &str) -> bool {
+ entry.starts_with('.')
+}
+
+// is_hidden checks for files or directories starting with a dot
+pub fn is_hidden_directory(entry: &DirEntry) -> bool {
+ let fname = entry.file_name();
+
+ if fname == "." {
+ return false;
+ };
+ if fname == "./" {
+ return false;
+ };
+
+ fname.to_str().map(|s| s.starts_with('.')).unwrap_or(false)
+}
+
+pub fn get_symlink(entry: &DirEntry) -> Result<Option<PathBuf>, io::Error> {
+ let r = entry.path().to_str().unwrap();
+
+ match read_link(r) {
+ Ok(o) => {
+ let full_path = if o.is_absolute() {
+ o
+ } else {
+ // make the relative path absolute
+ let p = entry.path().parent().unwrap();
+ p.join(&o)
+ };
+ if full_path.exists() {
+ Ok(Some(full_path))
+ } else {
+ Ok(None)
+ }
+ }
+ Err(e) => Err(e),
+ }
+}
+
+pub fn _is_symlink_broken(entry: &DirEntry) -> Result<bool, io::Error> {
+ let r = entry.path().to_str().unwrap();
+
+ match read_link(r) {
+ Ok(o) => {
+ let full_path = if o.is_absolute() {
+ o
+ } else {
+ // make the relative path absolute
+ let p = entry.path().parent().unwrap();
+ p.join(&o)
+ };
+ Ok(!full_path.exists())
+ }
+ Err(e) => Err(e),
+ }
+}
+
+// Runs `pdfinfo` to check a PDF document. If `pdfinfo`
+// returns non zero we assume that the PDF document is
+// corrupted.
+pub fn is_pdf_ok(fname: &str) -> CmdReturn {
+ run_cmd("/usr/bin/pdfinfo", &[fname])
+}
+
+pub fn get_perms(path: &Path) -> u32 {
+ match path.metadata() {
+ Ok(p) => p.permissions().mode(),
+ Err(_e) => 0,
+ }
+}
+
+pub fn others_match(p: u32, m: u32) -> bool {
+ p == m
+}
+
+#[test]
+fn test_others_have() {
+ assert_eq!(others_have(0o600, 4), false);
+ assert_eq!(others_have(0o601, 4), false);
+ assert_eq!(others_have(0o602, 4), false);
+ assert_eq!(others_have(0o603, 4), false);
+ assert_eq!(others_have(0o604, 4), true);
+ assert_eq!(others_have(0o605, 4), true);
+ assert_eq!(others_have(0o606, 4), true);
+ assert_eq!(others_have(0o607, 4), true);
+}
+
+// It checks if a permission `p` has the bits
+// given in `m` set for others
+pub fn others_have(p: u32, m: u32) -> bool {
+ let p1 = p & 0o0007;
+ p1 & m == m
+}
+
+#[test]
+fn test_owner_has() {
+ assert_eq!(owner_has(0o000, 4), false);
+ assert_eq!(owner_has(0o100, 4), false);
+ assert_eq!(owner_has(0o200, 4), false);
+ assert_eq!(owner_has(0o300, 4), false);
+ assert_eq!(owner_has(0o400, 4), true);
+ assert_eq!(owner_has(0o505, 4), true);
+ assert_eq!(owner_has(0o605, 4), true);
+ assert_eq!(owner_has(0o705, 4), true);
+}
+
+// It checks if a permission `p` has the bits
+// given in `m` set for the owner.
+pub fn owner_has(p: u32, m: u32) -> bool {
+ let p1 = p & 0o7777;
+ let m1 = m << 6;
+ p1 & m1 == m1
+}
+
+#[allow(dead_code)]
+fn owner_match(p: u32, m: u32) -> bool {
+ let p1 = p & 0o0700;
+
+ let m1 = m << 6;
+ p1 == m1
+}
+
+// Formats a permission value to octal for output
+pub fn perms_to_string(p: u32) -> String {
+ format!("{:04o}", p & 0o7777)
+}
+
+pub struct CmdReturn {
+ pub status: bool,
+ pub output: Option<String>,
+}
+
+// Runs a command in a shell
+// If the command returns 0 stdout gets captured. Otherwise
+// stderr gets captured and returned.
+pub fn run_cmd(cmd: &str, argument: &[&str]) -> CmdReturn {
+ let output = Command::new(cmd)
+ .args(argument.iter())
+ .output()
+ .expect("Failed to execute process");
+
+ if output.status.success() {
+ CmdReturn {
+ status: true,
+ output: Some(String::from_utf8_lossy(&output.stdout).to_string()),
+ }
+ } else {
+ CmdReturn {
+ status: false,
+ output: Some(String::from_utf8_lossy(&output.stderr).to_string()),
+ }
+ }
+}
+
+// Sets permissions for a file or directory
+// Sample invocation: set_perms("somfile", 0o644);
+pub fn set_perms(entry: &str, p: u32) -> bool {
+ let ps = &format!("{:o}", p);
+ let rc = run_cmd("chmod", &["-v", ps, entry]);
+
+ if rc.status {
+ if let Some(op) = rc.output {
+ print!("{}", op);
+ }
+ true
+ } else {
+ false
+ }
+}
+
+// returns true if file is a directory and does exist
+// returns false otherwise
+pub fn exists_dir(file: &str) -> bool {
+ match fs::metadata(file) {
+ Ok(attr) => attr.is_dir(),
+ Err(_) => false,
+ }
+}
+
+// returns true if file is a regular file and does exist
+// returns false otherwise
+pub fn exists_file(file: &str) -> bool {
+ match fs::metadata(file) {
+ Ok(attr) => attr.is_file(),
+ Err(_) => false,
+ }
+}
+
+#[test]
+fn test_dirname() {
+ assert!(dirname("/etc/fstab") == Some("/etc"));
+ assert!(dirname("/etc/") == Some("/etc"));
+ assert!(dirname("/") == Some("/"));
+}
+
+#[allow(dead_code)]
+pub fn dirname(entry: &str) -> Option<&str> {
+ if entry.ends_with('/') {
+ if entry.len() == 1 {
+ return Some(entry);
+ }
+ return Some(&entry[..entry.len() - 1]);
+ }
+ let pos = entry.rfind('/');
+ match pos {
+ None => None,
+ Some(pos) => Some(&entry[..pos]),
+ }
+}
+
+#[test]
+fn test_filename() {
+ assert!(filename("/etc/fstab") == Some("fstab"));
+ assert!(filename("fstab") == Some("fstab"));
+ assert!(filename("../pkgcheck.rs/testdirs/fira.tds.zip") == Some("fira.tds.zip"));
+ assert!(filename("/etc/") == None);
+ assert!(filename("/") == None);
+}
+
+pub fn filename(entry: &str) -> Option<&str> {
+ if entry.ends_with('/') {
+ return None;
+ }
+
+ let pos = entry.rfind('/');
+ match pos {
+ None => Some(entry),
+ Some(pos) => Some(&entry[pos + 1..]),
+ }
+}
+
+// Found here: https://codereview.stackexchange.com/questions/98536/extracting-the-last-component-basename-of-a-filesystem-path
+pub fn basename(path: &str) -> Cow<str> {
+ let mut pieces = path.rsplitn(2, |c| c == '/' || c == '\\');
+ match pieces.next() {
+ Some(p) => p.into(),
+ None => path.into(),
+ }
+}