Merge "Throw specific exception caused by invalid email regex in config" into stable-3.3
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
index 288afcf..b69fa04 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/BlockedKeywordValidator.java
@@ -221,15 +221,18 @@
   }
 
   private static Optional<CommentValidationFailure> validateComment(
-      ImmutableMap<String, Pattern>  blockedKeywordPatterns, CommentForValidation comment) {
+      ImmutableMap<String, Pattern> blockedKeywordPatterns, CommentForValidation comment) {
     // Uses HashSet data structure for de-duping found blocked keywords.
-    Set<String> findings = new LinkedHashSet<String>(
-        findBlockedKeywordsInString(blockedKeywordPatterns.values(), comment.getText()));
+    Set<String> findings =
+        new LinkedHashSet<String>(
+            findBlockedKeywordsInString(blockedKeywordPatterns.values(), comment.getText()));
     if (findings.isEmpty()) {
       return Optional.empty();
     }
-    return Optional.of(comment.failValidation(
-        String.format("banned words found in your comment (%s)", Iterables.toString(findings))));
+    return Optional.of(
+        comment.failValidation(
+            String.format(
+                "banned words found in your comment (%s)", Iterables.toString(findings))));
   }
 
   private static void checkCommitMessageForBlockedKeywords(
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
index e15ecb2..46ff9e1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/Module.java
@@ -24,6 +24,7 @@
     install(new PatternCacheModule());
     install(ContentTypeUtil.module());
 
+    install(PluginConfigValidator.module());
     install(FooterValidator.module());
     install(MaxPathLengthValidator.module());
     install(FileExtensionValidator.module());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigValidator.java b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigValidator.java
new file mode 100644
index 0000000..d9cc872
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigValidator.java
@@ -0,0 +1,194 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.uploadvalidator;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.flogger.FluentLogger;
+import com.google.common.primitives.Ints;
+import com.google.gerrit.entities.RefNames;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.ValidationMessage;
+import com.google.gerrit.server.project.ProjectConfig;
+import com.google.gerrit.server.project.ProjectLevelConfig;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+
+public class PluginConfigValidator implements CommitValidationListener {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private final String pluginName;
+
+  @Inject
+  PluginConfigValidator(@PluginName String pluginName) {
+    this.pluginName = pluginName;
+  }
+
+  public static AbstractModule module() {
+    return new AbstractModule() {
+      @Override
+      public void configure() {
+        DynamicSet.bind(binder(), CommitValidationListener.class).to(PluginConfigValidator.class);
+      }
+    };
+  }
+
+  @Override
+  public ImmutableList<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+      throws CommitValidationException {
+    String fileName = ProjectConfig.PROJECT_CONFIG;
+
+    try {
+      if (!receiveEvent.refName.equals(RefNames.REFS_CONFIG)) {
+        // the project.config file in refs/meta/config was not modified, so no need to
+        // modify
+        return ImmutableList.of();
+      }
+
+      ProjectLevelConfig.Bare cfg = loadConfig(receiveEvent, fileName);
+      // Project Level Config looks at what's in the refs/meta/config file.
+      ImmutableList<CommitValidationMessage> validationMessages =
+          validateConfig(fileName, cfg.getConfig());
+      if (!validationMessages.isEmpty()) {
+        throw new CommitValidationException(
+            exceptionMessage(fileName, cfg.getRevision()), validationMessages);
+      }
+      return ImmutableList.of();
+    } catch (IOException e) {
+      String errorMessage =
+          String.format(
+              "failed to validate file %s for revision %s in ref %s of project %s",
+              fileName,
+              receiveEvent.commit.getName(),
+              RefNames.REFS_CONFIG,
+              receiveEvent.project.getNameKey());
+      logger.atSevere().withCause(e).log("%s", errorMessage);
+      throw new CommitValidationException(errorMessage, e);
+    }
+  }
+
+  /**
+   * Loads the configuration from the file and revision.
+   *
+   * @param receiveEvent the receive event
+   * @param fileName the name of the config file
+   * @return the loaded configuration
+   * @throws CommitValidationException thrown if the configuration is invalid and cannot be parsed
+   */
+  private ProjectLevelConfig.Bare loadConfig(CommitReceivedEvent receiveEvent, String fileName)
+      throws CommitValidationException, IOException {
+    ProjectLevelConfig.Bare cfg = new ProjectLevelConfig.Bare(fileName);
+    try {
+      cfg.load(receiveEvent.project.getNameKey(), receiveEvent.revWalk, receiveEvent.commit);
+    } catch (ConfigInvalidException e) {
+      throw new CommitValidationException(
+          exceptionMessage(fileName, receiveEvent.commit),
+          new CommitValidationMessage(e.getMessage(), ValidationMessage.Type.ERROR));
+    }
+    return cfg;
+  }
+
+  /**
+   * Creates the message for {@link CommitValidationException}s that are thrown for validation
+   * errors in the project-level code-owners configuration.
+   *
+   * @param fileName the name of the config file
+   * @param revision the revision in which the configuration is invalid
+   * @return the created exception message
+   */
+  private static String exceptionMessage(String fileName, ObjectId revision) {
+    return String.format("invalid %s file in revision %s", fileName, revision.getName());
+  }
+
+  /**
+   * Validates the project.config for uploadvalidator
+   *
+   * @param fileName the name of the config file
+   * @param cfg the project-level code-owners configuration that should be validated
+   * @return list of messages with validation issues, empty list if there are no issues
+   */
+  public ImmutableList<CommitValidationMessage> validateConfig(String fileName, Config cfg) {
+    ImmutableList.Builder<CommitValidationMessage> validationMessages = ImmutableList.builder();
+    validationMessages.addAll(
+        validateRegex(fileName, cfg, ChangeEmailValidator.KEY_ALLOWED_AUTHOR_EMAIL_PATTERN));
+    validationMessages.addAll(
+        validateRegex(fileName, cfg, ChangeEmailValidator.KEY_ALLOWED_COMMITTER_EMAIL_PATTERN));
+    validationMessages.addAll(
+        validateInteger(fileName, cfg, MaxPathLengthValidator.KEY_MAX_PATH_LENGTH));
+    return validationMessages.build();
+  }
+
+  /**
+   * Validates the regex
+   *
+   * @param fileName the name of the config file
+   * @param cfg the project.config to validate
+   * @return list of messages with validation issues, empty list if there are no issues
+   */
+  @VisibleForTesting
+  public ImmutableList<CommitValidationMessage> validateRegex(
+      String fileName, Config cfg, String validatorKey) {
+
+    String pattern = cfg.getString("plugin", pluginName, validatorKey);
+
+    if (pattern != null) {
+      try {
+        Pattern.compile(pattern);
+      } catch (PatternSyntaxException e) {
+          return ImmutableList.of(
+            new CommitValidationMessage(
+                String.format(
+                    "The value '%s' configured in %s (parameter %s.%s) is invalid.",
+                    pattern, fileName, pluginName, validatorKey),
+                ValidationMessage.Type.ERROR));
+      }
+    }
+    return ImmutableList.of();
+  }
+
+  /**
+   * Validates an integer-only field
+   *
+   * @param fileName the name of the config file
+   * @param cfg the project.config to validate
+   * @return list of messages with validation issues, empty list if there are no issues
+   */
+  @VisibleForTesting
+  public ImmutableList<CommitValidationMessage> validateInteger(
+      String fileName, Config cfg, String validatorKey) {
+
+    String value = cfg.getString("plugin", pluginName, validatorKey);
+
+    if (Ints.tryParse(value) == null) {
+      return ImmutableList.of(
+          new CommitValidationMessage(
+              String.format(
+                  "The value '%s' configured in %s (parameter %s.%s) is invalid.",
+                  value, fileName, pluginName, validatorKey),
+              ValidationMessage.Type.ERROR));
+    }
+    return ImmutableList.of();
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigValidatorTest.java
new file mode 100644
index 0000000..685abb8
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/uploadvalidator/PluginConfigValidatorTest.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2022 The Android Open Source Project
+//
+// 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.
+
+package com.googlesource.gerrit.plugins.uploadvalidator;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.googlesource.gerrit.plugins.uploadvalidator.ChangeEmailValidator.KEY_ALLOWED_AUTHOR_EMAIL_PATTERN;
+import static com.googlesource.gerrit.plugins.uploadvalidator.ChangeEmailValidator.KEY_ALLOWED_COMMITTER_EMAIL_PATTERN;
+import static com.googlesource.gerrit.plugins.uploadvalidator.MaxPathLengthValidator.KEY_MAX_PATH_LENGTH;
+import static com.google.gerrit.server.project.ProjectConfig.PROJECT_CONFIG;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PluginConfigValidatorTest {
+  private static final String PLUGIN_NAME = "uploadvalidator";
+  private static final String ILLEGAL_REGEX = "*";
+  private static final String LEGAL_REGEX = ".*";
+  private static final String LEGAL_PATH_LENGTH = "100";
+  private static final String ILLEGAL_PATH_LENGTH = "10xi";
+
+  private PluginConfigValidator configValidator;
+  private Config cfg;
+
+  @Before
+  public void setUp() throws Exception {
+    configValidator = new PluginConfigValidator(PLUGIN_NAME);
+    cfg = new Config();
+  }
+
+  @Test
+  public void hasLegalAuthorEmailPattern_noMessages() throws Exception {
+    cfg.setString("plugin", PLUGIN_NAME, KEY_ALLOWED_AUTHOR_EMAIL_PATTERN, LEGAL_REGEX);
+    ImmutableList<CommitValidationMessage> messages =
+        configValidator.validateRegex(PROJECT_CONFIG, cfg, KEY_ALLOWED_AUTHOR_EMAIL_PATTERN);
+    assertThat(messages).isEmpty();
+  }
+
+  @Test
+  public void hasIllegalAuthorEmailPattern_messages() throws Exception {
+    cfg.setString("plugin", PLUGIN_NAME, KEY_ALLOWED_AUTHOR_EMAIL_PATTERN, ILLEGAL_REGEX);
+    ImmutableList<CommitValidationMessage> messages =
+        configValidator.validateRegex(PROJECT_CONFIG, cfg, KEY_ALLOWED_AUTHOR_EMAIL_PATTERN);
+    assertThat(messages).isNotEmpty();
+  }
+
+  @Test
+  public void hasLegalCommitterEmailPattern_noMessages() throws Exception {
+    cfg.setString("plugin", PLUGIN_NAME, KEY_ALLOWED_COMMITTER_EMAIL_PATTERN, LEGAL_REGEX);
+    ImmutableList<CommitValidationMessage> messages =
+        configValidator.validateRegex(PROJECT_CONFIG, cfg, KEY_ALLOWED_COMMITTER_EMAIL_PATTERN);
+    assertThat(messages).isEmpty();
+  }
+
+  @Test
+  public void hasIllegalCommitterEmailPattern_messages() throws Exception {
+    cfg.setString("plugin", PLUGIN_NAME, KEY_ALLOWED_COMMITTER_EMAIL_PATTERN, ILLEGAL_REGEX);
+    ImmutableList<CommitValidationMessage> messages =
+        configValidator.validateRegex(PROJECT_CONFIG, cfg, KEY_ALLOWED_COMMITTER_EMAIL_PATTERN);
+    assertThat(messages).isNotEmpty();
+  }
+  
+  @Test
+  public void hasLegalMaxPathLength_noMessages() throws Exception {
+    cfg.setString("plugin", PLUGIN_NAME, KEY_MAX_PATH_LENGTH, LEGAL_PATH_LENGTH);
+    ImmutableList<CommitValidationMessage> messages =
+        configValidator.validateInteger(PROJECT_CONFIG, cfg, KEY_MAX_PATH_LENGTH);
+    assertThat(messages).isEmpty();
+  }
+
+  @Test
+  public void hasIllegalMaxPathLength_messages() throws Exception {
+    cfg.setString("plugin", PLUGIN_NAME, KEY_MAX_PATH_LENGTH, ILLEGAL_PATH_LENGTH);
+
+    ImmutableList<CommitValidationMessage> messages =
+        configValidator.validateInteger(PROJECT_CONFIG, cfg, KEY_MAX_PATH_LENGTH);
+    assertThat(messages).isNotEmpty();
+  }
+}
\ No newline at end of file