From 07eacb008a770f2c38c64ef9e7114abd1bc00a73 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Fri, 3 Nov 2023 16:59:34 -0700 Subject: [PATCH] Extend clipboard infrastructure tests Copy the data to a new volume for the "xfer" tests. Added a MacZip-to-ProDOS test. --- AppCommon/ClipFileEntry.cs | 12 +- AppCommon/ClipFileSet.cs | 11 +- AppCommon/ClipPasteWorker.cs | 8 +- CiderPress2-notes.txt | 4 +- DiskArc/DAExtensions.cs | 7 +- DiskArc/FileAttribs.cs | 1 + cp2/Tests/TestClip.cs | 460 ++++++++++++++++++--- cp2_wpf/MainController.cs | 2 + cp2_wpf/Tools/DropTarget.xaml.cs | 5 +- cp2_wpf/WPFCommon/ClipHelper.cs | 5 + cp2_wpf/WPFCommon/VirtualFileDataObject.cs | 4 +- 11 files changed, 437 insertions(+), 82 deletions(-) diff --git a/AppCommon/ClipFileEntry.cs b/AppCommon/ClipFileEntry.cs index 5dfcf33..dc4458a 100644 --- a/AppCommon/ClipFileEntry.cs +++ b/AppCommon/ClipFileEntry.cs @@ -39,8 +39,8 @@ public sealed class ClipFileEntry { /// opening the archive or filesystem entry and just copying data out. /// /// - /// This class is NOT serializable. It only exists on the local side, and is - /// invoked when the remote side requests file contents. + /// This class is NOT serializable. It only exists on the sending side, and is + /// invoked when the remote receiver requests file contents. /// public class StreamGenerator { private object mArchiveOrFileSystem; @@ -203,6 +203,8 @@ public void OutputToStream(Stream outStream) { /// Opens one fork of a file entry, in a disk image or file archive. /// /// Opened stream. + /// Archive or filesystem is invalid, + /// probably because the remote side closed. private Stream OpenPart(FilePart part = FilePart.Unknown) { if (part == FilePart.Unknown) { part = mPart; // "default" part @@ -365,7 +367,8 @@ private void GenerateAS(Stream outStream) { } /// - /// Stream generator. + /// Stream generator. Copies data from the source archive to a stream connected to the + /// remote receiver. /// /// /// The NonSerialized attribute can only be applied to fields, not properties. @@ -445,6 +448,9 @@ public ClipFileEntry(object archiveOrFileSystem, IFileEntry entry, IFileEntry ad Type? expectedType, AppHook appHook) { Debug.Assert(!string.IsNullOrEmpty(attribs.FileNameOnly)); Debug.Assert(!string.IsNullOrEmpty(attribs.FullPathName)); + if (entry != IFileEntry.NO_ENTRY) { + Debug.Assert(attribs.FullPathSep == entry.DirectorySeparatorChar); + } mStreamGen = new StreamGenerator(archiveOrFileSystem, entry, adfEntry, part, attribs, preserveMode, exportSpec, defaultSpecs, expectedType, appHook); diff --git a/AppCommon/ClipFileSet.cs b/AppCommon/ClipFileSet.cs index a48ec33..f1f8821 100644 --- a/AppCommon/ClipFileSet.cs +++ b/AppCommon/ClipFileSet.cs @@ -166,6 +166,7 @@ private void GenerateFromArchive(IArchive arc, List entries) { // Generate file attributes. The same object will be used for both forks. FileAttribs attrs = new FileAttribs(entry); if (mStripPaths) { + // Configure extract path and attribute path. extractPath = Path.GetFileName(extractPath); attrs.FullPathName = PathName.GetFileName(attrs.FullPathName, attrs.FullPathSep); @@ -207,7 +208,10 @@ private static void GetMacZipAttribs(IArchive arc, IFileEntry adfEntry, using Stream adfStream = ArcTemp.ExtractToTemp(arc, adfEntry, FilePart.DataFork); using AppleSingle adfArchive = AppleSingle.OpenArchive(adfStream, appHook); IFileEntry adfArchiveEntry = adfArchive.GetFirstEntry(); + // Get the attributes from AppleSingle, but don't replace the filename. + string fileName = attrs.FileNameOnly; attrs.GetFromAppleSingle(adfArchiveEntry); + attrs.FileNameOnly = fileName; } catch (Exception ex) { // Never mind. appHook.LogW("Failed to get attributes from ADF header (" + adfEntry + @@ -253,6 +257,7 @@ private void AddPathDirEntries(IArchive arc, string pathName, char dirSep) { // Generate an entry for the directory. FileAttribs attrs = new FileAttribs(); attrs.FullPathName = dirName; + attrs.FullPathSep = dirSep; attrs.FileNameOnly = PathName.GetFileName(dirName, dirSep); attrs.IsDirectory = true; string extractPath = PathName.AdjustPathName(dirName, dirSep, @@ -397,10 +402,10 @@ private void AddMissingDirectories(IFileSystem fs, IFileEntry 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); + Debug.Assert(!string.IsNullOrEmpty(attrs.FullPathName)); string extractPath = ExtractFileWorker.GetAdjPathName(entry, aboveRootEntry, Path.DirectorySeparatorChar); ForeignEntries.Add(new ClipFileEntry(fs, entry, IFileEntry.NO_ENTRY, @@ -580,6 +585,7 @@ private void CreateForExport(object archiveOrFileSystem, IFileEntry entry, if (expectedType == null) { return; } + Debug.Assert(!string.IsNullOrEmpty(attrs.FileNameOnly)); string ext; if (expectedType == typeof(SimpleText)) { ext = TXTGenerator.FILE_EXT; @@ -645,7 +651,10 @@ private void CreateForExport(object archiveOrFileSystem, IFileEntry entry, adfStream = ArcTemp.ExtractToTemp(arc, adfEntry, dataPart); adfArchive = AppleSingle.OpenArchive(adfStream, appHook); IFileEntry adfArchiveEntry = adfArchive.GetFirstEntry(); + // Copy the ADF attributes out, but retain the original filename. + string fileName = attrs.FileNameOnly; attrs.GetFromAppleSingle(adfArchiveEntry); + attrs.FileNameOnly = fileName; if (adfArchiveEntry.HasRsrcFork && adfArchiveEntry.RsrcLength > 0) { rsrcStream = adfArchive.OpenPart(adfArchiveEntry, FilePart.RsrcFork); } diff --git a/AppCommon/ClipPasteWorker.cs b/AppCommon/ClipPasteWorker.cs index 317574c..5d28e45 100644 --- a/AppCommon/ClipPasteWorker.cs +++ b/AppCommon/ClipPasteWorker.cs @@ -38,10 +38,11 @@ public class ClipPasteWorker { public delegate CallbackFacts.Results CallbackFunc(CallbackFacts what); /// - /// Stream generator function interface definition. + /// Input stream generator function interface definition. This is invoked on the + /// receiving side to receive input from the sender. /// /// Entry to generate a stream for. - /// Write-only, non-seekable output stream, or null if no stream is + /// Read-only, non-seekable output stream, or null if no stream is /// available for the specified entry. public delegate Stream? ClipStreamGenerator(ClipFileEntry clipEntry); @@ -581,7 +582,8 @@ private void CopyFilePart(ClipFileEntry clipEntry, int progressPercent, using (Stream? inStream = mClipStreamGen(clipEntry)) { if (inStream == null) { - throw new IOException("Unable to open source stream"); + throw new IOException("Unable to open source stream. " + + "Make sure source archive is open and files have not been deleted."); } while (true) { int actual = inStream.Read(mCopyBuf, 0, mCopyBuf.Length); diff --git a/CiderPress2-notes.txt b/CiderPress2-notes.txt index ed1d2ce..53af83f 100644 --- a/CiderPress2-notes.txt +++ b/CiderPress2-notes.txt @@ -152,7 +152,9 @@ Windows Explorer. Data is read from the virtual streams at the time the files are dropped or pasted. Because of this, files copied onto the clipboard cannot be pasted after the source archive or disk image is closed. Similarly, files copied to the clipboard cannot be pasted if they are deleted from a -disk image or file archive after being copied. +disk image or file archive after being copied. This means you can't copy files between disk +images by opening the first image, copying the data, closing it, and then opening the second image +to do the paste. The application has no ability to influence how Windows Explorer handles the clipboard data prepared for "foreign" transfers, so the file data must be arranged in the clipboard in a way that diff --git a/DiskArc/DAExtensions.cs b/DiskArc/DAExtensions.cs index 8bcbce7..2c4baed 100644 --- a/DiskArc/DAExtensions.cs +++ b/DiskArc/DAExtensions.cs @@ -167,14 +167,14 @@ public static bool CheckStorageName(this IArchive archive, string storageName) { /// Filesystem to format the disk with. /// IFileSystem.Format() argument. /// IFileSystem.Format() argument. - /// IFileSystem.Format() argument. + /// IFileSystem.Format() argument. /// Application hook reference. /// ChunkAccess is null, or FileSystem is /// not null. /// Filesystem not supported by this /// function. public static void FormatDisk(this IDiskImage diskImage, Defs.FileSystemType fsType, - string volumeName, int volumeNum, bool makeBootable, AppHook appHook) { + string volumeName, int volumeNum, bool reserveBoot, AppHook appHook) { if (diskImage.ChunkAccess == null || diskImage.Contents != null) { throw new InvalidOperationException( "Disk image must have non-null ChunkAccess and no Contents"); @@ -208,7 +208,7 @@ public static void FormatDisk(this IDiskImage diskImage, Defs.FileSystemType fsT diskImage.ChunkAccess.Initialize(); // Format the filesystem. - fs.Format(volumeName, volumeNum, makeBootable); + fs.Format(volumeName, volumeNum, reserveBoot); // Dispose of the IFileSystem object to ensure everything has been written. fs.Dispose(); @@ -486,6 +486,7 @@ public static IFileEntry FindFileEntry(this IFileSystem fs, IFileEntry dirEntry, /// directory. public static bool TryFindFileEntry(this IFileSystem fs, IFileEntry dirEntry, string fileName, out IFileEntry entry) { + Debug.Assert(dirEntry.GetFileSystem() == fs); if (!dirEntry.IsDirectory) { throw new ArgumentException("Argument is not a directory"); } diff --git a/DiskArc/FileAttribs.cs b/DiskArc/FileAttribs.cs index 213d8bd..a3a4bbb 100644 --- a/DiskArc/FileAttribs.cs +++ b/DiskArc/FileAttribs.cs @@ -188,6 +188,7 @@ public void GetFromAppleSingle(Stream asStream, AppHook appHook) { /// /// Overwrites attributes with values from an AppleSingle/AppleDouble file. Notably, /// the FileNameOnly and RsrcLength fields are replaced with the value from the archive. + /// FullPathName and FullPathSep are not altered. /// /// Entry from AppleSingle/AppleDouble archive. public void GetFromAppleSingle(IFileEntry entry) { diff --git a/cp2/Tests/TestClip.cs b/cp2/Tests/TestClip.cs index 266d71d..86a3923 100644 --- a/cp2/Tests/TestClip.cs +++ b/cp2/Tests/TestClip.cs @@ -42,18 +42,18 @@ public static void RunTest(ParamsBag parms) { // // Basic plan: // - 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. + // - Simple test: compare the generated set to a set of expected results to confirm + // that the generation is working. // - 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 + // extract to the host 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. // - Export/extract to filesystem with attribute preservation. // - Filesystem copy with re-rooted source/target. - // - DOS text conversion. - // - MacZip at source and destination (rsrc fork, attributes). + // - MacZip at source and destination (but not at the same time). + // - DOS text conversion and path stripping. // parms.Preserve = ExtractFileWorker.PreserveMode.NAPS; @@ -61,25 +61,103 @@ public static void RunTest(ParamsBag parms) { parms.Raw = false; parms.MacZip = true; - using IArchive testArchive = CreateTestArchive(sSampleFiles, parms); + using NuFX nufxSrc = NuFX.CreateArchive(parms.AppHook); + CreateTestArchive(nufxSrc, sSampleFiles, parms); + + using Zip zipSrc = Zip.CreateArchive(parms.AppHook); + CreateTestArchive(zipSrc, sSampleFiles, parms); + using IDiskImage testProFS = CreateTestProDisk(sSampleFiles, parms); + IFileSystem proFs = (IFileSystem)testProFS.Contents!; - TestForeignExtract(testArchive, parms); - TestBasicXfer((IFileSystem)testProFS.Contents!, parms); - TestRerootXfer((IFileSystem)testProFS.Contents!, parms); + TestForeignExtract(nufxSrc, parms); + TestBasicXfer(proFs, parms); + TestRerootXfer(proFs, parms); + TestZipToProDOS(zipSrc, parms); Controller.RemoveTestTmp(parms); } + private static void TestForeignExtract(IArchive arc, ParamsBag parms) { + List entries = GatherArchiveEntries(arc, parms); + ClipFileSet clipSet = new ClipFileSet(arc, entries, IFileEntry.NO_ENTRY, + parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip, + null, null, parms.AppHook); + + CheckForeign(clipSet, sForeignExtractNAPS, parms); + } + + 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); + List xferStreams = GenerateXferStreams(srcFs, clipSet); + Debug.Assert(clipSet.XferEntries.Count == xferStreams.Count); + + CheckXfer(clipSet, sXferFromProDOS, parms); + + using IDiskImage disk = CreateTestDisk(FileSystemType.ProDOS, parms); + CheckOutput(entries, clipSet, (IFileSystem)disk.Contents!, IFileEntry.NO_ENTRY, + xferStreams, sOutputProDOS, parms); + } + + private static void TestRerootXfer(IFileSystem srcFs, ParamsBag parms) { + IFileEntry srcRootDir = srcFs.GetVolDirEntry(); + IFileEntry srcBaseDir = srcFs.FindFileEntry(srcRootDir, SUBDIR1); + // Deliberately use rootDir here rather than baseDir to exercise exclusion of + // entries that live below the root. + List entries = GatherDiskEntries(srcFs, srcRootDir); + + ClipFileSet clipSet = new ClipFileSet(srcFs, entries, srcBaseDir, + parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip, + null, null, parms.AppHook); + List xferStreams = GenerateXferStreams(srcFs, clipSet); + Debug.Assert(clipSet.XferEntries.Count == xferStreams.Count); + + CheckXfer(clipSet, sXferFromProDOSReroot, parms); + using IDiskImage dstDisk = CreateTestDisk(FileSystemType.ProDOS, parms); + IFileSystem dstFs = (IFileSystem)dstDisk.Contents!; + IFileEntry dstRootDir = dstFs.GetVolDirEntry(); + IFileEntry dstBaseDir = dstFs.CreateFile(dstRootDir, SUBDIR1, CreateMode.Directory); + CheckOutput(entries, clipSet, dstFs, dstBaseDir, xferStreams, sOutputProDOSReroot, + parms); + } + + private static void TestZipToProDOS(Zip zip, ParamsBag parms) { + parms.MacZip = true; + List entries = GatherArchiveEntries(zip, parms); + + ClipFileSet clipSet = new ClipFileSet(zip, entries, IFileEntry.NO_ENTRY, + parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip, + null, null, parms.AppHook); + List xferStreams = GenerateXferStreams(zip, clipSet); + Debug.Assert(clipSet.XferEntries.Count == xferStreams.Count); + + CheckXfer(clipSet, sXferFromZip, parms); + + using IDiskImage disk = CreateTestDisk(FileSystemType.ProDOS, parms); + CheckOutput(entries, clipSet, (IFileSystem)disk.Contents!, IFileEntry.NO_ENTRY, + xferStreams, sOutputProDOS, parms); + } + + private const string DATA_ONLY_NAME = "DataOnly"; private const string RSRC_ONLY_NAME = "RsrcOnly"; private const string DATA_RSRC_NAME = "DataRsrc"; + private const string ANOTHER_NAME = "Another"; private const string SUBDIR1 = "subdir1"; private const string SUBDIR2 = "subdir2"; private const int DATA_ONLY_DLEN = 1234; private const int RSRC_ONLY_RLEN = 4321; private const int DATA_RSRC_DLEN = 2345; private const int DATA_RSRC_RLEN = 5432; + private const int ANOTHER_DLEN = 1122; + private const byte ARC_DATA_VAL = 0x11; + private const byte ARC_RSRC_VAL = 0x22; + private const byte FS_DATA_VAL = 0x33; + private const byte FS_RSRC_VAL = 0x44; private static readonly DateTime CREATE_WHEN = new DateTime(1977, 6, 1, 1, 2, 0); private static readonly DateTime MOD_WHEN = new DateTime(1986, 9, 15, 4, 5, 0); @@ -90,6 +168,7 @@ public static void RunTest(ParamsBag parms) { private const string NAPS_STR = "#0612cd"; private const string NAPS_STR_R = NAPS_STR + "r"; private const char DIR_SEP = ':'; + private const char PRO_SEP = '/'; private class SampleFile { public string PathName { get; private set; } @@ -110,38 +189,77 @@ public SampleFile(string pathName, int dataLength, int rsrcLength) { new SampleFile(SUBDIR1 + DIR_SEP + DATA_ONLY_NAME, DATA_ONLY_DLEN, -1), new SampleFile(SUBDIR1 + DIR_SEP + SUBDIR2 + DIR_SEP + DATA_ONLY_NAME, DATA_ONLY_DLEN, -1), + new SampleFile(ANOTHER_NAME, ANOTHER_DLEN, -1), }; - private static IArchive CreateTestArchive(SampleFile[] specs, ParamsBag parms) { - NuFX arc = NuFX.CreateArchive(parms.AppHook); + private static void CreateTestArchive(IArchive arc, SampleFile[] specs, ParamsBag parms) { arc.StartTransaction(); foreach (SampleFile sample in specs) { + string pathName = + sample.PathName.Replace(DIR_SEP, arc.Characteristics.DefaultDirSep); IFileEntry newEntry = arc.CreateRecord(); - newEntry.FileName = sample.PathName; + newEntry.FileName = pathName; newEntry.FileType = PRODOS_FILETYPE; newEntry.AuxType = PRODOS_AUXTYPE; newEntry.CreateWhen = CREATE_WHEN; newEntry.ModWhen = MOD_WHEN; if (sample.DataLength >= 0) { - IPartSource dataSource = Controller.CreateSimpleSource(sample.DataLength, 0x11); + IPartSource dataSource = + Controller.CreateSimpleSource(sample.DataLength, ARC_DATA_VAL); + arc.AddPart(newEntry, FilePart.DataFork, dataSource, CompressionFormat.Default); + } else if (arc is Zip) { + // Create an empty data fork record. + IPartSource dataSource = Controller.CreateSimpleSource(0, ARC_DATA_VAL); arc.AddPart(newEntry, FilePart.DataFork, dataSource, CompressionFormat.Default); } - if (sample.RsrcLength >= 0) { - IPartSource rsrcSource = Controller.CreateSimpleSource(sample.RsrcLength, 0x22); - arc.AddPart(newEntry, FilePart.RsrcFork, rsrcSource, CompressionFormat.Default); + + if (arc is Zip) { + // Need to create a second entry in the ZIP archive, with the AppleDouble + // header, to hold the type info and possibly a resource fork. + MemoryStream headerStream = new MemoryStream(); + using AppleSingle header = AppleSingle.CreateDouble(2, parms.AppHook); + header.StartTransaction(); + IFileEntry hdrEntry = header.GetFirstEntry(); + hdrEntry.FileType = PRODOS_FILETYPE; + hdrEntry.AuxType = PRODOS_AUXTYPE; + hdrEntry.CreateWhen = CREATE_WHEN; + hdrEntry.ModWhen = MOD_WHEN; + if (sample.RsrcLength > 0) { + IPartSource rsrcSource = + Controller.CreateSimpleSource(sample.RsrcLength, ARC_RSRC_VAL); + header.AddPart(hdrEntry, FilePart.RsrcFork, rsrcSource, + CompressionFormat.Default); + } + header.CommitTransaction(headerStream); + + // Add the ADF data to the archive. + IFileEntry adfEntry = arc.CreateRecord(); + adfEntry.FileName = Zip.GenerateMacZipName(pathName); + arc.AddPart(adfEntry, FilePart.DataFork, new SimplePartSource(headerStream), + CompressionFormat.Default); + } else if (sample.RsrcLength > 0) { + // Just add a resource fork part to the entry we're creating. + IPartSource rsrcSource = + Controller.CreateSimpleSource(sample.RsrcLength, ARC_RSRC_VAL); + arc.AddPart(newEntry, FilePart.RsrcFork, rsrcSource, + CompressionFormat.Default); } } - arc.CommitTransaction(new MemoryStream()); - return arc; + MemoryStream outStream = new MemoryStream(); + arc.CommitTransaction(outStream); + + //if (arc is Zip) { + // outStream.Position = 0; + // using (FileStream stream = new FileStream(@"c:\src\ciderpress2\foo.zip", + // FileMode.Create)) { + // outStream.CopyTo(stream); + // } + //} } 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); + IDiskImage disk = CreateTestDisk(FileSystemType.ProDOS, parms); IFileSystem fs = (IFileSystem)disk.Contents!; - fs.PrepareFileAccess(true); foreach (SampleFile sample in specs) { string dirName = PathName.GetDirectoryName(sample.PathName, DIR_SEP); @@ -159,7 +277,7 @@ private static IDiskImage CreateTestProDisk(SampleFile[] specs, ParamsBag parms) using (Stream stream = fs.OpenFile(newEntry, FileAccessMode.ReadWrite, FilePart.DataFork)) { for (int i = 0; i < sample.DataLength; i++) { - stream.WriteByte(0x33); + stream.WriteByte(FS_DATA_VAL); } } } @@ -167,7 +285,7 @@ private static IDiskImage CreateTestProDisk(SampleFile[] specs, ParamsBag parms) using (Stream stream = fs.OpenFile(newEntry, FileAccessMode.ReadWrite, FilePart.RsrcFork)) { for (int i = 0; i < sample.RsrcLength; i++) { - stream.WriteByte(0x44); + stream.WriteByte(FS_RSRC_VAL); } } } @@ -175,14 +293,34 @@ private static IDiskImage CreateTestProDisk(SampleFile[] specs, ParamsBag parms) return disk; } + /// + /// Creates a formatted disk image in a memory stream. + /// + private static IDiskImage CreateTestDisk(FileSystemType fsType, ParamsBag parms) { + UnadornedSector disk; + if (fsType == FileSystemType.DOS33) { + disk = UnadornedSector.CreateSectorImage(new MemoryStream(), 35, 16, + SectorOrder.DOS_Sector, parms.AppHook); + } else { + disk = UnadornedSector.CreateBlockImage(new MemoryStream(), 1600, parms.AppHook); + + } + disk.FormatDisk(fsType, "Test", 254, false, parms.AppHook); + IFileSystem fs = (IFileSystem)disk.Contents!; + fs.PrepareFileAccess(true); + return disk; + } + + /// + /// Expected contents of the ForeignEntries list. + /// private class ExpectedForeign { public string PathName { get; private set; } public int MinLength { get; private set; } public int MaxLength { get; private set; } - public ExpectedForeign(string fileName, int length) { - PathName = fileName; - MinLength = MaxLength = length; + public ExpectedForeign(string fileName, int length) + : this(fileName, length, length) { } public ExpectedForeign(string fileName, int minLength, int maxLength) { PathName = fileName; @@ -201,26 +339,17 @@ public ExpectedForeign(string fileName, int minLength, int maxLength) { new ExpectedForeign(Path.Join(SUBDIR1, SUBDIR2), -1), new ExpectedForeign(Path.Join(SUBDIR1, SUBDIR2, DATA_ONLY_NAME + NAPS_STR), DATA_ONLY_DLEN), + new ExpectedForeign(ANOTHER_NAME + NAPS_STR, ANOTHER_DLEN), }; - private static void TestForeignExtract(IArchive arc, ParamsBag parms) { - List entries = new List(); - foreach (IFileEntry entry in arc) { - entries.Add(entry); - } - ClipFileSet clipSet = new ClipFileSet(arc, entries, IFileEntry.NO_ENTRY, - parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip, - null, null, parms.AppHook); - - TestForeign(clipSet, sForeignExtractNAPS, parms); - } - - private static void TestForeign(ClipFileSet clipSet, ExpectedForeign[] expected, + private static void CheckForeign(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); } + + // Check the foreign entries. for (int i = 0; i < clipSet.ForeignEntries.Count; i++) { ClipFileEntry clipEntry = clipSet.ForeignEntries[i]; ExpectedForeign exp = expected[i]; @@ -246,8 +375,14 @@ private static void TestForeign(ClipFileSet clipSet, ExpectedForeign[] expected, exp.MinLength + "," + exp.MaxLength + ", actual=" + clipEntry.OutputLength); } } + + // This would be sent to Windows Explorer or another application, so there isn't + // really more to test from here. } + /// + /// Expected contents of the XferEntries list. + /// private class ExpectedXfer { public string PathName { get; private set; } public FilePart Part { get; private set; } @@ -258,49 +393,98 @@ public ExpectedXfer(string pathName, FilePart part, int length) { Part = part; Length = length; } + public override string ToString() { + return "[ExpXfer: '" + PathName + "' part=" + Part + " len=" + Length + "]"; + } } - private static ExpectedXfer[] sXferProDOS = new ExpectedXfer[] { + /// + /// Expected result of pasting to a file archive or disk image. + /// + private class ExpectedOutput { + public string PathName { get; private set; } + public int DataLengthMin { get; private set; } + public int DataLengthMax { get; private set; } + public int RsrcLength { get; private set; } + public bool IsDirectory { get; private set; } + + public ExpectedOutput(string pathName, int dataLength, int rsrcLength, + bool hasFileType) + : this(pathName, dataLength, dataLength, rsrcLength, hasFileType) { + } + public ExpectedOutput(string pathName, int dataLengthMin, int dataLengthMax, + int rsrcLength, bool isDirectory) { + PathName = pathName; + DataLengthMin = dataLengthMin; + DataLengthMax = dataLengthMax; + RsrcLength = rsrcLength; + IsDirectory = isDirectory; + } + } + + private static ExpectedXfer[] sXferFromProDOS = 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), + new ExpectedXfer(SUBDIR1 + PRO_SEP + DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN), + new ExpectedXfer(SUBDIR1 + PRO_SEP + SUBDIR2, FilePart.Unknown, -1), + new ExpectedXfer(SUBDIR1 + PRO_SEP + SUBDIR2 + PRO_SEP + DATA_ONLY_NAME, + FilePart.DataFork, DATA_ONLY_DLEN), + new ExpectedXfer(ANOTHER_NAME, FilePart.DataFork, ANOTHER_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 ExpectedOutput[] sOutputProDOS = new ExpectedOutput[] { + new ExpectedOutput(DATA_ONLY_NAME, DATA_ONLY_DLEN, -1, false), + new ExpectedOutput(RSRC_ONLY_NAME, -1, RSRC_ONLY_RLEN, false), + new ExpectedOutput(DATA_RSRC_NAME, DATA_RSRC_DLEN, DATA_RSRC_RLEN, false), + new ExpectedOutput(SUBDIR1, -1, -1, true), + new ExpectedOutput(SUBDIR1 + PRO_SEP + DATA_ONLY_NAME, DATA_ONLY_DLEN, -1, false), + new ExpectedOutput(SUBDIR1 + PRO_SEP + SUBDIR2, -1, -1, true), + new ExpectedOutput(SUBDIR1 + PRO_SEP + SUBDIR2 + PRO_SEP + DATA_ONLY_NAME, + DATA_ONLY_DLEN, -1, false), + new ExpectedOutput(ANOTHER_NAME, ANOTHER_DLEN, -1, false), + }; - private static ExpectedXfer[] sXferProDOSReroot = new ExpectedXfer[] { + private static ExpectedXfer[] sXferFromProDOSReroot = 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), + new ExpectedXfer(SUBDIR2 + PRO_SEP + 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); + private static ExpectedOutput[] sOutputProDOSReroot = new ExpectedOutput[] { + new ExpectedOutput(SUBDIR1 + PRO_SEP + DATA_ONLY_NAME, DATA_ONLY_DLEN, -1, false), + new ExpectedOutput(SUBDIR1 + PRO_SEP + SUBDIR2, -1, -1, true), + new ExpectedOutput(SUBDIR1 + PRO_SEP + SUBDIR2 + PRO_SEP + DATA_ONLY_NAME, + DATA_ONLY_DLEN, -1, false), + }; - ClipFileSet clipSet = new ClipFileSet(srcFs, entries, baseDir, - parms.Preserve, parms.Raw, parms.StripPaths, parms.MacZip, - null, null, parms.AppHook); + private static ExpectedXfer[] sXferFromZip = 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 + PRO_SEP + DATA_ONLY_NAME, FilePart.DataFork, DATA_ONLY_DLEN), + new ExpectedXfer(SUBDIR1 + PRO_SEP + SUBDIR2, FilePart.Unknown, -1), + new ExpectedXfer(SUBDIR1 + PRO_SEP + SUBDIR2 + PRO_SEP + DATA_ONLY_NAME, + FilePart.DataFork, DATA_ONLY_DLEN), + new ExpectedXfer(ANOTHER_NAME, FilePart.DataFork, ANOTHER_DLEN), + }; - TestXfer(clipSet, sXferProDOSReroot, parms); + + private static List GatherArchiveEntries(IArchive arc, ParamsBag parms) { + List entryList = new List(); + foreach (IFileEntry entry in arc) { + if (arc is Zip && parms.MacZip && entry.IsMacZipHeader()) { + continue; + } + entryList.Add(entry); + } + return entryList; } private static List GatherDiskEntries(IFileSystem fs, IFileEntry rootDir) { @@ -319,11 +503,56 @@ private static void GetDiskEntries(IFileSystem fs, IFileEntry dir, } } - private static void TestXfer(ClipFileSet clipSet, ExpectedXfer[] expected, ParamsBag parms) { + /// + /// Synthesizes a parallel set of memory streams for the file contents. + /// + private static List GenerateXferStreams(object archiveOrFileSystem, + ClipFileSet clipSet) { + List entries = clipSet.XferEntries; + List xferStreams = new List(); + foreach (ClipFileEntry entry in entries) { + if (entry.Attribs.IsDirectory) { + xferStreams.Add(null); + continue; + } + + byte val; + long length; + if (entry.Part == FilePart.RsrcFork) { + length = entry.Attribs.RsrcLength; + Debug.Assert(length >= 0); + if (archiveOrFileSystem is IArchive) { + val = ARC_RSRC_VAL; + } else { + val = FS_RSRC_VAL; + } + } else { + length = entry.Attribs.DataLength; + Debug.Assert(length >= 0); + if (archiveOrFileSystem is IArchive) { + val = ARC_DATA_VAL; + } else { + val = FS_DATA_VAL; + } + } + + byte[] buf = new byte[length]; + RawData.MemSet(buf, 0, (int)length, val); + xferStreams.Add(new MemoryStream(buf)); + } + + Debug.Assert(xferStreams.Count == clipSet.XferEntries.Count); + return xferStreams; + } + + private static void CheckXfer(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); } + + // Check the xfer entries. for (int i = 0; i < clipSet.XferEntries.Count; i++) { ClipFileEntry clipEntry = clipSet.XferEntries[i]; ExpectedXfer exp = expected[i]; @@ -352,5 +581,100 @@ private static void TestXfer(ClipFileSet clipSet, ExpectedXfer[] expected, Param } } } + + private static void CheckOutput(List srcEntries, ClipFileSet clipSet, + object archiveOrFileSystem, IFileEntry dstBaseDir, List xferStreams, + ExpectedOutput[] expected, ParamsBag parms) { + ClipPasteWorker.CallbackFunc cbFunc = delegate (CallbackFacts what) { + // Do nothing. + return CallbackFacts.Results.Unknown; + }; + + ClipPasteWorker.ClipStreamGenerator streamGen = delegate (ClipFileEntry clipEntry) { + int index = clipSet.XferEntries.IndexOf(clipEntry); + Stream? stream = xferStreams[index]; + if (stream == null) { + Debug.WriteLine("StreamGen: returning null stream"); + } else { + Debug.Assert(stream.Position == 0); + Debug.WriteLine("StreamGen: returning stream, length=" + stream.Length); + } + return xferStreams[index]; + }; + + ClipPasteWorker worker = new ClipPasteWorker(clipSet.XferEntries, streamGen, + cbFunc, parms.Compress, parms.MacZip, parms.ConvertDOSText, parms.StripPaths, + parms.Raw, true, parms.AppHook); + + bool isCancelled; + if (archiveOrFileSystem is IArchive) { + worker.AddFilesToArchive((IArchive)archiveOrFileSystem, out isCancelled); + } else { + worker.AddFilesToDisk((IFileSystem)archiveOrFileSystem, dstBaseDir, out isCancelled); + } + Debug.Assert(!isCancelled); + + // Confirm contents match expectations. + List dstEntries; + if (archiveOrFileSystem is IArchive) { + IArchive arc = (IArchive)archiveOrFileSystem; + dstEntries = GatherArchiveEntries(arc, parms); + } else { + IFileSystem fs = (IFileSystem)archiveOrFileSystem; + if (dstBaseDir == IFileEntry.NO_ENTRY) { + dstBaseDir = fs.GetVolDirEntry(); + } + dstEntries = GatherDiskEntries(fs, dstBaseDir); + } + + if (dstEntries.Count != expected.Length) { + throw new Exception("Output has different number of entries: dst=" + + dstEntries.Count + " expect=" + expected.Length); + } + for (int i = 0; i < dstEntries.Count; i++) { + CompareEntries(i, dstEntries[i], expected[i]); + } + } + + private static void CompareEntries(int index, IFileEntry dstEntry, ExpectedOutput exp) { + string prefix = "Output " + index + " '" + exp.PathName + "': "; + + if (dstEntry.FullPathName != exp.PathName) { + throw new Exception(prefix + "pathname mismatch dst='" + + dstEntry.FullPathName + " exp=" + exp.PathName); + } + if (!exp.IsDirectory) { + if (dstEntry.FileType != PRODOS_FILETYPE || dstEntry.AuxType != PRODOS_AUXTYPE) { + throw new Exception(prefix + "lacks types"); + } + if (dstEntry.CreateWhen != CREATE_WHEN || dstEntry.ModWhen != MOD_WHEN) { + throw new Exception(prefix + "lacks dates"); + } + + // Confirm the file lengths. + // TODO? confirm contents too + if (exp.DataLengthMin < 0) { + if (dstEntry.DataLength > 0) { + throw new Exception(prefix + "has data fork"); + } + } else { + if (dstEntry.DataLength < exp.DataLengthMin || + dstEntry.DataLength > exp.DataLengthMax) { + throw new Exception(prefix + "incorrect data length: " + + dstEntry.DataLength); + } + } + if (exp.RsrcLength < 0) { + if (dstEntry.RsrcLength > 0) { + throw new Exception(prefix + "has rsrc fork"); + } + } else { + if (dstEntry.RsrcLength != exp.RsrcLength) { + throw new Exception(prefix + "incorrect rsrc length: " + + dstEntry.RsrcLength); + } + } + } + } } } diff --git a/cp2_wpf/MainController.cs b/cp2_wpf/MainController.cs index 84d1b98..2b2c054 100644 --- a/cp2_wpf/MainController.cs +++ b/cp2_wpf/MainController.cs @@ -1137,8 +1137,10 @@ public void PasteOrDrop(IDataObject? dropObj, IFileEntry dropTarget) { return null; } if (dropObj == null) { + // From clipboard. return ClipHelper.GetClipboardContentsSTA(index, ClipInfo.XFER_STREAMS); } else { + // From drag & drop object. return ClipHelper.GetFileContents(dropObj, index, ClipInfo.XFER_STREAMS); } }; diff --git a/cp2_wpf/Tools/DropTarget.xaml.cs b/cp2_wpf/Tools/DropTarget.xaml.cs index a6fb0fe..b966213 100644 --- a/cp2_wpf/Tools/DropTarget.xaml.cs +++ b/cp2_wpf/Tools/DropTarget.xaml.cs @@ -226,8 +226,9 @@ private void DumpXferEntries(IDataObject dataObj, StringBuilder sb) { } else { for (int index = 0; index < clipInfo.ClipEntries.Count; index++) { ClipFileEntry clipEntry = clipInfo.ClipEntries[index]; - sb.AppendFormat(" {0}: '{1}' len={2}: ", - index, clipEntry.Attribs.FullPathName, clipEntry.OutputLength); + sb.AppendFormat(" {0}: '{1}' part={2} len={3}: ", + index, clipEntry.Attribs.FullPathName, clipEntry.Part, + clipEntry.OutputLength); if (clipEntry.Attribs.IsDirectory) { sb.AppendLine("is directory"); continue; diff --git a/cp2_wpf/WPFCommon/ClipHelper.cs b/cp2_wpf/WPFCommon/ClipHelper.cs index 8944f60..85bbec5 100644 --- a/cp2_wpf/WPFCommon/ClipHelper.cs +++ b/cp2_wpf/WPFCommon/ClipHelper.cs @@ -72,6 +72,11 @@ public static class ClipHelper { Debug.WriteLine("GetData throw a COMException for index=" + index + ": " + ex.Message); return null; + } catch (Exception ex) { + // Unexpected. + Debug.WriteLine("GetFileContents failed: " + ex); + Debug.Assert(false); + return null; } // TYpe of MEDium - https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-tymed diff --git a/cp2_wpf/WPFCommon/VirtualFileDataObject.cs b/cp2_wpf/WPFCommon/VirtualFileDataObject.cs index 7d96060..0ac66ec 100644 --- a/cp2_wpf/WPFCommon/VirtualFileDataObject.cs +++ b/cp2_wpf/WPFCommon/VirtualFileDataObject.cs @@ -274,7 +274,9 @@ void System.Runtime.InteropServices.ComTypes.IDataObject.GetDataHere(ref FORMATE // that's what we want. The current ClipHelper implementation doesn't need this. // // We can also get here after an exception on the stream opener. - throw new NotImplementedException(); + // + // ThrowExceptionForHR seems like the right answer. E_FAIL turns into a COMException. + Marshal.ThrowExceptionForHR(NativeMethods.E_FAIL); } ///