Merge "Update JGit to 1.3.0.201202151440-r.190-g65f6e06"
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index c1b37fa..25cd9a9 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -46,8 +46,11 @@
 	Allows listing of projects together with their respective
 	description.
 +
-Line-feeds are escaped to allow ls-project to keep the
-"one project per line"-style.
+For text format output, all non-printable characters (ASCII value 31 or
+less) are escaped according to the conventions used in languages like C,
+Python, and Perl, employing standard sequences like `\n` and `\t`, and
+`\xNN` for all others. In shell scripts, the `printf` command can be
+used to unescape the output.
 
 --tree::
 -t::
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
new file mode 100644
index 0000000..fe1072d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2012 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.google.gerrit.server;
+
+public class StringUtil {
+  /**
+   * An array of the string representations that should be used in place
+   * of the non-printable characters in the beginning of the ASCII table
+   * when escaping a string. The index of each element in the array
+   * corresponds to its ASCII value, i.e. the string representation of
+   * ASCII 0 is found in the first element of this array.
+   */
+  static String[] NON_PRINTABLE_CHARS =
+    { "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\a",
+      "\\b",   "\\t",   "\\n",   "\\v",   "\\f",   "\\r",   "\\x0e", "\\x0f",
+      "\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17",
+      "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", "\\x1e", "\\x1f" };
+
+  /**
+   * Escapes the input string so that all non-printable characters
+   * (0x00-0x1f) are represented as a hex escape (\x00, \x01, ...)
+   * or as a C-style escape sequence (\a, \b, \t, \n, \v, \f, or \r).
+   * Backslashes in the input string are doubled (\\).
+   */
+  public static String escapeString(final String str) {
+    // Allocate a buffer big enough to cover the case with a string needed
+    // very excessive escaping without having to reallocate the buffer.
+    final StringBuilder result = new StringBuilder(3 * str.length());
+
+    for (int i = 0; i < str.length(); i++) {
+      char c = str.charAt(i);
+      if (c < NON_PRINTABLE_CHARS.length) {
+        result.append(NON_PRINTABLE_CHARS[c]);
+      } else if (c == '\\') {
+        result.append("\\\\");
+      } else {
+        result.append(c);
+      }
+    }
+    return result.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
new file mode 100644
index 0000000..a73f1cb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2012 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.google.gerrit.server.ioutil;
+
+import com.google.gerrit.server.StringUtil;
+
+import java.io.PrintWriter;
+
+/**
+ * Simple output formatter for column-oriented data, writing its output to
+ * a {@link java.io.PrintWriter} object. Handles escaping of the column
+ * data so that the resulting output is unambiguous and reasonably safe and
+ * machine parsable.
+ */
+public class ColumnFormatter {
+  private char columnSeparator;
+  private boolean firstColumn;
+  private final PrintWriter out;
+
+  /**
+   * @param out The writer to which output should be sent.
+   * @param columnSeparator A character that should serve as the separator
+   *        token between columns of output. As only non-printable characters
+   *        in the column text are ever escaped, the column separator must be
+   *        a non-printable character if the output needs to be unambiguously
+   *        parsed.
+   */
+  public ColumnFormatter(final PrintWriter out, final char columnSeparator) {
+    this.out = out;
+    this.columnSeparator = columnSeparator;
+    this.firstColumn = true;
+  }
+
+  /**
+   * Adds a text string as a new column in the current line of output,
+   * taking care of escaping as necessary.
+   *
+   * @param content the string to add.
+   */
+  public void addColumn(final String content) {
+    if (!firstColumn) {
+      out.print(columnSeparator);
+    }
+    out.print(StringUtil.escapeString(content));
+    firstColumn = false;
+  }
+
+  /**
+   * Finishes the output by flushing the current line and takes care of any
+   * other cleanup action.
+   */
+  public void finish() {
+    nextLine();
+    out.flush();
+  }
+
+  /**
+   * Flushes the current line of output and makes the formatter ready to
+   * start receiving new column data for a new line (or end-of-file).
+   * If the current line is empty nothing is done, i.e. consecutive calls
+   * to this method without intervening calls to {@link #addColumn} will
+   * be squashed.
+   */
+  public void nextLine() {
+    if (!firstColumn) {
+      out.print('\n');
+      firstColumn = true;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 716a5a8..8b4e000 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.StringUtil;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
@@ -269,7 +270,7 @@
 
         if (info.description != null) {
           // We still want to list every project as one-liners, hence escaping \n.
-          stdout.print(" - " + info.description.replace("\n", "\\n"));
+          stdout.print(" - " + StringUtil.escapeString(info.description));
         }
         stdout.print('\n');
       }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
new file mode 100644
index 0000000..24f3386
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/StringUtilTest.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2012 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.google.gerrit.server;
+
+import junit.framework.TestCase;
+
+public class StringUtilTest extends TestCase {
+  /**
+   * Test the boundary condition that the first character of a string
+   * should be escaped.
+   */
+  public void testEscapeFirstChar() {
+    assertEquals(StringUtil.escapeString("\tLeading tab"), "\\tLeading tab");
+  }
+
+  /**
+   * Test the boundary condition that the last character of a string
+   * should be escaped.
+   */
+  public void testEscapeLastChar() {
+    assertEquals(StringUtil.escapeString("Trailing tab\t"), "Trailing tab\\t");
+  }
+
+  /**
+   * Test that various forms of input strings are escaped (or left as-is)
+   * in the expected way.
+   */
+  public void testEscapeString() {
+    final String[] testPairs =
+      { "", "",
+        "plain string", "plain string",
+        "string with \"quotes\"", "string with \"quotes\"",
+        "string with 'quotes'", "string with 'quotes'",
+        "string with 'quotes'", "string with 'quotes'",
+        "C:\\Program Files\\MyProgram", "C:\\\\Program Files\\\\MyProgram",
+        "string\nwith\nnewlines", "string\\nwith\\nnewlines",
+        "string\twith\ttabs", "string\\twith\\ttabs" };
+    for (int i = 0; i < testPairs.length; i += 2) {
+      assertEquals(StringUtil.escapeString(testPairs[i]), testPairs[i + 1]);
+    }
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
new file mode 100644
index 0000000..2d432e6
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2012 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.google.gerrit.server.ioutil;
+
+import com.google.gerrit.server.ioutil.ColumnFormatter;
+
+import junit.framework.TestCase;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public class ColumnFormatterTest extends TestCase {
+  /**
+   * Holds an in-memory {@link java.io.PrintWriter} object and allows
+   * comparisons of its contents to a supplied string via an assert statement.
+   */
+  class PrintWriterComparator {
+    private PrintWriter printWriter;
+    private StringWriter stringWriter;
+
+    public PrintWriterComparator() {
+      stringWriter = new StringWriter();
+      printWriter = new PrintWriter(stringWriter);
+    }
+
+    public void assertEquals(String str) {
+      printWriter.flush();
+      TestCase.assertEquals(stringWriter.toString(), str);
+    }
+
+    public PrintWriter getPrintWriter() {
+      return printWriter;
+    }
+  }
+
+  /**
+   * Test that only lines with at least one column of text emit output.
+   */
+  public void testEmptyLine() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.finish();
+    comparator.assertEquals("foo\tbar\nfoo\tbar\n");
+  }
+
+  /**
+   * Test that there is no output if no columns are ever added.
+   */
+  public void testEmptyOutput() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("");
+  }
+
+  /**
+   * Test that there is no output (nor any exceptions) if we finalize
+   * the output immediately after the creation of the {@link ColumnFormatter}.
+   */
+  public void testNoNextLine() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.finish();
+    comparator.assertEquals("");
+  }
+
+  /**
+   * Test that the text in added columns is escaped while the column separator
+   * (which of course shouldn't be escaped) is left alone.
+   */
+  public void testEscapingTakesPlace() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn(
+        "\tan indented multi-line\ntext");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\t\\tan indented multi-line\\ntext\n");
+  }
+
+  /**
+   * Test that we get the correct output with multi-line input where the number
+   * of columns in each line varies.
+   */
+  public void testMultiLineDifferentColumnCount() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.addColumn("baz");
+    formatter.nextLine();
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\tbar\tbaz\nfoo\tbar\n");
+  }
+
+  /**
+   * Test that we get the correct output with a single column of input.
+   */
+  public void testOneColumn() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\n");
+  }
+}