mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
6 hours ago 069a1256c6ebdc1142e525a44733bc32fd834061
opendj-core/src/main/java/com/forgerock/opendj/util/SizeLimitInputStream.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2009 Sun Microsystems, Inc.
 * Portions Copyright 2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package com.forgerock.opendj.util;
@@ -130,7 +131,12 @@
            n = readLimit - bytesRead;
        }
        bytesRead += n;
        return parentStream.skip(n);
        // The parent stream is allowed to skip fewer bytes than requested, so only account for the
        // bytes which were actually skipped.
        final long skipped = parentStream.skip(n);
        if (skipped > 0) {
            bytesRead += (int) skipped;
        }
        return skipped;
    }
}
opendj-core/src/main/java/org/forgerock/opendj/io/ASN1InputStreamReader.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions copyright 2012-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.forgerock.opendj.io;
@@ -145,10 +146,14 @@
        // Ignore all unused trailing components.
        final SizeLimitInputStream subSq = (SizeLimitInputStream) in;
        if (subSq.getSizeLimit() - subSq.getBytesRead() > 0) {
            logger.trace("Ignoring %d unused trailing bytes in ASN.1 SEQUENCE",
                    subSq.getSizeLimit() - subSq.getBytesRead());
            subSq.skip(subSq.getSizeLimit() - subSq.getBytesRead());
        final long unusedBytes = subSq.getSizeLimit() - subSq.getBytesRead();
        if (unusedBytes > 0) {
            logger.trace("Ignoring %d unused trailing bytes in ASN.1 SEQUENCE", unusedBytes);
            if (skipFully(subSq, unusedBytes) != unusedBytes) {
                // The remaining components could not be skipped, so the reader would be left
                // positioned in the middle of the sequence.
                throw DecodeException.fatalError(ERR_ASN1_SKIP_TRUNCATED_VALUE.get(unusedBytes));
            }
        }
        logger.trace("READ ASN.1 END SEQUENCE");
@@ -385,7 +390,7 @@
        // Read the header if haven't done so already
        peekLength();
        final long bytesSkipped = in.skip(peekLength);
        final long bytesSkipped = skipFully(in, peekLength);
        if (bytesSkipped != peekLength) {
            final LocalizableMessage message = ERR_ASN1_SKIP_TRUNCATED_VALUE.get(peekLength);
            throw DecodeException.fatalError(message);
@@ -395,6 +400,36 @@
    }
    /**
     * Skips the requested number of bytes, retrying as needed since {@link InputStream#skip} is
     * allowed to skip fewer bytes than requested even when the end of the stream has not been
     * reached.
     *
     * @param stream
     *            The stream to skip bytes from.
     * @param length
     *            The number of bytes to skip.
     * @return The number of bytes actually skipped, which is smaller than {@code length} only if the
     *         end of the stream was reached.
     * @throws IOException
     *             If an error occurs while skipping bytes.
     */
    private static long skipFully(final InputStream stream, final long length) throws IOException {
        long remaining = length;
        while (remaining > 0) {
            final long skipped = stream.skip(remaining);
            if (skipped > 0) {
                remaining -= skipped;
            } else if (stream.read() < 0) {
                // End of stream reached: no more bytes can be skipped.
                break;
            } else {
                remaining--;
            }
        }
        return length - remaining;
    }
    /**
     * Internal helper method reading the additional ASN.1 length bytes and
     * transition to the next state if successful.
     *
opendj-core/src/test/java/com/forgerock/opendj/util/SizeLimitInputStreamTestCase.java
New file
@@ -0,0 +1,72 @@
/*
 * 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.util;
import static org.fest.assertions.Assertions.*;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import org.testng.annotations.Test;
/** Test {@code SizeLimitInputStream}. */
@SuppressWarnings("javadoc")
public final class SizeLimitInputStreamTestCase extends UtilTestCase {
    @Test
    public void testSkipAccountsForTheBytesActuallySkipped() throws Exception {
        // A BufferedInputStream only skips what its buffer holds, without reaching the end of the
        // parent stream, which is exactly the case getBytesRead() used to get wrong.
        final InputStream parent = new BufferedInputStream(new ByteArrayInputStream(new byte[32]), 4);
        // Fill the buffer with 4 bytes and consume one of them, leaving 3 skippable bytes in it.
        parent.read();
        final SizeLimitInputStream stream = new SizeLimitInputStream(parent, 32);
        assertThat(stream.skip(16)).isEqualTo(3);
        assertThat(stream.getBytesRead()).isEqualTo(3);
    }
    @Test
    public void testSkipIsCappedToTheSizeLimit() throws Exception {
        final SizeLimitInputStream stream =
                new SizeLimitInputStream(new ByteArrayInputStream(new byte[32]), 8);
        assertThat(stream.skip(20)).isEqualTo(8);
        assertThat(stream.getBytesRead()).isEqualTo(8);
        assertThat(stream.read()).isEqualTo(-1);
    }
    @Test
    public void testSkipStopsAtTheEndOfTheParentStream() throws Exception {
        final SizeLimitInputStream stream =
                new SizeLimitInputStream(new ByteArrayInputStream(new byte[2]), 8);
        assertThat(stream.skip(8)).isEqualTo(2);
        assertThat(stream.getBytesRead()).isEqualTo(2);
    }
    @Test
    public void testReadAccountsForTheBytesActuallyRead() throws Exception {
        final SizeLimitInputStream stream =
                new SizeLimitInputStream(new ByteArrayInputStream(new byte[32]), 8);
        assertThat(stream.read()).isEqualTo(0);
        assertThat(stream.read(new byte[16])).isEqualTo(7);
        assertThat(stream.getBytesRead()).isEqualTo(8);
        assertThat(stream.read()).isEqualTo(-1);
    }
}
opendj-core/src/test/java/org/forgerock/opendj/io/ASN1InputStreamReaderTestCase.java
@@ -13,11 +13,17 @@
 *
 * Copyright 2010 Sun Microsystems, Inc.
 * Portions copyright 2013 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.forgerock.opendj.io;
import static org.testng.Assert.*;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import org.testng.annotations.Test;
/**
 * Test class for ASN1InputStreamReader.
 */
@@ -27,4 +33,57 @@
        final ByteArrayInputStream inStream = new ByteArrayInputStream(b);
        return new ASN1InputStreamReader(inStream, maxElementSize);
    }
    /**
     * Returns a reader whose underlying stream skips fewer bytes than requested without having
     * reached its end, which is what {@code BufferedInputStream} does once its buffer is partially
     * consumed.
     */
    private ASN1Reader getBufferedReader(final byte[] b) {
        return new ASN1InputStreamReader(new BufferedInputStream(new ByteArrayInputStream(b), 4), 0);
    }
    /**
     * Tests that the trailing components of a sequence are fully skipped even when the underlying
     * stream skips fewer bytes than requested.
     *
     * @throws Exception
     *             If an unexpected problem occurs.
     */
    @Test
    public void testDecodeSequenceIncompleteReadOverBufferedStream() throws Exception {
        // A sequence holding ten booleans, of which only the first one is read, followed by an
        // integer which must still be decoded correctly once the sequence has been skipped.
        final byte[] b =
                new byte[] { 0x30, 0x0C, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
                    0x01, 0x01, 0x02, 0x01, 0x7F };
        final ASN1Reader reader = getBufferedReader(b);
        reader.readStartSequence();
        assertFalse(reader.readBoolean());
        reader.readEndSequence();
        assertEquals(reader.readInteger(), 127);
    }
    /**
     * Tests that {@code skipElement} does not report a truncated value when the underlying stream
     * skips fewer bytes than requested.
     *
     * @throws Exception
     *             If an unexpected problem occurs.
     */
    @Test
    public void testSkipElementOverBufferedStream() throws Exception {
        final byte[] b =
                new byte[] { 0x30, 0x0C, 0x02, 0x01, 0x05, 0x04, 0x04, 0x61, 0x62, 0x63, 0x64, 0x02,
                    0x01, 0x7F };
        final ASN1Reader reader = getBufferedReader(b);
        reader.readStartSequence();
        assertEquals(reader.readInteger(), 5);
        reader.skipElement();
        assertEquals(reader.readInteger(), 127);
        reader.readEndSequence();
    }
}
opendj-server-legacy/src/main/java/org/opends/quicksetup/LicenseFile.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.quicksetup;
@@ -22,7 +23,10 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.quicksetup.util.Utils;
import org.opends.server.util.ServerConstants;
@@ -33,6 +37,8 @@
 */
public class LicenseFile
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  private static final String INSTALL_ROOT_SYSTEM_PROPERTY = "INSTALL_ROOT";
  /** The license file name in Legal directory. */
  private static final String LICENSE_FILE_NAME = "Forgerock_License.txt";
@@ -61,9 +67,9 @@
    String instanceLegalDirName = Utils.getInstancePathFromInstallPath(getInstallDirectory())
        + File.separator + LEGAL_FOLDER_NAME;
    File instanceLegalDir = new File(instanceLegalDirName);
    if (!instanceLegalDir.exists())
    if (!instanceLegalDir.isDirectory() && !instanceLegalDir.mkdirs())
    {
      instanceLegalDir.mkdir();
      logger.warn(LocalizableMessage.raw("Unable to create the legal directory %s", instanceLegalDirName));
    }
    return instanceLegalDirName;
  }
@@ -164,17 +170,19 @@
      String instanceLegalDirName = instanceDirname + File.separator + LEGAL_FOLDER_NAME;
      File instanceLegalDir = new File(instanceLegalDirName);
      File approvalFile = new File(instanceLegalDir, ACCEPTED_LICENSE_FILE_NAME);
      try
      {
        if (!instanceLegalDir.exists())
        Files.createDirectories(instanceLegalDir.toPath());
        if (!approvalFile.createNewFile() && !approvalFile.isFile())
        {
          instanceLegalDir.mkdir();
          logger.warn(LocalizableMessage.raw("Unable to create the license approval file %s", approvalFile));
        }
        new File(instanceLegalDir, ACCEPTED_LICENSE_FILE_NAME).createNewFile();
      }
      catch (IOException e)
      {
        // do nothing
        logger.warn(LocalizableMessage.raw(
            "Unable to create the license approval file %s: %s", approvalFile, e.getLocalizedMessage()));
      }
    }
  }
opendj-server-legacy/src/main/java/org/opends/quicksetup/installer/InstallerHelper.java
@@ -977,17 +977,6 @@
        writer.write(getJavaArgPropertyForScript(scriptName) + "=" + argument);
      }
    }
    String libDir = Utils.getPath(
        Utils.getInstancePathFromInstallPath(installPath), LIBRARIES_PATH_RELATIVE);
    // Create directory if it doesn't exist yet
    File fLib = new File(libDir);
    if (!fLib.exists())
    {
      fLib.mkdir();
    }
//    final String destinationFile = Utils.getPath(libDir, isWindows() ? SET_JAVA_PROPERTIES_FILE_WINDOWS
//                                                                     : SET_JAVA_PROPERTIES_FILE_UNIX);
  }
  /**
opendj-server-legacy/src/main/java/org/opends/server/admin/doc/ConfigGuideGeneration.java
@@ -119,7 +119,7 @@
        generationDir = Files.createTempDirectory(CONFIG_GUIDE_DIR).toString();
      } else {
        // Create new dir if necessary
        new File(generationDir).mkdir();
        Files.createDirectories(new File(generationDir).toPath());
      }
    } catch (Exception e) {
      e.printStackTrace();
opendj-server-legacy/src/main/java/org/opends/server/backends/LDIFBackend.java
@@ -280,25 +280,13 @@
      throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), m);
    }
    if (tempFile.exists())
    {
      // Rename the existing "live" file out of the way and move the new file
      // into place.
      try
      {
        oldFile.delete();
      }
      catch (Exception e)
      {
        logger.traceException(e);
      }
    }
    // Rename the existing "live" file out of the way and move the new file into place.
    // renameFile() deletes an existing ".old" file and reports a failure to do so.
    try
    {
      if (ldifFile.exists())
      {
        ldifFile.renameTo(oldFile);
        renameFile(ldifFile, oldFile);
      }
    }
    catch (Exception e)
@@ -308,7 +296,7 @@
    try
    {
      tempFile.renameTo(ldifFile);
      renameFile(tempFile, ldifFile);
    }
    catch (Exception e)
    {
opendj-server-legacy/src/main/java/org/opends/server/backends/task/TaskScheduler.java
@@ -1249,28 +1249,15 @@
      writer.close();
      // See if there is a ".save" file.  If so, then delete it.
      File saveFile = getFileForPath(backingFilePath + ".save");
      try
      {
        if (saveFile.exists())
        {
          saveFile.delete();
        }
      }
      catch (Exception e)
      {
        logger.traceException(e);
      }
      // If there is an existing backing file, then rename it to ".save".
      // renameFile() deletes an existing ".save" file and reports a failure to do so.
      File saveFile = getFileForPath(backingFilePath + ".save");
      File backingFile = getFileForPath(backingFilePath);
      try
      {
        if (backingFile.exists())
        {
          backingFile.renameTo(saveFile);
          renameFile(backingFile, saveFile);
        }
      }
      catch (Exception e)
@@ -1289,7 +1276,7 @@
      File tmpFile = getFileForPath(tmpFilePath);
      try
      {
        tmpFile.renameTo(backingFile);
        renameFile(tmpFile, backingFile);
      }
      catch (Exception e)
      {
opendj-server-legacy/src/main/java/org/opends/server/config/ConfigurationHandler.java
@@ -817,25 +817,14 @@
    }
    // If a ".startok" file already exists, then move it to an ".old" file.
    // renameFile() deletes an existing ".old" file and reports a failure to do so.
    File oldFile = new File(oldFilePath);
    try
    {
      if (oldFile.exists())
      {
        oldFile.delete();
      }
    }
    catch (Exception e)
    {
      logger.traceException(e);
    }
    File startOKFile = new File(startOKFilePath);
    try
    {
      if (startOKFile.exists())
      {
        startOKFile.renameTo(oldFile);
        renameFile(startOKFile, oldFile);
      }
    }
    catch (Exception e)
@@ -846,7 +835,7 @@
    // Rename the temp file to the ".startok" file.
    try
    {
      tempFile.renameTo(startOKFile);
      renameFile(tempFile, startOKFile);
    }
    catch (Exception e)
    {
@@ -1609,20 +1598,12 @@
    // Move the current config file out of the way and replace it with the updated version.
    File oldSource = new File(sourceFile.getAbsolutePath() + ".prechanges");
    if (oldSource.exists())
    {
      oldSource.delete();
    }
    sourceFile.renameTo(oldSource);
    new File(tempFilePath).renameTo(sourceFile);
    renameFile(sourceFile, oldSource);
    renameFile(new File(tempFilePath), sourceFile);
    // Move the changes file out of the way so it doesn't get applied again.
    File newChanges = new File(changesFile.getAbsolutePath() + ".applied");
    if (newChanges.exists())
    {
      newChanges.delete();
    }
    changesFile.renameTo(newChanges);
    renameFile(changesFile, newChanges);
  }
  private void applyConfigChangesIfNeeded(File configFileToUse) throws InitializationException
opendj-server-legacy/src/main/java/org/opends/server/core/DefaultCompressedSchema.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.core;
@@ -23,6 +24,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@@ -228,14 +230,21 @@
        if (liveFile.exists())
        {
          // Keeping a copy of the previous version is best effort only: save() is called from the
          // entry encoding path, so failing here would turn a backup problem into a failed update.
          final File saveFile = new File(liveFile.getAbsolutePath() + ".save");
          if (saveFile.exists())
          try
          {
            saveFile.delete();
            renameFile(liveFile, saveFile);
          }
          liveFile.renameTo(saveFile);
          catch (final IOException e)
          {
            logger.traceException(e);
            logger.warn(WARN_COMPRESSEDSCHEMA_CANNOT_SAVE_PREVIOUS_DATA, liveFile, saveFile,
                stackTraceToSingleLineString(e));
          }
        }
        tempFile.renameTo(liveFile);
        renameFile(tempFile, liveFile);
      }
      catch (final Exception e)
      {
opendj-server-legacy/src/main/java/org/opends/server/core/LockFileManager.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2013-2015 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.core;
@@ -93,9 +94,9 @@
      File f = getFileForPath(lockFile);
      try
      {
        if (! f.exists())
        if (! f.createNewFile())
        {
          f.createNewFile();
          logger.trace("Lock file %s already exists", lockFile);
        }
      }
      catch (Exception e)
@@ -207,9 +208,9 @@
      File f = getFileForPath(lockFile);
      try
      {
        if (! f.exists())
        if (! f.createNewFile())
        {
          f.createNewFile();
          logger.trace("Lock file %s already exists", lockFile);
        }
      }
      catch (Exception e)
opendj-server-legacy/src/main/java/org/opends/server/loggers/MultifileTextWriter.java
@@ -84,6 +84,16 @@
  private long totalFilesRotated;
  private long totalFilesCleaned;
  /**
   * Set when the current file could not be renamed by {@link #rotate()} and cleared as soon as a
   * rotation succeeds again. While it is set, size based rotations are not triggered from
   * {@link #writeRecord(String)} and the failure is not logged again: the rotation is only retried
   * from the {@code RotaterThread}, once per interval.
   */
  private boolean rotationFailed;
  /** Same latch as {@link #rotationFailed}, for the log files a retention policy cannot delete. */
  private boolean cleanupFailed;
  /** The underlying output stream. */
  private MeteredStream outputStream;
  /** The underlying buffered writer using the output stream. */
@@ -172,12 +182,7 @@
                               int bufferSize)
      throws IOException, DirectoryException
  {
    // Create new file if it doesn't exist
    if(!file.exists())
    {
      file.createNewFile();
    }
    // The file is created by the output stream below if it does not exist yet.
    FileOutputStream stream = new FileOutputStream(file, append);
    outputStream = new MeteredStream(stream, file.length());
@@ -426,17 +431,28 @@
            File[] files =
                retentionPolicy.deleteFiles(writer.getNamingPolicy());
            int cleanedCount = 0;
            for(File file : files)
            {
              file.delete();
              totalFilesCleaned++;
              logger.trace("%s cleaned up log file %s", retentionPolicy, file);
              if (file.delete())
              {
                cleanedCount++;
                totalFilesCleaned++;
                cleanupFailed = false;
                logger.trace("%s cleaned up log file %s", retentionPolicy, file);
              }
              else if (!cleanupFailed)
              {
                // Only report the first failure: the same files are returned on every interval.
                cleanupFailed = true;
                logger.warn(WARN_LOGGER_ERROR_DELETING_FILE, file, retentionPolicy);
              }
            }
            if(files.length > 0)
            if(cleanedCount > 0)
            {
              lastCleanTime = TimeThread.getCalendar();
              lastCleanCount = files.length;
              lastCleanCount = cleanedCount;
            }
          }
          catch(DirectoryException de)
@@ -554,7 +570,9 @@
    synchronized(this)
    {
      if(sizeLimit > 0 && outputStream.written + size + 1 >= sizeLimit)
      // Once a rotation has failed the file stays over the size limit, so rotating it again for
      // every single record would only repeat the failure. Leave the retry to the RotaterThread.
      if(sizeLimit > 0 && !rotationFailed && outputStream.written + size + 1 >= sizeLimit)
      {
        rotate();
      }
@@ -590,9 +608,12 @@
  }
  /**
   * Tries to rotate the log files. If the new log file already exists, it
   * tries to rename the file. On failure, all subsequent log write requests
   * will throw exceptions.
   * Tries to rotate the log files by renaming the current file to the name provided by the naming
   * policy. When the rename fails, the current file is kept and appended to rather than truncated,
   * the failure is reported once and the rotation is retried on the next interval.
   * <p>
   * Note that {@code File.renameTo} silently replaces the target on most platforms, so a rotation
   * happening within the same second as a previous one overwrites the file it just rotated.
   */
  private synchronized void rotate()
  {
@@ -609,11 +630,17 @@
    File currentFile = namingPolicy.getInitialName();
    File newFile = namingPolicy.getNextName();
    currentFile.renameTo(newFile);
    final boolean renamed = currentFile.renameTo(newFile);
    // The latch must be set before the writer is re-opened: constructWriter() logs warnings of its
    // own, and when this writer backs the error log they come straight back into writeRecord() on
    // this thread, where they must not trigger another rotation attempt.
    final boolean report = !renamed && !rotationFailed;
    rotationFailed = !renamed;
    try
    {
      constructWriter(currentFile, filePermissions, encoding, append,
      // If the file could not be rotated then keep appending to it rather than truncating it.
      constructWriter(currentFile, filePermissions, encoding, append || !renamed,
                      bufferSize);
    }
    catch (Exception e)
@@ -622,9 +649,18 @@
      errorHandler.handleOpenError(currentFile, e);
    }
    logger.trace("Log file %s rotated and renamed to %s", currentFile, newFile);
    totalFilesRotated++;
    lastRotationTime = TimeThread.getCalendar();
    if (renamed)
    {
      logger.trace("Log file %s rotated and renamed to %s", currentFile, newFile);
      totalFilesRotated++;
      lastRotationTime = TimeThread.getCalendar();
    }
    else if (report)
    {
      logger.error(ERR_LOGGER_ERROR_ROTATING_FILE, currentFile, newFile);
    }
    // lastRotationTime is left untouched on failure so that a time based policy keeps asking for a
    // rotation on every interval instead of waiting for a whole new period.
  }
  @Override
opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
@@ -755,9 +755,9 @@
    try
    {
      if(!logFile.exists())
      if(!logFile.createNewFile())
      {
        logFile.createNewFile();
        logger.trace("Referential integrity update log file %s already exists", logFileName);
      }
    }
    catch (IOException io)
@@ -865,8 +865,9 @@
            }
          }
        }
        logFile.delete();
        logFile.createNewFile();
        if (!logFile.delete() || !logFile.createNewFile()) {
          logger.error(ERR_PLUGIN_REFERENT_CANNOT_REPLACE_LOGFILE, logFileName);
        }
      } catch (IOException io) {
        logger.error(ERR_PLUGIN_REFERENT_REPLACE_LOGFILE, io.getMessage());
      }
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
@@ -21,6 +21,7 @@
import static org.opends.server.util.StaticUtils.*;
import java.io.File;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
@@ -144,10 +145,7 @@
    final File dbDirectory = getFileForPath(dbDirName);
    try
    {
      if (!dbDirectory.exists())
      {
        dbDirectory.mkdir();
      }
      Files.createDirectories(dbDirectory.toPath());
      return dbDirectory;
    }
    catch (Exception e)
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/LogFile.java
@@ -199,9 +199,9 @@
  {
    try
    {
      if (!logfile.exists())
      if (!logfile.createNewFile())
      {
        logfile.createNewFile();
        logger.trace("Log file %s already exists", logfile.getPath());
      }
    }
    catch (IOException e)
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/ReplicationEnvironment.java
@@ -949,7 +949,11 @@
    final File newRotationFile = getLastRotationTimePath(lastRotationTime);
    try
    {
      newRotationFile.createNewFile();
      if (!newRotationFile.createNewFile() && !newRotationFile.isFile())
      {
        throw new ChangelogException(ERR_CHANGELOG_UNABLE_TO_CREATE_LAST_LOG_ROTATION_TIME_FILE.get(
            newRotationFile.getPath(), lastRotationTime));
      }
    }
    catch (IOException e)
    {
opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaFilesWriter.java
@@ -291,7 +291,7 @@
          matchingRuleUses, ldapSyntaxes);
      File upgradeDirectory = getUpgradeDirectory();
      upgradeDirectory.mkdir();
      Files.createDirectories(upgradeDirectory.toPath());
      File concatFile = new File(upgradeDirectory, SCHEMA_CONCAT_FILE_NAME);
      concatFilePath = concatFile.getAbsolutePath();
@@ -313,11 +313,7 @@
        writeLines(writer, ATTR_LDAP_SYNTAXES, ldapSyntaxes);
      }
      if (concatFile.exists())
      {
        concatFile.delete();
      }
      tempFile.renameTo(concatFile);
      renameFile(tempFile, concatFile);
    }
    catch (Exception e)
    {
opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/LicenseFile.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.tools.upgrade;
@@ -21,6 +22,8 @@
import java.io.FileReader;
import java.io.IOException;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.util.StaticUtils;
/**
@@ -31,6 +34,8 @@
 */
class LicenseFile
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  private static final String INSTALL_ROOT_SYSTEM_PROPERTY = "INSTALL_ROOT";
  /**
@@ -80,9 +85,9 @@
    final String instanceDirname = UpgradeUtils.getInstancePathFromInstallPath(getInstallRootPathFromSystem("."));
    final String instanceLegalDirName = instanceDirname + File.separator + LEGAL_FOLDER_NAME;
    final File instanceLegalDir = new File(instanceLegalDirName);
    if (!instanceLegalDir.exists())
    if (!instanceLegalDir.isDirectory() && !instanceLegalDir.mkdirs())
    {
      instanceLegalDir.mkdir();
      logger.warn(LocalizableMessage.raw("Unable to create the legal directory %s", instanceLegalDirName));
    }
    return instanceLegalDirName;
  }
@@ -197,12 +202,18 @@
  {
    if (getApproval())
    {
      final File approvalFile = new File(getInstanceLegalDirectory(), ACCEPTED_LICENSE_FILE_NAME);
      try
      {
        new File(getInstanceLegalDirectory(), ACCEPTED_LICENSE_FILE_NAME).createNewFile();
        if (!approvalFile.createNewFile() && !approvalFile.isFile())
        {
          logger.warn(LocalizableMessage.raw("Unable to create the license approval file %s", approvalFile));
        }
      }
      catch (IOException e)
      { // do  nothing
      {
        logger.warn(LocalizableMessage.raw(
            "Unable to create the license approval file %s: %s", approvalFile, e.getLocalizedMessage()));
      }
    }
  }
opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java
@@ -24,6 +24,7 @@
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -682,17 +683,14 @@
    {
      logger.debug(LocalizableMessage.raw("Parent file of %s doesn't exist", destination.getPath()));
      parentDirectory.mkdirs();
      Files.createDirectories(parentDirectory.toPath());
      logger.debug(LocalizableMessage.raw("Parent directory %s created.", parentDirectory.getPath()));
    }
    if (!destination.exists())
    {
      destination.createNewFile();
    }
    logger.debug(LocalizableMessage.raw("Writing entries in %s.", destination.getAbsolutePath()));
    // The destination file is created by the output stream below if it does not exist yet.
    try (LDIFEntryWriter writer = new LDIFEntryWriter(new FileOutputStream(destination)))
    {
      writer.writeEntry(theNewSchemaEntry);
opendj-server-legacy/src/main/java/org/opends/server/types/BackupDirectory.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.types;
@@ -22,6 +23,7 @@
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
@@ -289,7 +291,7 @@
    // Rename the new descriptor file to match the previous one.
    try
    {
      newDescriptorFile.renameTo(descriptorFile);
      renameFile(newDescriptorFile, descriptorFile);
    }
    catch (Exception e)
    {
@@ -307,7 +309,7 @@
    {
      try
      {
        dir.mkdirs();
        Files.createDirectories(dir.toPath());
      }
      catch (Exception e)
      {
@@ -328,24 +330,11 @@
    {
      String savedDescriptorFilePath = descriptorFilePath + ".save";
      File savedDescriptorFile = new File(savedDescriptorFilePath);
      if (savedDescriptorFile.exists())
      {
        try
        {
          savedDescriptorFile.delete();
        }
        catch (Exception e)
        {
          logger.traceException(e);
          LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR.get(
              savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath);
          throw new IOException(message.toString());
        }
      }
      try
      {
        descriptorFile.renameTo(savedDescriptorFile);
        // renameFile() deletes an existing target and reports a failure to do so.
        renameFile(descriptorFile, savedDescriptorFile);
      }
      catch (Exception e)
      {
opendj-server-legacy/src/main/java/org/opends/server/types/LDIFExportConfig.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2009 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.types;
@@ -195,31 +196,24 @@
        case APPEND:
          // Create new file if it doesn't exist ensuring that we can
          // set its permissions.
          if (!f.exists())
          {
            f.createNewFile();
            mustSetPermissions = true;
          }
          mustSetPermissions = f.createNewFile();
          ldifOutputStream = new FileOutputStream(ldifFile, true);
          break;
        case OVERWRITE:
          // Create new file if it doesn't exist ensuring that we can
          // set its permissions.
          if (!f.exists())
          {
            f.createNewFile();
            mustSetPermissions = true;
          }
          mustSetPermissions = f.createNewFile();
          ldifOutputStream = new FileOutputStream(ldifFile, false);
          break;
        case FAIL:
          if (f.exists())
          // Create new file ensuring that we can set its permissions. The creation is
          // atomic, hence it also fails if the file was created by someone else in
          // the mean time.
          if (!f.createNewFile())
          {
            LocalizableMessage message = ERR_LDIF_FILE_EXISTS.get(ldifFile);
            throw new IOException(message.toString());
          }
          // Create new file ensuring that we can set its permissions.
          f.createNewFile();
          mustSetPermissions = true;
          ldifOutputStream = new FileOutputStream(ldifFile);
          break;
opendj-server-legacy/src/messages/org/opends/messages/core.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2011-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
#
@@ -1076,6 +1077,9 @@
 provided object class set because it used an undefined token %s
ERR_COMPRESSEDSCHEMA_CANNOT_WRITE_UPDATED_DATA_622=Unable to write the \
 updated compressed schema token data: %s
WARN_COMPRESSEDSCHEMA_CANNOT_SAVE_PREVIOUS_DATA_756=Unable to keep a copy of \
 the previous compressed schema token data by renaming %s to %s: %s. The \
 updated token data has still been written
ERR_ENTRYENCODECFG_INVALID_LENGTH_623=Unable to decode the provided \
 entry encode configuration element because it has an invalid length
INFO_RESULT_NO_OPERATION_624=No Operation
opendj-server-legacy/src/messages/org/opends/messages/logger.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2008 Sun Microsystems, Inc.
# Portions Copyright 2015-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
#
# Global directives
@@ -100,3 +101,10 @@
 common audit log publisher %s, the keystore file %s could not be read: %s
ERR_COMMON_AUDIT_KEYSTORE_FILE_IS_EMPTY_33=Error while processing \
 common audit log publisher %s, the keystore file %s is empty
ERR_LOGGER_ERROR_ROTATING_FILE_34=Error occurred while rotating log file %s to \
 %s. The current log file will be kept and appended to, and the rotation will be \
 retried on the next interval. Any further rotation errors will be ignored until \
 a rotation succeeds
WARN_LOGGER_ERROR_DELETING_FILE_35=Error occurred while deleting log file %s \
 while enforcing retention policy %s. Any further deletion errors will be ignored \
 until a log file is deleted successfully
opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2014-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
@@ -235,6 +236,10 @@
 Referential Integrity plugin update log file: %s
ERR_PLUGIN_REFERENT_REPLACE_LOGFILE_84=An error occurred replacing the \
 Referential Integrity plugin update log file: %s
ERR_PLUGIN_REFERENT_CANNOT_REPLACE_LOGFILE_130=The Referential Integrity \
 plugin update log file %s could not be deleted and created again after its \
 records were processed. Those records will be processed again the next time \
 the log file is read
INFO_PLUGIN_REFERENT_LOGFILE_CHANGE_REQUIRES_RESTART_85=The file name that \
 the Referential Integrity plugin logs changes to during background \
 processing has been changed from %s to %s, but this change will not take \
opendj-server-legacy/src/test/java/org/opends/server/loggers/MultifileTextWriterTestCase.java
New file
@@ -0,0 +1,316 @@
/*
 * 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 org.opends.server.loggers;
import static org.mockito.Mockito.*;
import static org.testng.Assert.*;
import java.io.File;
import java.io.FilenameFilter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.server.config.server.SizeLimitLogRotationPolicyCfg;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.types.FilePermission;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/** Tests the rotation of {@link MultifileTextWriter}, in particular when the rename fails. */
@SuppressWarnings("javadoc")
public class MultifileTextWriterTestCase extends DirectoryServerTestCase
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  private static final DN PUBLISHER_DN = DN.valueOf("cn=Test Logger,cn=Loggers,cn=config");
  /** Small enough for two records to overflow it, large enough for one not to. */
  private static final long SIZE_LIMIT = 64;
  private static final String RECORD_A = "a".repeat(32);
  private static final String RECORD_B = "b".repeat(32);
  /** Long enough to leave the re-opened stream over the size limit together with the first record. */
  private static final String STAND_IN_WARNING =
      "stand-in for the permission warnings logged while the writer is re-opened";
  /** Naming policy with fixed names, so that the test does not depend on the current second. */
  private static final class FixedNamingPolicy implements FileNamingPolicy
  {
    private final File initialFile;
    private final File nextFile;
    private FixedNamingPolicy(File initialFile, File nextFile)
    {
      this.initialFile = initialFile;
      this.nextFile = nextFile;
    }
    @Override
    public File getInitialName()
    {
      return initialFile;
    }
    @Override
    public File getNextName()
    {
      return nextFile;
    }
    @Override
    public FilenameFilter getFilenameFilter()
    {
      return new FilenameFilter()
      {
        @Override
        public boolean accept(File dir, String name)
        {
          return name.equals(nextFile.getName());
        }
      };
    }
    @Override
    public File[] listFiles()
    {
      return new File[0];
    }
  }
  /**
   * A file which can run a hook from within constructWriter(): after a failed rotation the writer
   * is re-opened, and {@code FilePermission.setPermissions} checks {@code exists()} at the exact
   * point where the permission warnings are logged.
   */
  private static final class HookedFile extends File
  {
    private static final long serialVersionUID = 1L;
    private transient Runnable onExists;
    private HookedFile(File parent, String child)
    {
      super(parent, child);
    }
    @Override
    public boolean exists()
    {
      if (onExists != null)
      {
        onExists.run();
      }
      return super.exists();
    }
  }
  private File tempDir;
  private File logFile;
  private File rotatedFile;
  private MultifileTextWriter writer;
  @BeforeClass
  public void startServer() throws Exception
  {
    // The writer registers itself as a shutdown listener, which requires a bootstrapped server.
    TestCaseUtils.startServer();
  }
  @BeforeMethod
  public void setUp() throws Exception
  {
    tempDir = Files.createTempDirectory("MultifileTextWriterTestCase").toFile();
    logFile = new File(tempDir, "test.log");
    rotatedFile = new File(tempDir, "test.log.rotated");
  }
  @AfterMethod
  public void tearDown() throws Exception
  {
    if (writer != null)
    {
      writer.shutdown();
      writer = null;
    }
    deleteRecursively(tempDir);
  }
  /**
   * Makes the rename attempted by the rotation fail on every platform: renaming a file onto a
   * non-empty directory is refused both on POSIX systems and on Windows.
   */
  private void breakRotation() throws Exception
  {
    assertTrue(rotatedFile.mkdir());
    assertTrue(new File(rotatedFile, "blocker").createNewFile());
  }
  private MultifileTextWriter newWriter() throws Exception
  {
    MultifileTextWriter newWriter = new MultifileTextWriter("Multifile Text Writer for " + PUBLISHER_DN,
        Long.MAX_VALUE, new FixedNamingPolicy(logFile, rotatedFile), FilePermission.decodeUNIXMode("600"),
        new LogPublisherErrorHandler(PUBLISHER_DN), "UTF-8", true, true, 0);
    SizeLimitLogRotationPolicyCfg config = mock(SizeLimitLogRotationPolicyCfg.class);
    when(config.getFileSizeLimit()).thenReturn(SIZE_LIMIT);
    SizeBasedRotationPolicy policy = new SizeBasedRotationPolicy();
    policy.initializeLogRotationPolicy(config);
    newWriter.addRotationPolicy(policy);
    // The rotater thread is deliberately not started: the rotations under test are the ones
    // triggered inline by writeRecord().
    return newWriter;
  }
  private List<String> linesOf(File file) throws Exception
  {
    return Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
  }
  private static void deleteRecursively(File file)
  {
    if (file.isDirectory())
    {
      for (File child : file.listFiles())
      {
        deleteRecursively(child);
      }
    }
    file.delete();
  }
  @Test
  public void testSuccessfulRotationRenamesTheFile() throws Exception
  {
    writer = newWriter();
    writer.writeRecord(RECORD_A);
    writer.writeRecord(RECORD_B);
    writer.flush();
    assertEquals(writer.getTotalFilesRotated(), 1);
    assertEquals(linesOf(rotatedFile), List.of(RECORD_A));
    assertEquals(linesOf(logFile), List.of(RECORD_B));
  }
  @Test
  public void testFailedRotationAppendsInsteadOfTruncating() throws Exception
  {
    breakRotation();
    writer = newWriter();
    writer.writeRecord(RECORD_A);
    assertEquals(writer.getTotalFilesRotated(), 0);
    // Overflows the size limit, so a rotation is attempted and fails.
    writer.writeRecord(RECORD_B);
    writer.flush();
    assertEquals(writer.getTotalFilesRotated(), 0, "a failed rotation must not be counted");
    assertTrue(rotatedFile.isDirectory(), "the rotation target must have been left alone");
    assertEquals(linesOf(logFile), List.of(RECORD_A, RECORD_B),
        "the log file must have been appended to rather than truncated");
  }
  /**
   * A failed rotation of the file backing the error log used to come back into writeRecord() on the
   * same thread, from a writer which had just been closed and was still over the size limit, and to
   * recurse until the stack blew up.
   */
  @Test
  @SuppressWarnings({ "rawtypes", "unchecked" })
  public void testFailedRotationOfTheErrorLogDoesNotRecurse() throws Exception
  {
    breakRotation();
    writer = newWriter();
    MultifileTextWriter publishing = writer;
    ErrorLogPublisher publisher = TextErrorLogPublisher.getServerStartupTextErrorPublisher(publishing);
    ErrorLogger.getInstance().addLogPublisher(publisher);
    try
    {
      publishing.writeRecord(RECORD_A);
      // Overflows the size limit: the rotation fails and reports the failure through the very
      // logger this writer is backing.
      publishing.writeRecord(RECORD_B);
      publishing.flush();
    }
    finally
    {
      // Removing the publisher closes it, which already shuts this writer down.
      ErrorLogger.getInstance().removeLogPublisher(publisher);
      writer = null;
    }
    assertEquals(publishing.getTotalFilesRotated(), 0);
    List<String> lines = linesOf(logFile);
    assertEquals(lines.get(0), RECORD_A, "the log file must have been appended to");
    assertTrue(lines.contains(RECORD_B), "the record which triggered the rotation must not be lost");
    assertTrue(lines.stream().anyMatch(line -> line.contains("rotating log file")),
        "the rotation failure must be reported in the log file, but it contained: " + lines);
  }
  /**
   * constructWriter() logs permission warnings of its own, between re-seeding the stream with the
   * over-limit file length and the point where the failed rotation used to set its latch. When the
   * writer backs the error log, such a warning used to re-enter writeRecord() and to recurse until
   * the stack blew up. The hook stands in for the permission warning, firing at the same point of
   * the re-open.
   */
  @Test
  @SuppressWarnings({ "rawtypes", "unchecked" })
  public void testWarningDuringReopenAfterFailedRotationDoesNotRecurse() throws Exception
  {
    breakRotation();
    HookedFile hookedLogFile = new HookedFile(tempDir, logFile.getName());
    logFile = hookedLogFile;
    writer = newWriter();
    MultifileTextWriter publishing = writer;
    ErrorLogPublisher publisher = TextErrorLogPublisher.getServerStartupTextErrorPublisher(publishing);
    ErrorLogger.getInstance().addLogPublisher(publisher);
    try
    {
      publishing.writeRecord(RECORD_A);
      hookedLogFile.onExists = () -> logger.warn(LocalizableMessage.raw(STAND_IN_WARNING));
      // Overflows the size limit: the rotation fails, and while the writer is re-opened the hook
      // logs through the very logger this writer is backing, like the permission warnings do.
      publishing.writeRecord(RECORD_B);
      publishing.flush();
    }
    finally
    {
      // Removing the publisher closes it, which already shuts this writer down.
      ErrorLogger.getInstance().removeLogPublisher(publisher);
      writer = null;
    }
    assertEquals(publishing.getTotalFilesRotated(), 0);
    List<String> lines = linesOf(logFile);
    assertEquals(lines.get(0), RECORD_A, "the log file must have been appended to");
    assertTrue(lines.stream().anyMatch(line -> line.contains(STAND_IN_WARNING)),
        "the warning logged during the re-open must not be lost, but the log file contained: " + lines);
    assertTrue(lines.contains(RECORD_B), "the record which triggered the rotation must not be lost");
    assertEquals(lines.stream().filter(line -> line.contains("rotating log file")).count(), 1L,
        "the rotation failure must be reported exactly once, but the log file contained: " + lines);
  }
}