You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by dk...@apache.org on 2022/01/19 14:49:41 UTC

[sling-org-apache-sling-repoinit-parser] branch master updated: SLING-10952 - Adding support for whitespace characters in group names (#14)

This is an automated email from the ASF dual-hosted git repository.

dklco pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-repoinit-parser.git


The following commit(s) were added to refs/heads/master by this push:
     new 8e60c3c  SLING-10952 - Adding support for whitespace characters in group names (#14)
8e60c3c is described below

commit 8e60c3c5d9a0a70f6e39d4555decee74fec804f2
Author: Dan Klco <kl...@users.noreply.github.com>
AuthorDate: Wed Jan 19 09:49:33 2022 -0500

    SLING-10952 - Adding support for whitespace characters in group names (#14)
    
    * SLING-10952 - Adding support for whitespace characters in group names
    
    * Removing backtracking regex for determining if string contains whitespace
    
    * SLING-11051 - Fixing JavaDoc badge
    
    * Renaming to quotableString and adding some more test coverage
    
    * Adding missing classes
    
    * Reverting unrelated changes
---
 .../repoinit/parser/impl/QuotableStringUtil.java   |  57 +++++++++++
 .../repoinit/parser/operations/AclGroupBase.java   |   4 +-
 .../parser/operations/AddGroupMembers.java         |   4 +-
 .../repoinit/parser/operations/CreateGroup.java    |   3 +-
 .../repoinit/parser/operations/DeleteGroup.java    |   3 +-
 .../repoinit/parser/operations/Operation.java      |   4 +
 .../parser/operations/RemoveGroupMembers.java      |   4 +-
 .../parser/operations/SetAclPrincipalBased.java    |   4 +-
 .../parser/operations/SetAclPrincipals.java        |   7 +-
 src/main/javacc/RepoInitGrammar.jjt                |  22 +++--
 .../repoinit/parser/test/ParsingErrorsTest.java    |  12 ++-
 .../parser/test/QuotableStringParserTest.java      | 105 +++++++++++++++++++++
 .../parser/test/QuotableStringUtilTest.java        |  76 +++++++++++++++
 src/test/resources/testcases/test-71-output.txt    |  26 +++++
 src/test/resources/testcases/test-71.txt           |  36 +++++++
 15 files changed, 349 insertions(+), 18 deletions(-)

diff --git a/src/main/java/org/apache/sling/repoinit/parser/impl/QuotableStringUtil.java b/src/main/java/org/apache/sling/repoinit/parser/impl/QuotableStringUtil.java
new file mode 100644
index 0000000..1c0d327
--- /dev/null
+++ b/src/main/java/org/apache/sling/repoinit/parser/impl/QuotableStringUtil.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.repoinit.parser.impl;
+
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.jetbrains.annotations.NotNull;
+
+public class QuotableStringUtil {
+
+    private static final Pattern REQUIRES_QUOTES = Pattern.compile("[\\s|\\\\]");
+
+    private QuotableStringUtil() {
+        // hidden
+    }
+
+    /**
+     * Gets the string handling cases where the value should be quoted
+     * (i.e. the string contains whitespace)
+     * 
+     * @param string the string to get in repoinit compatible form
+     * @return the (potentially quoted) string
+     */
+    @NotNull
+    public static final String forRepoInitString(@NotNull String string) {
+        return REQUIRES_QUOTES.matcher(string).find() ? "\"" + string + "\""
+                : string;
+    }
+
+    /**
+     * Gets the strings handling cases where the value should be quoted
+     * (i.e. the string contains whitespace)
+     * 
+     * @param strings the strings to get
+     * @return the list of (potentially quoted) strings
+     */
+    @NotNull
+    public static final List<String> forRepoInitString(@NotNull List<String> strings) {
+        return strings.stream().map(QuotableStringUtil::forRepoInitString).collect(Collectors.toList());
+    }
+}
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/AclGroupBase.java b/src/main/java/org/apache/sling/repoinit/parser/operations/AclGroupBase.java
index 434a1f0..8f1bd85 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/AclGroupBase.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/AclGroupBase.java
@@ -23,6 +23,7 @@ import java.util.Collections;
 import java.util.Formatter;
 import java.util.List;
 
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
 import org.apache.sling.repoinit.parser.operations.AclLine.Action;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ProviderType;
@@ -74,7 +75,8 @@ abstract class AclGroupBase extends Operation {
                     String pathStr = pathsToString(line.getProperty(AclLine.PROP_PATHS));
                     onOrFor = (pathStr.isEmpty()) ? "" : " on " + pathStr;
                 } else {
-                    onOrFor = " for " + listToString(line.getProperty(AclLine.PROP_PRINCIPALS));
+                    onOrFor = " for " + listToString(
+                            QuotableStringUtil.forRepoInitString(line.getProperty(AclLine.PROP_PRINCIPALS)));
                 }
                 formatter.format("    %s %s%s%s%s%n", action, privileges, onOrFor,
                         nodetypesToString(line.getProperty(AclLine.PROP_NODETYPES)),
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/AddGroupMembers.java b/src/main/java/org/apache/sling/repoinit/parser/operations/AddGroupMembers.java
index 5ccb49f..4b5383c 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/AddGroupMembers.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/AddGroupMembers.java
@@ -19,6 +19,7 @@ package org.apache.sling.repoinit.parser.operations;
 
 import java.util.List;
 
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ProviderType;
 
@@ -55,7 +56,8 @@ public class AddGroupMembers extends Operation {
     @NotNull
     @Override
     public String asRepoInitString() {
-        return String.format("add %s to group %s%n", listToString(members), groupname);
+        return String.format("add %s to group %s%n", listToString(QuotableStringUtil.forRepoInitString(members)),
+                QuotableStringUtil.forRepoInitString(groupname));
     }
 
     public String getGroupname() {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/CreateGroup.java b/src/main/java/org/apache/sling/repoinit/parser/operations/CreateGroup.java
index 3384c46..56e1f69 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/CreateGroup.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/CreateGroup.java
@@ -17,6 +17,7 @@
 
 package org.apache.sling.repoinit.parser.operations;
 
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
 import org.apache.sling.repoinit.parser.impl.WithPathOptions;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ProviderType;
@@ -63,7 +64,7 @@ public class CreateGroup extends OperationWithPathOptions {
     @NotNull
     @Override
     public String asRepoInitString() {
-        return asRepoInitString("group", groupname);
+        return asRepoInitString("group", QuotableStringUtil.forRepoInitString(groupname));
     }
 
     public String getGroupname() {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/DeleteGroup.java b/src/main/java/org/apache/sling/repoinit/parser/operations/DeleteGroup.java
index 1fcb807..3fc1d85 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/DeleteGroup.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/DeleteGroup.java
@@ -17,6 +17,7 @@
 
 package org.apache.sling.repoinit.parser.operations;
 
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ProviderType;
 
@@ -46,7 +47,7 @@ public class DeleteGroup extends Operation {
     @NotNull
     @Override
     public String asRepoInitString() {
-        return String.format("delete group %s%n", groupname);
+        return String.format("delete group %s", QuotableStringUtil.forRepoInitString(groupname));
     }
 
     public String getGroupname() {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/Operation.java b/src/main/java/org/apache/sling/repoinit/parser/operations/Operation.java
index e1b1d80..9aa4157 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/Operation.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/Operation.java
@@ -17,6 +17,7 @@
 
 package org.apache.sling.repoinit.parser.operations;
 
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ProviderType;
 
@@ -72,6 +73,9 @@ public abstract class Operation {
                     if (s.startsWith(":") && s.contains("#")) {
                         String func = s.substring(1, s.indexOf(":",1));
                         String s2 = s.substring(func.length()+2, s.lastIndexOf('#'));
+                        if ("authorizable".equals(func)) {
+                            s2 = QuotableStringUtil.forRepoInitString(s2);
+                        }
                         String trailingPath = (s.endsWith("#")) ?  "" : s.substring(s.indexOf("#")+1);
                         return func + "(" + s2 +")" + trailingPath;
                     } else {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/RemoveGroupMembers.java b/src/main/java/org/apache/sling/repoinit/parser/operations/RemoveGroupMembers.java
index 320e647..5d2b7f7 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/RemoveGroupMembers.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/RemoveGroupMembers.java
@@ -19,6 +19,7 @@ package org.apache.sling.repoinit.parser.operations;
 
 import java.util.List;
 
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ProviderType;
 
@@ -55,7 +56,8 @@ public class RemoveGroupMembers extends Operation {
     @NotNull
     @Override
     public String asRepoInitString() {
-        return String.format("remove %s from group %s%n", listToString(members), groupname);
+        return String.format("remove %s from group %s%n", listToString(QuotableStringUtil.forRepoInitString(members)),
+                QuotableStringUtil.forRepoInitString(groupname));
     }
 
     public String getGroupname() {
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipalBased.java b/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipalBased.java
index da4b8b3..5f8b937 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipalBased.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipalBased.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ProviderType;
 
@@ -52,7 +53,8 @@ public class SetAclPrincipalBased extends AclGroupBase {
     @NotNull
     @Override
     public String asRepoInitString() {
-        String topline = String.format("set principal ACL for %s%s%n", listToString(principals), getAclOptionsString());
+        String topline = String.format("set principal ACL for %s%s%n",
+                listToString(QuotableStringUtil.forRepoInitString(principals)), getAclOptionsString());
         return asRepoInit(topline, true);
     }
 
diff --git a/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipals.java b/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipals.java
index fb96bc3..6abc2be 100644
--- a/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipals.java
+++ b/src/main/java/org/apache/sling/repoinit/parser/operations/SetAclPrincipals.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ProviderType;
 
@@ -56,10 +57,12 @@ public class SetAclPrincipals extends AclGroupBase {
             List<String> paths = line.getProperty(AclLine.PROP_PATHS);
             return paths == null || paths.isEmpty();
         })) {
-            String topline = String.format("set repository ACL for %s%s%n", listToString(principals), getAclOptionsString());
+            String topline = String.format("set repository ACL for %s%s%n",
+                    listToString(QuotableStringUtil.forRepoInitString(principals)), getAclOptionsString());
             return asRepoInit(topline, true);
         } else {
-            String topline = String.format("set ACL for %s%s%n", listToString(principals), getAclOptionsString());
+            String topline = String.format("set ACL for %s%s%n",
+                    listToString(QuotableStringUtil.forRepoInitString(principals)), getAclOptionsString());
             return asRepoInit(topline, true);
         }
     }
diff --git a/src/main/javacc/RepoInitGrammar.jjt b/src/main/javacc/RepoInitGrammar.jjt
index 1e4236d..3b035d6 100644
--- a/src/main/javacc/RepoInitGrammar.jjt
+++ b/src/main/javacc/RepoInitGrammar.jjt
@@ -178,8 +178,8 @@ List<String> principalsList() :
     List<String> principals = new ArrayList<String>(); 
 }
 {
-    t = <STRING> { principals.add(t.image); } 
-    ( <COMMA> t = <STRING> { principals.add(t.image); } )* 
+    t = quotableString() { principals.add(t.image); } 
+    ( <COMMA> t = quotableString() { principals.add(t.image); } )* 
     { return principals; }
 }
 
@@ -247,7 +247,7 @@ String usernameList() :
     Token t = null;
 }
 {
-    ( t = <STRING> ) { names.add(t.image); }
+    ( t = quotableString() ) { names.add(t.image); }
     // disable lists for now, not supported downstream
     // ( <COMMA> t = <STRING> { names.add(t.image); }) *
 
@@ -654,7 +654,7 @@ void createGroupStatement(List<Operation> result) :
 }
 {
     <CREATE> <GROUP> 
-    ( group = <STRING> )
+    ( group = quotableString() )
     ( wpopt = withPathStatement() ) ?
 
     {
@@ -668,7 +668,7 @@ void deleteGroupStatement(List<Operation> result) :
 }
 {
     <DELETE> <GROUP> 
-    ( group = <STRING> )
+    ( group = quotableString() )
 
     {
         result.add(new DeleteGroup(group.image));
@@ -754,7 +754,7 @@ void addToGroupStatement(List<Operation> result) :
     <ADD>
     members =  principalsList()
     <TO> <GROUP>
-    group = <STRING>
+    group = quotableString()
 
     {
         result.add(new AddGroupMembers(members, group.image));
@@ -771,13 +771,21 @@ void removeFromGroupStatement(List<Operation> result) :
     <REMOVE>
     members =  principalsList()
     <FROM> <GROUP>
-    group = <STRING>
+    group = quotableString()
 
     {
         result.add(new RemoveGroupMembers(members, group.image));
     }
 }
 
+Token quotableString() :
+{
+    Token t = null;
+}
+{
+    (t = <STRING> | t = quotedString() ) { return t; }
+}
+
 Token propertyValue() :
 {
     Token t = null;
diff --git a/src/test/java/org/apache/sling/repoinit/parser/test/ParsingErrorsTest.java b/src/test/java/org/apache/sling/repoinit/parser/test/ParsingErrorsTest.java
index 58565b3..002e398 100644
--- a/src/test/java/org/apache/sling/repoinit/parser/test/ParsingErrorsTest.java
+++ b/src/test/java/org/apache/sling/repoinit/parser/test/ParsingErrorsTest.java
@@ -129,6 +129,11 @@ public class ParsingErrorsTest {
 
             // SLING-6219 - delete user does not support lists
             add(new Object[] { "delete user alice,bob", ParseException.class });
+
+            // SLING-10952 - Support quoted group names
+            add(new Object[] { "create group My Group", ParseException.class });
+            add(new Object[] { "create group My\tGroup", ParseException.class });
+            add(new Object[] { "create group \"My\u200bGroup\"", ParseException.class });
         }};
         return result;
     }
@@ -151,8 +156,9 @@ public class ParsingErrorsTest {
     public void checkResult() throws ParseException, IOException {
         final StringReader r = new StringReader(input);
         boolean noException = false;
+        String parsed = null;
         try {
-            new RepoInitParserImpl(r).parse();
+            parsed = new RepoInitParserImpl(r).parse().toString();
             noException = true;
         } catch(Exception e) {
             assertEquals(getInfo(input, e), expected, e.getClass());
@@ -162,8 +168,8 @@ public class ParsingErrorsTest {
             r.close();
         }
         
-        if(noException && expected != null) {
-            fail("Expected a " + expected.getSimpleName() + " for [" + input + "]");
+        if (noException && expected != null) {
+            fail("Expected a " + expected.getSimpleName() + " for [" + input + "] parsed to [" + parsed + "]");
         }
     }
 }
diff --git a/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringParserTest.java b/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringParserTest.java
new file mode 100644
index 0000000..8685943
--- /dev/null
+++ b/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringParserTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.repoinit.parser.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.sling.repoinit.parser.impl.RepoInitParserImpl;
+import org.apache.sling.repoinit.parser.impl.ParseException;
+import org.apache.sling.repoinit.parser.operations.Operation;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class QuotableStringParserTest {
+
+    @Parameters(name = "{0}")
+    public static Collection<Object[]> data() throws IOException {
+
+        List<Object[]> testCases = new ArrayList<>();
+
+        testCases.add(new Object[] {
+                "backslashes",
+                "create group \"a\\group\"",
+                "CreateGroup a\\group",
+                true
+        });
+        testCases.add(new Object[] {
+                "space",
+                "create group \"a group\"",
+                "CreateGroup a group",
+                true
+        });
+        testCases.add(new Object[] {
+                "unnecessarily quoted",
+                "create group \"unnecessarily-quoted\"",
+                "CreateGroup unnecessarily-quoted",
+                false
+        });
+
+        return testCases;
+    }
+
+    private final String name;
+    private final String statement;
+    private final String expected;
+    private final boolean reproducable;
+
+    public QuotableStringParserTest(String name, String statement, String expected, boolean reproducable) {
+        this.name = name;
+        this.statement = statement;
+        this.expected = expected;
+        this.reproducable = reproducable;
+    }
+
+    @Test
+    public void testParse() throws ParseException, IOException {
+        Operation op = parseSingleStatement(statement);
+        assertEquals("Test " + name + " failed", expected, op.toString());
+    }
+
+    @Test
+    public void testReproduceParse() throws ParseException, IOException {
+        Operation op = parseSingleStatement(statement);
+        String expected = String.format("%s%n", statement);
+        if (reproducable) {
+            assertEquals(expected, op.asRepoInitString());
+        } else {
+            assertNotEquals(expected, op.asRepoInitString());
+        }
+    }
+
+    private Operation parseSingleStatement(String statement) throws ParseException, IOException {
+        final StringReader r = new StringReader(statement);
+        try {
+            List<Operation> operations = new RepoInitParserImpl(r).parse();
+            assertEquals(1, operations.size());
+            return operations.get(0);
+        } finally {
+            r.close();
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringUtilTest.java b/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringUtilTest.java
new file mode 100644
index 0000000..bee6d1b
--- /dev/null
+++ b/src/test/java/org/apache/sling/repoinit/parser/test/QuotableStringUtilTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.repoinit.parser.test;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sling.repoinit.parser.impl.QuotableStringUtil;
+import org.junit.Test;
+
+public class QuotableStringUtilTest {
+
+    @Test
+    public void testAllAlphaNumeric() {
+        assertEquals("administrators", QuotableStringUtil.forRepoInitString("administrators"));
+        assertEquals("administrators1", QuotableStringUtil.forRepoInitString("administrators1"));
+    }
+
+    @Test
+    public void testWithCommonSeparators() {
+        assertEquals("sling-search-path-reader", QuotableStringUtil.forRepoInitString("sling-search-path-reader"));
+        assertEquals("sling__search_path-reader", QuotableStringUtil.forRepoInitString("sling__search_path-reader"));
+    }
+
+    @Test
+    public void testWithDomain() {
+        assertEquals("auser@adomain.com", QuotableStringUtil.forRepoInitString("auser@adomain.com"));
+    }
+
+    @Test
+    public void testWithWhitespace() {
+        assertEquals("\"A User\"", QuotableStringUtil.forRepoInitString("A User"));
+        assertEquals("\"A\tGroup\"", QuotableStringUtil.forRepoInitString("A\tGroup"));
+    }
+
+    @Test
+    public void testWithNewLines() {
+        assertEquals("\"A\nUser\"", QuotableStringUtil.forRepoInitString("A\nUser"));
+        assertEquals("\"A\rGroup\"", QuotableStringUtil.forRepoInitString("A\rGroup"));
+    }
+
+    @Test
+    public void testWithBackslashOnly() {
+        assertEquals("\"A\\User\"", QuotableStringUtil.forRepoInitString("A\\User"));
+    }
+
+    @Test
+    public void testList() {
+        List<String> input = new ArrayList<>();
+        input.add("a-user");
+        input.add("A Group");
+
+        List<String> expected = new ArrayList<>();
+        expected.add("a-user");
+        expected.add("\"A Group\"");
+
+        assertEquals(expected, QuotableStringUtil.forRepoInitString(input));
+    }
+
+}
diff --git a/src/test/resources/testcases/test-71-output.txt b/src/test/resources/testcases/test-71-output.txt
new file mode 100644
index 0000000..992db86
--- /dev/null
+++ b/src/test/resources/testcases/test-71-output.txt
@@ -0,0 +1,26 @@
+CreateGroup Test Group
+CreateGroup Test Group With Spaces with path /thePathF
+DeleteGroup Test Group
+SetAclPaths on /content 
+  AclLine ALLOW {principals=[Test Group, user1], privileges=[jcr:read]}
+SetAclPaths on /content 
+  AclLine ALLOW {principals=[Test Group- Cool People, Test Group, user1], privileges=[jcr:read]}
+SetAclPrincipals for user1 Test Group u2 
+  AclLine ALLOW {paths=[/content], privileges=[jcr:read]}
+SetAclPrincipalBased for user1 Test Group ACLOptions=[mergePreserve]
+  AclLine REMOVE_ALL {paths=[/libs, /apps]}
+  AclLine ALLOW {paths=[/content], privileges=[jcr:read]}
+SetAclPaths on /test ACLOptions=[merge]
+  AclLine REMOVE_ALL {principals=[user1, Test Group, user2]}
+SetProperties on :authorizable:bob# :authorizable:Test Group#
+  PropertyLine stringProp{String}=[{String}hello, you again!]
+SetProperties on :authorizable:bob#/nested :authorizable:Test Group#/nested
+  PropertyLine stringProp{String}=[{String}hello, you nested again!]
+AddGroupMembers user1 Test Group 2000 user2 in group Parent Group
+RemoveGroupMembers user1 Test Group 2000 user2 in group Parent Group
+CreateGroup Tab	Group
+CreateGroup Untrimmed Group 
+CreateGroup  Really Untrimmed Group 
+CreateGroup Group\With\Backslash
+CreateGroup Group
+Newline
\ No newline at end of file
diff --git a/src/test/resources/testcases/test-71.txt b/src/test/resources/testcases/test-71.txt
new file mode 100644
index 0000000..eda04fc
--- /dev/null
+++ b/src/test/resources/testcases/test-71.txt
@@ -0,0 +1,36 @@
+# Support quoted Group IDs
+create group "Test Group"
+create group "Test Group With Spaces" with path /thePathF
+delete group "Test Group"
+set ACL on /content
+    allow jcr:read for "Test Group",user1
+end
+set ACL on /content
+    allow jcr:read for "Test Group- Cool People","Test Group",user1
+end
+set ACL for user1,"Test Group",u2
+    allow jcr:read on /content
+end
+set principal ACL for user1,"Test Group" (ACLOptions=mergePreserve)
+    remove * on /libs,/apps
+    allow jcr:read on /content
+end
+set ACL on /test (ACLOptions=merge)
+    remove * for user1,"Test Group",user2
+end
+set properties on authorizable(bob), authorizable("Test Group")
+  set stringProp to "hello, you again!"
+end
+set properties on authorizable(bob)/nested, authorizable("Test Group")/nested
+  set stringProp to "hello, you nested again!"
+end
+add user1,"Test Group 2000",user2 to group "Parent Group"
+remove user1,"Test Group 2000",user2 from group "Parent Group"
+
+# Test other escaped characters 
+create group "Tab	Group"
+create group "Untrimmed Group "
+create group " Really Untrimmed Group "
+create group "Group\With\Backslash"
+create group "Group
+Newline"
\ No newline at end of file