// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.util;

import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import com.google.common.escape.CharEscaperBuilder;
import com.google.common.escape.Escaper;
import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
import java.io.IOException;

/**
 * Utility class to escape strings for use with shell commands.
 *
 * <p>Escaped strings may safely be inserted into shell commands. Escaping is
 * only done if necessary. Strings containing only shell-neutral characters
 * will not be escaped.
 *
 * <p>This is a replacement for {@code ShellUtils.shellEscape(String)} and
 * {@code ShellUtils.prettyPrintArgv(java.util.List)} (see
 * {@link com.google.devtools.build.lib.shell.ShellUtils}). Its advantage is the use
 * of standard building blocks from the {@code com.google.common.base}
 * package, such as {@link Joiner} and {@link CharMatcher}, making this class
 * more efficient and reliable than {@code ShellUtils}.
 *
 * <p>The behavior is slightly different though: this implementation will
 * defensively escape non-ASCII letters and digits, whereas
 * {@code shellEscape} does not.
 */
@Immutable
public final class ShellEscaper extends Escaper {
  // Note: extending Escaper may seem desirable, but is in fact harmful.
  // The class would then need to implement escape(Appendable), returning an Appendable
  // that escapes everything it receives. In case of shell escaping, we most often join
  // string parts on spaces, using a Joiner. Spaces are escaped characters. Using the
  // Appendable returned by escape(Appendable) would escape these spaces too, which
  // is unwanted.

  public static final ShellEscaper INSTANCE = new ShellEscaper();

  private static final Function<String, String> AS_FUNCTION = INSTANCE.asFunction();

  private static final Joiner SPACE_JOINER = Joiner.on(' ');
  private static final Escaper STRONGQUOTE_ESCAPER =
      new CharEscaperBuilder().addEscape('\'', "'\\''").toEscaper();
  private static final CharMatcher SAFECHAR_MATCHER =
      CharMatcher.anyOf("@%-_+:,./")
          .or(CharMatcher.inRange('0', '9')) // We can't use CharMatcher.javaLetterOrDigit(),
          .or(CharMatcher.inRange('a', 'z')) // that would also accept non-ASCII digits and
          .or(CharMatcher.inRange('A', 'Z')) // letters.
          .precomputed();
  private static final CharMatcher SAFECHAR_MATCHER_WITH_TILDE =
      SAFECHAR_MATCHER.or(CharMatcher.is('~')).precomputed();

  /**
   * Escapes a string by adding strong (single) quotes around it if necessary.
   *
   * <p>A string is not escaped iff it only contains safe characters.
   * The following characters are safe:
   * <ul>
   * <li>ASCII letters and digits: [a-zA-Z0-9]
   * <li>shell-neutral characters: at symbol (@), percent symbol (%),
   *     dash/minus sign (-), underscore (_), plus sign (+), colon (:),
   *     comma(,), period (.) and slash (/).
   * </ul>
   *
   * <p>A string is escaped iff it contains at least one non-safe character.
   * Escaped strings are created by replacing every occurrence of single
   * quotes with the string '\'' and enclosing the result in a pair of
   * single quotes.
   *
   * <p>Examples:
   * <ul>
   * <li>"{@code foo}" becomes "{@code foo}" (remains the same)
   * <li>"{@code +bar}" becomes "{@code +bar}" (remains the same)
   * <li>"" becomes "{@code''}" (empty string becomes a pair of strong quotes)
   * <li>"{@code $BAZ}" becomes "{@code '$BAZ'}"
   * <li>"{@code quote'd}" becomes "{@code 'quote'\''d'}"
   * </ul>
   */
  @Override
  public String escape(String unescaped) {
    final String s = unescaped.toString();
    if (s.isEmpty()) {
      // Empty string is a special case: needs to be quoted to ensure that it
      // gets treated as a separate argument.
      return "''";
    } else {
      if (SAFECHAR_MATCHER.matchesAllOf(s)) {
        return s;
      }
      if (SAFECHAR_MATCHER_WITH_TILDE.matchesAllOf(s) && s.charAt(0) != '~') {
        return s;
      }
      return "'" + STRONGQUOTE_ESCAPER.escape(s) + "'";
    }
  }

  public static String escapeString(String unescaped) {
    return INSTANCE.escape(unescaped);
  }

  /**
   * Transforms the input {@code Iterable} of unescaped strings to an
   * {@code Iterable} of escaped ones. The escaping is done lazily.
   */
  public static Iterable<String> escapeAll(Iterable<? extends String> unescaped) {
    return Iterables.transform(unescaped, AS_FUNCTION);
  }

  /**
   * Escapes all strings in {@code argv} individually and joins them on single spaces into {@code
   * out}. The result is appended directly into {@code out}, without adding a separator.
   *
   * <p>This method works as if by invoking {@link #escapeJoinAll(Appendable, Iterable, Joiner)}
   * with {@code Joiner.on(' ')}.
   *
   * @param out what the result will be appended to
   * @param argv the strings to escape and join
   * @return the same reference as {@code out}, now containing the joined, escaped fragments
   * @throws IOException if an I/O error occurs while appending
   */
  public static Appendable escapeJoinAll(Appendable out, Iterable<? extends String> argv)
      throws IOException {
    return SPACE_JOINER.appendTo(out, escapeAll(argv));
  }

  /**
   * Escapes all strings in {@code argv} individually and joins them into {@code out} using the
   * specified {@link Joiner}. The result is appended directly into {@code out}, without adding a
   * separator.
   *
   * <p>The resulting strings are the same as if escaped one by one using {@link
   * #escapeString(String)}.
   *
   * <p>Example: if the joiner is {@code Joiner.on('|')}, then the input {@code ["abc", "de'f"]}
   * will be escaped as "{@code abc|'de'\''f'}". If {@code out} initially contains "{@code 123}",
   * then the returned {@code Appendable} will contain "{@code 123abc|'de'\''f'}".
   *
   * @param out what the result will be appended to
   * @param argv the strings to escape and join
   * @param joiner the {@link Joiner} to use to join the escaped strings
   * @return the same reference as {@code out}, now containing the joined, escaped fragments
   * @throws IOException if an I/O error occurs while appending
   */
  public static Appendable escapeJoinAll(
      Appendable out, Iterable<? extends String> argv, Joiner joiner) throws IOException {
    return joiner.appendTo(out, escapeAll(argv));
  }

  /**
   * Escapes all strings in {@code argv} individually and joins them on
   * single spaces, then returns the resulting string.
   *
   * <p>This method works as if by invoking
   * {@link #escapeJoinAll(Iterable, Joiner)} with {@code Joiner.on(' ')}.
   *
   * <p>Example: {@code ["abc", "de'f"]} will be escaped and joined as
   * "abc 'de'\''f'".
   *
   * @param argv the strings to escape and join
   * @return the string of escaped and joined input elements
   */
  public static String escapeJoinAll(Iterable<? extends String> argv) {
    return SPACE_JOINER.join(escapeAll(argv));
  }

  /**
   * Escapes all strings in {@code argv} individually and joins them using
   * the specified {@link Joiner}, then returns the resulting string.
   *
   * <p>The resulting strings are the same as if escaped one by one using
   * {@link #escapeString(String)}.
   *
   * <p>Example: if the joiner is {@code Joiner.on('|')}, then the input
   * {@code ["abc", "de'f"]} will be escaped and joined as "abc|'de'\''f'".
   *
   * @param argv the strings to escape and join
   * @param joiner the {@link Joiner} to use to join the escaped strings
   * @return the string of escaped and joined input elements
   */
  public static String escapeJoinAll(Iterable<? extends String> argv, Joiner joiner) {
    return joiner.join(escapeAll(argv));
  }

  private ShellEscaper() {
    // Utility class - do not instantiate.
  }
}
