diff --git a/src/main/java/io/github/ascopes/protobufmavenplugin/dependency/JvmPluginResolver.java b/src/main/java/io/github/ascopes/protobufmavenplugin/dependency/JvmPluginResolver.java index 9e4a140d..b51c89e2 100644 --- a/src/main/java/io/github/ascopes/protobufmavenplugin/dependency/JvmPluginResolver.java +++ b/src/main/java/io/github/ascopes/protobufmavenplugin/dependency/JvmPluginResolver.java @@ -20,6 +20,7 @@ import io.github.ascopes.protobufmavenplugin.system.Digests; import io.github.ascopes.protobufmavenplugin.system.FileUtils; import io.github.ascopes.protobufmavenplugin.system.HostSystem; +import io.github.ascopes.protobufmavenplugin.system.Shlex; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; @@ -151,70 +152,27 @@ private Path writeWindowsBatchScript( ) throws IOException { var fullScriptPath = resolvePluginScriptPath().resolve(scriptNamePrefix + ".bat"); - var script = new StringBuilder() - .append("@echo off\r\n"); - for (var arg : argLine) { - quoteBatchArg(script, arg); - script.append(' '); - } - script.append("\r\n"); + var script = "@echo off\r\n" + + Shlex.quoteBatchArgs(argLine) + + "\r\n"; Files.writeString(fullScriptPath, script, Charset.defaultCharset()); return fullScriptPath; } - private void quoteBatchArg(StringBuilder sb, String arg) { - for (var i = 0; i < arg.length(); ++i) { - var c = arg.charAt(i); - switch (c) { - case '\\': - case '"': - case '\'': - case ' ': - case '\r': - case '\t': - case '^': - case '&': - case '<': - case '>': - case '|': - sb.append('^'); - } - - sb.append(c); - } - } - private Path writeShellScript( String scriptNamePrefix, List argLine ) throws IOException { var fullScriptPath = resolvePluginScriptPath().resolve(scriptNamePrefix + ".sh"); - var script = new StringBuilder() - .append("#!/usr/bin/env sh\n") - .append("set -eux\n"); - for (var arg : argLine) { - quoteShellArg(script, arg); - script.append(' '); - } - script.append('\n'); + var script = "#!/usr/bin/env sh\n" + + "set -eu\n" + + Shlex.quoteShellArgs(argLine) + + "\n"; Files.writeString(fullScriptPath, script, Charset.defaultCharset()); FileUtils.makeExecutable(fullScriptPath); return fullScriptPath; } - - private void quoteShellArg(StringBuilder sb, String arg) { - sb.append('\''); - for (var i = 0; i < arg.length(); ++i) { - var c = arg.charAt(i); - if (c == '\'') { - sb.append("'\"'\"'"); - } else { - sb.append(c); - } - } - sb.append('\''); - } } diff --git a/src/main/java/io/github/ascopes/protobufmavenplugin/execute/CommandLineExecutor.java b/src/main/java/io/github/ascopes/protobufmavenplugin/execute/CommandLineExecutor.java index 7487305e..650a669e 100644 --- a/src/main/java/io/github/ascopes/protobufmavenplugin/execute/CommandLineExecutor.java +++ b/src/main/java/io/github/ascopes/protobufmavenplugin/execute/CommandLineExecutor.java @@ -15,6 +15,7 @@ */ package io.github.ascopes.protobufmavenplugin.execute; +import io.github.ascopes.protobufmavenplugin.system.Shlex; import java.io.IOException; import java.io.InterruptedIOException; import java.util.List; @@ -39,7 +40,7 @@ public CommandLineExecutor() { } public boolean execute(List args) throws IOException { - log.info("Calling protoc with the following command line: {}", args); + log.info("Calling protoc with the following command line: {}", Shlex.quoteShellArgs(args)); var procBuilder = new ProcessBuilder(args); procBuilder.environment().putAll(System.getenv()); diff --git a/src/main/java/io/github/ascopes/protobufmavenplugin/system/Shlex.java b/src/main/java/io/github/ascopes/protobufmavenplugin/system/Shlex.java new file mode 100644 index 00000000..7b0f561d --- /dev/null +++ b/src/main/java/io/github/ascopes/protobufmavenplugin/system/Shlex.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 - 2024, Ashley Scopes. + * + * 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 io.github.ascopes.protobufmavenplugin.system; + +import java.util.function.BiConsumer; + +/** + * Shell/batch file quoting. + * + *

Losely based on Python's {@code shlex} module. + * + * @author Ashley Scopes + */ +public final class Shlex { + + private Shlex() { + // Static-only class + } + + public static String quoteShellArgs(Iterable args) { + return quote(args, Shlex::quoteShellArg); + } + + public static String quoteBatchArgs(Iterable args) { + return quote(args, Shlex::quoteBatchArg); + } + + private static String quote(Iterable args, BiConsumer quoter) { + var iter = args.iterator(); + var sb = new StringBuilder(); + quoter.accept(sb, iter.next()); + + while (iter.hasNext()) { + sb.append(' '); + quoter.accept(sb, iter.next()); + } + + return sb.toString(); + } + + private static void quoteShellArg(StringBuilder sb, String arg) { + if (isSafe(arg)) { + sb.append(arg); + return; + } + + sb.append('\''); + for (var i = 0; i < arg.length(); ++i) { + var c = arg.charAt(i); + if (c == '\'') { + sb.append("'\"'\"'"); + } else { + sb.append(c); + } + } + sb.append('\''); + } + + private static void quoteBatchArg(StringBuilder sb, String arg) { + if (isSafe(arg)) { + sb.append(arg); + return; + } + + for (var i = 0; i < arg.length(); ++i) { + var c = arg.charAt(i); + switch (c) { + case '\\': + case '"': + case '\'': + case ' ': + case '\r': + case '\t': + case '^': + case '&': + case '<': + case '>': + case '|': + sb.append('^'); + } + + sb.append(c); + } + } + + private static boolean isSafe(String arg) { + for (var i = 0; i < arg.length(); ++i) { + var c = arg.charAt(i); + var safe = 'A' <= c && c <= 'Z' + || 'a' <= c && c <= 'z' + || '0' <= c && c <= '9' + || c == '-' + || c == '/' + || c == '_' + || c == '.' + || c == '='; + + if (!safe) { + return false; + } + } + + return true; + } +}