Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,18 @@
import static com.forgerock.opendj.cli.Utils.*;
import static com.forgerock.opendj.util.StaticUtils.*;

import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.StringReader;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -411,10 +419,7 @@ Properties checkExternalProperties() throws ArgumentException {
try {
final Properties argumentProperties = new Properties();
final String scriptName = getScriptName();
final Properties p = new Properties();
try (final FileInputStream fis = new FileInputStream(propertiesFilePath)) {
p.load(fis);
}
final Properties p = loadPropertiesFile(propertiesFilePath);

for (final Enumeration<?> e = p.propertyNames(); e.hasMoreElements();) {
final String currentPropertyName = (String) e.nextElement();
Expand All @@ -440,6 +445,26 @@ Properties checkExternalProperties() throws ArgumentException {
}
}

/**
* Reads a properties file as UTF-8, or as ISO-8859-1, which earlier releases used, when it is
* not valid UTF-8. A backslash keeps its meaning in the properties format and must be doubled.
*/
private static Properties loadPropertiesFile(final String path) throws IOException {
final byte[] content = Files.readAllBytes(Paths.get(path));
// A UTF-8 byte order mark is skipped before decoding, so that the ISO-8859-1 fallback skips it too.
final int bom = content.length >= 3 && content[0] == (byte) 0xEF && content[1] == (byte) 0xBB
&& content[2] == (byte) 0xBF ? 3 : 0;
String text;
try {
text = UTF_8.newDecoder().decode(ByteBuffer.wrap(content, bom, content.length - bom)).toString();
} catch (final CharacterCodingException e) {
text = new String(content, bom, content.length - bom, ISO_8859_1);
}
final Properties properties = new Properties();
properties.load(new StringReader(text));
return properties;
}

/**
* Retrieves the argument with the specified long identifier.
*
Expand Down Expand Up @@ -1268,10 +1293,8 @@ public void parseArguments(final String[] rawArguments, final String propertiesF
final boolean requirePropertiesFile) throws ArgumentException {
Properties argumentProperties = null;

try (final FileInputStream fis = new FileInputStream(propertiesFile)) {
final Properties p = new Properties();
p.load(fis);
argumentProperties = p;
try {
argumentProperties = loadPropertiesFile(propertiesFile);
} catch (final Exception e) {
if (requirePropertiesFile) {
final LocalizableMessage message =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2008-2009 Sun Microsystems, Inc.
* Portions Copyright 2014-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package com.forgerock.opendj.cli;

Expand Down Expand Up @@ -240,8 +241,18 @@ public boolean isObfuscated(final Argument argument) {
* @return The transformed value.
*/
public static String escapeValue(String value) {
return escapeValue(value, OperatingSystem.isUnix());
}

private static void appendBackslashes(final StringBuilder b, final int count) {
for (int i = 0; i < count; i++) {
b.append('\\');
}
}

static String escapeValue(String value, boolean unix) {
final StringBuilder b = new StringBuilder();
if (OperatingSystem.isUnix()) {
if (unix) {
for (int i = 0; i < value.length(); i++) {
final char c = value.charAt(i);
if (CHARSTOESCAPE.contains(c)) {
Expand All @@ -250,7 +261,25 @@ public static String escapeValue(String value) {
b.append(c);
}
} else {
b.append('"').append(value).append('"');
// The C runtime takes backslashes literally unless they precede a double quote: there
// each pair gives one backslash and an odd one escapes the quote. So the backslashes
// before a quote, or before the closing quote, are doubled and the quote is escaped.
b.append('"');
int backslashes = 0;
for (int i = 0; i < value.length(); i++) {
final char c = value.charAt(i);
if (c == '\\') {
backslashes++;
} else {
if (c == '"') {
appendBackslashes(b, backslashes + 1);
}
backslashes = 0;
}
b.append(c);
}
appendBackslashes(b, backslashes);
b.append('"');
}
return b.toString();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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]".
*
* Copyright 2026 3A Systems, LLC.
*/
package com.forgerock.opendj.cli;

import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.fest.assertions.Assertions.assertThat;

import java.io.File;
import java.nio.file.Files;

import org.forgerock.i18n.LocalizableMessage;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/** Reading argument defaults from a {@code tools.properties} file. */
@SuppressWarnings("javadoc")
public final class ArgumentParserPropertiesFileTestCase extends CliTestCase {

/** Both ways a tool reads the file: through --propertiesFilePath and through an explicit path. */
@DataProvider
public Object[][] readers() {
return new Object[][] { { true }, { false } };
}

@Test(dataProvider = "readers")
public void testUtf8FileKeepsNonAsciiCharacters(boolean viaArgument) throws Exception {
final File file = write("basedn=ou=Jörg Ж,dc=example,dc=com\n".getBytes(UTF_8));
assertThat(baseDN(file, viaArgument)).isEqualTo("ou=Jörg Ж,dc=example,dc=com");
}

/** Some Windows editors start a UTF-8 file with a byte order mark. */
@Test(dataProvider = "readers")
public void testUtf8ByteOrderMarkIsSkipped(boolean viaArgument) throws Exception {
final File file = write("\uFEFFbasedn=dc=example,dc=com\n".getBytes(UTF_8));
assertThat(baseDN(file, viaArgument)).isEqualTo("dc=example,dc=com");
}

/** A file that starts as UTF-8 with a byte order mark but later gets a Latin-1 byte. */
@Test(dataProvider = "readers")
public void testByteOrderMarkIsSkippedInIso88591Fallback(boolean viaArgument) throws Exception {
final byte[] bom = { (byte) 0xEF, (byte) 0xBB, (byte) 0xBF };
final byte[] text = "basedn=dc=example,dc=com\nx=ö\n".getBytes(ISO_8859_1);
final byte[] content = new byte[bom.length + text.length];
System.arraycopy(bom, 0, content, 0, bom.length);
System.arraycopy(text, 0, content, bom.length, text.length);
assertThat(baseDN(write(content), viaArgument)).isEqualTo("dc=example,dc=com");
}

/** Files written for earlier releases, which read them as ISO-8859-1, keep working. */
@Test(dataProvider = "readers")
public void testIso88591FileIsStillAccepted(boolean viaArgument) throws Exception {
final File file = write("basedn=ou=Jörg,dc=example,dc=com\n".getBytes(ISO_8859_1));
assertThat(baseDN(file, viaArgument)).isEqualTo("ou=Jörg,dc=example,dc=com");
}

/** The file keeps the properties format: a backslash in a value must be doubled. */
@Test(dataProvider = "readers")
public void testDoubledBackslashIsOneBackslash(boolean viaArgument) throws Exception {
final File file = write("basedn=cn=a\\\\,b,dc=example,dc=com\n".getBytes(UTF_8));
assertThat(baseDN(file, viaArgument)).isEqualTo("cn=a\\,b,dc=example,dc=com");
}

private static String baseDN(File file, boolean viaArgument) throws Exception {
final ArgumentParser parser =
new ArgumentParser(ArgumentParserPropertiesFileTestCase.class.getName(),
LocalizableMessage.raw("test"), false);
final StringArgument baseDN = StringArgument.builder("baseDN")
.valuePlaceholder(LocalizableMessage.raw("{baseDN}"))
.description(LocalizableMessage.raw("base DN"))
.buildAndAddToParser(parser);
if (viaArgument) {
final StringArgument propertiesFile =
CommonArguments.propertiesFileArgument();
parser.addArgument(propertiesFile);
parser.setFilePropertiesArgument(propertiesFile);
parser.parseArguments(new String[] { "--" + propertiesFile.getLongIdentifier(), file.getPath() });
} else {
parser.parseArguments(new String[0], file.getPath(), true);
}
return baseDN.getValue();
}

private static File write(byte[] content) throws Exception {
final File file = File.createTempFile("tools", ".properties");
file.deleteOnExit();
Files.write(file.toPath(), content);
return file;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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]".
*
* Copyright 2026 3A Systems, LLC.
*/
package com.forgerock.opendj.cli;

import static org.fest.assertions.Assertions.assertThat;

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/** Quoting of the values in the equivalent command lines that the tools print. */
@SuppressWarnings("javadoc")
public final class CommandBuilderTestCase extends CliTestCase {

/**
* Values and their Windows form. A Windows program splits its command line by the rules of
* the Microsoft C runtime: backslashes are literal unless they precede a double quote, where
* each pair gives one backslash and an odd one escapes the quote.
*/
@DataProvider
public Object[][] windowsValues() {
return new Object[][] {
{ "cn=a\\,b,dc=x", "\"cn=a\\,b,dc=x\"" },
{ "o=My Company", "\"o=My Company\"" },
{ "(targetattr=\"cn\")(version 3.0; acl \"x\"; allow (read) userdn=\"ldap:///self\";)",
"\"(targetattr=\\\"cn\\\")(version 3.0; acl \\\"x\\\"; allow (read) userdn=\\\"ldap:///self\\\";)\"" },
{ "abc\\", "\"abc\\\\\"" },
{ "a\\\"b", "\"a\\\\\\\"b\"" },
{ "", "\"\"" },
};
}

@Test(dataProvider = "windowsValues")
public void testWindowsValueIsQuotedForTheCRuntime(String value, String expected) {
assertThat(CommandBuilder.escapeValue(value, false)).isEqualTo(expected);
}

@Test
public void testUnixValueIsBackslashEscaped() {
assertThat(CommandBuilder.escapeValue("cn=a\\,b \"x\"", true)).isEqualTo("cn=a\\\\,b\\ \\\"x\\\"");
}

/** A dsconfig batch line takes an unescaped single quote as an opening quote. */
@Test
public void testUnixApostropheIsEscaped() {
assertThat(CommandBuilder.escapeValue("description:O'Brien", true)).isEqualTo("description:O\\'Brien");
}
}
Loading
Loading