diff --git a/AppCommon/AddFileWorker.cs b/AppCommon/AddFileWorker.cs
index 9bb06ed..a683125 100644
--- a/AppCommon/AddFileWorker.cs
+++ b/AppCommon/AddFileWorker.cs
@@ -409,7 +409,7 @@ public void AddFilesToDisk(IFileSystem fileSystem, IFileEntry targetDir,
// directory if we remembered the path / entry from the previous iteration.
string storageDir = doStripPaths ? string.Empty : addEnt.StorageDir;
IFileEntry subDirEnt;
- subDirEnt = CreateSubdirectories(fileSystem, targetDirEnt, storageDir,
+ subDirEnt = fileSystem.CreateSubdirectories(targetDirEnt, storageDir,
addEnt.StorageDirSep);
// Add the new file to subDirEnt. See if it already exists.
@@ -524,49 +524,6 @@ public void AddFilesToDisk(IFileSystem fileSystem, IFileEntry targetDir,
isCancelled = false;
}
- ///
- /// Ensures that all of the directories in the path exist. If they don't exist, they
- /// will be created.
- ///
- /// Filesystem to modify.
- /// Base directory.
- /// Partial pathname, directories only.
- /// Directory separator character used in storage dir.
- /// File entry for destination directory.
- /// Something failed.
- internal static IFileEntry CreateSubdirectories(IFileSystem fileSystem,
- IFileEntry targetDirEnt, string storageDir, char storageDirSep) {
- if (string.IsNullOrEmpty(storageDir)) {
- return targetDirEnt;
- }
- IFileEntry subDirEnt = targetDirEnt;
- string[] dirStrings = storageDir.Split(storageDirSep);
- foreach (string dirName in dirStrings) {
- // Adjust this directory name to be compatible with the target filesystem.
- string adjDirName = fileSystem.AdjustFileName(dirName);
-
- // See if it exists. If it does, and it's not a directory, is very bad.
- if (fileSystem.TryFindFileEntry(subDirEnt, adjDirName,
- out IFileEntry nextDirEnt)) {
- if (!nextDirEnt.IsDirectory) {
- throw new IOException("Error: path component '" + adjDirName +
- "' (" + dirName + ") is not a directory");
- }
- subDirEnt = nextDirEnt;
- } else {
- // Not found, create new.
- try {
- subDirEnt = fileSystem.CreateFile(subDirEnt, adjDirName,
- CreateMode.Directory);
- } catch (IOException ex) {
- throw new IOException("Error: unable to create directory '" +
- adjDirName + "': " + ex.Message);
- }
- }
- }
- return subDirEnt;
- }
-
///
/// Copies a part (i.e fork) of a file to a disk image file stream. The file source may
/// be a plain or structured file.
diff --git a/AppCommon/ClipFileEntry.cs b/AppCommon/ClipFileEntry.cs
index 7fa709f..5dfcf33 100644
--- a/AppCommon/ClipFileEntry.cs
+++ b/AppCommon/ClipFileEntry.cs
@@ -445,6 +445,7 @@ public ClipFileEntry(object archiveOrFileSystem, IFileEntry entry, IFileEntry ad
Type? expectedType, AppHook appHook) {
Debug.Assert(!string.IsNullOrEmpty(attribs.FileNameOnly));
Debug.Assert(!string.IsNullOrEmpty(attribs.FullPathName));
+
mStreamGen = new StreamGenerator(archiveOrFileSystem, entry, adfEntry, part, attribs,
preserveMode, exportSpec, defaultSpecs, expectedType, appHook);
diff --git a/AppCommon/ClipFileSet.cs b/AppCommon/ClipFileSet.cs
index 13d2def..a48ec33 100644
--- a/AppCommon/ClipFileSet.cs
+++ b/AppCommon/ClipFileSet.cs
@@ -81,7 +81,7 @@ public class ClipFileSet {
private bool mUseRawData;
// Keep track of directories we've already added to the output.
- private Dictionary mAddedDirs =
+ private Dictionary mAddedFiles =
new Dictionary();
@@ -316,8 +316,13 @@ private void GenerateFromDisk(IFileSystem fs, List entries) {
/// Generates an entry in the ClipFileEntry set for the specified entry. If the entry is
/// a directory, we recursively generate clip objects for the entry's children.
///
+ ///
+ /// We need to avoid creating two entries for the same file. This can happen if
+ /// the entry list includes "dir1" and "dir1/foo.txt"; note either of these could
+ /// appear first.
+ ///
/// Filesystem reference.
- /// File entry to process.
+ /// File entry to process. May be a file or directory.
/// Entry above the root, used to limit the partial path
/// prefix.
private void GenerateDiskEntries(IFileSystem fs, IFileEntry entry,
@@ -329,7 +334,9 @@ private void GenerateDiskEntries(IFileSystem fs, IFileEntry entry,
// create empty directories, and a way to pass the directory file dates along.
AddMissingDirectories(fs, entry, aboveRootEntry);
}
- if (entry.IsDirectory) {
+ if (mAddedFiles.ContainsKey(entry)) {
+ // Already added.
+ } else if (entry.IsDirectory) {
// Current directory, if not stripped, was added above.
foreach (IFileEntry child in entry) {
GenerateDiskEntries(fs, child, aboveRootEntry);
@@ -350,6 +357,10 @@ private void GenerateDiskEntries(IFileSystem fs, IFileEntry entry,
// Generate file attributes. The same object will be used for both forks.
FileAttribs attrs = new FileAttribs(entry);
attrs.FullPathName = ReRootedPathName(entry, aboveRootEntry);
+ if (string.IsNullOrEmpty(attrs.FullPathName)) {
+ mAppHook.LogW("Not adding below-root entry: " + entry);
+ return;
+ }
if (mStripPaths) {
extractPath = Path.GetFileName(extractPath);
attrs.FullPathName =
@@ -361,10 +372,10 @@ private void GenerateDiskEntries(IFileSystem fs, IFileEntry entry,
} else {
CreateForExport(fs, entry, IFileEntry.NO_ENTRY, attrs, extractPath);
}
+ mAddedFiles.Add(entry, entry);
}
}
-
///
/// Adds entries for the directory hierarchy that contains the specified entry, if
/// they haven't yet been added. If the entry is itself a directory, it will be added.
@@ -381,10 +392,12 @@ private void AddMissingDirectories(IFileSystem fs, IFileEntry entry,
}
// Recursively check parents.
AddMissingDirectories(fs, entry.ContainingDir, aboveRootEntry);
- if (entry.IsDirectory && !mAddedDirs.ContainsKey(entry)) {
+ // Add this one if it's not already present.
+ if (entry.IsDirectory && !mAddedFiles.ContainsKey(entry)) {
// Add this directory to the output list.
FileAttribs attrs = new FileAttribs(entry);
attrs.FullPathName = ReRootedPathName(entry, aboveRootEntry);
+ Debug.Assert(!string.IsNullOrEmpty(attrs.FullPathName));
attrs.FullPathSep = entry.DirectorySeparatorChar;
attrs.FileNameOnly = PathName.GetFileName(attrs.FullPathName, attrs.FullPathSep);
Debug.Assert(attrs.IsDirectory);
@@ -395,7 +408,7 @@ private void AddMissingDirectories(IFileSystem fs, IFileEntry entry,
mAppHook));
XferEntries.Add(new ClipFileEntry(fs, entry, IFileEntry.NO_ENTRY,
FilePart.DataFork, attrs, mAppHook));
- mAddedDirs.Add(entry, entry);
+ mAddedFiles.Add(entry, entry);
}
}
@@ -407,7 +420,23 @@ private void AddMissingDirectories(IFileSystem fs, IFileEntry entry,
/// File entry.
/// Entry above the root directory. For the volume dir,
/// or non-hierarchical filesystems, this will be NO_ENTRY.
+ /// Partial pathname, or the empty string if the above-root entry is not actually
+ /// above the entry.
private static string ReRootedPathName(IFileEntry entry, IFileEntry aboveRootEntry) {
+ // Test to see if entry is at or above the root. If it's at the same level as the
+ // root, or above it, it shouldn't be part of the set.
+ if (entry.ContainingDir == aboveRootEntry) {
+ return string.Empty;
+ }
+ IFileEntry scanEntry = entry;
+ while (scanEntry.ContainingDir != aboveRootEntry) {
+ scanEntry = scanEntry.ContainingDir;
+ if (scanEntry == IFileEntry.NO_ENTRY) {
+ // Walked to the top without finding it.
+ return string.Empty;
+ }
+ }
+
StringBuilder sb = new StringBuilder();
ReRootedPathName(entry, aboveRootEntry, sb);
return sb.ToString();
diff --git a/AppCommon/ClipPasteWorker.cs b/AppCommon/ClipPasteWorker.cs
index 4d8c1ee..317574c 100644
--- a/AppCommon/ClipPasteWorker.cs
+++ b/AppCommon/ClipPasteWorker.cs
@@ -409,7 +409,7 @@ public void AddFilesToDisk(IFileSystem fileSystem, IFileEntry targetDir,
string storageName = PathName.GetFileName(clipEntry.Attribs.FullPathName,
clipEntry.Attribs.FullPathSep);
IFileEntry subDirEnt;
- subDirEnt = AddFileWorker.CreateSubdirectories(fileSystem, targetDirEnt, storageDir,
+ subDirEnt = fileSystem.CreateSubdirectories(targetDirEnt, storageDir,
clipEntry.Attribs.FullPathSep);
// Add the new file to subDirEnt. See if it already exists.
diff --git a/DiskArc/DAExtensions.cs b/DiskArc/DAExtensions.cs
index 56f0730..8bcbce7 100644
--- a/DiskArc/DAExtensions.cs
+++ b/DiskArc/DAExtensions.cs
@@ -157,7 +157,8 @@ public static bool CheckStorageName(this IArchive archive, string storageName) {
///
/// Formats an IDiskImage. This should only be used on a newly-created IDiskImage,
/// when the ChunkAccess property is non-null and the FileSystem property is null.
- /// Query the FileSystem out of the IDiskImage when this returns.
+ /// The disk will be analyzed, so the caller can access the filesystem object via
+ /// .
///
///
/// The chunks will be zeroed out before the new filesystem is written.
@@ -570,6 +571,50 @@ public static IFileEntry CreateFile(this IFileSystem fs, IFileEntry dirEntry,
return entry;
}
+ ///
+ /// Ensures that all of the directories in the path exist. If they don't exist, they
+ /// will be created.
+ ///
+ /// Base directory.
+ /// Partial pathname, with directories only.
+ /// Directory separator character used in storage dir.
+ /// File entry for destination directory.
+ /// I/O failure, or part of the path existed but was not
+ /// a regular file.
+ public static IFileEntry CreateSubdirectories(this IFileSystem fs,
+ IFileEntry baseDir, string partialPath, char dirSep) {
+ if (string.IsNullOrEmpty(partialPath)) {
+ return baseDir;
+ }
+ Debug.Assert(fs.Characteristics.IsHierarchical);
+ IFileEntry subDirEnt = baseDir;
+ string[] dirStrings = partialPath.Split(dirSep);
+ foreach (string dirName in dirStrings) {
+ // Adjust this directory name to be compatible with the target filesystem.
+ string adjDirName = fs.AdjustFileName(dirName);
+
+ // See if it exists. If it does, and it's not a directory, is very bad.
+ if (fs.TryFindFileEntry(subDirEnt, adjDirName,
+ out IFileEntry nextDirEnt)) {
+ if (!nextDirEnt.IsDirectory) {
+ throw new IOException("Error: path component '" + adjDirName +
+ "' (" + dirName + ") is not a directory");
+ }
+ subDirEnt = nextDirEnt;
+ } else {
+ // Not found, create new.
+ try {
+ subDirEnt = fs.CreateFile(subDirEnt, adjDirName,
+ IFileSystem.CreateMode.Directory);
+ } catch (IOException ex) {
+ throw new IOException("Error: unable to create directory '" +
+ adjDirName + "': " + ex.Message);
+ }
+ }
+ }
+ return subDirEnt;
+ }
+
//public static DiskFileStream OpenDataForkRO(this IFileSystem fs, IFileEntry entry) {
// return fs.OpenFile(entry, IFileSystem.FileAccessMode.ReadOnly, Defs.FilePart.DataFork);
//}
diff --git a/cp2/Tests/Controller.cs b/cp2/Tests/Controller.cs
index ae5ea56..0584bab 100644
--- a/cp2/Tests/Controller.cs
+++ b/cp2/Tests/Controller.cs
@@ -85,6 +85,10 @@ public static bool HandleRunTests(string cmdName, string[] args, ParamsBag parms
ResetConsole();
}
+ // Give any finalizer-based checks a chance to run.
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
Console.WriteLine("Success");
return true;
}
diff --git a/cp2/Tests/TestClip.cs b/cp2/Tests/TestClip.cs
index e9ede4d..266d71d 100644
--- a/cp2/Tests/TestClip.cs
+++ b/cp2/Tests/TestClip.cs
@@ -24,10 +24,11 @@
using DiskArc.Disk;
using DiskArc.FS;
using static DiskArc.Defs;
+using static DiskArc.IFileSystem;
namespace cp2.Tests {
///
- /// Test clipboard copy/paste infrastructure. This does not use the system clipboard.
+ /// Test clipboard copy/paste infrastructure. This does not actually use the system clipboard.
///
///
/// This is primarily a test of the AppCommon infrastructure rather than a test of cp2
@@ -40,10 +41,12 @@ public static void RunTest(ParamsBag parms) {
//
// Basic plan:
- // - Use ClipFileSet to generate a list of ClipFileEntry objects, then use
- // ClipFileWorker to add the entries to a different archive or to extract to the
- // filesystem. A custom stream generator is used in place of the system clipboard
- // mechanism.
+ // - Use ClipFileSet to generate a list of ClipFileEntry objects.
+ // - Simple test: compare the set to a set of expected results to confirm that
+ // the set is being generated correctly.
+ // - Full test: use ClipFileWorker to add the entries to a different archive or to
+ // extract to the filesystem. A custom stream generator is used in place of the
+ // system clipboard mechanism.
//
// Some things to test:
// - Direct file transfer of entries with data, rsrc, data+rsrc.
@@ -58,9 +61,12 @@ public static void RunTest(ParamsBag parms) {
parms.Raw = false;
parms.MacZip = true;
- IArchive testArchive = CreateTestArchive(sSampleFiles, parms);
+ using IArchive testArchive = CreateTestArchive(sSampleFiles, parms);
+ using IDiskImage testProFS = CreateTestProDisk(sSampleFiles, parms);
- TestForeignExtract(testArchive, sForeignExtractNAPS, parms);
+ TestForeignExtract(testArchive, parms);
+ TestBasicXfer((IFileSystem)testProFS.Contents!, parms);
+ TestRerootXfer((IFileSystem)testProFS.Contents!, parms);
Controller.RemoveTestTmp(parms);
}
@@ -129,6 +135,46 @@ private static IArchive CreateTestArchive(SampleFile[] specs, ParamsBag parms) {
return arc;
}
+ private static IDiskImage CreateTestProDisk(SampleFile[] specs, ParamsBag parms) {
+ // Create a disk image in a memory stream.
+ UnadornedSector disk = UnadornedSector.CreateBlockImage(new MemoryStream(),
+ 1600, parms.AppHook);
+ disk.FormatDisk(FileSystemType.ProDOS, "Test", 0, true, parms.AppHook);
+ IFileSystem fs = (IFileSystem)disk.Contents!;
+ fs.PrepareFileAccess(true);
+
+ foreach (SampleFile sample in specs) {
+ string dirName = PathName.GetDirectoryName(sample.PathName, DIR_SEP);
+ string fileName = PathName.GetFileName(sample.PathName, DIR_SEP);
+
+ IFileEntry subDir = fs.CreateSubdirectories(fs.GetVolDirEntry(), dirName, DIR_SEP);
+ IFileEntry newEntry = fs.CreateFile(subDir, fs.AdjustFileName(fileName),
+ sample.RsrcLength >= 0 ? CreateMode.Extended : CreateMode.File);
+ newEntry.FileName = fileName;
+ newEntry.FileType = PRODOS_FILETYPE;
+ newEntry.AuxType = PRODOS_AUXTYPE;
+ newEntry.CreateWhen = CREATE_WHEN;
+ newEntry.ModWhen = MOD_WHEN;
+ if (sample.DataLength >= 0) {
+ using (Stream stream = fs.OpenFile(newEntry, FileAccessMode.ReadWrite,
+ FilePart.DataFork)) {
+ for (int i = 0; i < sample.DataLength; i++) {
+ stream.WriteByte(0x33);
+ }
+ }
+ }
+ if (sample.RsrcLength >= 0) {
+ using (Stream stream = fs.OpenFile(newEntry, FileAccessMode.ReadWrite,
+ FilePart.RsrcFork)) {
+ for (int i = 0; i < sample.RsrcLength; i++) {
+ stream.WriteByte(0x44);
+ }
+ }
+ }
+ }
+ return disk;
+ }
+
private class ExpectedForeign {
public string PathName { get; private set; }
public int MinLength { get; private set; }
@@ -157,8 +203,7 @@ public ExpectedForeign(string fileName, int minLength, int maxLength) {
DATA_ONLY_DLEN),
};
- private static void TestForeignExtract(IArchive arc, ExpectedForeign[] expected,
- ParamsBag parms) {
+ private static void TestForeignExtract(IArchive arc, ParamsBag parms) {
List entries = new List();
foreach (IFileEntry entry in arc) {
entries.Add(entry);
@@ -167,6 +212,11 @@ private static void TestForeignExtract(IArchive arc, ExpectedForeign[] expected,
parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip,
null, null, parms.AppHook);
+ TestForeign(clipSet, sForeignExtractNAPS, parms);
+ }
+
+ private static void TestForeign(ClipFileSet clipSet, ExpectedForeign[] expected,
+ ParamsBag parms) {
if (clipSet.ForeignEntries.Count != expected.Length) {
throw new Exception("Foreign extract count mismatch: expected=" + expected.Length +
", actual=" + clipSet.ForeignEntries.Count);
@@ -197,5 +247,110 @@ private static void TestForeignExtract(IArchive arc, ExpectedForeign[] expected,
}
}
}
+
+ private class ExpectedXfer {
+ public string PathName { get; private set; }
+ public FilePart Part { get; private set; }
+ public int Length { get; private set; }
+
+ public ExpectedXfer(string pathName, FilePart part, int length) {
+ PathName = pathName;
+ Part = part;
+ Length = length;
+ }
+ }
+
+ private static ExpectedXfer[] sXferProDOS = new ExpectedXfer[] {
+ new ExpectedXfer(DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN),
+ new ExpectedXfer(RSRC_ONLY_NAME, FilePart.DataFork, 0),
+ new ExpectedXfer(RSRC_ONLY_NAME, FilePart.RsrcFork, RSRC_ONLY_RLEN),
+ new ExpectedXfer(DATA_RSRC_NAME, FilePart.DataFork, DATA_RSRC_DLEN),
+ new ExpectedXfer(DATA_RSRC_NAME, FilePart.RsrcFork, DATA_RSRC_RLEN),
+ new ExpectedXfer(SUBDIR1, FilePart.Unknown, -1),
+ new ExpectedXfer(SUBDIR1 + '/' + DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN),
+ new ExpectedXfer(SUBDIR1 + '/' + SUBDIR2, FilePart.Unknown, -1),
+ new ExpectedXfer(SUBDIR1 + '/' + SUBDIR2 + '/' + DATA_ONLY_NAME, FilePart.DataFork,
+ DATA_ONLY_DLEN),
+ };
+
+ private static void TestBasicXfer(IFileSystem srcFs, ParamsBag parms) {
+ List entries = GatherDiskEntries(srcFs, srcFs.GetVolDirEntry());
+
+ ClipFileSet clipSet = new ClipFileSet(srcFs, entries, IFileEntry.NO_ENTRY,
+ parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip,
+ null, null, parms.AppHook);
+
+ TestXfer(clipSet, sXferProDOS, parms);
+ }
+
+ private static ExpectedXfer[] sXferProDOSReroot = new ExpectedXfer[] {
+ new ExpectedXfer(DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN),
+ new ExpectedXfer(SUBDIR2, FilePart.Unknown, -1),
+ new ExpectedXfer(SUBDIR2 + '/' + DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN),
+ };
+
+ private static void TestRerootXfer(IFileSystem srcFs, ParamsBag parms) {
+ IFileEntry rootDir = srcFs.GetVolDirEntry();
+ IFileEntry baseDir = srcFs.FindFileEntry(rootDir, SUBDIR1);
+ // Deliberately use rootDir here rather than baseDir to exercise exclusion of
+ // entries that live below the root.
+ List entries = GatherDiskEntries(srcFs, rootDir);
+
+ ClipFileSet clipSet = new ClipFileSet(srcFs, entries, baseDir,
+ parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip,
+ null, null, parms.AppHook);
+
+ TestXfer(clipSet, sXferProDOSReroot, parms);
+ }
+
+ private static List GatherDiskEntries(IFileSystem fs, IFileEntry rootDir) {
+ List entryList = new List();
+ GetDiskEntries(fs, rootDir, entryList);
+ return entryList;
+ }
+
+ private static void GetDiskEntries(IFileSystem fs, IFileEntry dir,
+ List entryList) {
+ foreach (IFileEntry entry in dir) {
+ entryList.Add(entry);
+ if (entry.IsDirectory) {
+ GetDiskEntries(fs, entry, entryList);
+ }
+ }
+ }
+
+ private static void TestXfer(ClipFileSet clipSet, ExpectedXfer[] expected, ParamsBag parms) {
+ if (clipSet.XferEntries.Count != expected.Length) {
+ throw new Exception("Xfer extract count mismatch: expected=" + expected.Length +
+ ", actual=" + clipSet.XferEntries.Count);
+ }
+ for (int i = 0; i < clipSet.XferEntries.Count; i++) {
+ ClipFileEntry clipEntry = clipSet.XferEntries[i];
+ ExpectedXfer exp = expected[i];
+ if (clipEntry.ExtractPath != exp.PathName) {
+ throw new Exception("Xfer mismatch " + i + ": expected='" + exp.PathName +
+ "', actual='" + clipEntry.ExtractPath + "'");
+ }
+ if (clipEntry.Attribs.IsDirectory) {
+ continue;
+ }
+ if (clipEntry.Part != exp.Part) {
+ throw new Exception("Xfer part mismatch " + i + ": expected=" + exp.Part +
+ ", actual=" + clipEntry.Part);
+ }
+ if (clipEntry.Attribs.FileType != PRODOS_FILETYPE ||
+ clipEntry.Attribs.AuxType != PRODOS_AUXTYPE ||
+ clipEntry.Attribs.CreateWhen != CREATE_WHEN ||
+ clipEntry.Attribs.ModWhen != MOD_WHEN) {
+ throw new Exception("Xfer attribute mismatch " + i + ": " +
+ clipEntry.ExtractPath);
+ }
+ // Test reported length. In many cases the output length can't be known.
+ if (clipEntry.OutputLength >= 0 && clipEntry.OutputLength != exp.Length) {
+ throw new Exception("Xfer output length mismatch " + i + ": expected=" +
+ exp.Length + ", actual=" + clipEntry.OutputLength);
+ }
+ }
+ }
}
}