/*
* Copyright 2015-present Facebook, Inc.
*
* 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.facebook.buck.apple;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class to substitute Xcode Info.plist variables in the forms: <code>
* ${FOO}
* $(FOO)
* ${FOO:modifier}
* $(FOO:modifier)
* </code> with specified string values.
*/
public class InfoPlistSubstitution {
// Utility class, do not instantiate.
private InfoPlistSubstitution() {}
private static final String VARIABLE_GROUP_NAME = "variable";
private static final String OPEN_PAREN_GROUP_NAME = "openparen";
private static final String CLOSE_PAREN_GROUP_NAME = "closeparen";
private static final String MODIFIER_GROUP_NAME = "modifier";
private static final Pattern PLIST_VARIABLE_PATTERN =
Pattern.compile(
"\\$(?<"
+ OPEN_PAREN_GROUP_NAME
+ ">[\\{\\(])"
+ "(?<"
+ VARIABLE_GROUP_NAME
+ ">[^\\}\\):]+)"
+ "(?::(?<"
+ MODIFIER_GROUP_NAME
+ ">[^\\}\\)]+))?"
+ "(?<"
+ CLOSE_PAREN_GROUP_NAME
+ ">[\\}\\)])");
private static final ImmutableMap<String, String> MATCHING_PARENS =
ImmutableMap.of(
"{", "}",
"(", ")");
/**
* Returns a variable expansion for keys which may depend on platform name, trying from most to
* least specific. While it doesn't capture all arbitrary wildcard expansions, it should handle
* everything likely to occur in practice.
*
* <p>e.g.<code>VALID_ARCHS[sdk=iphoneos*]</code>
*
* @param keyName The name of the parent key. e.g. "VALID_ARCHS"
* @param platformName The name of the platform. e.g. "iphoneos"
* @param variablesToExpand The mapping of variable keys to values.
* @return Optional containing the string value if found, or absent.
*/
public static Optional<String> getVariableExpansionForPlatform(
String keyName, String platformName, Map<String, String> variablesToExpand) {
final String[] keysToTry;
if (platformName.equals("iphoneos") || platformName.equals("iphonesimulator")) {
keysToTry =
new String[] {
keyName + "[sdk=" + platformName + "]",
keyName + "[sdk=" + platformName + "*]",
keyName + "[sdk=iphone*]",
keyName
};
} else {
keysToTry =
new String[] {
keyName + "[sdk=" + platformName + "]", keyName + "[sdk=" + platformName + "*]", keyName
};
}
for (String keyToTry : keysToTry) {
if (variablesToExpand.containsKey(keyToTry)) {
return Optional.of(
InfoPlistSubstitution.replaceVariablesInString(
variablesToExpand.get(keyToTry), variablesToExpand));
}
}
return Optional.empty();
}
public static String replaceVariablesInString(
String input, Map<String, String> variablesToExpand) {
return replaceVariablesInString(input, variablesToExpand, ImmutableList.of());
}
private static String replaceVariablesInString(
String input, Map<String, String> variablesToExpand, List<String> maskedVariables) {
Matcher variableMatcher = PLIST_VARIABLE_PATTERN.matcher(input);
StringBuffer result = new StringBuffer();
while (variableMatcher.find()) {
String openParen = variableMatcher.group(OPEN_PAREN_GROUP_NAME);
String closeParen = variableMatcher.group(CLOSE_PAREN_GROUP_NAME);
String expectedCloseParen = Preconditions.checkNotNull(MATCHING_PARENS.get(openParen));
if (!expectedCloseParen.equals(closeParen)) {
// Mismatching parens; don't substitute.
variableMatcher.appendReplacement(
result, Matcher.quoteReplacement(variableMatcher.group(0)));
continue;
}
String variableName = variableMatcher.group(VARIABLE_GROUP_NAME);
if (maskedVariables.contains(variableName)) {
throw new HumanReadableException(
"Recursive plist variable: %s -> %s",
Joiner.on(" -> ").join(maskedVariables), variableName);
}
String expansion = variablesToExpand.get(variableName);
if (expansion == null) {
throw new HumanReadableException(
"Unrecognized plist variable: %s", variableMatcher.group(0));
}
// Variable replacements are allowed to reference other variables (but be careful to mask
// so we don't end up in a recursive loop)
expansion =
replaceVariablesInString(
expansion,
variablesToExpand,
new ImmutableList.Builder<String>()
.addAll(maskedVariables)
.add(variableName)
.build());
// TODO(beng): Add support for "rfc1034identifier" modifier and sanitize
// expansion so it's a legal hostname (a-zA-Z0-9, dash, period).
variableMatcher.appendReplacement(result, Matcher.quoteReplacement(expansion));
}
variableMatcher.appendTail(result);
return result.toString();
}
public static Function<String, String> createVariableExpansionFunction(
Map<String, String> variablesToExpand) {
final ImmutableMap<String, String> variablesToExpandCopy =
ImmutableMap.copyOf(variablesToExpand);
return input -> replaceVariablesInString(input, variablesToExpandCopy);
}
}