Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package com.defold.extender.process;

import java.util.ArrayList;
import java.util.List;

public final class CommandLineTokenizer {
private CommandLineTokenizer() {}

public static List<String> parse(String command) {
return tokenize(command, false);
}

public static List<String> splitPreservingEscapedWhitespace(String arguments) {
return tokenize(arguments, true);
}

public static String escapeWhitespace(String argument) {
StringBuilder result = new StringBuilder();
boolean escaping = false;

for (int i = 0; i < argument.length(); ++i) {
char c = argument.charAt(i);
if (escaping) {
result.append('\\');
result.append(c);
escaping = false;
} else if (c == '\\') {
escaping = true;
} else if (Character.isWhitespace(c)) {
result.append('\\');
result.append(c);
} else {
result.append(c);
}
}

if (escaping) {
result.append('\\');
}

return result.toString();
}

private static List<String> tokenize(String input, boolean preserveEscapes) {
List<String> result = new ArrayList<>();
if (input == null || input.trim().isEmpty()) {
return result;
}

StringBuilder token = new StringBuilder();
boolean inToken = false;
boolean escaping = false;
char quote = 0;
boolean literalQuote = false;

for (int i = 0; i < input.length(); ++i) {
char c = input.charAt(i);

if (escaping) {
appendEscaped(token, c, preserveEscapes);
inToken = true;
escaping = false;
continue;
}

if (c == '\\') {
escaping = true;
inToken = true;
continue;
}

if (quote != 0) {
if (c == quote) {
if (literalQuote) {
token.append(c);
}
quote = 0;
literalQuote = false;
} else {
appendLiteral(token, c, preserveEscapes);
}
inToken = true;
continue;
}

if (c == '\'' || c == '"') {
literalQuote = isLiteralQuote(token, preserveEscapes);
if (literalQuote) {
token.append(c);
}
quote = c;
inToken = true;
continue;
}

if (Character.isWhitespace(c)) {
if (inToken) {
result.add(token.toString());
token.setLength(0);
inToken = false;
}
continue;
}

token.append(c);
inToken = true;
}

if (escaping) {
throw new IllegalArgumentException("Dangling escape in command line: " + input);
}
if (quote != 0) {
throw new IllegalArgumentException("Unclosed quote in command line: " + input);
}
if (inToken) {
result.add(token.toString());
}

return result;
}

private static void appendEscaped(StringBuilder token, char c, boolean preserveEscapes) {
if (preserveEscapes) {
token.append('\\');
token.append(c);
} else if (isEscapable(c)) {
token.append(c);
} else {
token.append('\\');
token.append(c);
}
}

private static void appendLiteral(StringBuilder token, char c, boolean preserveEscapes) {
if (preserveEscapes && Character.isWhitespace(c)) {
token.append('\\');
token.append(c);
} else {
token.append(c);
}
}

private static boolean isEscapable(char c) {
return Character.isWhitespace(c) || c == '\\' || c == '\'' || c == '"';
}

private static boolean isLiteralQuote(StringBuilder token, boolean preserveEscapes) {
if (preserveEscapes) {
return false;
}

String currentToken = token.toString();
if (currentToken.startsWith("-D") && currentToken.indexOf("=") != -1) {
return true;
}

int assignmentIndex = currentToken.indexOf("=");
return assignmentIndex != -1 && assignmentIndex < currentToken.length() - 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.io.StringWriter;
import java.io.PrintWriter;
import java.util.*;
import java.util.stream.Collectors;

import com.defold.extender.ExtenderException;

Expand All @@ -23,9 +22,7 @@ public class ProcessExecutor {

public int execute(String command) throws IOException, InterruptedException {
// To avoid an issue where an extra space was interpreted as an argument
List<String> args = Arrays.stream(command.split(" "))
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
List<String> args = CommandLineTokenizer.parse(command);
return execute(args);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -14,7 +13,8 @@
import java.util.Map;
import java.util.Set;

import org.apache.commons.text.StringEscapeUtils;
import com.defold.extender.process.CommandLineTokenizer;


// similar to PodSpec but contains some runtime information that used during the build
public class PodBuildSpec {
Expand Down Expand Up @@ -132,7 +132,7 @@ void updateFlagsFromConfig(Map<String, String> parsedConfig) {
// defines
List<String> defs = argumentsAsList(parsedConfig.getOrDefault("GCC_PREPROCESSOR_DEFINITIONS", null));
if (defs != null) {
this.defines.addAll(unescapeStrings(defs));
this.defines.addAll(defs);
}
// linker flags
// https://xcodebuildsettings.com/#other_ldflags
Expand Down Expand Up @@ -285,14 +285,6 @@ static boolean hasString(Map<String, String> parsedConfig, String key) {
return value != null && !value.trim().isEmpty();
}

static List<String> unescapeStrings(List<String> strings) {
List<String> unescapedStrings = new ArrayList<>();
for (String s : strings) {
unescapedStrings.add(StringEscapeUtils.unescapeJava(s));
}
return unescapedStrings;
}

// check if the value for a specific key matches an expected value
static boolean compareString(Map<String, String> config, String key, String expected) {
String value = config.get(key);
Expand All @@ -307,7 +299,7 @@ static List<String> argumentsAsList(String arguments) {
if (arguments == null || arguments.isEmpty()) {
return null;
}
return new ArrayList<>(Arrays.asList(arguments.split(" ")));
return new ArrayList<>(CommandLineTokenizer.splitPreservingEscapedWhitespace(arguments));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand All @@ -18,9 +17,11 @@
import org.slf4j.LoggerFactory;

import com.defold.extender.ExtenderBuildState;
import com.defold.extender.process.CommandLineTokenizer;

public class XCConfigParser implements IConfigParser {
private static final Logger LOGGER = LoggerFactory.getLogger(XCConfigParser.class);
private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$[\\(|{]([\\w]+)[\\)|}]");
private File buildDir;
private File podsDir;
private String platform;
Expand Down Expand Up @@ -74,35 +75,86 @@
* Merged values from base values (like directory paths) and values obtained from xcconfig
*/
String postProcessValue(String value, Map<String, String> allValues) {
List<String> tmpList = new ArrayList<>(Arrays.asList(value.split(" ")));
tmpList.remove("$(inherited)");

// check for $(....) pattern
Pattern p = Pattern.compile("\\$[\\(|{]([\\w]+)[\\)|}]");

// substitute values if any placeholders are presented
for (int idx = 0 ; idx < tmpList.size(); ++idx) {
Set<String> visitedKeys = new HashSet<>();
String element = tmpList.get(idx);
Matcher matcher = p.matcher(element);
while (matcher.find()) {
String replaceKey = matcher.group(1);
String replaceValue = allValues.containsKey(replaceKey) && !visitedKeys.contains(replaceKey) ? allValues.get(replaceKey) : null;
visitedKeys.add(replaceKey);
if (replaceValue != null) {
element = element.replace(matcher.group(0), replaceValue);
element = element.replaceAll("\"", "");
// update matcher every time because during replace new values for substitution can be introduced.
// For example: ${PODS_ROOT}/Headers (where PODS_ROOT=${SRCROOT}) -> ${SRCROOT}/Headers
matcher = p.matcher(element);
} else {
LOGGER.warn("Can't find value for substitution for key {}", replaceKey);
return String.join(" ", postProcessTokens(value, allValues, new HashSet<>()));
}

private List<String> postProcessTokens(String value, Map<String, String> allValues, Set<String> visitedKeys) {
List<String> result = new ArrayList<>();
List<String> tmpList = new ArrayList<>(CommandLineTokenizer.splitPreservingEscapedWhitespace(value));

for (String token : tmpList) {
if ("$(inherited)".equals(token)) {
continue;
}
result.addAll(postProcessToken(token, allValues, visitedKeys));
}

return result;
}

private List<String> postProcessToken(String token, Map<String, String> allValues, Set<String> visitedKeys) {
Matcher matcher = VARIABLE_PATTERN.matcher(token);
if (matcher.matches()) {
String replaceKey = matcher.group(1);
if (visitedKeys.contains(replaceKey)) {
return List.of(CommandLineTokenizer.escapeWhitespace(token));
}

String replaceValue = allValues.get(replaceKey);
if (replaceValue != null) {
Set<String> nextVisitedKeys = new HashSet<>(visitedKeys);
nextVisitedKeys.add(replaceKey);
if (isListBuildSetting(replaceKey)) {
return postProcessTokens(replaceValue, allValues, nextVisitedKeys);
}
return List.of(postProcessScalarValue(replaceValue, allValues, nextVisitedKeys));
} else {
LOGGER.warn("Can't find value for substitution for key {}", replaceKey);

Check failure

Code scanning / CodeQL

Insertion of sensitive information into log files High

This
potentially sensitive information
is written to a log file.
return List.of(CommandLineTokenizer.escapeWhitespace(token));
}
}

return List.of(postProcessSingleToken(token, allValues, visitedKeys));
}

private boolean isListBuildSetting(String key) {
return key.endsWith("FLAGS")
|| key.endsWith("PATHS")
|| key.endsWith("DEFINITIONS")
|| key.endsWith("ARCHS")
|| key.endsWith("LIBRARIES");
}

private String postProcessScalarValue(String value, Map<String, String> allValues, Set<String> visitedKeys) {
String scalarValue = String.join(" ", CommandLineTokenizer.splitPreservingEscapedWhitespace(value));
return postProcessSingleToken(scalarValue, allValues, visitedKeys);
}

private String postProcessSingleToken(String token, Map<String, String> allValues, Set<String> visitedKeys) {
String element = token;
Set<String> localVisitedKeys = new HashSet<>(visitedKeys);
Matcher matcher = VARIABLE_PATTERN.matcher(element);
while (matcher.find()) {
String replaceKey = matcher.group(1);
if (localVisitedKeys.contains(replaceKey)) {
continue;
}

String replaceValue = allValues.get(replaceKey);
localVisitedKeys.add(replaceKey);
if (replaceValue != null) {
String resolvedValue = String.join(" ", postProcessTokens(replaceValue, allValues, localVisitedKeys));
element = element.replace(matcher.group(0), resolvedValue);
element = element.replaceAll("(?<!\\\\)\"", "");
// update matcher every time because during replace new values for substitution can be introduced.
// For example: ${PODS_ROOT}/Headers (where PODS_ROOT=${SRCROOT}) -> ${SRCROOT}/Headers
matcher = VARIABLE_PATTERN.matcher(element);
} else {
LOGGER.warn("Can't find value for substitution for key {}", replaceKey);

Check failure

Code scanning / CodeQL

Insertion of sensitive information into log files High

This
potentially sensitive information
is written to a log file.
}
tmpList.set(idx, element);
}

return String.join(" ", tmpList);
return CommandLineTokenizer.escapeWhitespace(element);
}

Pair<String, String> parseLine(String line) {
Expand All @@ -125,8 +177,6 @@
parseIncludes(line);
return null;
}
// replace " to avoid problem with arguments in ProcessBuilder
line = line.replaceAll("(?<!\\\\)\"", "");
char[] charsArray = line.toCharArray();
ParseMode currentMode = ParseMode.VAR_START;
StringBuilder varBuilder = new StringBuilder();
Expand Down Expand Up @@ -182,7 +232,21 @@
break;
}
}
return Pair.of(varBuilder.toString(), valueBuilder.toString());
return Pair.of(varBuilder.toString(), normalizeParsedValue(valueBuilder.toString()));
}

private String normalizeParsedValue(String value) {
if (value.length() >= 2) {
char quote = value.charAt(0);
if ((quote == '\'' || quote == '"') && value.charAt(value.length() - 1) == quote) {
String quotedValue = value.substring(1, value.length() - 1);
if (quotedValue.chars().noneMatch(Character::isWhitespace)) {
return quotedValue;
}
}
}

return value;
}

@Override
Expand Down
Loading