From fc0edcb55feb2e35e60564de1eeee97b48880a00 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 04 Aug 2026 08:25:45 +0000
Subject: [PATCH] [#828] Fix infinite loop picking the errors-encountered LDIF file name (#839)

---
 opendj-server-legacy/src/main/java/org/opends/server/protocols/LDIFConnectionHandler.java         |   62 +++++++--------
 opendj-server-legacy/src/test/java/org/opends/server/protocols/LDIFConnectionHandlerTestCase.java |  120 ++++++++++++++++++++++++++++++
 2 files changed, 150 insertions(+), 32 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/LDIFConnectionHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/LDIFConnectionHandler.java
index 771f625..535a5b8 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/LDIFConnectionHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/LDIFConnectionHandler.java
@@ -13,11 +13,13 @@
  *
  * Copyright 2008-2009 Sun Microsystems, Inc.
  * Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.protocols;
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -311,21 +313,8 @@
     importConfig.setInvokeImportPlugins(false);
     importConfig.setValidateSchema(true);
 
-    String outputPath = inputPath + ".applied." + TimeThread.getGMTTime();
-    if (new File(outputPath).exists())
-    {
-      int i=2;
-      while (true)
-      {
-        if (! new File(outputPath + "." + i).exists())
-        {
-          outputPath = outputPath + "." + i;
-          break;
-        }
-
-        i++;
-      }
-    }
+    String outputPath = selectUnusedPath(inputPath + ".applied." +
+                                         TimeThread.getGMTTime());
 
     LDIFExportConfig exportConfig =
          new LDIFExportConfig(outputPath, ExistingFileBehavior.APPEND);
@@ -470,21 +459,8 @@
 
     if (errorEncountered || !fullyProcessed)
     {
-      String renamedPath = inputPath + ".errors-encountered." +
-                           TimeThread.getGMTTime();
-      if (new File(renamedPath).exists())
-      {
-        int i=2;
-        while (true)
-        {
-          if (! new File(renamedPath + "." + i).exists())
-          {
-            renamedPath = renamedPath + "." + i;
-          }
-
-          i++;
-        }
-      }
+      String renamedPath = selectUnusedPath(inputPath + ".errors-encountered." +
+                                            TimeThread.getGMTTime());
 
       try
       {
@@ -493,7 +469,7 @@
           logger.trace("Renaming source file to " + renamedPath);
         }
 
-        ldifFile.renameTo(new File(renamedPath));
+        renameFile(ldifFile, new File(renamedPath));
       }
       catch (Exception e)
       {
@@ -515,7 +491,7 @@
           logger.trace("Deleting source file");
         }
 
-        ldifFile.delete();
+        Files.delete(ldifFile.toPath());
       }
       catch (Exception e)
       {
@@ -532,6 +508,28 @@
 
 
 
+  /**
+   * Returns the provided path if no file exists at it.  Otherwise, appends
+   * ".2", ".3", ... to the provided path and returns the first resulting path
+   * at which no file exists.
+   *
+   * @param  basePath  The desired path for the file.
+   *
+   * @return  The first path at which no file exists.
+   */
+  static String selectUnusedPath(String basePath)
+  {
+    String path = basePath;
+    for (int i=2; new File(path).exists(); i++)
+    {
+      path = basePath + "." + i;
+    }
+
+    return path;
+  }
+
+
+
   @Override
   public void toString(StringBuilder buffer)
   {
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/protocols/LDIFConnectionHandlerTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/protocols/LDIFConnectionHandlerTestCase.java
index 801798d..e9005ef 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/protocols/LDIFConnectionHandlerTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/protocols/LDIFConnectionHandlerTestCase.java
@@ -22,6 +22,9 @@
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
 
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -336,6 +339,123 @@
 
 
   /**
+   * Tests that the path selected for the applied and errors-encountered files
+   * skips the names which are already taken and stops at the first free one.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test
+  public void testSelectUnusedPath()
+         throws Exception
+  {
+    File tempDir = TestCaseUtils.createTemporaryDirectory("testSelectUnusedPath");
+
+    try
+    {
+      String basePath = new File(tempDir,
+           "test.ldif.errors-encountered.20260803120000Z").getPath();
+
+      // No file exists at the base path, so it is used as-is.
+      assertEquals(LDIFConnectionHandler.selectUnusedPath(basePath), basePath);
+
+      // The base path is taken, so ".2" is appended.
+      assertTrue(new File(basePath).createNewFile());
+      assertEquals(LDIFConnectionHandler.selectUnusedPath(basePath),
+                   basePath + ".2");
+
+      // The base path and several numbered variants are taken, so the first
+      // free variant is picked, with exactly one suffix appended.
+      assertTrue(new File(basePath + ".2").createNewFile());
+      assertTrue(new File(basePath + ".3").createNewFile());
+      assertEquals(LDIFConnectionHandler.selectUnusedPath(basePath),
+                   basePath + ".4");
+    }
+    finally
+    {
+      TestCaseUtils.deleteDirectory(tempDir);
+    }
+  }
+
+
+
+  /**
+   * Tests that an unparseable LDIF file is renamed out of the way even when
+   * the errors-encountered name is already taken.  The name-picking loop used
+   * to hang forever in that case (issue #828).
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test
+  public void testUnparseableLDIFErrorsNameTaken()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(false);
+
+    File tempDir =
+         TestCaseUtils.createTemporaryDirectory("testErrorsNameTaken");
+
+    TestCaseUtils.dsconfig(
+      "set-connection-handler-prop",
+      "--handler-name", "LDIF Connection Handler",
+      "--set", "ldif-directory:" + tempDir.getAbsolutePath(),
+      "--set", "enabled:true");
+
+    try
+    {
+      // Occupy every errors-encountered name the handler may pick while this
+      // test runs, so that it has to fall back to a numbered variant.
+      SimpleDateFormat timestampFormat =
+           new SimpleDateFormat("yyyyMMddHHmmss'Z'");
+      timestampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+      long now = System.currentTimeMillis();
+      for (long t = now - 5000L; t <= now + 30000L; t += 1000L)
+      {
+        assertTrue(new File(tempDir, "testTaken.ldif.errors-encountered." +
+             timestampFormat.format(new Date(t))).createNewFile());
+      }
+
+      String path = TestCaseUtils.createTempFile(
+        "unparseable");
+
+      File tempFile = new File(path);
+      File newFile = new File(tempDir, "testTaken.ldif");
+      assertTrue(tempFile.renameTo(newFile));
+
+      boolean numberedErrorsFound = false;
+      long stopTime  = System.currentTimeMillis() + 20000L;
+      while (System.currentTimeMillis() < stopTime)
+      {
+        if (! newFile.exists())
+        {
+          // The file should have been renamed to the first numbered variant
+          // of the errors-encountered name which was already taken.
+          for (File f : tempDir.listFiles())
+          {
+            if (f.getName().startsWith("testTaken.ldif.errors-encountered.") &&
+                f.getName().endsWith(".2"))
+            {
+              numberedErrorsFound = true;
+            }
+          }
+
+          break;
+        }
+
+        Thread.sleep(10);
+      }
+
+      assertFalse(newFile.exists());
+      assertTrue(numberedErrorsFound);
+    }
+    finally
+    {
+      TestCaseUtils.deleteDirectory(tempDir);
+    }
+  }
+
+
+
+  /**
    * Tests a number of methods that are part of the generic connection handler
    * API.
    *

--
Gitblit v1.10.0