Repository: KSP-CKAN/CKAN (2026-04-12T01:00:58Z)

Issue:
[Bug]: CKAN copies files infinitely if clone target folder is subdirectory of source folder
### Is there an existing issue for this?

- [x] I have checked the existing issues for an existing issue. I even actually looked at the ones that are pinned to the top!

### Operating System

Linux

### CKAN Version

v1.36.0

### Games

KSP 1

### Game Version

v1.24.5 or something

### Did you make any manual changes to your game folder (i.e., not via CKAN)?

_No response_

### Describe the bug

I tried to clone my default instance, Kerbal Space Program, to a new instance called RP1, and thought I would fit the new instance into a subdirectory of the original instance folder. It was taking a while so I left it and forgot about it, and lo and behold, I have 600 gigabytes of RP1 instance, due to CKAN repeatedly copying the files it copied into RP1 into itself.

CKAN should either detect when to not copy recursively, using some standard operating system copy tool that doesn't do this, or throw an error if the canonicalised path of the target folder is a subdirectory of the canonicalised path of the source directory.

<img width="1920" height="937" alt="Image" src="https://github.com/user-attachments/assets/50dadd96-b521-4c17-b25f-1d4934f1ea91" />

<img width="403" height="34" alt="Image" src="https://github.com/user-attachments/assets/a46f339c-ab59-41b4-bd39-cafca450bd0a" />

### Steps to reproduce

Clone an instance and set the target directory to a subdirectory of the original

### Relevant log output

_No response_


Discussion:
Thanks!



Patch:
diff --git a/Core/Extensions/IOExtensions.cs b/Core/Extensions/IOExtensions.cs
index 3aee54056..620ade18c 100644
--- a/Core/Extensions/IOExtensions.cs
+++ b/Core/Extensions/IOExtensions.cs
@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using System.IO;
 using System.Collections.Generic;
 using System.Threading;
@@ -17,6 +18,11 @@ public static class IOExtensions
         public static DriveInfo? GetDrive(this DirectoryInfo dir)
             => Utilities.DefaultIfThrows(() => new DriveInfo(dir.FullName));
 
+        public static bool AncestorPathOf(this DirectoryInfo outer, DirectoryInfo inner)
+            => inner.TraverseNodes(di => di.Parent)
+                    .Select(di => di.FullName)
+                    .Any(di => outer.FullName.Equals(di, Platform.PathComparison));
+
         /// <summary>
         /// File.WriteAllText replacement that doesn't sometimes write all NULs instead on Windows.
         /// https://stackoverflow.com/questions/54078564
diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx
index 758e63892..0e2e4bc74 100644
--- a/Core/Properties/Resources.resx
+++ b/Core/Properties/Resources.resx
@@ -338,6 +338,7 @@ Free up space on that device or change your settings to use another location.
   <data name="KrakenInstanceNameTaken" xml:space="preserve"><value>This instance name is already taken: {0}</value></data>
   <data name="KrakenNoInstance" xml:space="preserve"><value>No instance with this name or at this path: {0}</value></data>
   <data name="DirectoryNotEmpty" xml:space="preserve"><value>Directory not empty: {0}</value></data>
+  <data name="CannotCloneIntoSelf" xml:space="preserve"><value>Cannot clone {0} into itself</value></data>
   <data name="RelationshipResolverConflictsWith" xml:space="preserve"><value>{0} conflicts with {1}</value></data>
   <data name="RelationshipResolverConflictingModDescription" xml:space="preserve"><value>{0} (needed for: {1})</value></data>
   <data name="RelationshipResolverRequiredButResolver" xml:space="preserve"><value>{0} required, but an incompatible version is in the resolver</value></data>
diff --git a/Core/Utilities.cs b/Core/Utilities.cs
index 215fc3805..88c43e21d 100644
--- a/Core/Utilities.cs
+++ b/Core/Utilities.cs
@@ -126,6 +126,13 @@ private static void CopyDirectory(string        sourceDirPath,
                                                         destDirPath));
             }
 
+            if (new DirectoryInfo(sourceDirPath).AncestorPathOf(new DirectoryInfo(destDirPath)))
+            {
+                throw new PathErrorKraken(destDirPath,
+                                          string.Format(Properties.Resources.CannotCloneIntoSelf,
+                                                        Platform.FormatPath(sourceDirPath)));
+            }
+
             // Get the files in the directory and copy them to the new location
             foreach (var file in sourceDir.GetFiles())
             {


Test patch:
diff --git a/Tests/Core/GameInstanceManager.cs b/Tests/Core/GameInstanceManager.cs
index b2e9fca14..64bd9ad5f 100644
--- a/Tests/Core/GameInstanceManager.cs
+++ b/Tests/Core/GameInstanceManager.cs
@@ -208,6 +208,21 @@ public void CloneInstance_WithCKANDirFiles_OmittedAndCopied()
             }
         }
 
+        [Test]
+        public void CloneInstance_TargetInsideSource_Throws()
+        {
+            // Arrange
+            var inst = tidy!.KSP;
+            string instanceName = "newInstance";
+            var target = Path.Combine(inst.GameDir, "subclone");
+
+            // Act / Assert
+            var exc = Assert.Throws<PathErrorKraken>(() =>
+            {
+                manager!.CloneInstance(inst, instanceName, target);
+            });
+        }
+
         // FakeInstance
 
         [Test]