From c6ebbb9c329f326aa3d932505c89e0401c599fe4 Mon Sep 17 00:00:00 2001 From: tibetiroka Date: Wed, 28 Feb 2024 09:42:49 +0100 Subject: [PATCH] Create dedicated linter types --- .../com/tibetiroka/deblint/ControlType.java | 15 +- .../com/tibetiroka/deblint/FieldLinter.java | 17 ++ .../com/tibetiroka/deblint/FieldSpec.java | 4 +- .../com/tibetiroka/deblint/FileLinter.java | 16 ++ .../java/com/tibetiroka/deblint/Linters.java | 171 +++++++++--------- .../com/tibetiroka/deblint/StanzaLinter.java | 16 ++ .../com/tibetiroka/deblint/StanzaSpec.java | 5 +- .../deblint/TypeCopyrightLinterTest.java | 26 +-- 8 files changed, 157 insertions(+), 113 deletions(-) create mode 100644 src/main/java/com/tibetiroka/deblint/FieldLinter.java create mode 100644 src/main/java/com/tibetiroka/deblint/FileLinter.java create mode 100644 src/main/java/com/tibetiroka/deblint/StanzaLinter.java diff --git a/src/main/java/com/tibetiroka/deblint/ControlType.java b/src/main/java/com/tibetiroka/deblint/ControlType.java index cc0a59f..0f6b049 100644 --- a/src/main/java/com/tibetiroka/deblint/ControlType.java +++ b/src/main/java/com/tibetiroka/deblint/ControlType.java @@ -11,7 +11,6 @@ package com.tibetiroka.deblint; import java.util.List; -import java.util.function.BiConsumer; /** * The types of supported control files. THey each have a description for use with {@code --type-info}, and their own linter configurations. @@ -29,21 +28,21 @@ public enum ControlType { /** * The file-wide linter for this type. */ - private final BiConsumer linter; + private final FileLinter linter; /** * The list of stanzas that can appear in this type, in their expected order. */ private final List stanzas; - /** - * The debian standard name for this control file type. - */ - private final String typeName; /** * Whether the file supports OpenPGP signatures. */ private final boolean supportsPgp; + /** + * The debian standard name for this control file type. + */ + private final String typeName; - private ControlType(String typeName, String defaultFile, String description, List stanzas, BiConsumer linter, boolean supportsPgp) { + private ControlType(String typeName, String defaultFile, String description, List stanzas, FileLinter linter, boolean supportsPgp) { this.typeName = typeName; this.defaultFile = defaultFile; this.description = description; @@ -76,7 +75,7 @@ public String getDescription() { * * @return {@link #linter} */ - public BiConsumer getLinter() { + public FileLinter getLinter() { return linter; } diff --git a/src/main/java/com/tibetiroka/deblint/FieldLinter.java b/src/main/java/com/tibetiroka/deblint/FieldLinter.java new file mode 100644 index 0000000..38d749e --- /dev/null +++ b/src/main/java/com/tibetiroka/deblint/FieldLinter.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2024 by tibetiroka. + * + * debian-control-linter is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * debian-control-linter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + +package com.tibetiroka.deblint; + +import java.util.function.BiConsumer; + +@FunctionalInterface +public interface FieldLinter extends BiConsumer { +} \ No newline at end of file diff --git a/src/main/java/com/tibetiroka/deblint/FieldSpec.java b/src/main/java/com/tibetiroka/deblint/FieldSpec.java index 847344a..51af2c8 100644 --- a/src/main/java/com/tibetiroka/deblint/FieldSpec.java +++ b/src/main/java/com/tibetiroka/deblint/FieldSpec.java @@ -10,8 +10,6 @@ package com.tibetiroka.deblint; -import java.util.function.BiConsumer; - /** * Specification for a field. * @@ -19,7 +17,7 @@ * @param type The type of this field, as the stanza requires it * @param linter The linter used for the value of this field */ -public record FieldSpec(RequirementStatus required, FieldType type, BiConsumer linter) { +public record FieldSpec(RequirementStatus required, FieldType type, FieldLinter linter) { /** * The importance of a data field. {@link #MANDATORY mandatory} fields are required for a {@link StanzaSpec} to apply to a parsed {@link Stanza}, */ diff --git a/src/main/java/com/tibetiroka/deblint/FileLinter.java b/src/main/java/com/tibetiroka/deblint/FileLinter.java new file mode 100644 index 0000000..4e9e334 --- /dev/null +++ b/src/main/java/com/tibetiroka/deblint/FileLinter.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 by tibetiroka. + * + * debian-control-linter is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * debian-control-linter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + +package com.tibetiroka.deblint; + +import java.util.function.BiConsumer; + +public interface FileLinter extends BiConsumer { +} \ No newline at end of file diff --git a/src/main/java/com/tibetiroka/deblint/Linters.java b/src/main/java/com/tibetiroka/deblint/Linters.java index 4483a15..accedd0 100644 --- a/src/main/java/com/tibetiroka/deblint/Linters.java +++ b/src/main/java/com/tibetiroka/deblint/Linters.java @@ -19,7 +19,6 @@ import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.function.BiConsumer; import java.util.regex.Pattern; import static com.tibetiroka.deblint.FieldSpec.RequirementStatus.MANDATORY; @@ -64,7 +63,7 @@ class Linters { * The list of operating systems, extracted from {@link #architectures}. */ public static final String[] systems = Arrays.stream(architectures).map(a -> a.indexOf('-') == a.lastIndexOf('-') ? a.split("-")[0] : a.split("-")[1]).toArray(String[]::new); - protected static final BiConsumer ARCHITECTURE_LINTER = (s, config) -> { + protected static final FieldLinter ARCHITECTURE_LINTER = (s, config) -> { boolean inverted = s.contains("!"); String[] declared = s.split(" "); for(String arch : declared) { @@ -105,7 +104,7 @@ class Linters { } } }; - protected static final BiConsumer BINARY_LIST_LINTER = (s, config) -> { + protected static final FieldLinter BINARY_LIST_LINTER = (s, config) -> { HashSet files = new HashSet<>(); if(config.checkedType == ControlType.SOURCE_CONTROL) { for(String string : s.split(",")) { @@ -131,19 +130,19 @@ class Linters { } } }; - protected static final BiConsumer BOOLEAN_LINTER = (s, config) -> { + protected static final FieldLinter BOOLEAN_LINTER = (s, config) -> { if(!s.equals("yes") && !s.equals("no")) { Main.error("Invalid boolean value; should be 'yes' or 'no': " + s, null, "https://www.debian.org/doc/debian-policy/ch-customized-programs.html#s-arch-wildcard-spec"); } }; - protected static final BiConsumer CHANGE_LIST_LINTER = (s, config) -> { + protected static final FieldLinter CHANGE_LIST_LINTER = (s, config) -> { String[] lines = s.split("\\n", -1); if(!lines[0].isBlank()) { Main.error("The first line of changes should be empty", null, "https://www.debian.org/doc/debian-policy/ch-controlfields#changes"); } //todo: check all title requirements from https://www.debian.org/doc/debian-policy/ch-controlfields#changes }; - protected static final BiConsumer COPYRIGHT_FILE_LIST_LINTER = (s, config) -> { + protected static final FieldLinter COPYRIGHT_FILE_LIST_LINTER = (s, config) -> { String[] patterns = s.split("\\n", -1); for(String pattern : patterns) { pattern = pattern.strip(); @@ -155,7 +154,7 @@ class Linters { } } }; - protected static final BiConsumer DATE_LINTER = (s, config) -> { + protected static final FieldLinter DATE_LINTER = (s, config) -> { // day-of-week, dd month yyyy hh:mm:ss +zzzz if(!Pattern.matches("^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \\d\\d? (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \\d{4} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4}$", s)) { Main.error("Invalid date: " + s); @@ -174,9 +173,9 @@ class Linters { /** * A linter that accepts any input. */ - protected static final BiConsumer DEFAULT_LINTER = (s, config) -> { + protected static final FieldLinter DEFAULT_LINTER = (s, config) -> { }; - protected static final BiConsumer DESCRIPTION_LINTER = (s, config) -> { + protected static final FieldLinter DESCRIPTION_LINTER = (s, config) -> { String[] lines = s.split("\n"); if(lines[0].isBlank()) { Main.error("Missing synopsys: ", null, "https://www.debian.org/doc/debian-policy/ch-controlfields#description"); @@ -187,7 +186,7 @@ class Linters { } } }; - protected static final BiConsumer DGIT_LINTER = (s, config) -> { + protected static final FieldLinter DGIT_LINTER = (s, config) -> { String[] parts = s.split(" "); if(config.dgitExtraData && parts.length > 1) { Main.error("Extra data after the commit hash is reserved for future expansion; do not use: " + s, "dgitExtraData", "https://www.debian.org/doc/debian-policy/ch-controlfields#dgit"); @@ -196,12 +195,12 @@ class Linters { Main.error("Invalid git hash: " + parts[0]); } }; - protected static final BiConsumer DISTRIBUTION_LINTER = (s, config) -> { + protected static final FieldLinter DISTRIBUTION_LINTER = (s, config) -> { if(config.multipleDistributions && s.contains(" ")) { Main.error("Please only use a single distribution: " + s, "multipleDistributions", "https://www.debian.org/doc/debian-policy/ch-controlfields#s-f-distribution"); } }; - protected static final BiConsumer LICENSE_LINTER = (s, config) -> { + protected static final FieldLinter LICENSE_LINTER = (s, config) -> { String[] parts = s.split("\\n"); if(s.split("\\n")[0].isBlank()) { Main.error("License must have a short name in the first line: ", null, "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-field"); @@ -231,12 +230,12 @@ class Linters { } } }; - protected static final BiConsumer MD5_LINTER = (s, config) -> { + protected static final FieldLinter MD5_LINTER = (s, config) -> { if(!Pattern.matches("^[a-fA-F0-9]{32}$", s)) { Main.error("Invalid MD5 hash: " + s); } }; - protected static final BiConsumer NUMBER_LIST_LINTER = (s, config) -> { + protected static final FieldLinter NUMBER_LIST_LINTER = (s, config) -> { String[] parts = s.split(" "); HashSet numbers = new HashSet<>(); for(String part : parts) { @@ -258,12 +257,12 @@ class Linters { } } }; - protected static final BiConsumer PACKAGE_NAME_LINTER = (s, config) -> { + protected static final FieldLinter PACKAGE_NAME_LINTER = (s, config) -> { if(!Pattern.matches("^[a-z0-9][a-z0-9+.\\-]+$", s)) { Main.error("Invalid package name: " + s); } }; - protected static final BiConsumer PACKAGE_TYPE_LINTER = (s, config) -> { + protected static final FieldLinter PACKAGE_TYPE_LINTER = (s, config) -> { if(config.unknownPackageType && !s.equals("deb") && !s.equals("udeb")) { Main.error("Unknown package type: " + s, "unknownPackageType"); } @@ -271,7 +270,7 @@ class Linters { Main.error("Package-Type should be omitted when using the default value: " + s, "redundantPackageType", "https://www.debian.org/doc/debian-policy/ch-controlfields#package-type"); } }; - protected static final BiConsumer PRIORITY_LINTER = (s, config) -> { + protected static final FieldLinter PRIORITY_LINTER = (s, config) -> { if(config.unknownPriority) { String[] priorities = {"required", "important", "standard", "optional", "extra"}; if(Arrays.stream(priorities).noneMatch(p -> p.equals(s))) { @@ -282,7 +281,7 @@ class Linters { Main.error("The 'extra' priority is deprecated, use 'optional' instead", "extraPriority", "https://www.debian.org/doc/debian-policy/ch-archive.html#s-priorities"); } }; - protected static final BiConsumer REQUIRES_ROOT_LINTER = (s, config) -> { + protected static final FieldLinter REQUIRES_ROOT_LINTER = (s, config) -> { if(s.equals("no") || s.equals("binary-targets")) { return; } @@ -295,7 +294,7 @@ class Linters { } } }; - protected static final BiConsumer RFC_822_LINTER = (s, config) -> { + protected static final FieldLinter RFC_822_LINTER = (s, config) -> { try { InternetAddress emailAddr = new InternetAddress(s); emailAddr.validate(); @@ -303,7 +302,7 @@ class Linters { Main.error("Invalid email address: " + s, null, "https://www.w3.org/Protocols/rfc822/"); } }; - protected static final BiConsumer ADDRESS_LINTER = (s, config) -> { + protected static final FieldLinter ADDRESS_LINTER = (s, config) -> { int begin = s.indexOf('<'); int end = s.lastIndexOf('>'); if(begin == -1 || end == -1 || end < begin) { @@ -321,12 +320,12 @@ class Linters { Main.error("Missing name: " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#maintainer"); } }; - protected static final BiConsumer MULTI_ADDRESS_LINTER = (s, config) -> { + protected static final FieldLinter MULTI_ADDRESS_LINTER = (s, config) -> { for(String address : s.split(",", -1)) { ADDRESS_LINTER.accept(address, config); } }; - protected static final BiConsumer UPSTREAM_CONTACT_LINTER = (s, config) -> { + protected static final FieldLinter UPSTREAM_CONTACT_LINTER = (s, config) -> { if(config.upstreamContactStyle) { try { URL u = new URI(s).toURL(); @@ -336,7 +335,7 @@ class Linters { } } }; - protected static final BiConsumer SECTION_LINTER = (s, config) -> { + protected static final FieldLinter SECTION_LINTER = (s, config) -> { String[] areas = {"contrib", "non-free"}; String[] sections = {"admin", "cli-mono", "comm", "database", "debian-installer", "debug", "devel", "doc", "editors", "education", "electronics", "embedded", "fonts", "games", "gnome", "gnu-r", "gnustep", "graphics", "hamradio", "haskell", "httpd", "interpreters", "introspection", "java", "javascript", "kde", "kernel", "libdevel", "libs", "lisp", "localization", "mail", "math", "metapackages", "misc", "net", "news", "ocaml", "oldlibs", "otherosfs", "perl", "php", "python", "ruby", "rust", "science", "shells", "sound", "tasks", "tex", "text", "utils", "vcs", "video", "web", "x11", "xfce", "zope"}; String section; @@ -356,7 +355,7 @@ class Linters { Main.error("debian-installer section should not be used here: " + s, "debianInstallerSection", "https://www.debian.org/doc/debian-policy/ch-archive.html#s-subsections"); } }; - protected static final BiConsumer PACKAGE_LIST_LINTER = (s, config) -> { + protected static final FieldLinter PACKAGE_LIST_LINTER = (s, config) -> { String[] lines = s.split("\\n", -1); if(!lines[0].isBlank()) { Main.error("Package-List must begin with an empty line: " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#s-f-package-list"); @@ -380,7 +379,42 @@ class Linters { } } }; - protected static final BiConsumer SIZE_LINTER = (s, config) -> { + protected static final FieldLinter SINGLE_ARCHITECTURE_LINTER = (s, config) -> { + ArrayList arches = new ArrayList<>(List.of(s.split(" "))); + if(config.duplicateArchitecture && arches.size() > new HashSet<>(arches).size()) { + Main.error("Duplicated architecture: " + s, "duplicateArchitecture"); + } + if(config.checkedType == ControlType.SOURCE_PACKAGE_CONTROL) { + if(!s.equals("all") && !s.equals("any")) { + if(arches.stream().anyMatch(a -> a.equals("all") || a.equals("any"))) { + Main.error("'all' or 'any' must be the only entries, if present: " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#architecture"); + } else { + ARCHITECTURE_LINTER.accept(String.join(" ", arches.stream().filter(a -> !a.equals("all")).toList()), config); + } + } + } else if(config.checkedType == ControlType.SOURCE_CONTROL) { + if(arches.contains("any")) { + if(!arches.stream().allMatch(a -> a.equals("any") || a.equals("all"))) { + Main.error("When 'any' is present in a list, the only other value allowed is 'all': " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#architecture"); + } + } else { + ARCHITECTURE_LINTER.accept(String.join(" ", arches.stream().filter(a -> !a.equals("all")).toList()), config); + } + } else if(config.checkedType == ControlType.CHANGES) { + HashSet archSet = new HashSet<>(arches); + archSet.remove("source"); + if(archSet.contains("any") || archSet.stream().anyMatch(a -> a.startsWith("any-") || a.endsWith("-any"))) { + Main.error("Architecture wildcards are not allowed in .changes files: " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#architecture"); + } else { + if(!archSet.isEmpty()) { + ARCHITECTURE_LINTER.accept(String.join(" ", archSet), config); + } + } + } else { + ARCHITECTURE_LINTER.accept(s, config); + } + }; + protected static final FieldLinter SIZE_LINTER = (s, config) -> { try { Long l = Long.parseLong(s); if(l < 0) { @@ -392,7 +426,7 @@ class Linters { Main.error("Invalid size: " + s); } }; - protected static final BiConsumer SHA1_LINTER = (s, config) -> { + protected static final FieldLinter SHA1_LINTER = (s, config) -> { String[] lines = s.split("\\n"); if(!lines[0].isBlank()) { Main.error("The first line of checksums should be empty"); @@ -409,7 +443,7 @@ class Linters { SIZE_LINTER.accept(parts[1], config); }); }; - protected static final BiConsumer SHA256_LINTER = (s, config) -> { + protected static final FieldLinter SHA256_LINTER = (s, config) -> { String[] lines = s.split("\n"); if(!lines[0].isBlank()) { Main.error("The first line of checksums should be empty"); @@ -426,42 +460,7 @@ class Linters { SIZE_LINTER.accept(parts[1], config); }); }; - protected static final BiConsumer SINGLE_ARCHITECTURE_LINTER = (s, config) -> { - ArrayList arches = new ArrayList<>(List.of(s.split(" "))); - if(config.duplicateArchitecture && arches.size() > new HashSet<>(arches).size()) { - Main.error("Duplicated architecture: " + s, "duplicateArchitecture"); - } - if(config.checkedType == ControlType.SOURCE_PACKAGE_CONTROL) { - if(!s.equals("all") && !s.equals("any")) { - if(arches.stream().anyMatch(a -> a.equals("all") || a.equals("any"))) { - Main.error("'all' or 'any' must be the only entries, if present: " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#architecture"); - } else { - ARCHITECTURE_LINTER.accept(String.join(" ", arches.stream().filter(a -> !a.equals("all")).toList()), config); - } - } - } else if(config.checkedType == ControlType.SOURCE_CONTROL) { - if(arches.contains("any")) { - if(!arches.stream().allMatch(a -> a.equals("any") || a.equals("all"))) { - Main.error("When 'any' is present in a list, the only other value allowed is 'all': " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#architecture"); - } - } else { - ARCHITECTURE_LINTER.accept(String.join(" ", arches.stream().filter(a -> !a.equals("all")).toList()), config); - } - } else if(config.checkedType == ControlType.CHANGES) { - HashSet archSet = new HashSet<>(arches); - archSet.remove("source"); - if(archSet.contains("any") || archSet.stream().anyMatch(a -> a.startsWith("any-") || a.endsWith("-any"))) { - Main.error("Architecture wildcards are not allowed in .changes files: " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#architecture"); - } else { - if(!archSet.isEmpty()) { - ARCHITECTURE_LINTER.accept(String.join(" ", archSet), config); - } - } - } else { - ARCHITECTURE_LINTER.accept(s, config); - } - }; - protected static final BiConsumer FILE_LIST_LINTER = (s, config) -> { + protected static final FieldLinter FILE_LIST_LINTER = (s, config) -> { String[] lines = s.split("\\n"); if(!lines[0].isBlank()) { Main.error("The first line of 'Files' should be empty: " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#s-f-files"); @@ -518,7 +517,7 @@ class Linters { } } }; - protected static final BiConsumer STANDARDS_VERSION_LINTER = (s, config) -> { + protected static final FieldLinter STANDARDS_VERSION_LINTER = (s, config) -> { String[] parts = s.split("\\."); int[] latest = {4, 6, 2, 1}; if(parts.length < 3 || parts.length > 4) { @@ -540,7 +539,7 @@ class Linters { } } }; - protected static final BiConsumer STANZA_CHECKSUM_LINTER = (s, config) -> { + protected static final StanzaLinter STANZA_CHECKSUM_LINTER = (s, config) -> { String[] hashes = {"Checksums-Sha1", "Checksums-Sha256"}; HashSet files = new HashSet<>(); DataField fileField = s.getField("Files"); @@ -565,9 +564,9 @@ class Linters { } } }; - protected static final BiConsumer STANZA_DEFAULT_LINTER = (s, config) -> { + protected static final StanzaLinter STANZA_DEFAULT_LINTER = (s, config) -> { }; - protected static final BiConsumer STANZA_SOURCE_LINTER = (s, config) -> { + protected static final StanzaLinter STANZA_SOURCE_LINTER = (s, config) -> { if(config.sourceRedundantVersion) { DataField source = s.getField("source"); DataField version = s.getField("Version"); @@ -584,11 +583,11 @@ class Linters { } } }; - protected static final BiConsumer STANZA_SOURCE_AND_CHECKSUM_LINTER = (s, config) -> { + protected static final StanzaLinter STANZA_SOURCE_AND_CHECKSUM_LINTER = (s, config) -> { STANZA_SOURCE_LINTER.accept(s, config); STANZA_CHECKSUM_LINTER.accept(s, config); }; - protected static final BiConsumer STANZA_VCS_LINTER = (s, config) -> { + protected static final StanzaLinter STANZA_VCS_LINTER = (s, config) -> { String[] vcsFields = {"Vcs-Arch", "Vcs-Bzr", "Vcs-Cvs", "Vcs-Darcs", "Vcs-Git", "Vcs-Hg", "Vcs-Mtn", "Vcs-Svn"}; boolean found = false; for(String vcsField : vcsFields) { @@ -601,23 +600,23 @@ class Linters { } } }; - protected static final BiConsumer STANZA_SOURCE_AND_VCS_LINTER = (s, config) -> { + protected static final StanzaLinter STANZA_SOURCE_AND_VCS_LINTER = (s, config) -> { STANZA_VCS_LINTER.accept(s, config); STANZA_SOURCE_LINTER.accept(s, config); }; - protected static final BiConsumer STANZA_SOURCE_CONTROL_LINTER = (s, config) -> { + protected static final StanzaLinter STANZA_SOURCE_CONTROL_LINTER = (s, config) -> { STANZA_VCS_LINTER.accept(s, config); STANZA_SOURCE_LINTER.accept(s, config); STANZA_CHECKSUM_LINTER.accept(s, config); }; - protected static final BiConsumer TYPE_COPYRIGHT_LINTER = new TypeCopyrightLinter(); - protected static final BiConsumer UPSTREAM_VERSION_LINTER = (s, config) -> { + protected static final FileLinter TYPE_COPYRIGHT_LINTER = new TypeCopyrightLinter(); + protected static final FieldLinter UPSTREAM_VERSION_LINTER = (s, config) -> { Pattern upstream = Pattern.compile("^[0-9][A-Za-z0-9.+~\\-]*$"); if(!upstream.matcher(s).matches()) { Main.error("Upstream version uses an invalid format: " + s, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#version"); } }; - protected static final BiConsumer FORMAT_VERSION_LINTER = (s, config) -> { + protected static final FieldLinter FORMAT_VERSION_LINTER = (s, config) -> { if(config.checkedType == ControlType.CHANGES) { UPSTREAM_VERSION_LINTER.accept(s, config); if(config.exactFormatVersion && !s.equals("1.8")) { @@ -635,7 +634,7 @@ class Linters { } } }; - protected static final BiConsumer URGENCY_LINTER = (s, config) -> { + protected static final FieldLinter URGENCY_LINTER = (s, config) -> { String[] urgencies = {"low", "medium", "high", "emergency", "critical"}; String[] parts = s.split(" ", 2); if(config.customUrgencies && Arrays.stream(urgencies).noneMatch(a -> a.equalsIgnoreCase(parts[0]))) { @@ -647,7 +646,7 @@ class Linters { } } }; - protected static final BiConsumer URL_LINTER = (s, config) -> { + protected static final FieldLinter URL_LINTER = (s, config) -> { try { URL u = new URI(s).toURL(); checkUrl(u, config); @@ -655,7 +654,7 @@ class Linters { Main.error("Invalid URL: " + s); } }; - protected static final BiConsumer GIT_VCS_LINTER = (s, config) -> { + protected static final FieldLinter GIT_VCS_LINTER = (s, config) -> { String[] parts = s.split(" ", 2); String url = parts[0]; URL_LINTER.accept(url, config); @@ -691,7 +690,7 @@ class Linters { Main.error("Missing branch definition for Git: " + s, "vcsBranch", "https://www.debian.org/doc/debian-policy/ch-controlfields#s-f-vcs-fields"); } }; - protected static final BiConsumer MERCURIAL_VCS_LINTER = (s, config) -> { + protected static final FieldLinter MERCURIAL_VCS_LINTER = (s, config) -> { String[] parts = s.split(" ", 2); String url = parts[0]; URL_LINTER.accept(url, config); @@ -707,7 +706,7 @@ class Linters { Main.error("Missing branch definition for Mercurial: " + s, "vcsBranch", "https://www.debian.org/doc/debian-policy/ch-controlfields#s-f-vcs-fields"); } }; - protected static final BiConsumer COPYRIGHT_FORMAT_LINTER = (s, config) -> { + protected static final FieldLinter COPYRIGHT_FORMAT_LINTER = (s, config) -> { URL_LINTER.accept(s, config); if(config.strictCopyrightFormatVersion) { if(!s.equals("https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/")) { @@ -715,7 +714,7 @@ class Linters { } } }; - protected static final BiConsumer COPYRIGHT_SOURCE_LINTER = (s, config) -> { + protected static final FieldLinter COPYRIGHT_SOURCE_LINTER = (s, config) -> { if(config.copyrightSourceStyle) { try { URL_LINTER.accept(s, config); @@ -724,7 +723,7 @@ class Linters { } } }; - protected static final BiConsumer VERSION_LINTER = (s, config) -> { + protected static final FieldLinter VERSION_LINTER = (s, config) -> { String[] epochSplit = s.split(":", 2); String epoch = epochSplit.length == 2 ? epochSplit[0] : "0"; String remaining = epochSplit.length == 2 ? epochSplit[1] : epochSplit[0]; @@ -748,7 +747,7 @@ class Linters { Main.error("Debian version uses an invalid format: " + debianRevision, null, "https://www.debian.org/doc/debian-policy/ch-controlfields#version"); } }; - protected static final BiConsumer DEPENDENCY_LINTER = (s, config) -> { + protected static final FieldLinter DEPENDENCY_LINTER = (s, config) -> { String[] packages = s.split("[|,]"); String[] operators = {"<<", "<=", "=", ">=", ">>"}; for(String aPackage : packages) { @@ -790,7 +789,7 @@ class Linters { } } }; - protected static final BiConsumer EXACT_DEPENDENCY_LINTER = (s, config) -> { + protected static final FieldLinter EXACT_DEPENDENCY_LINTER = (s, config) -> { String[] forbidden_operators = {"<<", "<=", ">=", ">>"}; for(String op : forbidden_operators) { if(s.contains(op)) { @@ -799,7 +798,7 @@ class Linters { } DEPENDENCY_LINTER.accept(s, config); }; - protected static final BiConsumer SOURCE_LINTER = (s, config) -> { + protected static final FieldLinter SOURCE_LINTER = (s, config) -> { String[] parts = s.split("\\(", 2); PACKAGE_NAME_LINTER.accept(parts[0].stripTrailing(), config); if(parts.length == 2) { @@ -810,7 +809,7 @@ class Linters { VERSION_LINTER.accept(version, config); } }; - private static final BiConsumer STANZA_COPYRIGHT_HEADER_LINTER = (s, config) -> { + private static final StanzaLinter STANZA_COPYRIGHT_HEADER_LINTER = (s, config) -> { if(s.getField("Copyright") != null && s.getField("License") == null) { Main.error("A Copyright field alone is not sufficient; please include a License field as well when an explanation is needed: Copyright", null, "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#header-stanza"); } @@ -1009,7 +1008,7 @@ private static void checkUrl(URL u, Configuration config) { /** * A linter fpr {@link ControlType#COPYRIGHT} files. */ - public static class TypeCopyrightLinter implements BiConsumer { + public static class TypeCopyrightLinter implements FileLinter { /** * A cache for compiled patters, used in {@link #toRegex(String)}. */ diff --git a/src/main/java/com/tibetiroka/deblint/StanzaLinter.java b/src/main/java/com/tibetiroka/deblint/StanzaLinter.java new file mode 100644 index 0000000..e0b58e1 --- /dev/null +++ b/src/main/java/com/tibetiroka/deblint/StanzaLinter.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 by tibetiroka. + * + * debian-control-linter is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * debian-control-linter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with this program. If not, see . + */ + +package com.tibetiroka.deblint; + +import java.util.function.BiConsumer; + +public interface StanzaLinter extends BiConsumer { +} \ No newline at end of file diff --git a/src/main/java/com/tibetiroka/deblint/StanzaSpec.java b/src/main/java/com/tibetiroka/deblint/StanzaSpec.java index 78ab31e..9e06753 100644 --- a/src/main/java/com/tibetiroka/deblint/StanzaSpec.java +++ b/src/main/java/com/tibetiroka/deblint/StanzaSpec.java @@ -12,9 +12,8 @@ import com.tibetiroka.deblint.FieldSpec.RequirementStatus; -import java.util.HashMap; +import java.util.Map; import java.util.Map.Entry; -import java.util.function.BiConsumer; /** * A stanza spec is a description of what a stanza should or could contain. @@ -25,7 +24,7 @@ * @param fields The fields this stanza can contain * @param linter The linter used for this stanza */ -public record StanzaSpec(String name, boolean mandatory, boolean repeatable, HashMap fields, BiConsumer linter) { +public record StanzaSpec(String name, boolean mandatory, boolean repeatable, Map fields, StanzaLinter linter) { /** * Checks whether this spec matches the given stanza. A spec matches a stanza if all required fields are present. The types of the fields are not checked, so it is possible that this method returns true for a match that does not strictly follow the specification. These are reported in {@link #match(Stanza, Configuration)}. * diff --git a/src/test/java/com/tibetiroka/deblint/TypeCopyrightLinterTest.java b/src/test/java/com/tibetiroka/deblint/TypeCopyrightLinterTest.java index a159b7e..bbdb66b 100644 --- a/src/test/java/com/tibetiroka/deblint/TypeCopyrightLinterTest.java +++ b/src/test/java/com/tibetiroka/deblint/TypeCopyrightLinterTest.java @@ -21,19 +21,6 @@ import static org.junit.jupiter.api.Assertions.*; public final class TypeCopyrightLinterTest { - @ParameterizedTest - @CsvSource({"*,*,true", "a,*,false", "a,?,false", "*,a,true", "*,?,true", "?,*,false", "??????????,*,false", "*,??????????,true", "?,a,true", "a*a,aa?a,true", "aa?a,a*a,false", "images/ship/pointedstick?vanguard*,images/ship/pointedstick?vanguard*,true"}) - public void isMoreGeneric(String a, String b, boolean result) { - assertEquals(result, new TypeCopyrightLinter().isMoreGeneric(a, b)); - } - - @ParameterizedTest - @CsvSource({"a,^(\\./)?a$", "hello?there.txt,^(\\./)?hello.there\\.txt$", "file(name)*,^(\\./)?file\\(name\\).*$"}) - public void toRegex(String pattern, String regex) { - Pattern p = new TypeCopyrightLinter().toRegex(pattern); - assertEquals(regex, p.pattern()); - } - @Test public void checkCopyrightNames() { Configuration config = Configuration.PRESET_EXACT.clone(); @@ -132,6 +119,19 @@ public void checkCopyrightNames() { """)); } + @ParameterizedTest + @CsvSource({"*,*,true", "a,*,false", "a,?,false", "*,a,true", "*,?,true", "?,*,false", "??????????,*,false", "*,??????????,true", "?,a,true", "a*a,aa?a,true", "aa?a,a*a,false", "images/ship/pointedstick?vanguard*,images/ship/pointedstick?vanguard*,true"}) + public void isMoreGeneric(String a, String b, boolean result) { + assertEquals(result, new TypeCopyrightLinter().isMoreGeneric(a, b)); + } + + @ParameterizedTest + @CsvSource({"a,^(\\./)?a$", "hello?there.txt,^(\\./)?hello.there\\.txt$", "file(name)*,^(\\./)?file\\(name\\).*$", "hello[there?]],^(\\./)?hello\\[there.]]$"}) + public void toRegex(String pattern, String regex) { + Pattern p = new TypeCopyrightLinter().toRegex(pattern); + assertEquals(regex, p.pattern()); + } + private void lint(Configuration config, String text) throws Exception { ControlFile file = new ControlFile(config); file.parse(Arrays.asList(text.split("\\n")));