/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Portions copyright 2016 ForgeRock AS. * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.opendj.config.dsconfig; import com.forgerock.opendj.cli.ArgumentException; import com.forgerock.opendj.cli.ReturnCode; import org.forgerock.testng.ForgeRockTestCase; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; @Test(groups = { "precommit", "config" }) public class DSConfigParseTest extends ForgeRockTestCase { @DataProvider public Object[][] escapeSequences() { return new Object[][] { {"global-aci:\\(targetattr=\\\"userPassword\\|\\|authPassword\\\"\\)" + "\\(version\\ 3.0\\;\\ acl\\ \\\"Self\\ entry\\ read\\'\\\"\\;" + "\\ allow\\ \\(read,search,compare\\)\\ userdn=\\\"ldap:///self\\\"\\;\\)", "global-aci:(targetattr=\"userPassword||authPassword\")" + "(version 3.0; acl \"Self entry read'\";" + " allow (read,search,compare) userdn=\"ldap:///self\";)" }, {"cn=\"admin data\"", "cn=admin data"}, {"\"cn=admin data\"", "cn=admin data"}, {"cn=\\\"admin", "cn=\"admin"} }; } @Test(dataProvider = "escapeSequences") public void testEscapeSequenceInCommandArgument(String arg, String value) throws Exception { Collection cmdLine = DSConfig.toCommandArgs(arg); Assert.assertEquals(cmdLine.iterator().next(), value); } /** Batch lines and the arguments a POSIX shell splits them into. */ @DataProvider public Object[][] shellQuoting() { return new Object[][] { // Inside double quotes a backslash is kept unless it precedes ", \, $ or `. { "--set base-dn:\"cn=a\\,b,dc=x\"", args("--set", "base-dn:cn=a\\,b,dc=x") }, { "--set \"base-dn:cn=a\\2Cb,dc=x\"", args("--set", "base-dn:cn=a\\2Cb,dc=x") }, { "\"a\\\"b\\\\c\\$d\\`e\"", args("a\"b\\c$d`e") }, // Single quotes keep everything literally, a backslash included. { "--set 'base-dn:o=My Company'", args("--set", "base-dn:o=My Company") }, { "--set 'base-dn:cn=a\\,b,dc=x'", args("--set", "base-dn:cn=a\\,b,dc=x") }, { "'say \"hi\"' \"it's\"", args("say \"hi\"", "it's") }, // Quoted and unquoted parts of one word make a single argument. { "base-dn:\"o=My \"'Company'", args("base-dn:o=My Company") }, { "\"a\"b c", args("ab", "c") }, // An empty quoted word is an empty argument. { "--set description:\"\" \"\"", args("--set", "description:", "") }, // Tabs separate arguments as spaces do. { "a\tb c", args("a", "b", "c") }, // A trailing backslash has nothing to escape and is kept. { "a b\\", args("a", "b\\") }, }; } @Test(dataProvider = "shellQuoting") public void testShellQuotingInCommandLine(String line, List expected) throws Exception { Assert.assertEquals(new ArrayList<>(DSConfig.toCommandArgs(line)), expected); } /** Batch lines with a quote that is not closed: a POSIX shell rejects them. */ @DataProvider public Object[][] unclosedQuotes() { return new Object[][] { // Before single quotes were supported, this line gave three arguments. { "--set description:O'Brien --advanced" }, { "--set 'base-dn:o=My Company" }, { "--set \"base-dn:o=My Company" }, { "--set \"base-dn:o=My Company\\\"" }, }; } @Test(dataProvider = "unclosedQuotes", expectedExceptions = ArgumentException.class, expectedExceptionsMessageRegExp = "The following batch command has a quote that is not closed: .*") public void testUnclosedQuoteIsRejected(String line) throws Exception { DSConfig.toCommandArgs(line); } @Test public void testBatchCommandWithUnclosedQuoteFails() { final ByteArrayOutputStream err = new ByteArrayOutputStream(); final int exitCode = DSConfig.runBatchCommand(args("--noPropertiesFile", "-n"), "set-x --set 'a", new PrintStream(new ByteArrayOutputStream()), new PrintStream(err)); Assert.assertEquals(exitCode, ReturnCode.ERROR_USER_DATA.get()); Assert.assertTrue(text(err).contains("The following batch command has a quote that is not closed: " + "set-x --set 'a"), text(err)); } @Test public void testBatchCommandKeepsEscapedTrailingBlank() { // dsconfig fails on the first argument, before it reads --no-prompt, so it still writes // the error to the output stream. final ByteArrayOutputStream output = new ByteArrayOutputStream(); final PrintStream stream = new PrintStream(output); // dsconfig has no such subcommand: the error quotes the argument it was given. final int exitCode = DSConfig.runBatchCommand(args("--noPropertiesFile", "-n"), "set-x\\ ", stream, stream); Assert.assertEquals(exitCode, ReturnCode.CONFLICTING_ARGS.get()); Assert.assertTrue(text(output).contains("The provided argument \"set-x \" is not recognized"), text(output)); } /** The output with every run of blanks and line breaks as a single space, as the console wraps lines. */ private static String text(ByteArrayOutputStream out) { return new String(out.toByteArray()).replaceAll("\\s+", " "); } /** Batch files and the arguments of each command they hold. */ @DataProvider public Object[][] batchFiles() { return new Object[][] { { "# comment\n\nset-x --foo bar\nset-y\n", commands(args("set-x", "--foo", "bar"), args("set-y")) }, // A line of blanks is skipped as an empty line is. { "set-x\n \nset-y\n", commands(args("set-x"), args("set-y")) }, { "set-x\n \t\n", commands(args("set-x")) }, // Empty lines and comments are skipped inside a continued command too, so a line of a // long command can be commented out. { "set-x \\\n# --foo bar \\\n --baz qux\n", commands(args("set-x", "--baz", "qux")) }, { "set-x \\\n\n --baz qux\n", commands(args("set-x", "--baz", "qux")) }, // A line that ends in a backslash continues on the next line. { "set-x \\\n --foo bar\n", commands(args("set-x", "--foo", "bar")) }, { "set-x --foo ab\\\ncd\n", commands(args("set-x", "--foo", "abcd")) }, // An escaped backslash at the end of a line does not continue it, as dsconfig // --commandFilePath writes a value that ends in a backslash on UNIX. { "set-x --bindPassword pa\\\\\nset-y --foo bar\n", commands(args("set-x", "--bindPassword", "pa\\"), args("set-y", "--foo", "bar")) }, { "set-x --foo a\\\\\\\nb\n", commands(args("set-x", "--foo", "a\\b")) }, // An escaped blank at the end of a line is kept. { "set-x --set description:value\\ \n", commands(args("set-x", "--set", "description:value ")) }, // A command still runs when its last line continues at the end of the file. { "set-x --foo bar \\", commands(args("set-x", "--foo", "bar")) }, { "set-x\n \\\n", commands(args("set-x")) }, }; } @Test(dataProvider = "batchFiles") public void testBatchFileCommands(String batch, List> expected) throws Exception { final List> actual = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new StringReader(batch))) { String command; while ((command = DSConfig.nextBatchCommand(reader)) != null) { actual.add(new ArrayList<>(DSConfig.toCommandArgs(command))); } } Assert.assertEquals(actual, expected); } private static List args(String... args) { return Arrays.asList(args); } @SafeVarargs private static List> commands(List... commands) { return Arrays.asList(commands); } }