From e9d8a7c0f81b33c797c3b7832a65022e186945f0 Mon Sep 17 00:00:00 2001 From: Solomon Blount <74916907+siblount@users.noreply.github.com> Date: Sat, 13 Jan 2024 21:23:50 -0500 Subject: [PATCH] Source code maintenance + bug fixes + features (#25) * Minor update to DatabaseView designer. * Fixed #15 Cause for app hang was due to a deadlock while creating connections (ex: waiting for Initialization when called by the Init func). Added new CreateInitialConnection which does not check if the database was initialized and InsertDefaultTables uses the connection instead of null. * Fixed #16 first point - release image handle when done * Clear DPFiles at start & fix file handle leak Fixed the issue where the zip archive file handle was not released even when done; call ReleaseArchiveHandles() at the end of ProcessArchive. Updated 7zArchive to kill the 7z process tree if it still exists. * try..catch..me...if..you..can for all archives try catch ReadMetaFiles, ReadContentFiles and others. * Added sorting combo & functionality. * Order by product name not case sensitive, update limit offset for library queries. * added some remove right click action for library items. * updated remove actions for product record form remove now ask for confirmation remove product is implemented and more advanced than the one on library item. * fix compiler error * fixed noImageFound replaced with random image * Changed logo & app appearance and implemented minor UI perf increase. * Update nuget packages * Added new UpdateProductRecord() & UpdateExtractionRecord() along with helper funcs and proprogating changes across DPDatabase * added delegate w/ 5 args * Added functionality to modify product records in the product record form. Note: Doesn't successfully save. * i have no idea what this is. * added new RestoreTriggers() and TempDeleteTriggers() * fixed RemoveValuesWithCondition(), removed transaction?.Dispose() from finally, typo fixes * removed additional update invocations TableUpdated and ProductRecordModified() already invokes the event, no need for it twice. * Updated UpdateRecordQ() * Fixed #19 - explictly unregister form when closed * Added new properties for DPAbstractFile Also removed FileName for DPAbstractArchive since it will be covered by DPAbstractFile. * Addressed #18, still needs work as content folders are being targetted to DAZ directory root folder. Added new CalculateChildRelativePath() and CalculateChildRelativeTargetPath() funcs for DPFolder. Added GetTargetPath() and propogated changes from DPFolder onto here for DPProcessor. * Remove Queue tab temporarily; still in designer. * fixed missing mutex for app * Minor UI adjustment to DB viewer Anchored button and combobox to the right. * Removed tool strip and other unused UI elements. * Added about form, logo links to about form * slight fix for out of bounds exception thrown here * Added new settings * Settings is now using JSON.Net & using serialization * fixed NoImageFound property returning null * Addresses #21 - tags, etc now have char limits Tags, Product Name, Author, SKU, Folders, Files, errored files, now have char limits. * minor ui change - modified text anchor * fixed settings invalid messasge always showing up * fixed length of n + 1 instead of n * Prevent >70 char limit for tags * Disable thumbnail strip if no image is shown. * update version * remove invalid folder * Split up project into multiple NOTE: You will not be able to build at this commit. * rename namespaces, setup dependencies, and more NOTE: You will not be able to build at this commit. * Split up UsefulFuncs.cs, removed ArrayHelper, Up in PathHelper * moved namespace to DAZ_Installer.Core * moved namespace to DAZ_Installer.Core.External * added some vars from DPCommon into Program * prepared for removal of associations from DPAbstractFile - Flipped dictionary keys and valeus - Now use nodes and listviewitem tags. - Added additional condition for 'Select in hierachy' visibility for list view. - Added select in file list view action. * Added new DPProcessSettings * fuck it just upload everything * Addressed destination not enough space infinite loop when cancelled. * Added new properties to MockedDPIOContext due to some weird NSubstitute errors * Updated ProcessArchiveTest_OutOfStorage to now pass. * Replace NSubstitute with Moq and proprogate changes. Moved Fakes to Test projects instead. * Splitting up DPProcessor - added tag provider module Created new AbstractTagProvider and it's default implementation DPTagProvider. Propogated changes to DPProcessor. * Splitting up DPProcessor - added destination determiner Created an abstract destination determiner to determine which files get extracted and to where. Created it's default implentation at DPDestinationDeterminer. Propogated changes. * got rid of some ugly try-catch blocks * Minor logging update to DPProcessor. * Update SetupProcessor to provide fake DestinationDeterminer and TagProvider * Try-Catch everything in processArchievInternal. * add documentation to (Destination/Temp)HasEnoguhSpace functions * Relay extractor extract progress event * Add testing for processing archive after process. * ._. * Create dotnet-test.yml * delete extra iotests. * cancellation token update (not working atm) * Remove unused variables. * Made adjustments to DPProcessorErrorArgs for clarity. * Clarity for DPArchiveEnterArgs * Added func to skip archives * Update to make extractors cancelable. * Removed redundant CancellationToken. * Process archives iteratively (vs recursively) * Add support for cancelling archive and processing entirely. * Remove skip variable * Add CancellationToken * Make interface for DPDatabase. * Update database to be more async friendly. * Remove unnecessary parameter for ProductRecordAdded * Added locks to DPTaskManager * Return task for RemoveProductRecord. * Update IDPDatabase documentation * Add task execute sequentially helper method. * Add DPDatabase tests. * Fix compile errors. * Fix StringWriter import error. * Update app name to Product Manager for DAZ Studio to avoid trademark and other legal issues :) * fix unintentional hiding for Scope * Search for normalized path of current rar file. * Add test subjects for integration testing. * Fix RAR not extracting files split in multi-volume. * Update test subjects * Skip testing files that are directories. * Always copy test subjects to output directory. * Upload integration tests for RAR Extractor * Check for cancellation at start of readheader loop. This is needed to pass the "PeekTest_CancelledDuringOp" test. But also, it's just better to check for cancellation first thing. * Delete src.rar * Add support for Git LFS in dotnet-test.yml * Cache Nuget Packages, change dotnet version, cd to dir * Cache Nuget Packages, cd to dir before test run * figure out this github action weirdness * Remove action, remove warnings, and update verbosity. * Add logging to figure out github actions weridness * fix dotnet cmd * fix dotnet cmd * add logging to solve this mystery * change verbosity again. * update dotnet-test * Add support for Git LFS in dotnet-test.yml * Update to copy always * Addresses #24 - NormalizePath doesnt trim drive letters * Fix #24 - updated path calculation logic. * Remove duplicate WinApp project. * Update README.md * Rename app * get rid of comment * Fix creating incorrect directory * Implement 7z extractor integration tests * Remove unnecessary test. * Implement zip archive integration tests * More reliable Peek, Extract, and Move events. * Moved stream test helpers * Added new GetRootDirectory() * Fixed relative path issues * fixed destination determiner + add tests * Remove DPIDManager class * Make streams more customizable * Fix bug, add content type as tag, add tests * make helper methods private * Fix this. * Fix logic bugs * change security for constants * Remove extracted check + add comment * Add integration testing for DPProcessor * Add testing for DPDazFile * Add testing for DPDSXFile * Downgrading NuGet packages * Make TargetPath setter public. * Fixed rar extractor extracting incorrectly for select files Fixed DPRarExtractor from extracting other files instead of the selected files if not all of the files were being extracted. * Added tests for partial extraction * Fixed subarchive duplication in destination determiner. * Fixed RAR extractor throwing archive corrupt errors Fixed the RAR extractor throwing archive corrupt errors and added tests and updated FakeRAR based on new findings. If the current mode is Extract, then a Test, Extract, or Skip must be called after calling ReadHeader(), otherwise "Archive is corrupt" will be the exception thrown. * Extract DSX Files instead of DazFiles * Fix path is null exception in ExtractedToTarget * Fix subarchives not being processed + scope fix Moved adding the subarchives to process at the end instead of beginning. Removed combining the destination path with the content folder and redirects since the manifest can skip this behavior since the content folders are for when a manifest does not exist. * fixed subarchives having empty TargetPath + typo If Automatic was an InstallOption, the filePathPart would always be empty since no overridePath is being provided and RelativeTargetPath is empty since the archive is not in a content folder, but is targeted by the algorithm. The subarchives would have an incorrect path only returning the temp path. Additionally, in the DetermineViaFileSense the parent archive's TargetPath was accidentially being set to the child's TargetPath which was incomplete. This resulted in the subarchive having an empty TargetPath. * Created new Testing Suite tool * Target x64 only * Setup real data testing for DPProcessor * Remove logging * Complete real data testing * addressed handling encrypted 7z archives * implement cancel + updated ui responsiveness * fixed not handling encrypted headers correctly * adjust xcopy parameters * fixed success percentage. * Make processor more responsive to cancellations. * update dpprocessor logging * update success method, emit error for bad archive * add more default redirects * update documentation * fix anchor for tabcontrol & cancel not working * added hopefully a working PostPublish target * fix issue with xcopy * remove source files subject to delete action * update archive names after the extract job finished * extract ui optimizations + documentation * use SetProgressBarValue instead * Invoke on CreateProgressCombo. * use calc instead of ref. * remove check if marque is already set * better RecursivelyGetControls func (might remove) * remove useless func * add cancellation support Replaced DPProgressCombo with an actual UserConrtol ProgressCombo. * auto switch page on file drag * ref app info in assembly --- .gitattributes | 8 + .github/workflows/dotnet-test.yml | 6 +- .gitignore | 4 + .vscode/settings.json | 3 + README.md | 5 +- src/.editorconfig | 77 +- src/Custom Controls/Extract.Designer.cs | 321 --- src/Custom Controls/Extract.cs | 273 --- src/Custom Controls/Home.Designer.cs | 229 -- src/Custom Controls/Library.resx | 63 - src/Custom Controls/QueueControl.cs | 23 - src/Custom Controls/Settings.cs | 425 ---- .../DAZ_Installer.Common.projitems | 19 + .../DAZ_Installer.Common.shproj | 13 + src/DAZ_Installer.Common/DPArchiveMap.cs | 16 + .../DPProcessorTestManifest.cs | 32 + src/DAZ_Installer.Common/MSTestLoggerSink.cs | 26 + .../SerilogLoggerConstants.cs | 21 + .../SpanExtensions.cs | 2 +- src/DAZ_Installer.Common/TryHelper.cs | 132 ++ src/DAZ_Installer.Core/.vscode/settings.json | 3 + .../AbstractDestinationDeterminer.cs | 24 + .../Abstractions/AbstractTagProvider.cs | 22 + src/DAZ_Installer.Core/ContentType.cs | 45 + .../DAZ_Installer.Core.csproj | 27 + src/DAZ_Installer.Core/DAZ_Installer.Core.sln | 25 + src/DAZ_Installer.Core/DPAbstractNode.cs | 96 + src/DAZ_Installer.Core/DPArchive.cs | 495 +++++ src/DAZ_Installer.Core/DPArchiveEnterArgs.cs | 16 + src/DAZ_Installer.Core/DPArchiveErrorArgs.cs | 51 + src/DAZ_Installer.Core/DPArchiveExitArgs.cs | 30 + .../DPContentInfo.cs | 19 +- .../DPDSXElement.cs | 43 +- .../DPDSXElementCollection.cs | 50 + src/DAZ_Installer.Core/DPDSXFile.cs | 116 + src/{DP => DAZ_Installer.Core}/DPDSXParser.cs | 77 +- src/{DP => DAZ_Installer.Core}/DPDazFile.cs | 58 +- .../DPDestinationDeterminer.cs | 238 +++ src/DAZ_Installer.Core/DPErrorArgs.cs | 17 + src/DAZ_Installer.Core/DPEventHandler.cs | 8 + src/DAZ_Installer.Core/DPFile.cs | 254 +++ src/DAZ_Installer.Core/DPFolder.cs | 258 +++ src/DAZ_Installer.Core/DPProcessSettings.cs | 85 + src/DAZ_Installer.Core/DPProcessor.cs | 453 ++++ .../DPProcessorErrorArgs.cs | 37 + src/DAZ_Installer.Core/DPProcessorState.cs | 30 + src/DAZ_Installer.Core/DPProductInfo.cs | 44 + src/DAZ_Installer.Core/DPTagProvider.cs | 173 ++ src/DAZ_Installer.Core/DPTaskManager.cs | 320 +++ src/{ => DAZ_Installer.Core}/External/7za.exe | Bin src/{ => DAZ_Installer.Core}/External/RAR.cs | 204 +- .../Extraction/DP7zExtractor.cs | 454 ++++ .../Extraction/DPAbstractExtractor.cs | 198 ++ .../Extraction/DPExtractProgressArgs.cs | 29 + .../Extraction/DPExtractSettings.cs | 44 + .../Extraction/DPExtractionReport.cs | 28 + .../Extraction/DPRARExtractor.cs | 318 +++ .../Extraction/DPZipExtractor.cs | 226 ++ .../Extraction/Factories/ProcessFactory.cs | 7 + .../Extraction/Factories/RARFactory.cs | 9 + .../Factories/ZipArchiveWrapperFactory.cs | 7 + .../Extraction/Interfaces/IProcess.cs | 42 + .../Extraction/Interfaces/IProcessFactory.cs | 7 + .../Extraction/Interfaces/IRAR.cs | 39 + .../Extraction/Interfaces/IRARFactory.cs | 7 + .../Extraction/Interfaces/IZipArchive.cs | 17 + .../Extraction/Interfaces/IZipArchiveEntry.cs | 29 + .../Interfaces/IZipArchiveFactory.cs | 7 + .../Extraction/Wrappers/ProcessWrapper.cs | 45 + .../Wrappers/ZipArchiveEntryWrapper.cs | 36 + .../Extraction/Wrappers/ZipArchiveWrapper.cs | 32 + src/{ => DAZ_Installer.Core}/Libs/UnRAR.dll | Bin .../Properties/AssemblyInfo1.cs | 20 + .../DAZ_Installer.CoreTests.csproj | 59 + src/DAZ_Installer.CoreTests/DPDSXFileTests.cs | 77 + src/DAZ_Installer.CoreTests/DPDazFileTests.cs | 61 + .../DPDestinationDeterminerTests.cs | 434 ++++ .../DPProcessorTests.cs | 271 +++ .../DPTagProviderTests.cs | 188 ++ .../Extraction/DP7zExtractorTests.cs | 540 +++++ .../Extraction/DPRARExtractorTests.cs | 461 ++++ .../Extraction/DPZipExtractorTests.cs | 513 +++++ .../Extraction/Fakes/FakeProcess.cs | 94 + .../Extraction/Fakes/FakeRAR.cs | 143 ++ .../Extraction/Fakes/FakeZipArchive.cs | 45 + .../Extraction/Fakes/FakeZipArchiveEntry.cs | 27 + .../Helpers/DPArchiveTestHelpers.cs | 203 ++ .../Integration/DP7zExtractorTests.cs | 433 ++++ .../Integration/DPRARExtractorTests.cs | 222 ++ .../Integration/DPZipExtractorTests.cs | 230 ++ .../DPDestinationDeterminerTestHelpers.cs | 80 + .../Helpers/DPProcessorTestHelpers.cs | 193 ++ .../DPIntegrationArchiveHelpers.cs | 168 ++ .../Integration/DPProcessorTests.cs | 73 + .../Integration/Test Subjects/Test.rar | 3 + .../Integration/Test Subjects/Test.zip | 3 + .../Test Subjects/Test_split.part1.rar | 3 + .../Test Subjects/Test_split.part2.rar | 3 + .../Test Subjects/Test_split_solid.part1.rar | 3 + .../Test Subjects/Test_split_solid.part2.rar | 3 + .../--INSERT YOUR ARCHIVES HERE--.txt | 1 + .../RealData/DPProcessorTests.cs | 81 + .../--INSERT YOUR MANIFESTS HERE--.txt | 1 + .../RealData/RealDataHelper.cs | 55 + .../DAZ_Installer.Database.csproj | 21 + .../DPDatabase.Abstraction.cs | 494 +++-- .../DPDatabase.Public.cs | 188 ++ .../DPDatabase.QueryProcessing.cs | 146 +- .../DPDatabase.cs | 324 +-- .../DPExtractionRecord.cs | 9 +- .../DPProductRecord.cs | 5 +- .../DPSortMethod.cs | 2 +- .../External/SQLRegexFunction.cs | 10 +- src/DAZ_Installer.Database/IDPDatabase.cs | 191 ++ .../DAZ_Installer.DatabaseTests.csproj | 25 + .../DPDatabaseTests.cs | 305 +++ .../Helpers/DPDatabaseTestHelpers.cs | 162 ++ .../Abstractions/AbstractFileSystem.cs | 60 + .../Abstractions/IContextFactory.cs | 14 + .../Abstractions/IDPDirectoryInfo.cs | 41 + .../Abstractions/IDPDriveInfo.cs | 8 + .../Abstractions/IDPFileInfo.cs | 34 + .../Abstractions/IDPFileScopeSettings.cs | 13 + .../Abstractions/IDPIONode.cs | 34 + .../Abstractions/IDirectoryInfo.cs | 22 + .../Abstractions/IFileInfo.cs | 17 + src/DAZ_Installer.IO/DAZ_Installer.IO.csproj | 9 + src/DAZ_Installer.IO/DPDirectoryInfo.cs | 287 +++ src/DAZ_Installer.IO/DPDriveInfo.cs | 17 + src/DAZ_Installer.IO/DPFileInfo.cs | 306 +++ src/DAZ_Installer.IO/DPFileScopeSettings.cs | 169 ++ src/DAZ_Installer.IO/DPFileSystem.cs | 32 + src/DAZ_Installer.IO/DPIONodeBase.cs | 13 + .../Extensions/DirectoryInfoExtensions.cs | 15 + .../Extensions/FileInfoExtensions.cs | 15 + src/DAZ_Installer.IO/OutOfScopeException.cs | 16 + src/DAZ_Installer.IO/PathHelper.cs | 258 +++ .../PathTransversalException.cs | 20 + .../Properties/AssemblyInfo1.cs | 20 + .../Wrappers/DirectoryInfoWrapper.cs | 40 + .../Wrappers/FileInfoWrapper.cs | 40 + .../DAZ_Installer.IOTests.csproj | 24 + .../DPDirectoryInfoTests.cs | 302 +++ src/DAZ_Installer.IOTests/DPFileInfoTests.cs | 678 ++++++ .../DPFileScopeSettingsTests.cs | 331 +++ .../Fakes/FakeDPDirectoryInfo.cs | 32 + .../Fakes/FakeDPDriveInfo.cs | 19 + .../Fakes/FakeDPFileInfo.cs | 52 + .../Fakes/FakeDirectoryInfo.cs | 69 + .../Fakes/FakeFileInfo.cs | 62 + .../Fakes/FakeFileSystem.cs | 25 + src/DAZ_Installer.IOTests/PathHelperTests.cs | 153 ++ .../PathTransversalExceptionTests.cs | 45 + .../DAZ_Installer.TestingSuiteWindows.csproj | 31 + .../DPDestinationDeterminerEx.cs | 93 + .../MainForm.Designer.cs | 539 +++++ .../MainForm.cs | 682 ++++++ .../MainForm.resx | 132 ++ .../ProcessSettingsDialogue.Designer.cs | 305 +++ .../ProcessSettingsDialogue.cs | 173 ++ .../ProcessSettingsDialogue.resx | 123 ++ .../Program.cs | 34 + .../RecursiveDestinationDeterminer.cs | 27 + .../ResultCompiler.cs | 68 + .../RichTextBoxSink.cs | 29 + src/DAZ_Installer.UI/DAZ_Installer.UI.csproj | 37 + .../LibraryItem.Designer.cs | 0 .../LibraryItem.cs | 69 +- .../LibraryItem.resx | 0 .../LibraryPanel.Designer.cs | 2 +- .../LibraryPanel.cs | 80 +- .../LibraryPanel.resx | 0 .../PageButtonControl.Designer.cs | 0 .../PageButtonControl.cs | 22 +- .../PageButtonControl.resx | 0 .../ProgressCombo.Designer.cs | 102 + src/DAZ_Installer.UI/ProgressCombo.cs | 104 + src/DAZ_Installer.UI/ProgressCombo.resx | 120 ++ .../QueueControl.Designer.cs | 0 src/DAZ_Installer.UI/QueueControl.cs | 10 + .../QueueControl.resx | 0 .../Assets}/ArrowDown.png | Bin .../Assets}/ArrowRight.jpg | Bin .../Assets}/ArrowRight.png | Bin .../DAZ_Installer.Windows/Assets}/ArrowUp.png | Bin src/DAZ_Installer.Windows/Assets/ArrowUp1.png | Bin 0 -> 4365 bytes .../DAZ_Installer.Windows/Assets}/Icon1.ico | Bin .../Assets}/Logo2-256x.png | Bin .../Assets}/NoImageFound.jpg | Bin .../Assets}/RAR-Icon-New-Original-APK.png | Bin .../Assets}/WindowsFolderIcon.png | Bin .../DAZ_Installer.Windows/Assets}/favicon.ico | Bin .../DAZ_Installer.Windows/Assets}/loading.gif | Bin .../DAZ_Installer.Windows/Assets}/logo.png | Bin .../Assets}/thumb_14366704070ZIP.png | Bin .../DAZ_Installer.Windows.csproj | 186 ++ src/DAZ_Installer.Windows/DP/DPExtractJob.cs | 295 +++ .../DP/DPGlobal.cs | 8 +- .../DP/DPNetwork.cs | 43 +- src/DAZ_Installer.Windows/DP/DPRegistry.cs | 63 + src/DAZ_Installer.Windows/DP/DPSettings.cs | 177 ++ .../Forms/AboutForm.Designer.cs | 115 + src/DAZ_Installer.Windows/Forms/AboutForm.cs | 20 + .../Forms/AboutForm.resx | 69 +- .../ContentFolderAliasManager.Designer.cs | 2 +- .../Forms/ContentFolderAliasManager.cs | 38 +- .../Forms/ContentFolderAliasManager.resx | 0 .../Forms/ContentFolderManager.Designer.cs | 2 +- .../Forms/ContentFolderManager.cs | 27 +- .../Forms/ContentFolderManager.resx | 0 .../Forms/DatabaseView.Designer.cs | 2 +- .../Forms/DatabaseView.cs | 16 +- .../Forms/DatabaseView.resx | 0 .../Forms/MainForm.Designer.cs | 254 +++ .../Forms/MainForm.cs | 71 +- src/DAZ_Installer.Windows/Forms/MainForm.resx | 1891 +++++++++++++++++ .../Forms/PasswordInput.Designer.cs | 2 +- .../Forms/PasswordInput.cs | 27 +- .../Forms/PasswordInput.resx | 0 .../Forms/ProductRecordForm.Designer.cs | 4 +- .../Forms/ProductRecordForm.cs | 160 +- .../Forms/ProductRecordForm.resx | 0 .../Forms/TagsManager.Designer.cs | 2 +- .../Forms/TagsManager.cs | 34 +- .../Forms/TagsManager.resx | 0 .../Pages/Extract.Designer.cs | 260 +++ src/DAZ_Installer.Windows/Pages/Extract.cs | 201 ++ .../Pages}/Extract.resx | 64 +- .../Pages/Home.Designer.cs | 220 ++ .../Pages}/Home.cs | 152 +- .../Pages}/Home.resx | 62 +- .../Pages}/Library.Designer.cs | 2 +- .../Pages}/Library.cs | 83 +- .../Pages/Library.resx} | 4 +- .../Pages}/Settings.Designer.cs | 2 +- src/DAZ_Installer.Windows/Pages/Settings.cs | 618 ++++++ .../Pages}/Settings.resx | 0 src/DAZ_Installer.Windows/Program.cs | 91 + .../Properties/Resources - Copy.ignore | 0 .../Properties/Resources.Designer.cs | 14 +- .../Properties/Resources.resx | 13 +- .../Properties/Settings.Designer.cs | 4 +- .../Properties/Settings.settings | 0 .../Resources/Logo2-256x.png | Bin .../Resources/favicon.ico | Bin .../Resources/loading.gif | Bin src/DAZ_Installer.Windows/favicon.ico | Bin 0 -> 105768 bytes src/DAZ_Installer.csproj | 90 - src/DAZ_Installer.sln | 116 +- src/DP/ContentType.cs | 43 - src/DP/DP7zArchive.cs | 339 --- src/DP/DPAbstractArchive.cs | 459 ---- src/DP/DPAbstractFile.cs | 163 -- src/DP/DPCache.cs | 57 - src/DP/DPDSXElementCollection.cs | 54 - src/DP/DPDSXFile.cs | 88 - src/DP/DPDatabase.Public.cs | 334 --- src/DP/DPExtractJob.cs | 111 - src/DP/DPFile.cs | 131 -- src/DP/DPFolder.cs | 267 --- src/DP/DPIDManager.cs | 17 - src/DP/DPProcessor.cs | 443 ---- src/DP/DPProductInfo.cs | 21 - src/DP/DPProgressCombo.cs | 93 - src/DP/DPRARArchive.cs | 404 ---- src/DP/DPRegistry.cs | 55 - src/DP/DPSettings.cs | 200 -- src/DP/DPTaskManager.cs | 276 --- src/DP/DPZipArchive.cs | 202 -- src/DP/Program.cs | 53 - src/DP/UsefulFuncs.cs | 326 --- src/Forms/AboutForm.Designer.cs | 119 -- src/Forms/AboutForm.cs | 20 - src/Forms/MainForm.Designer.cs | 251 --- src/Utilities/ListExtensions.cs | 15 - 275 files changed, 22110 insertions(+), 7179 deletions(-) create mode 100644 .gitattributes create mode 100644 .vscode/settings.json delete mode 100644 src/Custom Controls/Extract.Designer.cs delete mode 100644 src/Custom Controls/Extract.cs delete mode 100644 src/Custom Controls/Home.Designer.cs delete mode 100644 src/Custom Controls/Library.resx delete mode 100644 src/Custom Controls/QueueControl.cs delete mode 100644 src/Custom Controls/Settings.cs create mode 100644 src/DAZ_Installer.Common/DAZ_Installer.Common.projitems create mode 100644 src/DAZ_Installer.Common/DAZ_Installer.Common.shproj create mode 100644 src/DAZ_Installer.Common/DPArchiveMap.cs create mode 100644 src/DAZ_Installer.Common/DPProcessorTestManifest.cs create mode 100644 src/DAZ_Installer.Common/MSTestLoggerSink.cs create mode 100644 src/DAZ_Installer.Common/SerilogLoggerConstants.cs rename src/{Utilities => DAZ_Installer.Common}/SpanExtensions.cs (96%) create mode 100644 src/DAZ_Installer.Common/TryHelper.cs create mode 100644 src/DAZ_Installer.Core/.vscode/settings.json create mode 100644 src/DAZ_Installer.Core/Abstractions/AbstractDestinationDeterminer.cs create mode 100644 src/DAZ_Installer.Core/Abstractions/AbstractTagProvider.cs create mode 100644 src/DAZ_Installer.Core/ContentType.cs create mode 100644 src/DAZ_Installer.Core/DAZ_Installer.Core.csproj create mode 100644 src/DAZ_Installer.Core/DAZ_Installer.Core.sln create mode 100644 src/DAZ_Installer.Core/DPAbstractNode.cs create mode 100644 src/DAZ_Installer.Core/DPArchive.cs create mode 100644 src/DAZ_Installer.Core/DPArchiveEnterArgs.cs create mode 100644 src/DAZ_Installer.Core/DPArchiveErrorArgs.cs create mode 100644 src/DAZ_Installer.Core/DPArchiveExitArgs.cs rename src/{DP => DAZ_Installer.Core}/DPContentInfo.cs (60%) rename src/{DP => DAZ_Installer.Core}/DPDSXElement.cs (52%) create mode 100644 src/DAZ_Installer.Core/DPDSXElementCollection.cs create mode 100644 src/DAZ_Installer.Core/DPDSXFile.cs rename src/{DP => DAZ_Installer.Core}/DPDSXParser.cs (84%) rename src/{DP => DAZ_Installer.Core}/DPDazFile.cs (54%) create mode 100644 src/DAZ_Installer.Core/DPDestinationDeterminer.cs create mode 100644 src/DAZ_Installer.Core/DPErrorArgs.cs create mode 100644 src/DAZ_Installer.Core/DPEventHandler.cs create mode 100644 src/DAZ_Installer.Core/DPFile.cs create mode 100644 src/DAZ_Installer.Core/DPFolder.cs create mode 100644 src/DAZ_Installer.Core/DPProcessSettings.cs create mode 100644 src/DAZ_Installer.Core/DPProcessor.cs create mode 100644 src/DAZ_Installer.Core/DPProcessorErrorArgs.cs create mode 100644 src/DAZ_Installer.Core/DPProcessorState.cs create mode 100644 src/DAZ_Installer.Core/DPProductInfo.cs create mode 100644 src/DAZ_Installer.Core/DPTagProvider.cs create mode 100644 src/DAZ_Installer.Core/DPTaskManager.cs rename src/{ => DAZ_Installer.Core}/External/7za.exe (100%) rename src/{ => DAZ_Installer.Core}/External/RAR.cs (85%) create mode 100644 src/DAZ_Installer.Core/Extraction/DP7zExtractor.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPAbstractExtractor.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPExtractProgressArgs.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPExtractSettings.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPExtractionReport.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPRARExtractor.cs create mode 100644 src/DAZ_Installer.Core/Extraction/DPZipExtractor.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Factories/ProcessFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Factories/RARFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Factories/ZipArchiveWrapperFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IProcess.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IProcessFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IRAR.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IRARFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchive.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveEntry.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveFactory.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Wrappers/ProcessWrapper.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveEntryWrapper.cs create mode 100644 src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveWrapper.cs rename src/{ => DAZ_Installer.Core}/Libs/UnRAR.dll (100%) create mode 100644 src/DAZ_Installer.Core/Properties/AssemblyInfo1.cs create mode 100644 src/DAZ_Installer.CoreTests/DAZ_Installer.CoreTests.csproj create mode 100644 src/DAZ_Installer.CoreTests/DPDSXFileTests.cs create mode 100644 src/DAZ_Installer.CoreTests/DPDazFileTests.cs create mode 100644 src/DAZ_Installer.CoreTests/DPDestinationDeterminerTests.cs create mode 100644 src/DAZ_Installer.CoreTests/DPProcessorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/DPTagProviderTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/DP7zExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/DPRARExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/DPZipExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeProcess.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeRAR.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchive.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchiveEntry.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Helpers/DPArchiveTestHelpers.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Integration/DP7zExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Integration/DPRARExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Extraction/Integration/DPZipExtractorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Helpers/DPDestinationDeterminerTestHelpers.cs create mode 100644 src/DAZ_Installer.CoreTests/Helpers/DPProcessorTestHelpers.cs create mode 100644 src/DAZ_Installer.CoreTests/Integration/DPIntegrationArchiveHelpers.cs create mode 100644 src/DAZ_Installer.CoreTests/Integration/DPProcessorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.rar create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test.zip create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part1.rar create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split.part2.rar create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part1.rar create mode 100644 src/DAZ_Installer.CoreTests/Integration/Test Subjects/Test_split_solid.part2.rar create mode 100644 src/DAZ_Installer.CoreTests/RealData/Archives/--INSERT YOUR ARCHIVES HERE--.txt create mode 100644 src/DAZ_Installer.CoreTests/RealData/DPProcessorTests.cs create mode 100644 src/DAZ_Installer.CoreTests/RealData/Manifests/--INSERT YOUR MANIFESTS HERE--.txt create mode 100644 src/DAZ_Installer.CoreTests/RealData/RealDataHelper.cs create mode 100644 src/DAZ_Installer.Database/DAZ_Installer.Database.csproj rename src/{DP => DAZ_Installer.Database}/DPDatabase.Abstraction.cs (76%) create mode 100644 src/DAZ_Installer.Database/DPDatabase.Public.cs rename src/{DP => DAZ_Installer.Database}/DPDatabase.QueryProcessing.cs (59%) rename src/{DP => DAZ_Installer.Database}/DPDatabase.cs (72%) rename src/{DP => DAZ_Installer.Database}/DPExtractionRecord.cs (82%) rename src/{DP => DAZ_Installer.Database}/DPProductRecord.cs (78%) rename src/{DP => DAZ_Installer.Database}/DPSortMethod.cs (87%) rename src/{ => DAZ_Installer.Database}/External/SQLRegexFunction.cs (62%) create mode 100644 src/DAZ_Installer.Database/IDPDatabase.cs create mode 100644 src/DAZ_Installer.DatabaseTests/DAZ_Installer.DatabaseTests.csproj create mode 100644 src/DAZ_Installer.DatabaseTests/DPDatabaseTests.cs create mode 100644 src/DAZ_Installer.DatabaseTests/Helpers/DPDatabaseTestHelpers.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/AbstractFileSystem.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IContextFactory.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPDriveInfo.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPFileInfo.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPFileScopeSettings.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDPIONode.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IO/Abstractions/IFileInfo.cs create mode 100644 src/DAZ_Installer.IO/DAZ_Installer.IO.csproj create mode 100644 src/DAZ_Installer.IO/DPDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IO/DPDriveInfo.cs create mode 100644 src/DAZ_Installer.IO/DPFileInfo.cs create mode 100644 src/DAZ_Installer.IO/DPFileScopeSettings.cs create mode 100644 src/DAZ_Installer.IO/DPFileSystem.cs create mode 100644 src/DAZ_Installer.IO/DPIONodeBase.cs create mode 100644 src/DAZ_Installer.IO/Extensions/DirectoryInfoExtensions.cs create mode 100644 src/DAZ_Installer.IO/Extensions/FileInfoExtensions.cs create mode 100644 src/DAZ_Installer.IO/OutOfScopeException.cs create mode 100644 src/DAZ_Installer.IO/PathHelper.cs create mode 100644 src/DAZ_Installer.IO/PathTransversalException.cs create mode 100644 src/DAZ_Installer.IO/Properties/AssemblyInfo1.cs create mode 100644 src/DAZ_Installer.IO/Wrappers/DirectoryInfoWrapper.cs create mode 100644 src/DAZ_Installer.IO/Wrappers/FileInfoWrapper.cs create mode 100644 src/DAZ_Installer.IOTests/DAZ_Installer.IOTests.csproj create mode 100644 src/DAZ_Installer.IOTests/DPDirectoryInfoTests.cs create mode 100644 src/DAZ_Installer.IOTests/DPFileInfoTests.cs create mode 100644 src/DAZ_Installer.IOTests/DPFileScopeSettingsTests.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeDPDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeDPDriveInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeDPFileInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeDirectoryInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeFileInfo.cs create mode 100644 src/DAZ_Installer.IOTests/Fakes/FakeFileSystem.cs create mode 100644 src/DAZ_Installer.IOTests/PathHelperTests.cs create mode 100644 src/DAZ_Installer.IOTests/PathTransversalExceptionTests.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/DAZ_Installer.TestingSuiteWindows.csproj create mode 100644 src/DAZ_Installer.TestingSuiteWindows/DPDestinationDeterminerEx.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/MainForm.Designer.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/MainForm.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/MainForm.resx create mode 100644 src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.Designer.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/ProcessSettingsDialogue.resx create mode 100644 src/DAZ_Installer.TestingSuiteWindows/Program.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/RecursiveDestinationDeterminer.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/ResultCompiler.cs create mode 100644 src/DAZ_Installer.TestingSuiteWindows/RichTextBoxSink.cs create mode 100644 src/DAZ_Installer.UI/DAZ_Installer.UI.csproj rename src/{Custom Controls => DAZ_Installer.UI}/LibraryItem.Designer.cs (100%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryItem.cs (76%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryItem.resx (100%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryPanel.Designer.cs (98%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryPanel.cs (56%) rename src/{Custom Controls => DAZ_Installer.UI}/LibraryPanel.resx (100%) rename src/{Custom Controls => DAZ_Installer.UI}/PageButtonControl.Designer.cs (100%) rename src/{Custom Controls => DAZ_Installer.UI}/PageButtonControl.cs (90%) rename src/{Custom Controls => DAZ_Installer.UI}/PageButtonControl.resx (100%) create mode 100644 src/DAZ_Installer.UI/ProgressCombo.Designer.cs create mode 100644 src/DAZ_Installer.UI/ProgressCombo.cs create mode 100644 src/DAZ_Installer.UI/ProgressCombo.resx rename src/{Custom Controls => DAZ_Installer.UI}/QueueControl.Designer.cs (100%) create mode 100644 src/DAZ_Installer.UI/QueueControl.cs rename src/{Custom Controls => DAZ_Installer.UI}/QueueControl.resx (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/ArrowDown.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/ArrowRight.jpg (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/ArrowRight.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/ArrowUp.png (100%) create mode 100644 src/DAZ_Installer.Windows/Assets/ArrowUp1.png rename {Assets => src/DAZ_Installer.Windows/Assets}/Icon1.ico (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/Logo2-256x.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/NoImageFound.jpg (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/RAR-Icon-New-Original-APK.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/WindowsFolderIcon.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/favicon.ico (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/loading.gif (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/logo.png (100%) rename {Assets => src/DAZ_Installer.Windows/Assets}/thumb_14366704070ZIP.png (100%) create mode 100644 src/DAZ_Installer.Windows/DAZ_Installer.Windows.csproj create mode 100644 src/DAZ_Installer.Windows/DP/DPExtractJob.cs rename src/{ => DAZ_Installer.Windows}/DP/DPGlobal.cs (82%) rename src/{ => DAZ_Installer.Windows}/DP/DPNetwork.cs (71%) create mode 100644 src/DAZ_Installer.Windows/DP/DPRegistry.cs create mode 100644 src/DAZ_Installer.Windows/DP/DPSettings.cs create mode 100644 src/DAZ_Installer.Windows/Forms/AboutForm.Designer.cs create mode 100644 src/DAZ_Installer.Windows/Forms/AboutForm.cs rename src/{ => DAZ_Installer.Windows}/Forms/AboutForm.resx (98%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderAliasManager.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderAliasManager.cs (76%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderAliasManager.resx (100%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderManager.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderManager.cs (86%) rename src/{ => DAZ_Installer.Windows}/Forms/ContentFolderManager.resx (100%) rename src/{ => DAZ_Installer.Windows}/Forms/DatabaseView.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/DatabaseView.cs (76%) rename src/{ => DAZ_Installer.Windows}/Forms/DatabaseView.resx (100%) create mode 100644 src/DAZ_Installer.Windows/Forms/MainForm.Designer.cs rename src/{ => DAZ_Installer.Windows}/Forms/MainForm.cs (70%) create mode 100644 src/DAZ_Installer.Windows/Forms/MainForm.resx rename src/{ => DAZ_Installer.Windows}/Forms/PasswordInput.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/PasswordInput.cs (69%) rename src/{ => DAZ_Installer.Windows}/Forms/PasswordInput.resx (100%) rename src/{ => DAZ_Installer.Windows}/Forms/ProductRecordForm.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/ProductRecordForm.cs (85%) rename src/{ => DAZ_Installer.Windows}/Forms/ProductRecordForm.resx (100%) rename src/{ => DAZ_Installer.Windows}/Forms/TagsManager.Designer.cs (99%) rename src/{ => DAZ_Installer.Windows}/Forms/TagsManager.cs (59%) rename src/{ => DAZ_Installer.Windows}/Forms/TagsManager.resx (100%) create mode 100644 src/DAZ_Installer.Windows/Pages/Extract.Designer.cs create mode 100644 src/DAZ_Installer.Windows/Pages/Extract.cs rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Extract.resx (71%) create mode 100644 src/DAZ_Installer.Windows/Pages/Home.Designer.cs rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Home.cs (63%) rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Home.resx (52%) rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Library.Designer.cs (99%) rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Library.cs (84%) rename src/{Forms/MainForm.resx => DAZ_Installer.Windows/Pages/Library.resx} (99%) rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Settings.Designer.cs (99%) create mode 100644 src/DAZ_Installer.Windows/Pages/Settings.cs rename src/{Custom Controls => DAZ_Installer.Windows/Pages}/Settings.resx (100%) create mode 100644 src/DAZ_Installer.Windows/Program.cs rename src/{ => DAZ_Installer.Windows}/Properties/Resources - Copy.ignore (100%) rename src/{ => DAZ_Installer.Windows}/Properties/Resources.Designer.cs (91%) rename src/{ => DAZ_Installer.Windows}/Properties/Resources.resx (90%) rename src/{ => DAZ_Installer.Windows}/Properties/Settings.Designer.cs (93%) rename src/{ => DAZ_Installer.Windows}/Properties/Settings.settings (100%) rename src/{ => DAZ_Installer.Windows}/Resources/Logo2-256x.png (100%) rename src/{ => DAZ_Installer.Windows}/Resources/favicon.ico (100%) rename src/{ => DAZ_Installer.Windows}/Resources/loading.gif (100%) create mode 100644 src/DAZ_Installer.Windows/favicon.ico delete mode 100644 src/DAZ_Installer.csproj delete mode 100644 src/DP/ContentType.cs delete mode 100644 src/DP/DP7zArchive.cs delete mode 100644 src/DP/DPAbstractArchive.cs delete mode 100644 src/DP/DPAbstractFile.cs delete mode 100644 src/DP/DPCache.cs delete mode 100644 src/DP/DPDSXElementCollection.cs delete mode 100644 src/DP/DPDSXFile.cs delete mode 100644 src/DP/DPDatabase.Public.cs delete mode 100644 src/DP/DPExtractJob.cs delete mode 100644 src/DP/DPFile.cs delete mode 100644 src/DP/DPFolder.cs delete mode 100644 src/DP/DPIDManager.cs delete mode 100644 src/DP/DPProcessor.cs delete mode 100644 src/DP/DPProductInfo.cs delete mode 100644 src/DP/DPProgressCombo.cs delete mode 100644 src/DP/DPRARArchive.cs delete mode 100644 src/DP/DPRegistry.cs delete mode 100644 src/DP/DPSettings.cs delete mode 100644 src/DP/DPTaskManager.cs delete mode 100644 src/DP/DPZipArchive.cs delete mode 100644 src/DP/Program.cs delete mode 100644 src/DP/UsefulFuncs.cs delete mode 100644 src/Forms/AboutForm.Designer.cs delete mode 100644 src/Forms/AboutForm.cs delete mode 100644 src/Forms/MainForm.Designer.cs delete mode 100644 src/Utilities/ListExtensions.cs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bf2d58c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +*.7z.003 filter=lfs diff=lfs merge=lfs -text +*.7z.004 filter=lfs diff=lfs merge=lfs -text +*.7z.005 filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.7z filter=lfs diff=lfs merge=lfs -text +*.7z.001 filter=lfs diff=lfs merge=lfs -text +*.7z.002 filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/dotnet-test.yml b/.github/workflows/dotnet-test.yml index e330ce3..0ad368d 100644 --- a/.github/workflows/dotnet-test.yml +++ b/.github/workflows/dotnet-test.yml @@ -5,9 +5,9 @@ name: .NET Test on: push: - branches: [ "pending-main" ] + branches: [ "pending-main", "main" ] pull_request: - branches: [ "pending-main" ] + branches: [ "pending-main", "main" ] jobs: build: @@ -16,6 +16,8 @@ jobs: steps: - uses: actions/checkout@v3 + with: + lfs: 'true' - name: Setup .NET uses: actions/setup-dotnet@v3 with: diff --git a/.gitignore b/.gitignore index 45956bc..5cd2c1e 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,7 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Testing for Daz Product Manager +src/DAZ_Installer.CoreTests/RealData/Archives/* +src/DAZ_Installer.CoreTests/RealData/Manifests/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bf6bb4f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "src/DAZ_Installer.sln" +} \ No newline at end of file diff --git a/README.md b/README.md index 79bcaaa..a5e54db 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# DAZ Product Installer +# Product Manager for DAZ Studio +[![.NET Test](https://github.com/siblount/DazProductInstaller/actions/workflows/dotnet-test.yml/badge.svg?branch=pending-main)](https://github.com/siblount/DazProductInstaller/actions/workflows/dotnet-test.yml) This is an application currently only for Windows users to install their products regardless of the vendor's format; it accepts common file structures and archive formats. ### Watch a preview here (click on image): -[![Daz Product Installer Alpha Preview Video](https://i.postimg.cc/VL7qpd4B/image.png)](https://www.youtube.com/watch?v=FwLc-dcl8W0) +[![Product Manager for DAZ Studio Alpha Preview Video](https://i.postimg.cc/VL7qpd4B/image.png)](https://www.youtube.com/watch?v=FwLc-dcl8W0) ## What does this project aim to do? Firstly, it aims to be a **free** and **open-source*** application to gain trust of users, allow others to contribute, and release a product that is useful for all. diff --git a/src/.editorconfig b/src/.editorconfig index fe99dc6..349192c 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -7,13 +7,13 @@ csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_braces = true:silent csharp_style_namespace_declarations = block_scoped:silent -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_methods = when_on_single_line:suggestion +csharp_style_expression_bodied_constructors = when_on_single_line:suggestion +csharp_style_expression_bodied_operators = when_on_single_line:suggestion +csharp_style_expression_bodied_properties = true:suggestion +csharp_style_expression_bodied_indexers = true:suggestion +csharp_style_expression_bodied_accessors = true:suggestion +csharp_style_expression_bodied_lambdas = true:suggestion csharp_style_expression_bodied_local_functions = false:silent csharp_style_throw_expression = true:suggestion csharp_prefer_simple_default_expression = true:suggestion @@ -32,6 +32,23 @@ csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent csharp_style_conditional_delegate_call = true:suggestion +csharp_space_around_binary_operators = before_and_after +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:silent +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion [*.{cs,vb}] #### Naming styles #### @@ -54,46 +71,54 @@ dotnet_naming_rule.private_members_should_be_private_camel.severity = warning dotnet_naming_rule.private_members_should_be_private_camel.symbols = private_members dotnet_naming_rule.private_members_should_be_private_camel.style = private_camel +dotnet_naming_rule.private_non_class_should_be_private_camel.severity = warning +dotnet_naming_rule.private_non_class_should_be_private_camel.symbols = private_non_class +dotnet_naming_rule.private_non_class_should_be_private_camel.style = private_camel + # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = +dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = dotnet_naming_symbols.private_members.applicable_kinds = property dotnet_naming_symbols.private_members.applicable_accessibilities = private, private_protected -dotnet_naming_symbols.private_members.required_modifers = +dotnet_naming_symbols.private_members.required_modifers = + +dotnet_naming_symbols.private_non_class.applicable_kinds = property, event, method, enum, delegate +dotnet_naming_symbols.private_non_class.applicable_accessibilities = private, private_protected +dotnet_naming_symbols.private_non_class.required_modifers = # Naming styles dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case -dotnet_naming_style.private_camel.required_prefix = _ -dotnet_naming_style.private_camel.required_suffix = -dotnet_naming_style.private_camel.word_separator = +dotnet_naming_style.private_camel.required_prefix = +dotnet_naming_style.private_camel.required_suffix = +dotnet_naming_style.private_camel.word_separator = dotnet_naming_style.private_camel.capitalization = camel_case dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 @@ -121,3 +146,11 @@ dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent dotnet_style_allow_multiple_blank_lines_experimental = true:silent dotnet_style_allow_statement_immediately_after_block_experimental = true:silent dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent diff --git a/src/Custom Controls/Extract.Designer.cs b/src/Custom Controls/Extract.Designer.cs deleted file mode 100644 index 5e556e9..0000000 --- a/src/Custom Controls/Extract.Designer.cs +++ /dev/null @@ -1,321 +0,0 @@ - -namespace DAZ_Installer -{ - partial class Extract - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Extract)); - this.mainTableLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); - this.tabControl1 = new System.Windows.Forms.TabControl(); - this.fileListPage = new System.Windows.Forms.TabPage(); - this.fileListView = new System.Windows.Forms.ListView(); - this.filePathColumn = new System.Windows.Forms.ColumnHeader(); - this.fileListContextStrip = new System.Windows.Forms.ContextMenuStrip(this.components); - this.inspectFileListMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.selectInHierachyToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.openInExplorerToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.noFilesSelectedToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.fileHierachyPage = new System.Windows.Forms.TabPage(); - this.fileHierachyTree = new System.Windows.Forms.TreeView(); - this.archiveFolderIcons = new System.Windows.Forms.ImageList(this.components); - this.queuePage = new System.Windows.Forms.TabPage(); - this.queueControl1 = new DAZ_Installer.Custom_Controls.QueueControl(); - this.panel1 = new System.Windows.Forms.Panel(); - this.mainProcLbl = new System.Windows.Forms.Label(); - this.fileHierachyContextStrip = new System.Windows.Forms.ContextMenuStrip(this.components); - this.inspectToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.selectInFileListToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.openInExplorerToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem(); - this.tabControl1.SuspendLayout(); - this.fileListPage.SuspendLayout(); - this.fileListContextStrip.SuspendLayout(); - this.fileHierachyPage.SuspendLayout(); - this.queuePage.SuspendLayout(); - this.panel1.SuspendLayout(); - this.fileHierachyContextStrip.SuspendLayout(); - this.SuspendLayout(); - // - // mainTableLayoutPanel - // - this.mainTableLayoutPanel.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.mainTableLayoutPanel.ColumnCount = 1; - this.mainTableLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.mainTableLayoutPanel.Location = new System.Drawing.Point(31, 65); - this.mainTableLayoutPanel.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.mainTableLayoutPanel.Name = "mainTableLayoutPanel"; - this.mainTableLayoutPanel.RowCount = 1; - this.mainTableLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.mainTableLayoutPanel.Size = new System.Drawing.Size(491, 153); - this.mainTableLayoutPanel.TabIndex = 0; - // - // tabControl1 - // - this.tabControl1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.tabControl1.Controls.Add(this.fileListPage); - this.tabControl1.Controls.Add(this.fileHierachyPage); - this.tabControl1.Controls.Add(this.queuePage); - this.tabControl1.Location = new System.Drawing.Point(31, 222); - this.tabControl1.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.tabControl1.Name = "tabControl1"; - this.tabControl1.SelectedIndex = 0; - this.tabControl1.Size = new System.Drawing.Size(491, 97); - this.tabControl1.TabIndex = 1; - // - // fileListPage - // - this.fileListPage.Controls.Add(this.fileListView); - this.fileListPage.Location = new System.Drawing.Point(4, 24); - this.fileListPage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileListPage.Name = "fileListPage"; - this.fileListPage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileListPage.Size = new System.Drawing.Size(483, 69); - this.fileListPage.TabIndex = 0; - this.fileListPage.Text = "File List"; - this.fileListPage.UseVisualStyleBackColor = true; - // - // fileListView - // - this.fileListView.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { - this.filePathColumn}); - this.fileListView.ContextMenuStrip = this.fileListContextStrip; - this.fileListView.Dock = System.Windows.Forms.DockStyle.Fill; - this.fileListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; - this.fileListView.Location = new System.Drawing.Point(4, 2); - this.fileListView.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileListView.MultiSelect = false; - this.fileListView.Name = "fileListView"; - this.fileListView.Size = new System.Drawing.Size(475, 65); - this.fileListView.TabIndex = 0; - this.fileListView.UseCompatibleStateImageBehavior = false; - this.fileListView.View = System.Windows.Forms.View.Details; - // - // filePathColumn - // - this.filePathColumn.Text = "File Path"; - this.filePathColumn.Width = 530; - // - // fileListContextStrip - // - this.fileListContextStrip.DropShadowEnabled = false; - this.fileListContextStrip.ImageScalingSize = new System.Drawing.Size(20, 20); - this.fileListContextStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.inspectFileListMenuItem, - this.selectInHierachyToolStripMenuItem, - this.openInExplorerToolStripMenuItem, - this.noFilesSelectedToolStripMenuItem}); - this.fileListContextStrip.Name = "contextMenuStrip1"; - this.fileListContextStrip.RenderMode = System.Windows.Forms.ToolStripRenderMode.Professional; - this.fileListContextStrip.ShowImageMargin = false; - this.fileListContextStrip.Size = new System.Drawing.Size(144, 92); - this.fileListContextStrip.Opening += new System.ComponentModel.CancelEventHandler(this.fileListContextStrip_Opening); - // - // inspectFileListMenuItem - // - this.inspectFileListMenuItem.Name = "inspectFileListMenuItem"; - this.inspectFileListMenuItem.Size = new System.Drawing.Size(143, 22); - this.inspectFileListMenuItem.Text = "Inspect"; - this.inspectFileListMenuItem.Visible = false; - // - // selectInHierachyToolStripMenuItem - // - this.selectInHierachyToolStripMenuItem.Name = "selectInHierachyToolStripMenuItem"; - this.selectInHierachyToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.selectInHierachyToolStripMenuItem.Text = "Select in Hierachy"; - this.selectInHierachyToolStripMenuItem.Visible = false; - this.selectInHierachyToolStripMenuItem.Click += new System.EventHandler(this.selectInHierachyToolStripMenuItem_Click); - // - // openInExplorerToolStripMenuItem - // - this.openInExplorerToolStripMenuItem.Name = "openInExplorerToolStripMenuItem"; - this.openInExplorerToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.openInExplorerToolStripMenuItem.Text = "Open in Explorer"; - this.openInExplorerToolStripMenuItem.Visible = false; - // - // noFilesSelectedToolStripMenuItem - // - this.noFilesSelectedToolStripMenuItem.Enabled = false; - this.noFilesSelectedToolStripMenuItem.Name = "noFilesSelectedToolStripMenuItem"; - this.noFilesSelectedToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.noFilesSelectedToolStripMenuItem.Text = "No Files Selected"; - // - // fileHierachyPage - // - this.fileHierachyPage.Controls.Add(this.fileHierachyTree); - this.fileHierachyPage.Location = new System.Drawing.Point(4, 24); - this.fileHierachyPage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileHierachyPage.Name = "fileHierachyPage"; - this.fileHierachyPage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileHierachyPage.Size = new System.Drawing.Size(483, 69); - this.fileHierachyPage.TabIndex = 1; - this.fileHierachyPage.Text = "File Hierachy"; - this.fileHierachyPage.UseVisualStyleBackColor = true; - // - // fileHierachyTree - // - this.fileHierachyTree.Dock = System.Windows.Forms.DockStyle.Fill; - this.fileHierachyTree.Indent = 21; - this.fileHierachyTree.Location = new System.Drawing.Point(4, 2); - this.fileHierachyTree.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.fileHierachyTree.Name = "fileHierachyTree"; - this.fileHierachyTree.Size = new System.Drawing.Size(475, 65); - this.fileHierachyTree.StateImageList = this.archiveFolderIcons; - this.fileHierachyTree.TabIndex = 0; - // - // archiveFolderIcons - // - this.archiveFolderIcons.ColorDepth = System.Windows.Forms.ColorDepth.Depth8Bit; - this.archiveFolderIcons.ImageStream = ((System.Windows.Forms.ImageListStreamer)(resources.GetObject("archiveFolderIcons.ImageStream"))); - this.archiveFolderIcons.TransparentColor = System.Drawing.SystemColors.Window; - this.archiveFolderIcons.Images.SetKeyName(0, "FolderIcon.png"); - this.archiveFolderIcons.Images.SetKeyName(1, "RARIcon.png"); - this.archiveFolderIcons.Images.SetKeyName(2, "ZIPIcon.png"); - // - // queuePage - // - this.queuePage.Controls.Add(this.queueControl1); - this.queuePage.Location = new System.Drawing.Point(4, 24); - this.queuePage.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.queuePage.Name = "queuePage"; - this.queuePage.Padding = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.queuePage.Size = new System.Drawing.Size(483, 69); - this.queuePage.TabIndex = 2; - this.queuePage.Text = "Queue"; - this.queuePage.UseVisualStyleBackColor = true; - // - // queueControl1 - // - this.queueControl1.Dock = System.Windows.Forms.DockStyle.Fill; - this.queueControl1.Location = new System.Drawing.Point(4, 2); - this.queueControl1.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.queueControl1.Name = "queueControl1"; - this.queueControl1.Size = new System.Drawing.Size(475, 65); - this.queueControl1.TabIndex = 0; - // - // panel1 - // - this.panel1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.panel1.Controls.Add(this.mainProcLbl); - this.panel1.Location = new System.Drawing.Point(31, 22); - this.panel1.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.panel1.Name = "panel1"; - this.panel1.Size = new System.Drawing.Size(486, 40); - this.panel1.TabIndex = 2; - // - // mainProcLbl - // - this.mainProcLbl.AutoEllipsis = true; - this.mainProcLbl.Dock = System.Windows.Forms.DockStyle.Fill; - this.mainProcLbl.Font = new System.Drawing.Font("Segoe UI Variable Display Semil", 17.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.mainProcLbl.Location = new System.Drawing.Point(0, 0); - this.mainProcLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0); - this.mainProcLbl.Name = "mainProcLbl"; - this.mainProcLbl.Size = new System.Drawing.Size(486, 40); - this.mainProcLbl.TabIndex = 0; - this.mainProcLbl.Text = "Nothing to extract."; - this.mainProcLbl.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - this.mainProcLbl.Click += new System.EventHandler(this.mainProcLbl_Click); - // - // fileHierachyContextStrip - // - this.fileHierachyContextStrip.ImageScalingSize = new System.Drawing.Size(20, 20); - this.fileHierachyContextStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.inspectToolStripMenuItem, - this.selectInFileListToolStripMenuItem, - this.openInExplorerToolStripMenuItem1}); - this.fileHierachyContextStrip.Name = "fileHierachyContextStrip"; - this.fileHierachyContextStrip.Size = new System.Drawing.Size(163, 70); - // - // inspectToolStripMenuItem - // - this.inspectToolStripMenuItem.Name = "inspectToolStripMenuItem"; - this.inspectToolStripMenuItem.Size = new System.Drawing.Size(162, 22); - this.inspectToolStripMenuItem.Text = "Inspect"; - // - // selectInFileListToolStripMenuItem - // - this.selectInFileListToolStripMenuItem.Name = "selectInFileListToolStripMenuItem"; - this.selectInFileListToolStripMenuItem.Size = new System.Drawing.Size(162, 22); - this.selectInFileListToolStripMenuItem.Text = "Select in File List"; - // - // openInExplorerToolStripMenuItem1 - // - this.openInExplorerToolStripMenuItem1.Name = "openInExplorerToolStripMenuItem1"; - this.openInExplorerToolStripMenuItem1.Size = new System.Drawing.Size(162, 22); - this.openInExplorerToolStripMenuItem1.Text = "Open in Explorer"; - // - // Extract - // - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit; - this.BackColor = System.Drawing.Color.White; - this.Controls.Add(this.tabControl1); - this.Controls.Add(this.mainTableLayoutPanel); - this.Controls.Add(this.panel1); - this.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2); - this.Name = "Extract"; - this.Size = new System.Drawing.Size(542, 344); - this.tabControl1.ResumeLayout(false); - this.fileListPage.ResumeLayout(false); - this.fileListContextStrip.ResumeLayout(false); - this.fileHierachyPage.ResumeLayout(false); - this.queuePage.ResumeLayout(false); - this.panel1.ResumeLayout(false); - this.fileHierachyContextStrip.ResumeLayout(false); - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.TableLayoutPanel mainTableLayoutPanel; - private System.Windows.Forms.TabControl tabControl1; - private System.Windows.Forms.TabPage fileListPage; - private System.Windows.Forms.TabPage fileHierachyPage; - private System.Windows.Forms.Panel panel1; - internal System.Windows.Forms.Label mainProcLbl; - private System.Windows.Forms.ListView fileListView; - private System.Windows.Forms.TreeView fileHierachyTree; - private System.Windows.Forms.ColumnHeader filePathColumn; - private System.Windows.Forms.ContextMenuStrip fileListContextStrip; - private System.Windows.Forms.ToolStripMenuItem inspectFileListMenuItem; - private System.Windows.Forms.ToolStripMenuItem selectInHierachyToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem openInExplorerToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem noFilesSelectedToolStripMenuItem; - private System.Windows.Forms.ContextMenuStrip fileHierachyContextStrip; - private System.Windows.Forms.ToolStripMenuItem inspectToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem selectInFileListToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem openInExplorerToolStripMenuItem1; - private System.Windows.Forms.TabPage queuePage; - private Custom_Controls.QueueControl queueControl1; - internal System.Windows.Forms.ImageList archiveFolderIcons; - } -} diff --git a/src/Custom Controls/Extract.cs b/src/Custom Controls/Extract.cs deleted file mode 100644 index 1ef1a63..0000000 --- a/src/Custom Controls/Extract.cs +++ /dev/null @@ -1,273 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Diagnostics; -using System.Collections.Generic; -using System.IO; -using System.ComponentModel; -using System.Windows.Forms; -using DAZ_Installer.DP; - -namespace DAZ_Installer -{ - - public partial class Extract : UserControl - { - /// - /// Returns the integer of the first available slot in List. Returns -1 if not available. - /// - public static Extract ExtractPage; - internal static Dictionary associatedListItems = new(4096); - internal static Dictionary associatedTreeNodes = new(4096); - - public void resetMainTable() - { - mainTableLayoutPanel.SuspendLayout(); - try - { - if (mainTableLayoutPanel.Controls.Count != 0) - { - var arr = DPCommon.RecursivelyGetControls(mainTableLayoutPanel); - foreach (var control in arr) - { - control.Dispose(); - } - } - } - catch { } - mainTableLayoutPanel.Controls.Clear(); - mainTableLayoutPanel.RowStyles.Clear(); - mainTableLayoutPanel.ColumnCount = 1; - mainTableLayoutPanel.RowStyles.Add(new RowStyle()); - mainTableLayoutPanel.RowCount = 1; - updateMainTableRowSizing(); - mainTableLayoutPanel.ResumeLayout(); - } - public void updateMainTableRowSizing() - { - // TO DO : Invoke. - mainTableLayoutPanel.SuspendLayout(); - float percentageMultiplied = 1f / mainTableLayoutPanel.Controls.Count * 100f; - for (var i = 0; i < mainTableLayoutPanel.RowStyles.Count; i++) - { - mainTableLayoutPanel.RowStyles[i] = new RowStyle(SizeType.Percent, percentageMultiplied); - } - - mainTableLayoutPanel.ResumeLayout(); - mainTableLayoutPanel.Update(); - } - - public DialogResult DoPromptMessage(string message, string title, MessageBoxButtons buttons = MessageBoxButtons.YesNo) - { - - return MessageBox.Show(message, title, buttons, MessageBoxIcon.Hand); - } - - public Extract() - { - InitializeComponent(); - ExtractPage = this; - tabControl1.TabPages.Remove(queuePage); - queuePage.Dispose(); - } - - internal void AddToList(DPAbstractArchive archive) - { - fileListView.BeginUpdate(); - foreach (var content in archive.Contents) - { - var item = fileListView.Items.Add($"{archive.FileName}\\{content.Path}"); - content.AssociatedListItem = item; - associatedListItems.Add(item, content); - } - fileListView.Columns[0].AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent); - fileListView.EndUpdate(); - } - - private void ProcessChildNodes(DPFolder folder, ref TreeNode parentNode) - { - var fileName = Path.GetFileName(folder.Path); - TreeNode folder1 = null; - // We don't need associations for folders. - if (InvokeRequired) - { - folder1 = (TreeNode) Invoke(new Func(parentNode.Nodes.Add), fileName); - AddIcon(folder1, null); - } else - { - folder1 = parentNode.Nodes.Add(fileName); - AddIcon(folder1, null); - } - // Add the DPFiles. - foreach (var file in folder.GetFiles()) - { - fileName = Path.GetFileName(file.Path); - // TO DO: Add condition if file is a DPArchive & extract == true - if (InvokeRequired) - { - var node = (TreeNode) Invoke(new Func(folder1.Nodes.Add), fileName); - file.AssociatedTreeNode = node; - AddIcon(node, file.Ext); - } - else - { - var node = folder1.Nodes.Add(fileName); - file.AssociatedTreeNode = node; - AddIcon(node, file.Ext); - } - } - foreach (var subfolder in folder.subfolders) - { - ProcessChildNodes(subfolder, ref folder1); - } - } - - internal void AddToHierachy(DPAbstractArchive workingArchive) - { - fileHierachyTree.BeginUpdate(); - // Add root node for DPArchive. - var fileName = workingArchive.HierachyName; - TreeNode rootNode = null; - if (InvokeRequired) - { - var func = new Func(fileHierachyTree.Nodes.Add); - rootNode = (TreeNode) Invoke(func,fileName); - workingArchive.AssociatedTreeNode = rootNode; - AddIcon(rootNode, workingArchive.Ext); - - } else - { - rootNode = fileHierachyTree.Nodes.Add(fileName); - workingArchive.AssociatedTreeNode = rootNode; - AddIcon(rootNode, workingArchive.Ext); - } - - - // Add any files that aren't in any folder. - foreach (var file in workingArchive.RootContents) - { - fileName = Path.GetFileName(file.Path); - if (InvokeRequired) - { - var node = (TreeNode) Invoke(new Func(rootNode.Nodes.Add), fileName); - file.AssociatedTreeNode = node; - AddIcon(node, file.Ext); - } else - { - var node = rootNode.Nodes.Add(fileName); - file.AssociatedTreeNode = node; - AddIcon(node, file.Ext); - } - } - - // Recursively add files & folder within each folder. - foreach (var folder in workingArchive.RootFolders) - { - ProcessChildNodes(folder, ref rootNode); - } - fileHierachyTree.ExpandAll(); - fileHierachyTree.EndUpdate(); - } - - // Object to satisfy Invoke. - private void AddIcon(TreeNode node, string ext) - { - if (InvokeRequired) - { - Invoke(AddIcon, node, ext); - } - if (string.IsNullOrEmpty(ext)) - node.StateImageIndex = 0; - else if (ext.Contains("zip") || ext.Contains("7z")) - node.StateImageIndex = 2; - else if (ext.Contains("rar")) - node.StateImageIndex = 1; - } - - public void ResetExtractPage() - { - // Later show nothing to extract panel. - resetMainTable(); - fileListView.Items.Clear(); - fileHierachyTree.Nodes.Clear(); - associatedListItems.Clear(); - associatedTreeNodes.Clear(); - } - /// - /// Creates a progress bar and adds it to the table. [0] - TableLayout, [1] - label, [2] - ProgressBar - /// - /// An array of controls - /// - internal void AddNewProgressCombo(DPProgressCombo combo) { - mainTableLayoutPanel.SuspendLayout(); - if (mainTableLayoutPanel.Controls.Count != 0) - { - mainTableLayoutPanel.RowCount += 1; - mainTableLayoutPanel.RowStyles.Add(new RowStyle(SizeType.AutoSize)); - } - mainTableLayoutPanel.Controls.Add(combo.Panel); - updateMainTableRowSizing(); - mainTableLayoutPanel.ResumeLayout(true); - } - - private void mainProcLbl_Click(object sender, EventArgs e) - { - - } - - #region Handle DPPrecssor Events - internal void DeleteProgressionCombo(DPProgressCombo combo) - { - if (InvokeRequired) - { - Invoke(DeleteProgressionCombo, combo); - return; - } - - mainTableLayoutPanel.SuspendLayout(); - mainTableLayoutPanel.Controls.Remove(combo.Panel); - mainTableLayoutPanel.RowCount = Math.Max(1, mainTableLayoutPanel.Controls.Count); - mainTableLayoutPanel.RowStyles.Clear(); - for (var i = 0; i < mainTableLayoutPanel.RowCount; i++) - mainTableLayoutPanel.RowStyles.Add(new RowStyle()); - mainTableLayoutPanel.ResumeLayout(); - updateMainTableRowSizing(); - } - - #endregion - - #region Context Strip Events - private void selectInHierachyToolStripMenuItem_Click(object sender, EventArgs e) - { - // Get the associated file with listviewitem. - associatedListItems.TryGetValue(fileListView.SelectedItems[0], out DPAbstractFile file); - if (file != null) - { - fileHierachyTree.SelectedNode = file.AssociatedTreeNode; - } - // Switch tab. - tabControl1.SelectTab(fileHierachyPage); - - } - - private void fileListContextStrip_Opening(object sender, CancelEventArgs e) - { - var filesSelected = fileListView.SelectedItems.Count != 0; - inspectFileListMenuItem.Visible = filesSelected; - openInExplorerToolStripMenuItem.Visible = filesSelected; - selectInHierachyToolStripMenuItem.Visible = filesSelected; - noFilesSelectedToolStripMenuItem.Visible = !filesSelected; - } - - public void OpenFileInExplorer(string path) - { - Process.Start(@"explorer.exe", $"/select, \"{path}\""); - } - #endregion - - } - -} - - diff --git a/src/Custom Controls/Home.Designer.cs b/src/Custom Controls/Home.Designer.cs deleted file mode 100644 index 0e58c86..0000000 --- a/src/Custom Controls/Home.Designer.cs +++ /dev/null @@ -1,229 +0,0 @@ - -namespace DAZ_Installer -{ - partial class Home - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Component Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - this.components = new System.ComponentModel.Container(); - this.titleLbl = new System.Windows.Forms.Label(); - this.extractBtn = new System.Windows.Forms.Button(); - this.addMoreFilesBtn = new System.Windows.Forms.Button(); - this.clearListBtn = new System.Windows.Forms.Button(); - this.listView1 = new System.Windows.Forms.ListView(); - this.columnHeader1 = new System.Windows.Forms.ColumnHeader(); - this.homeListContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); - this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.addMoreItemsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.openFileDialog1 = new System.Windows.Forms.OpenFileDialog(); - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - this.dropBtn = new System.Windows.Forms.Button(); - this.homeListContextMenuStrip.SuspendLayout(); - this.tableLayoutPanel1.SuspendLayout(); - this.SuspendLayout(); - // - // titleLbl - // - this.titleLbl.BackColor = System.Drawing.Color.White; - this.titleLbl.Dock = System.Windows.Forms.DockStyle.Top; - this.titleLbl.Font = new System.Drawing.Font("Segoe UI Variable Text Light", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.titleLbl.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(31)))), ((int)(((byte)(31)))), ((int)(((byte)(31))))); - this.titleLbl.Location = new System.Drawing.Point(0, 0); - this.titleLbl.Name = "titleLbl"; - this.titleLbl.Size = new System.Drawing.Size(542, 55); - this.titleLbl.TabIndex = 0; - this.titleLbl.Text = "Daz Product Installer"; - this.titleLbl.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; - // - // extractBtn - // - this.extractBtn.Dock = System.Windows.Forms.DockStyle.Fill; - this.extractBtn.Location = new System.Drawing.Point(130, 208); - this.extractBtn.Name = "extractBtn"; - this.extractBtn.Size = new System.Drawing.Size(248, 38); - this.extractBtn.TabIndex = 2; - this.extractBtn.Text = "Extract File(s)"; - this.extractBtn.UseVisualStyleBackColor = true; - this.extractBtn.Click += new System.EventHandler(this.button1_Click); - // - // addMoreFilesBtn - // - this.addMoreFilesBtn.Dock = System.Windows.Forms.DockStyle.Fill; - this.addMoreFilesBtn.Location = new System.Drawing.Point(3, 208); - this.addMoreFilesBtn.Name = "addMoreFilesBtn"; - this.addMoreFilesBtn.Size = new System.Drawing.Size(121, 38); - this.addMoreFilesBtn.TabIndex = 4; - this.addMoreFilesBtn.Text = "Add more files"; - this.addMoreFilesBtn.UseVisualStyleBackColor = true; - this.addMoreFilesBtn.Click += new System.EventHandler(this.addMoreFilesBtn_Click); - // - // clearListBtn - // - this.clearListBtn.Dock = System.Windows.Forms.DockStyle.Fill; - this.clearListBtn.Location = new System.Drawing.Point(384, 208); - this.clearListBtn.Name = "clearListBtn"; - this.clearListBtn.Size = new System.Drawing.Size(122, 38); - this.clearListBtn.TabIndex = 3; - this.clearListBtn.Text = "Clear List"; - this.clearListBtn.UseVisualStyleBackColor = true; - this.clearListBtn.Click += new System.EventHandler(this.clearListBtn_Click); - // - // listView1 - // - this.listView1.AllowDrop = true; - this.listView1.Columns.AddRange(new System.Windows.Forms.ColumnHeader[] { - this.columnHeader1}); - this.tableLayoutPanel1.SetColumnSpan(this.listView1, 3); - this.listView1.ContextMenuStrip = this.homeListContextMenuStrip; - this.listView1.Dock = System.Windows.Forms.DockStyle.Fill; - this.listView1.ForeColor = System.Drawing.SystemColors.WindowText; - this.listView1.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None; - this.listView1.Location = new System.Drawing.Point(3, 3); - this.listView1.Name = "listView1"; - this.listView1.Size = new System.Drawing.Size(503, 199); - this.listView1.TabIndex = 4; - this.listView1.UseCompatibleStateImageBehavior = false; - this.listView1.View = System.Windows.Forms.View.Details; - this.listView1.DragDrop += new System.Windows.Forms.DragEventHandler(this.dropBtn_DragDrop); - this.listView1.DragEnter += new System.Windows.Forms.DragEventHandler(this.listView1_DragEnter); - // - // columnHeader1 - // - this.columnHeader1.Text = "a"; - this.columnHeader1.Width = 550; - // - // homeListContextMenuStrip - // - this.homeListContextMenuStrip.DropShadowEnabled = false; - this.homeListContextMenuStrip.ImageScalingSize = new System.Drawing.Size(20, 20); - this.homeListContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.removeToolStripMenuItem, - this.addMoreItemsToolStripMenuItem}); - this.homeListContextMenuStrip.Name = "homeListContextMenuStrip"; - this.homeListContextMenuStrip.ShowImageMargin = false; - this.homeListContextMenuStrip.Size = new System.Drawing.Size(144, 48); - this.homeListContextMenuStrip.Opening += new System.ComponentModel.CancelEventHandler(this.homeListContextMenuStrip_Opening); - // - // removeToolStripMenuItem - // - this.removeToolStripMenuItem.Name = "removeToolStripMenuItem"; - this.removeToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.removeToolStripMenuItem.Text = "Remove"; - this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click); - // - // addMoreItemsToolStripMenuItem - // - this.addMoreItemsToolStripMenuItem.Name = "addMoreItemsToolStripMenuItem"; - this.addMoreItemsToolStripMenuItem.Size = new System.Drawing.Size(143, 22); - this.addMoreItemsToolStripMenuItem.Text = "Add more items..."; - this.addMoreItemsToolStripMenuItem.Click += new System.EventHandler(this.addMoreItemsToolStripMenuItem_Click); - // - // openFileDialog1 - // - this.openFileDialog1.AddExtension = false; - this.openFileDialog1.DefaultExt = "zip"; - this.openFileDialog1.Filter = "RAR files (*.rar)|*.rar|ZIP files (*.zip)|*.zip|7z files (*.7z)|*.7z|7z part file" + - " base(*.001)|*.001|All files (*.*)|*.*"; - this.openFileDialog1.Multiselect = true; - this.openFileDialog1.SupportMultiDottedExtensions = true; - // - // tableLayoutPanel1 - // - this.tableLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.tableLayoutPanel1.ColumnCount = 3; - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F)); - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tableLayoutPanel1.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 25F)); - this.tableLayoutPanel1.Controls.Add(this.extractBtn, 1, 1); - this.tableLayoutPanel1.Controls.Add(this.clearListBtn, 2, 1); - this.tableLayoutPanel1.Controls.Add(this.addMoreFilesBtn, 0, 1); - this.tableLayoutPanel1.Controls.Add(this.listView1, 0, 0); - this.tableLayoutPanel1.Location = new System.Drawing.Point(18, 55); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; - this.tableLayoutPanel1.RowCount = 2; - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanel1.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 44F)); - this.tableLayoutPanel1.Size = new System.Drawing.Size(509, 249); - this.tableLayoutPanel1.TabIndex = 5; - // - // dropBtn - // - this.dropBtn.AllowDrop = true; - this.dropBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) - | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.dropBtn.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(192)))), ((int)(((byte)(255)))), ((int)(((byte)(192))))); - this.dropBtn.Cursor = System.Windows.Forms.Cursors.Hand; - this.dropBtn.FlatAppearance.BorderSize = 0; - this.dropBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.dropBtn.Font = new System.Drawing.Font("Segoe UI Variable Display Semil", 18F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.dropBtn.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(32)))), ((int)(((byte)(32))))); - this.dropBtn.Location = new System.Drawing.Point(18, 55); - this.dropBtn.Name = "dropBtn"; - this.dropBtn.Size = new System.Drawing.Size(509, 249); - this.dropBtn.TabIndex = 6; - this.dropBtn.Text = "Click here to select file(s) or drag them here."; - this.dropBtn.UseVisualStyleBackColor = false; - this.dropBtn.Click += new System.EventHandler(this.dropBtn_Click); - this.dropBtn.DragDrop += new System.Windows.Forms.DragEventHandler(this.dropBtn_DragDrop); - this.dropBtn.DragEnter += new System.Windows.Forms.DragEventHandler(this.dropBtn_DragEnter); - this.dropBtn.DragLeave += new System.EventHandler(this.dropBtn_DragLeave); - // - // Home - // - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Inherit; - this.AutoSize = true; - this.BackColor = System.Drawing.Color.White; - this.Controls.Add(this.dropBtn); - this.Controls.Add(this.titleLbl); - this.Controls.Add(this.tableLayoutPanel1); - this.Name = "Home"; - this.Size = new System.Drawing.Size(542, 344); - this.homeListContextMenuStrip.ResumeLayout(false); - this.tableLayoutPanel1.ResumeLayout(false); - this.ResumeLayout(false); - - } - - #endregion - - private System.Windows.Forms.Label titleLbl; - private System.Windows.Forms.Button extractBtn; - private System.Windows.Forms.OpenFileDialog openFileDialog1; - internal System.Windows.Forms.ListView listView1; - private System.Windows.Forms.ContextMenuStrip homeListContextMenuStrip; - private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem addMoreItemsToolStripMenuItem; - private System.Windows.Forms.Button addMoreFilesBtn; - private System.Windows.Forms.Button clearListBtn; - private System.Windows.Forms.ColumnHeader columnHeader1; - private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1; - private System.Windows.Forms.Button dropBtn; - } -} diff --git a/src/Custom Controls/Library.resx b/src/Custom Controls/Library.resx deleted file mode 100644 index 1632ce3..0000000 --- a/src/Custom Controls/Library.resx +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 214, 17 - - \ No newline at end of file diff --git a/src/Custom Controls/QueueControl.cs b/src/Custom Controls/QueueControl.cs deleted file mode 100644 index d9101cf..0000000 --- a/src/Custom Controls/QueueControl.cs +++ /dev/null @@ -1,23 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace DAZ_Installer.Custom_Controls -{ - public partial class QueueControl : UserControl - { - public QueueControl() - { - InitializeComponent(); - } - } -} diff --git a/src/Custom Controls/Settings.cs b/src/Custom Controls/Settings.cs deleted file mode 100644 index d19bdba..0000000 --- a/src/Custom Controls/Settings.cs +++ /dev/null @@ -1,425 +0,0 @@ -// This code is licensed under the Keep It Free License V1. -// You may find a full copy of this license at root project directory\LICENSE - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; -using DAZ_Installer.DP; -using DAZ_Installer.Forms; -using Microsoft.VisualBasic.FileIO; - -namespace DAZ_Installer -{ - public partial class Settings : UserControl - { - internal static bool setupComplete { get; set; } = false; - internal static readonly string[] names = new string[] { "Manifest Only", "Manifest and File Sense", "File Sense Only" }; - internal static bool validating { get; set; } = false; - internal static Settings settingsPage { get; set; } = null; - public Settings() - { - InitializeComponent(); - settingsPage = this; - } - - private void Settings_Load(object sender, EventArgs e) - { - Task.Run(LoadSettings); - loadingPanel.Visible = true; - loadingPanel.BringToFront(); - } - - // ._. - private void LoadSettings() - { - DPCommon.WriteToLog("Loading settings..."); - // Get our settings. - DPSettings.currentSettingsObject.Initalize(); - validating = true; - - - SetupDownloadThumbnailsSetting(); - SetupDestinationPathSetting(); - SetupFileHandling(); - SetupTempPath(); - SetupContentFolders(); - SetupContentRedirects(); - SetupDeleteSourceFiles(); - SetupPreviouslyInstalledProducts(); - SetupAllowOverwriting(); - SetupRemoveAction(); - - loadingPanel.Visible = false; - loadingPanel.Dispose(); - validating = false; - - applySettingsBtn.Enabled = DPSettings.invalidSettings; - } - - private void SetupContentRedirects() - { - foreach (var keypair in DPSettings.currentSettingsObject.folderRedirects) - { - contentFolderRedirectsListBox.Items.Add($"{keypair.Key} --> {keypair.Value}"); - } - } - - private void SetupContentFolders() - { - foreach (var folder in DPSettings.currentSettingsObject.commonContentFolderNames) - { - contentFoldersListBox.Items.Add(folder); - } - } - - private void SetupTempPath() - { - tempTxtBox.Text = DPSettings.currentSettingsObject.tempPath; - } - - private void SetupDestinationPathSetting() - { - // If no detected daz content paths, all handled in the initalization phase of DPSettings.currentSettingsObject. - // First, we will add our selected path. - destinationPathCombo.Items.Add(DPSettings.currentSettingsObject.destinationPath); - destinationPathCombo.SelectedIndex = 0; - foreach (var path in DPSettings.currentSettingsObject.detectedDazContentPaths) - { - destinationPathCombo.Items.Add(path); - } - } - - private void SetupFileHandling() - { - - fileHandlingCombo.Items.AddRange(names); - - // Now show the one we selected. - var fileMethod = DPSettings.currentSettingsObject.handleInstallation; - switch (fileMethod) - { - case InstallOptions.ManifestOnly: - fileHandlingCombo.SelectedIndex = 0; - break; - case InstallOptions.ManifestAndAuto: - fileHandlingCombo.SelectedIndex = 1; - break; - case InstallOptions.Automatic: - fileHandlingCombo.SelectedIndex = 2; - break; - } - } - - private void SetupDownloadThumbnailsSetting() - { - - foreach (var option in Enum.GetNames(typeof(SettingOptions))) - { - downloadThumbnailsComboBox.Items.Add(option); - } - - var choice = DPSettings.currentSettingsObject.downloadImages; - downloadThumbnailsComboBox.SelectedItem = Enum.GetName(choice); - } - - private void SetupDeleteSourceFiles() - { - foreach (var option in Enum.GetNames(typeof(SettingOptions))) - { - removeSourceFilesCombo.Items.Add(option); - } - - var choice = DPSettings.currentSettingsObject.permDeleteSource; - removeSourceFilesCombo.SelectedItem = Enum.GetName(choice); - } - - private void SetupPreviouslyInstalledProducts() - { - foreach (var option in Enum.GetNames(typeof(SettingOptions))) - { - installPrevProductsCombo.Items.Add(option); - } - - var choice = DPSettings.currentSettingsObject.installPrevProducts; - installPrevProductsCombo.SelectedItem = Enum.GetName(choice); - } - - private void SetupAllowOverwriting() - { - foreach (var option in Enum.GetNames(typeof(SettingOptions))) - { - allowOverwritingCombo.Items.Add(option); - } - allowOverwritingCombo.SelectedItem = Enum.GetName(DPSettings.currentSettingsObject.OverwriteFiles); - } - - private void SetupRemoveAction() - { - removeActionCombo.Items.AddRange(new string[]{ "Delete permanently", "Move to Recycle Bin"}); - switch (DPSettings.currentSettingsObject.DeleteAction) - { - case RecycleOption.DeletePermanently: - removeActionCombo.SelectedItem = "Delete permanently"; - return; - default: - removeActionCombo.SelectedItem = "Move to Recycle Bin"; - return; - } - } - - private void tableLayoutPanel1_Paint(object sender, PaintEventArgs e) - { - - } - - private void downloadThumbnailsComboBox_SelectedIndexChanged(object sender, EventArgs e) - { - - } - - private void applySettingsBtn_Click(object sender, EventArgs e) - { - // Update settings. - var updateResult = UpdateSettings(); - - if (!updateResult) return; - - // Try saving settings. - var saveResult = DPSettings.currentSettingsObject.SaveSettings(); - - // If something failed... - if (!saveResult) - MessageBox.Show("An error occurred while saving settings. You're settings have NOT been saved. Please try saving again.", - "Error saving settings", MessageBoxButtons.OK, MessageBoxIcon.Error); - - applySettingsBtn.Enabled = false; - } - - private bool UpdateSettings() - { - // We don't update content folders. - if (validating) return false; - var invalidReponses = false; - DPSettings.currentSettingsObject.downloadImages = Enum.Parse((string) downloadThumbnailsComboBox.SelectedItem); - validating = true; - // Destination Path - // A loop occurred when the path was G:/ but G:/ was not mounted. - DESTCHECK: - if (Directory.Exists(destinationPathCombo.Text.Trim())) DPSettings.currentSettingsObject.destinationPath = destinationPathCombo.Text.Trim(); - else { - try - { - Directory.CreateDirectory(tempTxtBox.Text.Trim()); - goto DESTCHECK; - } catch { }; - destinationPathCombo.Text = DPSettings.currentSettingsObject.destinationPath; - invalidReponses = true; - } - - // Temp Path - // TODO: We don't want to delete the temp folder itself. - // For example: We don't want to delete D:/temp, we want to delete D:/temp/. - // The difference is that currently D:/temp will be deleted whereas, - // D:/temp/ will not delete the temp folder but all subfolders and files in it. - TEMPCHECK: - if (Directory.Exists(tempTxtBox.Text.Trim())) DPSettings.currentSettingsObject.tempPath = tempTxtBox.Text.Trim(); - else - { - try - { - Directory.CreateDirectory(tempTxtBox.Text.Trim()); - goto TEMPCHECK; - } catch {} - tempTxtBox.Text = DPSettings.currentSettingsObject.tempPath; - invalidReponses = true; - } - - // File Handling Method - DPSettings.currentSettingsObject.handleInstallation = (InstallOptions)fileHandlingCombo.SelectedIndex; - - //Content Folders - var contentFolders = new HashSet(contentFoldersListBox.Items.Count); - for (var i = 0; i < contentFoldersListBox.Items.Count; i++) - { - contentFolders.Add((string)contentFoldersListBox.Items[i]); - } - DPSettings.currentSettingsObject.commonContentFolderNames = contentFolders; - - // Alias Content Folders - var aliasMap = new Dictionary(contentFolderRedirectsListBox.Items.Count); - foreach (string item in contentFolderRedirectsListBox.Items) - { - var tokens = item.Split(" --> "); - aliasMap[tokens[0]] = tokens[1]; - } - DPSettings.currentSettingsObject.folderRedirects = aliasMap; - - // Permanate Delete Source - DPSettings.currentSettingsObject.permDeleteSource = (SettingOptions)removeSourceFilesCombo.SelectedIndex; - - // Install Prev Products - DPSettings.currentSettingsObject.installPrevProducts = (SettingOptions)installPrevProductsCombo.SelectedIndex; - - if (invalidReponses) - { - MessageBox.Show("Some inputs were invalid and were reset to their previous state. See log for more info.", "Invalid inputs", MessageBoxButtons.OK, MessageBoxIcon.Information); - validating = false; - return false; - } - validating = false; - return true; - } - - private void tempTxtBox_Leave(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && tempTxtBox.Text != DPSettings.currentSettingsObject.tempPath) - { - applySettingsBtn.Enabled = true; - } - } - - private void tempTxtBox_KeyUp(object sender, KeyEventArgs e) - { - if (!applySettingsBtn.Enabled && tempTxtBox.Text != DPSettings.currentSettingsObject.tempPath) - { - applySettingsBtn.Enabled = true; - } - } - - private void destinationPathCombo_Leave(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && destinationPathCombo.Text != DPSettings.currentSettingsObject.destinationPath) - { - applySettingsBtn.Enabled = true; - } - } - - private void destinationPathCombo_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && destinationPathCombo.Text != DPSettings.currentSettingsObject.destinationPath) - { - applySettingsBtn.Enabled = true; - } - } - - private void downloadThumbnailsComboBox_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && downloadThumbnailsComboBox.Text != Enum.GetName(DPSettings.currentSettingsObject.downloadImages)) - { - applySettingsBtn.Enabled = true; - } - } - - private void fileHandlingCombo_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && fileHandlingCombo.Text != names[(int) DPSettings.currentSettingsObject.handleInstallation]) - { - applySettingsBtn.Enabled = true; - } - } - - private void removeSourceFiles_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && removeSourceFilesCombo.Text != Enum.GetName(DPSettings.currentSettingsObject.permDeleteSource)) - { - applySettingsBtn.Enabled = true; - } - } - - private void installPrevProducts_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && installPrevProductsCombo.Text != Enum.GetName(DPSettings.currentSettingsObject.installPrevProducts)) - { - applySettingsBtn.Enabled = true; - } - } - - internal string AskForDirectory() - { - if (InvokeRequired) - { - string result = Invoke(new Func(AskForDirectory)); - return result; - } else - { - using (var folderBrowser = new FolderBrowserDialog()) - { - folderBrowser.Description = "Select folder for product installs"; - folderBrowser.UseDescriptionForTitle = true; - var dialogResult = folderBrowser.ShowDialog(); - if (dialogResult == DialogResult.Cancel) return string.Empty; - return folderBrowser.SelectedPath; - } - } - } - - private void chooseDestPathBtn_Click(object sender, EventArgs e) - { - using var browser = new FolderBrowserDialog(); - browser.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyComputer); - browser.Description = "Select folder to install products into"; - browser.UseDescriptionForTitle = true; - var result = browser.ShowDialog(); - if (result == DialogResult.OK) - { - destinationPathCombo.Items[0] = browser.SelectedPath; - destinationPathCombo.SelectedIndex = 0; - destinationPathCombo_TextChanged(null, null); - } - } - - private void chooseTempPathBtn_Click(object sender, EventArgs e) - { - using var browser = new FolderBrowserDialog(); - browser.InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyComputer); - browser.Description = "Select folder to temporarily extract products into"; - browser.UseDescriptionForTitle = true; - var result = browser.ShowDialog(); - if (result == DialogResult.OK) - { - tempTxtBox.Text = browser.SelectedPath; - tempTxtBox_Leave(null, null); - } - } - - private void modifyContentFoldersBtn_Click(object sender, EventArgs e) - { - var contentManager = new ContentFolderManager(); - contentManager.ShowDialog(); - contentFoldersListBox.Items.Clear(); - foreach (var item in contentManager.ContentFolders) - { - contentFoldersListBox.Items.Add(item); - } - applySettingsBtn.Enabled = true; - } - - private void modifyContentRedirectsBtn_Click_1(object sender, EventArgs e) - { - var contentManager = new ContentFolderAliasManager(); - contentManager.ShowDialog(); - if (contentManager.AliasListView is null) return; - contentFolderRedirectsListBox.BeginUpdate(); - contentFolderRedirectsListBox.Items.Clear(); - for (var i = 0; i < contentManager.AliasListView.Items.Count; i++) - { - contentFolderRedirectsListBox.Items.Add(contentManager.AliasListView.Items[i].Text); - } - contentFolderRedirectsListBox.EndUpdate(); - applySettingsBtn.Enabled = true; - } - - private void allowOverwritingCombo_TextChanged(object sender, EventArgs e) - { - if (!applySettingsBtn.Enabled && allowOverwritingCombo.Text != Enum.GetName(DPSettings.currentSettingsObject.OverwriteFiles)) - { - applySettingsBtn.Enabled = true; - } - } - - private void openDatabaseBtn_Click(object _, EventArgs __) => new DatabaseView().ShowDialog(); - } -} diff --git a/src/DAZ_Installer.Common/DAZ_Installer.Common.projitems b/src/DAZ_Installer.Common/DAZ_Installer.Common.projitems new file mode 100644 index 0000000..b33c0fa --- /dev/null +++ b/src/DAZ_Installer.Common/DAZ_Installer.Common.projitems @@ -0,0 +1,19 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + b52a5c81-d9c5-4b68-a117-b0aec3dd5d17 + + + DAZ_Installer.Common + + + + + + + + + + \ No newline at end of file diff --git a/src/DAZ_Installer.Common/DAZ_Installer.Common.shproj b/src/DAZ_Installer.Common/DAZ_Installer.Common.shproj new file mode 100644 index 0000000..3acf3aa --- /dev/null +++ b/src/DAZ_Installer.Common/DAZ_Installer.Common.shproj @@ -0,0 +1,13 @@ + + + + b52a5c81-d9c5-4b68-a117-b0aec3dd5d17 + 14.0 + + + + + + + + diff --git a/src/DAZ_Installer.Common/DPArchiveMap.cs b/src/DAZ_Installer.Common/DPArchiveMap.cs new file mode 100644 index 0000000..4e2f585 --- /dev/null +++ b/src/DAZ_Installer.Common/DPArchiveMap.cs @@ -0,0 +1,16 @@ +using DAZ_Installer.Core; +using System.Collections.Generic; +using System.Text.Json; + +public struct DPArchiveMap +{ + public string ArchiveName { get; set; } + public Dictionary Mappings; + + public DPArchiveMap(string archiveName, Dictionary mappings) + { + ArchiveName = archiveName; + Mappings = mappings; + } + +} \ No newline at end of file diff --git a/src/DAZ_Installer.Common/DPProcessorTestManifest.cs b/src/DAZ_Installer.Common/DPProcessorTestManifest.cs new file mode 100644 index 0000000..8d9c347 --- /dev/null +++ b/src/DAZ_Installer.Common/DPProcessorTestManifest.cs @@ -0,0 +1,32 @@ +using DAZ_Installer.Core; +using System.Collections.Generic; +using System.Text.Json; + +public struct DPProcessorTestManifest +{ + public DPProcessSettings Settings; + public string ArchiveName; + public List Results; + + public static readonly JsonSerializerOptions options = new() + { + IncludeFields = true, + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + IgnoreReadOnlyProperties = false, + IgnoreReadOnlyFields = false, + }; + + public DPProcessorTestManifest(DPProcessSettings settings, string name, List results) + { + Settings = settings; + ArchiveName = name; + Results = results; + } + + public string ToJson() => JsonSerializer.Serialize(this, options); + + public static DPProcessorTestManifest FromJson(string json) => JsonSerializer.Deserialize(json, options); + + public override string ToString() => ToJson(); +} \ No newline at end of file diff --git a/src/DAZ_Installer.Common/MSTestLoggerSink.cs b/src/DAZ_Installer.Common/MSTestLoggerSink.cs new file mode 100644 index 0000000..76815a0 --- /dev/null +++ b/src/DAZ_Installer.Common/MSTestLoggerSink.cs @@ -0,0 +1,26 @@ +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using System.IO; +using System; + +namespace DAZ_Installer +{ + internal class MSTestLoggerSink : ILogEventSink + { + private ITextFormatter formatter = SerilogLoggerConstants.Template; + Action logMessageFunc; + // We can't directly do this because the namespace is not exposed to non-testing solutions. + public MSTestLoggerSink(ITextFormatter? formatter = null, Action logMessageFunc = null) + { + if (formatter != null) this.formatter = formatter; + this.logMessageFunc = logMessageFunc ?? throw new ArgumentNullException(nameof(logMessageFunc)); + } + public void Emit(LogEvent logEvent) + { + using StringWriter stringWriter = new StringWriter(); + formatter.Format(logEvent, stringWriter); + logMessageFunc(stringWriter.ToString(), Array.Empty()); + } + } +} diff --git a/src/DAZ_Installer.Common/SerilogLoggerConstants.cs b/src/DAZ_Installer.Common/SerilogLoggerConstants.cs new file mode 100644 index 0000000..47c9d4f --- /dev/null +++ b/src/DAZ_Installer.Common/SerilogLoggerConstants.cs @@ -0,0 +1,21 @@ +using Serilog.Templates; + +namespace DAZ_Installer +{ + internal static class SerilogLoggerConstants + { + internal static ExpressionTemplate Template = new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss} {@l:u3}] [Thread {ThreadId}]" + + "{#if SourceContext is not null} [{Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}]{#end}" + + //"{#each _, v in Rest()}" + " [{v}]{#end}" + + "{#if Archive is not null} [{Archive}]{#end}" + + "{#if File is not null} [{File}]{#end}" + + ": {@m}\n{@x}"); + internal static ExpressionTemplate LoggerTemplate = new ExpressionTemplate("[{@l:u3}]" + + "{#if SourceContext is not null} [{Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}]{#end}" + + //"{#each _, v in Rest()}" + " [{v}]{#end}" + + "{#if Archive is not null} [{Archive}]{#end}" + + "{#if File is not null} [{File}]{#end}" + + ": {@m}\n{@x}"); + + } +} diff --git a/src/Utilities/SpanExtensions.cs b/src/DAZ_Installer.Common/SpanExtensions.cs similarity index 96% rename from src/Utilities/SpanExtensions.cs rename to src/DAZ_Installer.Common/SpanExtensions.cs index bedda3b..b2d9135 100644 --- a/src/Utilities/SpanExtensions.cs +++ b/src/DAZ_Installer.Common/SpanExtensions.cs @@ -1,6 +1,6 @@ using System; -namespace DAZ_Installer.Utilities +namespace DAZ_Installer { internal static class SpanExtensions { diff --git a/src/DAZ_Installer.Common/TryHelper.cs b/src/DAZ_Installer.Common/TryHelper.cs new file mode 100644 index 0000000..5206309 --- /dev/null +++ b/src/DAZ_Installer.Common/TryHelper.cs @@ -0,0 +1,132 @@ +using DAZ_Installer.IO; +using System; +using System.IO; +using System.Diagnostics.CodeAnalysis; + +namespace DAZ_Installer +{ + /// + /// A class dedicated to try and resolve exceptions in try/catch blocks. For example, dealing with files that are hidden, this + /// class contains methods that will attempt to unhide the file and then try again. + /// + public static class TryHelper + { + /// + /// + /// + /// The file that we may not have access to. + /// Whether the application now has access to the file. + public static bool TryFixFilePermissions(IDPFileInfo info) => TryFixFilePermissions(info, out _); + /// + /// Tries to fix the file permissions of . If it fails, it will return false. + /// + /// The file that we may not have access to. + /// The exception that was thrown, if any. + /// Whether the application now has access to the file. + public static bool TryFixFilePermissions(IDPFileInfo info, [NotNullWhen(false)] out Exception? ex) + { + // If exists returns false, it means that either the file does not exist or we do not have access to it. + // We can't do anything if we don't have access to the file. But we can if we can not open, move, rename it + // due to file permissions. + ex = null; + if (!info.Exists) return false; + try + { + info.Attributes = FileAttributes.Normal; + info.OpenRead().Close(); // test that we have access. + return true; + } + catch (Exception e) + { + ex = e; + return false; + } + } + + /// + /// Tries to fix the directory permissions of . If it fails, it will return false. + /// + /// The directory that we may not have access to. + /// Whether + public static bool TryFixDirectoryPermissions(DirectoryInfo info) => TryFixDirectoryPermissions(info, out _); + /// + /// Tries to fix the directory permissions of . If it fails, it will return false. + /// + /// The directory that we may not have access to. + /// param name="ex">The exception that was thrown, if any. + /// Whether + public static bool TryFixDirectoryPermissions(DirectoryInfo info, [NotNullWhen(false)] out Exception? ex) + { + // If exists returns false, it means that either the file does not exist or we do not have access to it. + // We can't do anything if we don't have access to the file. But we can if we can not open, move, rename it + // due to file permissions. + ex = null; + if (!info.Exists) return false; + try + { + info.Attributes = FileAttributes.Normal; + info.EnumerateDirectories(); // test that it works. + return true; + } + catch (Exception e) + { + ex = e; + return false; + } + } + #region Generic Try Methods + /// + /// + /// + /// The function to execute in the try/catch block. + /// Whether the action ran without any exception thrown. + public static bool Try(Action action) => Try(action, out _); + /// + /// Runs in a try/catch block and returns whether it succeeded or not. + /// Succeeded means whether it threw an exception or not. + /// + /// The function to execute in the try/catch block. + /// The exception that was thrown; this will be null if is returned. + /// Whether the action ran without any exception thrown. + public static bool Try(Action action, [NotNullWhen(false)] out Exception? ex) + { + ex = null; + try { action(); } + catch (Exception e) + { + ex = e; + return false; + } + return true; + } + /// + /// + /// + /// he function to execute in the try/catch block. + /// The result that returned if it did not error, otherwise this will return default. + /// Whether the function ran without any exception thrown. + public static bool Try(Func func, out T? result) => Try(func, out result, out _); + /// + /// Runs in a try/catch block and returns whether it succeeded or not. + /// Succeeded means whether it threw an exception or not. + /// It also returns the result of in . + /// + /// The function to execute in the try/catch block. + /// The result that returned if it did not error, otherwise this will return default. + /// The exception that was thrown; this will be null if is returned. + /// Whether the function ran without any exception thrown. + public static bool Try(Func func, out T? result, [NotNullWhen(false)] out Exception? ex) + { + result = default; + ex = null; + try { result = func(); } + catch (Exception e) + { + ex = e; + return false; + } + return true; + } + #endregion + } +} diff --git a/src/DAZ_Installer.Core/.vscode/settings.json b/src/DAZ_Installer.Core/.vscode/settings.json new file mode 100644 index 0000000..b38f46d --- /dev/null +++ b/src/DAZ_Installer.Core/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "DAZ_Installer.Core.sln" +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/Abstractions/AbstractDestinationDeterminer.cs b/src/DAZ_Installer.Core/Abstractions/AbstractDestinationDeterminer.cs new file mode 100644 index 0000000..3cc0764 --- /dev/null +++ b/src/DAZ_Installer.Core/Abstractions/AbstractDestinationDeterminer.cs @@ -0,0 +1,24 @@ +using Serilog; + +namespace DAZ_Installer.Core +{ + /// + /// An abstract class for determining the destination of files inside of an archive. + /// + public abstract class AbstractDestinationDeterminer + { + virtual protected ILogger Logger { get; set; } = Log.Logger.ForContext(); + /// + /// Determines the files to extract inside of the and sets their to their destination based on the . + /// + /// The archive to determine files to extract and determine destinations. + /// The settings to base decisions off of. + /// A collection of s determined to be processed. + /// + public abstract HashSet DetermineDestinations(DPArchive arc, DPProcessSettings settings); + + public AbstractDestinationDeterminer() { } + + public AbstractDestinationDeterminer(ILogger logger) => Logger = logger; + } +} diff --git a/src/DAZ_Installer.Core/Abstractions/AbstractTagProvider.cs b/src/DAZ_Installer.Core/Abstractions/AbstractTagProvider.cs new file mode 100644 index 0000000..7274b7f --- /dev/null +++ b/src/DAZ_Installer.Core/Abstractions/AbstractTagProvider.cs @@ -0,0 +1,22 @@ +using Serilog; + +namespace DAZ_Installer.Core +{ + /// + /// An abstract class for determining the tags of an archive. + /// + public abstract class AbstractTagProvider + { + protected virtual ILogger Logger { get; set; } = Log.Logger.ForContext(); + /// + /// Provides the tags based on and it's contents. + /// + /// The archive to get tags from. + /// The settings provided if needed. + /// A collection of tags determined for . + public abstract HashSet GetTags(DPArchive arc, DPProcessSettings settings); + + public AbstractTagProvider() { } + public AbstractTagProvider(ILogger logger) => Logger = logger; + } +} diff --git a/src/DAZ_Installer.Core/ContentType.cs b/src/DAZ_Installer.Core/ContentType.cs new file mode 100644 index 0000000..ba4b79e --- /dev/null +++ b/src/DAZ_Installer.Core/ContentType.cs @@ -0,0 +1,45 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE +namespace DAZ_Installer.Core +{ + /// + /// ContentType represents the type of the content for user daz files (DSON user files). + /// + /// For example, type: "wearable" found in a DSON user file + /// should have a ContentType of . + /// + /// + public enum ContentType + { + Scene, + Scene_Subset, + Hierachical_Material, + Preset_Hierarchical_Pose, + Wearable, + Character, + Figure, + Prop, + Preset_Properties, + Preset_Shape, + Preset_Pose, + Preset_Material, + Preset_Shader, + Preset_Camera, + Preset_Light, + Preset_Render_Settings, + Preset_Simulation_Settings, + Preset_DFormer, + Preset_Layered_Image, + Preset_Puppeteer, + Modifier, // aka morph + UV_Set, + Script, + Library, + Program, + Media, + Document, + Geometry, + DAZ_File, + Unknown + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/DAZ_Installer.Core.csproj b/src/DAZ_Installer.Core/DAZ_Installer.Core.csproj new file mode 100644 index 0000000..85875e5 --- /dev/null +++ b/src/DAZ_Installer.Core/DAZ_Installer.Core.csproj @@ -0,0 +1,27 @@ + + + + net6.0 + enable + enable + x64 + + + + + + + + + + + + + + + + + + + + diff --git a/src/DAZ_Installer.Core/DAZ_Installer.Core.sln b/src/DAZ_Installer.Core/DAZ_Installer.Core.sln new file mode 100644 index 0000000..d3a62c0 --- /dev/null +++ b/src/DAZ_Installer.Core/DAZ_Installer.Core.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DAZ_Installer.Core", "DAZ_Installer.Core.csproj", "{53B9B499-922B-44BE-85C9-7D8277842326}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {53B9B499-922B-44BE-85C9-7D8277842326}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53B9B499-922B-44BE-85C9-7D8277842326}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53B9B499-922B-44BE-85C9-7D8277842326}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53B9B499-922B-44BE-85C9-7D8277842326}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0D484C5E-E2C0-4DAD-9C25-6840A246968C} + EndGlobalSection +EndGlobal diff --git a/src/DAZ_Installer.Core/DPAbstractNode.cs b/src/DAZ_Installer.Core/DPAbstractNode.cs new file mode 100644 index 0000000..78211fe --- /dev/null +++ b/src/DAZ_Installer.Core/DPAbstractNode.cs @@ -0,0 +1,96 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE +using DAZ_Installer.IO; +using Serilog; +using IOPath = System.IO.Path; + +namespace DAZ_Installer.Core +{ + /// + /// Abstract class for all elements found in archives (including archives). + /// This means that all files, and archives (which are files) should extend + /// this class. + /// + public abstract class DPAbstractNode + { + public abstract ILogger Logger { get; set; } + /// + /// The file name of a file or folder. + /// + public virtual string FileName => IOPath.GetFileName(Path); + /// + /// The full path of the file (or folder) in the archive space. Using this property is not recommended + /// for comparing or listing files as delimiters vary, use instead. + /// However since this property holds the exact path given from the archive, you can use to compare + /// to match a to the archive's native format. For example, + /// + /// RARFileInfo info = e.fileInfo; + /// info.FileName == Path // returns true + /// + /// + public string Path = string.Empty; + /// + /// The path with all forward slashes replaced with backslashes. Use this property for comparing + /// and listing files, folders, and archives. + /// + public virtual string NormalizedPath => PathHelper.NormalizePath(Path); + /// + /// The extension of the file in lowercase characters and without the dot. ext can be empty. + /// + public virtual string Ext => GetExtension(Path); + /// + /// The folder the file (or folder) is a child of. Can be null. + /// + public DPFolder? Parent { get => parent; set => UpdateParent(value); } + /// + /// The archive this file is associated to. Can be null. + /// Should only be null if this file is an AND + /// the initial archive called by + /// + public DPArchive? AssociatedArchive { get; set; } + + protected abstract void UpdateParent(DPFolder? parent); + + #region Processing Properties + /// + /// The final, absolute path that the file is supposed to be extracted to. + /// + public string TargetPath { get; set; } = string.Empty; + /// + /// The full relative path of the file (or folder) relative to the determined content folder (if any). + /// If no content folder is detected, relative path will be . + /// Currently, relative path is not set for folders. + /// + public string RelativePathToContentFolder { get; set; } = string.Empty; + /// + /// The relative directory path at which will be used to determine which the file will go to in the system. + /// This property is used to determine the target path of a file. + /// The value will be equal to + /// if the is not in . + /// + public string RelativeTargetPath { get; set; } = string.Empty; + #endregion + + protected DPFolder? parent; + + /// + /// Returns the extension of a given name without the dot and lowered to all lowercase. + /// + public static string GetExtension(string path) => IOPath.GetExtension(path).Substring(path.Length > 0 ? 1 : 0).ToLower(); + /// + /// A constructor that does nothing. Only recommended for creating init-archives. + /// + public DPAbstractNode() { } + /// + /// Constructor for creating file, folder, and even archive objects from the archive space. + /// + /// Thrown when or is . + public DPAbstractNode(string _path, DPArchive? associatedArchive = null) + { + ArgumentNullException.ThrowIfNull(_path); + Path = _path; + AssociatedArchive = associatedArchive; + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/DPArchive.cs b/src/DAZ_Installer.Core/DPArchive.cs new file mode 100644 index 0000000..fbcc083 --- /dev/null +++ b/src/DAZ_Installer.Core/DPArchive.cs @@ -0,0 +1,495 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.IO; +using Serilog; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using IOPath = System.IO.Path; + +namespace DAZ_Installer.Core +{ + /// + /// Defines the archive type of an archive. + /// + // TODO: Add a type for Multi-Product and Multi-Bundle to help determine whether + // an archive should be added to the database/library + public enum ArchiveType + { + Product, Bundle, Unknown + } + + /// + /// Defines the archive format of an archive. + /// + public enum ArchiveFormat + { + SevenZ, WinZip, RAR, Unknown + } + /// + /// Abstract class for all supported archive files. + /// Currently the supported archive files are RAR, WinZip, and 7z (partially). + /// + public class DPArchive : DPFile + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + public override string FileName => !IsInnerArchive ? FileInfo!.Name : IOPath.GetFileName(NormalizedPath); + public override string Ext => IsInnerArchive ? base.Ext : GetExtension(FileInfo?.Name ?? string.Empty); + /// + /// The product name of the archive. If the archive has not been successfully processed, the product name will be equivalent to . + /// Otherwise, it is either the product name of the archive determined via the manifest file, a regex-filtered file name, or simply . + /// + public virtual string ProductName => getProductName(); + /// + /// The archive format of the archive. If the archive has not been successfully processed, the archive format will be equivalent to . + /// + public ArchiveFormat ArchiveFormat { get; protected set; } = ArchiveFormat.Unknown; + /// + /// The extractor that the archive will use. This could be null if the is . But after construction of this object, it is usually not null. + /// + public DPAbstractExtractor? Extractor { get; set; } + /// + /// The file system to use, this is derived from the which is from . + /// + public AbstractFileSystem FileSystem => FileInfo!.FileSystem; + /// + /// The name that will be used for the list view. The list name is the working archive's FileName + $"\\{Path}". + /// + /// The working archive's FileName + $"\\{Path}". + public string ListName => AssociatedArchive is null ? string.Empty : AssociatedArchive.FileName + '\\' + Path; + /// + /// A list of archives that are children of this archive. + /// Or, in other words, archives that are contained within this archive. + /// + public List Subarchives { get; init; } = new(); + /// + /// A file that has been detected as a manifest file. + /// + // TODO: Make this a list of manifest files and fetch from here. + public List ManifestFiles { get; protected set; } = new(2); + /// + /// A file that has been detected as a supplement file. + /// + // TODO: Make this a list of supplement files and fetch from here. + public List SupplementFiles { get; protected set; } = new(1); + /// + /// A boolean value to describe if this archive is a child of another archive. Default is false. + /// + public bool IsInnerArchive => AssociatedArchive is not null; + /// + /// The type of this archive. Default is . + /// + public ArchiveType Type { get; set; } = ArchiveType.Unknown; + /// + /// The product info connected to this archive. + /// + public DPProductInfo ProductInfo = new(); + + /// + /// A map of all of the folders parented to this archive. + /// + /// The NormalizedPath of the Folder. + /// The folder. + + public Dictionary Folders { get; } = new(); + + /// + /// A list of folders at the root level of this archive. + /// + public List RootFolders { get; } = new(); + /// + /// A dictionary of all of the contents and their normalized paths () in the archive. + /// + /// The file content in this archive. + /// The of a file. + public Dictionary Contents { get; } = new(); + + /// + /// A list of the root contents/ the contents at root level (DPAbstractFiles) of this archive. + /// + /// The file content in this archive. + public List RootContents { get; } = new(); + /// + /// A list of all .dsx files in this archive. + /// + /// A file that is either a manifest, supplementary, or support file (.dsx). + public List DSXFiles { get; } = new(); + /// + /// A list of all readable daz files in this archive. This consists of types with extension: .duf, .dsf. + /// + /// A file with the extension .duf OR .dsf. + /// + public List DazFiles { get; } = new(); + /// + /// The true uncompressed size of the archive contents in bytes. + /// + public ulong TrueArchiveSize { get; internal set; } = 0; + /// + /// The expected tag count for this archive. This value is updated when an applicable file has discovered new tags. + /// + public uint ExpectedTagCount { get; set; } = 0; + + /// + /// The regex expression used for creating a product name. + /// + public static Regex ProductNameRegex { get; protected set; } = new(@"([^+|\-|_|\s]+)", RegexOptions.Compiled); + + public DPArchive() { } + /// + /// Create an archive that is on the disk. + /// + /// A FileInfo object that represents an archive on the file system. + /// Thrown when is . + public DPArchive(IDPFileInfo info) : base(string.Empty, null, null) + { + FileInfo = info; + // Try to determine the archive format NOW using most reliable method if possible. + ArchiveFormat = GetArchiveFormat(); + Extractor = GetDefaultExtractorForArchiveFormat(ArchiveFormat); + } + public DPArchive(string _path, DPArchive? parent = null, DPFolder? parentFolder = null) : base(_path, parent, parentFolder) + { + // Make a file but we don't want to check anything. + //if (IsInnerArchive) Parent = null; + //else base.parent = null; + RelativePathToContentFolder = FileName; + ProductInfo = new DPProductInfo(IOPath.GetFileNameWithoutExtension(Path)); + parent?.Subarchives.Add(this); + + // Try to determine the archive format NOW using most reliable method if possible. + ArchiveFormat = GetArchiveFormat(); + Extractor = GetDefaultExtractorForArchiveFormat(ArchiveFormat); + } + + /// + /// Constructor for testing purposes. + /// + internal DPArchive(string _path, ILogger logger, IDPFileInfo info, DPAbstractExtractor extractor, DPArchive? parent = null, DPFolder? parentFolder = null) : base(_path, parent, parentFolder, info, logger) + { + // Make a file but we don't want to check anything. + //if (IsInnerArchive) Parent = null; + //else base.parent = null; + RelativePathToContentFolder = FileName; + ProductInfo = new DPProductInfo(IOPath.GetFileNameWithoutExtension(Path)); + parent?.Subarchives.Add(this); + ArchiveFormat = GetArchiveFormat(); + Extractor = extractor; + } + + // This is a great use for an AI solution. + protected virtual string getProductName() + { + // If we found the product name from the manifest, then use that since it is the most reliable. + if (!string.IsNullOrWhiteSpace(ProductInfo.ProductName)) return ProductInfo.ProductName; + // otherwise, try to get the product name from the archive name. + // Get the product name from the archive file name without extension. + // Product name excludes any +, -, _, or whitespaces. + var path = IOPath.GetFileNameWithoutExtension(IsInnerArchive ? FileName : FileInfo!.Name); + var matches = ProductNameRegex.Matches(path); + if (matches.Count == 0) return path; + else return string.Join(' ', matches.Select(x => x.Value)); + } + #region Public Methods + /// + /// Creates a new archive that lives on the disk and has no parent. + /// + /// The file info to use for I/O operations. + /// A new . + public static DPArchive CreateNewParentArchive(IDPFileInfo info) => new(info); + /// + /// Peeks the archive contents if possible and will extract ALL archive contents to . + /// + /// The destination path to extract the archive contents to. + /// The temporary path to extract the archive contents to. + /// Determines whether to overwrite the files on disk if they exist. + /// or does not exist or do not have access to it. + public DPExtractionReport ExtractAllContents(string tempLocation, bool overwrite = true) => + ExtractContents(new DPExtractSettings(tempLocation, Contents.Values, overwrite)); + /// + /// Extracts contents from the archive (using ), then peeks the archive contents if possible and + /// will attempt to extract files specifed in to . + /// + /// The settings to use for extraction. + /// Archive needed to be extracted first, but it failed to be extracted. + public DPExtractionReport ExtractContents(DPExtractSettings settings) + { + if (!Extracted && !ExtractToTemp(settings)) + throw new IOException("Archive was not on disk and could not be extracted."); + if (Extractor is null) + throw new InvalidOperationException("Extractor is null. Cannot extract archive contents."); + return Extractor.Extract(settings); + } + /// + /// Extracts contents from the archive (using ), then peeks the archive contents if possible and + /// will attempt to extract files specifed in to . + /// + /// The settings to use for extraction. will be ignored. + /// Archive needed to be extracted first, but it failed to be extracted. + public DPExtractionReport ExtractContentsToTemp(DPExtractSettings settings) + { + if (!Extracted && !ExtractToTemp(settings)) + throw new IOException("Archive was not on disk and could not be extracted."); + if (Extractor is null) + throw new InvalidOperationException("Extractor is null. Cannot extract archive contents."); + return Extractor.ExtractToTemp(settings); + } + + /// + /// Previews the archive by discovering files in this archive. If the archive is not on disk, then it will be first extracted to . + /// If is null, then it will be extracted to the temp directory. + /// + /// The temp path to extract if the archive is not on disk, otherwise it will extract to + public void PeekContents(string? temp = null) + { + // Just extract to temp. + var settings = new DPExtractSettings(temp ?? IOPath.GetTempPath(), Array.Empty(), archive: this); + if (!Extracted && !ExtractToTemp(settings)) + throw new IOException("Archive was not on disk and could not be extracted."); + if (Extractor is null) + throw new InvalidOperationException("Extractor is null. Cannot peek archive contents."); + Extractor.Peek(this); + } + + /// + /// Extracts the from the archive to the file's TargetPath. If this archive needs to be extracted first, + /// then the archive will extract this archive first then extract the requested . + /// + /// The file to extract. + /// The temp path to use if needed. + /// Determines whether to overwrite the files on disk if they exist. + public bool ExtractContent(DPFile file, string tempLocation, bool overwrite = true) => ExtractContents(new DPExtractSettings(tempLocation, new DPFile[] { file }, overwrite)).SuccessPercentage == 1; + + /// + /// Checks whether or not the given ext is what is expected. Checks file headers. Does not throw exceptions. + /// + /// Returns an extension of the appropriate archive extraction method. + /// Returns on errors and when it doesn't match archive magic strings. + + public static ArchiveFormat DetermineArchiveFormatPrecise(string location) + { + try + { + using FileStream stream = File.OpenRead(location); + var bytes = new byte[8]; + stream.Read(bytes, 0, 8); + stream.Close(); + // ZIP File Header + // 50 4B OR 57 69 + if ((bytes[0] == 80 || bytes[0] == 87) && (bytes[1] == 75 || bytes[2] == 105)) + return ArchiveFormat.WinZip; + // RAR 5 consists of 8 bytes. 0x52 0x61 0x72 0x21 0x1A 0x07 0x01 0x00 + // RAR 4.x consists of 7. 0x52 0x61 0x72 0x21 0x1A 0x07 0x00 + // Rar! + if (bytes[0] == 82 && bytes[1] == 97 && bytes[2] == 114 && bytes[3] == 33) + return ArchiveFormat.RAR; + + if (bytes[0] == 55 && bytes[1] == 122 && bytes[2] == 188 && bytes[3] == 175) + return ArchiveFormat.SevenZ; + + return ArchiveFormat.Unknown; + } + catch { return ArchiveFormat.Unknown; } + } + /// + /// + /// + /// The stream to use. + /// Determines whether to close the stream when finished. + /// Returns an extension of the appropriate archive extraction method. Otherwise, null. + public static ArchiveFormat DetermineArchiveFormatPrecise(Stream stream, bool closeWhenFinished) + { + try + { + var bytes = new byte[8]; + stream.Read(bytes, 0, 8); + if (closeWhenFinished) stream.Close(); + // ZIP File Header + // 50 4B OR 57 69 + if ((bytes[0] == 80 || bytes[0] == 87) && (bytes[1] == 75 || bytes[2] == 105)) + return ArchiveFormat.WinZip; + // RAR 5 consists of 8 bytes. 0x52 0x61 0x72 0x21 0x1A 0x07 0x01 0x00 + // RAR 4.x consists of 7. 0x52 0x61 0x72 0x21 0x1A 0x07 0x00 + // Rar! + if (bytes[0] == 82 && bytes[1] == 97 && bytes[2] == 114 && bytes[3] == 33) + return ArchiveFormat.RAR; + + if (bytes[0] == 55 && bytes[1] == 122 && bytes[2] == 188 && bytes[3] == 175) + return ArchiveFormat.SevenZ; + + return ArchiveFormat.Unknown; + } + catch { return ArchiveFormat.Unknown; } + } + + /// + /// Returns an enum describing the archive's format based on the file extension. + /// This is used for determining archive files inside of an archive. + /// + /// The path of the archive. + /// A ArchiveFormat enum determining the archive format. + public static ArchiveFormat DetermineArchiveFormat(string ext) + { + // ADDITONAL NOTE: This is called for determing archive files inside of an + // archive file. + ext = ext.ToLower(); + switch (ext) + { + case "7z": + return ArchiveFormat.SevenZ; + case "rar": + return ArchiveFormat.RAR; + case "zip": + return ArchiveFormat.WinZip; + default: + if (uint.TryParse(ext, out var _)) return ArchiveFormat.SevenZ; + return ArchiveFormat.Unknown; + } + } + /// + /// Calls on the property and + /// potentially calls if is true. + /// + /// + protected ArchiveFormat GetArchiveFormat() + { + ArchiveFormat result1 = DetermineArchiveFormat(Ext); + if (!Extracted) return result1; + ArchiveFormat result2 = DetermineArchiveFormatPrecise(FileInfo!.OpenRead(), true); + return result2; + } + + /// + /// Updates the property. If is null, it will be set to the + /// extractor that matches the property. + /// If is , will be set to null. + /// If is not null, will be set to and no checks will be made. + /// + /// + public void SetExtractor(DPAbstractExtractor? extractor = null) + { + if (extractor != null) + { + Extractor = extractor; + return; + } + ArchiveFormat = GetArchiveFormat(); + if (ArchiveFormat == ArchiveFormat.Unknown) + { + Extractor = null; + return; + } + Extractor = GetDefaultExtractorForArchiveFormat(ArchiveFormat); + } + + /// + /// Returns the default extractor given an archive format. + /// + /// The format to get default extractor for. + /// The default extractor, null if is + public static DPAbstractExtractor? GetDefaultExtractorForArchiveFormat(ArchiveFormat format) + { + return format switch + { + ArchiveFormat.SevenZ => new DP7zExtractor(), + ArchiveFormat.RAR => new DPRARExtractor(), + ArchiveFormat.WinZip => new DPZipExtractor(), + _ => null, + }; + } + + /// + /// Searches for all files that contains the name specified by . + /// + /// The name to search for. + /// The first file that contains ; null if not found. + public DPFile? FindFileViaNameContains(string name) => Contents.Values.First(x => x.FileName.Contains(name)); + + private static List ConvertDPFoldersToStringArr(Dictionary folders) + { + var a = new List(folders.Count); + foreach (DPFolder v in folders.Values) a.Add(v.Path); + return a; + } + + /// + /// This function should be called after all the files have been extracted. If no content folders have been found, this is a bundle. + /// + public ArchiveType DetermineArchiveType() + { + foreach (DPFolder folder in Folders.Values) + { + if (folder.IsContentFolder) + { + return ArchiveType.Product; + } + } + foreach (DPFile content in Contents.Values) + { + if (content is DPArchive) return ArchiveType.Bundle; + } + return ArchiveType.Unknown; + + } + + public int GetEstimateTagCount() + { + var count = 0; + foreach (DPFile content in Contents.Values) + { + count += content.Tags.Count; + } + count += ProductInfo.Authors.Count; + return count; + } + + public DPFolder? FindParent(DPAbstractNode obj) + { + var fileName = PathHelper.GetFileName(obj.Path); + // This means obj.Path contains trailing seperator, so do it again but without the seperator. + if (fileName == string.Empty) fileName = IOPath.GetFileName(obj.Path.TrimEnd(PathHelper.GetSeperator(obj.Path))); + var relativePathOnly = ""; + try + { + relativePathOnly = PathHelper.CleanDirPath(obj.Path.Remove(obj.Path.LastIndexOf(fileName))); + } + catch { } + if (FindFolder(relativePathOnly, out DPFolder? folder)) + { + return folder; + } + return null; + } + + public bool FolderExists(string fPath) => Folders.ContainsKey(fPath); + /// + /// Simply finds the folder given a relative path. + /// + /// + /// + /// + public bool FindFolder(string relativePath, [NotNullWhen(true)] out DPFolder? folder) + { + var normalizedRelativePath = PathHelper.NormalizePath(relativePath); + foreach (DPFolder _folder in Folders.Values) + { + if (_folder.NormalizedPath == normalizedRelativePath) + { + folder = _folder; + return true; + } + } + folder = null; + return false; + } + + /// + /// Uses the to split the product name into tokens. + /// + /// The name of a file to split. + /// The tokens of . + public static IEnumerable RegexSplitName(string name) => + ProductNameRegex.Matches(name).Select(x => x.Value); + + #endregion + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/DPArchiveEnterArgs.cs b/src/DAZ_Installer.Core/DPArchiveEnterArgs.cs new file mode 100644 index 0000000..4afb555 --- /dev/null +++ b/src/DAZ_Installer.Core/DPArchiveEnterArgs.cs @@ -0,0 +1,16 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents arguments for when the is about + /// to process an archive. + /// + public class DPArchiveEnterArgs : EventArgs + { + /// + /// The archive that is about to be processed. + /// + public readonly DPArchive Archive; + + internal DPArchiveEnterArgs(DPArchive archive) => Archive = archive; + } +} diff --git a/src/DAZ_Installer.Core/DPArchiveErrorArgs.cs b/src/DAZ_Installer.Core/DPArchiveErrorArgs.cs new file mode 100644 index 0000000..5f31826 --- /dev/null +++ b/src/DAZ_Installer.Core/DPArchiveErrorArgs.cs @@ -0,0 +1,51 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents error arguments for errors. + /// + public class DPArchiveErrorArgs : DPProcessorErrorArgs + { + /// + /// The archive the was processing when the error occurred. + /// + public DPArchive Archive { get; init; } + /// + /// Determine whether the archive should cancel the operation or not. + /// + /// Change this value to if you wish to cancel processing the + /// archive. + /// This will only be honored if is . + /// + public new bool CancelOperation { get; set; } = true; + /// + /// + /// + /// The exception thrown by the error, if any. + /// The additional explaination for the error/situation. + /// The archive that errored. + internal DPArchiveErrorArgs(DPArchive archive, Exception? ex = null, + string? explaination = null) : base(ex, explaination) + { + Ex = ex; + if (explaination != null) + Explaination = explaination; + Archive = archive; + } + /// + /// Explanation for when the archive is encrypted. + /// + internal const string EncryptedArchiveExplanation = "Cannot process encrypted archives at this time."; + /// + /// Explanation for when the archive contains encrypted files. + /// + internal const string EncryptedFilesExplanation = "Cannot process archives with encrypted files at this time."; + internal const string UnauthorizedAccessExplanation = "Failed to extract file due to unauthorized access."; + internal const string UnauthorizedAccessAfterExplanation = "Failed to extract file due to unauthorized access (even after attempting to fix file attribute)."; + internal const string ArchiveDoesNotExistOrNoAccessExplanation = "Archive does not exist on disk or has permissions issue."; + /// + /// Format for explanation for when a file is not part of the archive. + /// + + internal const string FileNotPartOfArchiveErrorFormat = "File {0} is not part of this archive."; + } +} diff --git a/src/DAZ_Installer.Core/DPArchiveExitArgs.cs b/src/DAZ_Installer.Core/DPArchiveExitArgs.cs new file mode 100644 index 0000000..f3a604d --- /dev/null +++ b/src/DAZ_Installer.Core/DPArchiveExitArgs.cs @@ -0,0 +1,30 @@ +using DAZ_Installer.Core.Extraction; +using System.Diagnostics.CodeAnalysis; + +namespace DAZ_Installer.Core +{ + public class DPArchiveExitArgs + { + /// + /// Represents whether the file had been successfully processed by or not. + /// Will be if cancelled or an error occurred preventing main operations. + /// + public readonly bool Processed = false; + + /// + /// The archive that has finished processing. + /// + public readonly DPArchive Archive; + /// + /// The extraction report generated by the archive. This will not be null if is . + /// + public readonly DPExtractionReport? Report; + + internal DPArchiveExitArgs(DPArchive archive, DPExtractionReport? report, bool processed = true) + { + Archive = archive; + Report = report; + Processed = processed; + } + } +} diff --git a/src/DP/DPContentInfo.cs b/src/DAZ_Installer.Core/DPContentInfo.cs similarity index 60% rename from src/DP/DPContentInfo.cs rename to src/DAZ_Installer.Core/DPContentInfo.cs index d53439b..b79fb38 100644 --- a/src/DP/DPContentInfo.cs +++ b/src/DAZ_Installer.Core/DPContentInfo.cs @@ -1,31 +1,32 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System.Collections.Generic; -namespace DAZ_Installer.DP { - internal struct DPContentInfo { +namespace DAZ_Installer.Core +{ + public struct DPContentInfo + { /// /// The content type of the DSON User File. Default is ContentType.Unknown. /// - internal ContentType ContentType {get; set;} = ContentType.Unknown; + public ContentType ContentType { get; set; } = ContentType.Unknown; /// /// The list of authors found in the DSON user file. Default is an empty list. /// - internal List Authors = new List(); + public List Authors = new(); /// /// The website found in the DSON user file. Default is an empty string. /// - internal string Website {get; set;} = string.Empty; + public string Website { get; set; } = string.Empty; /// /// The email found in the DSON user file. Default is an empty string. /// - internal string Email {get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; /// /// The ID found in the DSX user file. Default is an empty string. /// - internal string ID {get; set; } = string.Empty; + public string ID { get; set; } = string.Empty; - public DPContentInfo() {} // microsoftttttttt (╯‵□′)╯︵┻━┻┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻ + public DPContentInfo() { } // microsoftttttttt (╯‵□′)╯︵┻━┻┻━┻ ︵ヽ(`Д´)ノ︵ ┻━┻ } } \ No newline at end of file diff --git a/src/DP/DPDSXElement.cs b/src/DAZ_Installer.Core/DPDSXElement.cs similarity index 52% rename from src/DP/DPDSXElement.cs rename to src/DAZ_Installer.Core/DPDSXElement.cs index 1afcf58..2c0c1ed 100644 --- a/src/DP/DPDSXElement.cs +++ b/src/DAZ_Installer.Core/DPDSXElement.cs @@ -1,35 +1,33 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -using System.Collections.Generic; - -namespace DAZ_Installer.DP { - internal class DPDSXElement +namespace DAZ_Installer.Core +{ + public class DPDSXElement { - internal readonly Dictionary attributes = new Dictionary(); - internal Memory InnerText = new char[] { }; - internal string TagName = string.Empty; - internal Memory TotalMessage = new char[] { }; - internal bool IsSelfClosingElement = false; - internal bool MessageIncludesEnding = false; - internal int BeginningIndex = -1; - internal int EndIndex = -1; + public readonly Dictionary attributes = new(); + public Memory InnerText = new char[] { }; + public string TagName = string.Empty; + public Memory TotalMessage = new char[] { }; + public bool IsSelfClosingElement = false; + public bool MessageIncludesEnding = false; + public int BeginningIndex = -1; + public int EndIndex = -1; - internal DPDSXElement Parent; - internal DPDSXElement NextSibling; - internal DPDSXElement PreviousSibling; - internal List Children = new List(); - internal DPDSXElementCollection File; - internal DPDSXElement() { } + public DPDSXElement Parent; + public DPDSXElement NextSibling; + public DPDSXElement PreviousSibling; + public List Children = new(); + public DPDSXElementCollection File; + public DPDSXElement() { } /// /// DSXElements within the given index range will have its parent set to this DSXElement and added to children array. /// /// The total beginning index of the buffer array. /// The total end index of the buffer array. - internal void ParentChildrenWithinIndexRange() + public void ParentChildrenWithinIndexRange() { - var workingSibling = NextSibling; + DPDSXElement workingSibling = NextSibling; while (workingSibling != null && IndexInRange(BeginningIndex, EndIndex, workingSibling)) { workingSibling.Parent = this; @@ -45,5 +43,4 @@ protected static bool IndexInRange(int beginningIndex, int endIndex, DPDSXElemen } } -} - \ No newline at end of file +} diff --git a/src/DAZ_Installer.Core/DPDSXElementCollection.cs b/src/DAZ_Installer.Core/DPDSXElementCollection.cs new file mode 100644 index 0000000..257f24d --- /dev/null +++ b/src/DAZ_Installer.Core/DPDSXElementCollection.cs @@ -0,0 +1,50 @@ +namespace DAZ_Installer.Core +{ + public class DPDSXElementCollection + { + public Dictionary> selfClosingElements = new(3); + public Dictionary> nonSelfClosingElements = new(3); + public int Count { get; private set; } = 0; + public void AddElement(DPDSXElement element) + { + Count++; + var newLinkedList = new LinkedList(); + if (element.IsSelfClosingElement) + { + if (!selfClosingElements.TryAdd(element.TagName, newLinkedList)) + { + selfClosingElements[element.TagName].AddLast(element); + } + newLinkedList.AddFirst(element); + } + else + { + if (!nonSelfClosingElements.TryAdd(element.TagName, newLinkedList)) + { + nonSelfClosingElements[element.TagName].AddLast(element); + } + else + { + newLinkedList.AddFirst(element); + } + } + } + + + public IEnumerable GetAllElements() => selfClosingElements.Values.SelectMany(list => list) + .Concat(nonSelfClosingElements.Values.SelectMany(list => list)); + + public List FindElementViaTag(string tagName) + { + if (nonSelfClosingElements.ContainsKey(tagName)) + { + return new List(nonSelfClosingElements[tagName]); + } + if (selfClosingElements.ContainsKey(tagName)) + { + return new List(selfClosingElements[tagName]); + } + return new List(0); + } + } +} diff --git a/src/DAZ_Installer.Core/DPDSXFile.cs b/src/DAZ_Installer.Core/DPDSXFile.cs new file mode 100644 index 0000000..31eaa59 --- /dev/null +++ b/src/DAZ_Installer.Core/DPDSXFile.cs @@ -0,0 +1,116 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.IO; +using Serilog; + +namespace DAZ_Installer.Core +{ + /// + /// A special class that marks the DPFile as .dsx file which typically is a Supplement file or a Manifest file. + /// + public class DPDSXFile : DPFile + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + public bool isSupplementFile, isManifestFile = false; + public bool isSupportingFile = false; + public bool contentChecked = false; + public DPContentInfo ContentInfo = new(); + + public DPDSXFile(string _path, DPArchive arc, DPFolder? __parent) : + base(_path, arc, __parent) + { + arc.DSXFiles.Add(this); + if (FileName == "Manifest.dsx") arc.ManifestFiles.Add(this); + Tags.Clear(); // Do not include us. + } + + /// + /// Reads the contents of this file and updates the ContentInfo variables. + /// This is usually called for the Support dsx file in the Runtime/Support folder. + /// + public void CheckContents(StreamReader stream) + { + if (AssociatedArchive is null) return; + var parser = new DPDSXParser(stream); + DPDSXElementCollection collection = parser.GetDSXFile(); + List search = collection.FindElementViaTag("ProductName"); + if (search.Count != 0) + { + AssociatedArchive.ProductInfo.ProductName = search[0].attributes["VALUE"]; + } + search = collection.FindElementViaTag("Artist"); + foreach (DPDSXElement artist in search) + { + ContentInfo.Authors.Add(artist.attributes["VALUE"]); + AssociatedArchive.ProductInfo.Authors.Add(artist.attributes["VALUE"]); + } + search = collection.FindElementViaTag("ProductToken"); + if (search.Count != 0) + { + ContentInfo.ID = search[0].attributes["VALUE"]; + AssociatedArchive.ProductInfo.SKU = ContentInfo.ID; + } + contentChecked = true; + } + + /// + /// Opens the manifest file, and returns a map with the keys being the full path of the file and the value being the path without "Content\" included. + /// This is only for Daz Product since they require this manifest. + /// + /// + /// Returns a dictionary containing files to extract and their destination. + /// Key is the file path in the archive, and value is the path relative to Content folder. + /// + public Dictionary GetManifestDestinations() + { + var dict = new Dictionary(); + try + { + if (!Extracted) + { + Logger.Error("Cannot get manifest destinations for a file that has not been extracted."); + return dict; + } + if (FileInfo is null) { + Logger.Error("FileInfo is null. Cannot get manifest destinations"); + return dict; + } + if (!FileInfo.TryAndFixOpenRead(out var stream, out var ex)) + { + Logger.Error(ex, "Failed to open file for reading"); + return dict; + } + using var streamReader = new StreamReader(stream!); + var parser = new DPDSXParser(streamReader); + DPDSXElementCollection collection = parser.GetDSXFile(); + IEnumerable elements = collection.GetAllElements(); + dict.EnsureCapacity(collection.Count); + foreach (DPDSXElement element in elements) + { + if (element.attributes.ContainsKey("ACTION") && new string(element.TagName) == "File") + { + var target = element.attributes["TARGET"]; + if (target == "Content") + { + // Get value. + ReadOnlySpan filePath = element.attributes["VALUE"]; + ReadOnlySpan pathWithoutContent = filePath.Slice(7).TrimStart(PathHelper.GetSeperator(filePath)); + dict[filePath.ToString()] = pathWithoutContent.ToString(); + } + else if (target == "Application") + { + Logger.Warning("Got a target where the value was Application. This is not supported yet."); + } + } + } + } + catch (Exception ex) + { + Logger.Error(ex, "An unexpected error occurred while attempting to determine destination paths through the manifest."); + } + return dict; + + } + } +} \ No newline at end of file diff --git a/src/DP/DPDSXParser.cs b/src/DAZ_Installer.Core/DPDSXParser.cs similarity index 84% rename from src/DP/DPDSXParser.cs rename to src/DAZ_Installer.Core/DPDSXParser.cs index b504f50..b053804 100644 --- a/src/DP/DPDSXParser.cs +++ b/src/DAZ_Installer.Core/DPDSXParser.cs @@ -1,41 +1,38 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -using System.IO; -using System.Collections.Generic; +using Serilog; using System.Text; -using System.Threading.Tasks; -namespace DAZ_Installer.DP +namespace DAZ_Installer.Core { - internal class DPDSXParser + public class DPDSXParser { - protected readonly StreamReader stream; + protected readonly StreamReader stream = null!; protected int lastIndex = 0; - protected DPDSXElementCollection workingFileContents = new DPDSXElementCollection(); + protected DPDSXElementCollection workingFileContents = new(); protected const int BUFFER_SIZE = 16384; // in chars. 32 KB. protected int chunk = 0; - internal bool hasErrored; - internal string fileName = string.Empty; - protected Task asyncTask { get; set; } = null; + public bool hasErrored; + public string fileName = string.Empty; + protected Task asyncTask { get; set; } = null!; protected bool Disposed = false; - internal DPDSXParser(string path) + public DPDSXParser(StreamReader stream) { - fileName = Path.GetFileName(fileName); try { - stream = new StreamReader(path, Encoding.UTF8, true); + this.stream = stream; asyncTask = new Task(ReadFile); asyncTask.Start(); } catch (Exception e) { - DPCommon.WriteToLog(e); + Log.Logger.ForContext().Error(e, "Failed to open file {FileName}", fileName); stream?.Dispose(); Disposed = true; hasErrored = true; } + } ~DPDSXParser() { @@ -45,16 +42,16 @@ internal DPDSXParser(string path) protected void ReadFile() { var watch = new System.Diagnostics.Stopwatch(); - int offset = 0; + var offset = 0; DPDSXElement lastElement = null; Span chars = new char[BUFFER_SIZE]; - DPCommon.WriteToLog($"Reading file {fileName}..."); + // DPCommon.WriteToLog($"Reading file {fileName}..."); watch.Start(); try { while (stream.Read(chars) != 0) { - var tmp = GetNextElement(chars, 0, out lastIndex); + DPDSXElement? tmp = GetNextElement(chars, 0, out lastIndex); if (lastElement != null && tmp != null) { workingFileContents.AddElement(tmp); @@ -70,7 +67,7 @@ protected void ReadFile() while (lastElement != null) { var nextIndex = lastIndex; - var element = GetNextElement(chars, nextIndex, out nextIndex); + DPDSXElement? element = GetNextElement(chars, nextIndex, out nextIndex); lastElement.NextSibling = element; if (element != null) { @@ -84,24 +81,26 @@ protected void ReadFile() offset = chars.Length - 1 - lastIndex; } watch.Stop(); - DPCommon.WriteToLog($"Execution Time: {watch.ElapsedMilliseconds} ms"); - foreach (var element in workingFileContents.GetAllElements()) + // DPCommon.WriteToLog($"Execution Time: {watch.ElapsedMilliseconds} ms"); + foreach (DPDSXElement element in workingFileContents.GetAllElements()) { - DPCommon.WriteToLog($"Element Tag Name: {new string(element.TagName)}"); - foreach (var attribute in element.attributes) + // DPCommon.WriteToLog($"Element Tag Name: {new string(element.TagName)}"); + foreach (KeyValuePair attribute in element.attributes) { - DPCommon.WriteToLog($"Attribute Name: {attribute.Key} | Attribute Value: {attribute.Value}"); + // DPCommon.WriteToLog($"Attribute Name: {attribute.Key} | Attribute Value: {attribute.Value}"); } } - } catch (Exception e) + } + catch (Exception e) { - DPCommon.WriteToLog($"An error occurred while attempting to read DSX file. REASON: {e}"); - } finally + // DPCommon.WriteToLog($"An error occurred while attempting to read DSX file. REASON: {e}"); + } + finally { if (!Disposed) stream?.Dispose(); Disposed = true; } - + } /// @@ -113,7 +112,7 @@ protected void ReadFile() Memory totalMessage = Memory.Empty; // includes < and > lastIndex = -1; var workingElement = new DPDSXElement(); - string attributeName = string.Empty; + var attributeName = string.Empty; var attributeValue = new StringBuilder(30); var tagName = new StringBuilder(30); var isInTagCaptureMode = true; @@ -126,7 +125,7 @@ protected void ReadFile() { if (nextLessThanIndex != -1) { - + // stringBuild.Add('<'); for (var i = nextLessThanIndex + 1; i < arr.Length; i++) { @@ -177,8 +176,9 @@ protected void ReadFile() } } } - - if (nextLessThanIndex == -1) { + + if (nextLessThanIndex == -1) + { lastIndex = -1; return null; } @@ -192,11 +192,12 @@ protected void ReadFile() { // var ourTagName = arr[(nextLessThanIndex + 2)..lastIndex]; // TODO: Check if ourTagName == workingElement.TagName - var ourTagName = arr.Slice(nextLessThanIndex + 2, lastIndex - nextLessThanIndex + 1); - var ourElements = workingFileContents.FindElementViaTag(new string(ourTagName)); - if (ourElements?.Length != 0) + ReadOnlySpan ourTagName = arr.Slice(nextLessThanIndex + 2, lastIndex - nextLessThanIndex + 1); + List ourElements = workingFileContents.FindElementViaTag(new string(ourTagName)); + if (ourElements.Count != 0) { - foreach (var element in ourElements) { + foreach (DPDSXElement element in ourElements) + { element.MessageIncludesEnding = true; element.TotalMessage = totalMessage; var closingTagBeginningIndex = GetClosingTagLessThan(element.TotalMessage.Span, element.TotalMessage.Length); @@ -214,7 +215,7 @@ protected void ReadFile() } catch (Exception e) { - DPCommon.WriteToLog(e); + // DPCommon.WriteToLog(e); } lastIndex = -1; return null; @@ -293,7 +294,7 @@ protected static int GetClosingTagLessThan(ReadOnlySpan arr, int offset) return -1; } - internal DPDSXElementCollection GetDSXFile() + public DPDSXElementCollection GetDSXFile() { asyncTask.Wait(); return workingFileContents; diff --git a/src/DP/DPDazFile.cs b/src/DAZ_Installer.Core/DPDazFile.cs similarity index 54% rename from src/DP/DPDazFile.cs rename to src/DAZ_Installer.Core/DPDazFile.cs index 28d84f6..d165a90 100644 --- a/src/DP/DPDazFile.cs +++ b/src/DAZ_Installer.Core/DPDazFile.cs @@ -1,22 +1,24 @@ // This code is licensed under the Keep It Free License V1. // You may find a full copy of this license at root project directory\LICENSE -using System; -using System.IO; -namespace DAZ_Installer.DP { - internal class DPDazFile : DPFile { - internal DPContentInfo ContentInfo = new DPContentInfo(); - internal DPDazFile(string _path, DPFolder? __parent) : base(_path, __parent) { - DPProcessor.workingArchive.DazFiles.Add(this); - } +using Serilog; + +namespace DAZ_Installer.Core +{ + public class DPDazFile : DPFile + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + public DPContentInfo ContentInfo = new(); + public DPDazFile(string _path, DPArchive arc, DPFolder? __parent) : base(_path, arc, __parent) => arc.DazFiles.Add(this); /// /// Reads and updates ContentInfo struct. /// /// The file stream to read from. - internal void ReadContents(StreamReader stream) { - var tenLines = new string[10]; - for (var i = 0; i < 10; i++) + public void ReadContents(StreamReader stream) + { + var tenLines = new string?[10]; + for (var i = 0; i < 10; i++) tenLines[i] = stream.ReadLine(); UpdateContentInfo(tenLines); } @@ -25,34 +27,33 @@ internal void ReadContents(StreamReader stream) { /// Parses through the content info and updates the ContentInfo struct. Only reads lines 1 - 8. /// /// A collection of strings. - public void UpdateContentInfo(ReadOnlySpan contents) + private void UpdateContentInfo(ReadOnlySpan contents) { // 1..9 - foreach (var line in contents.Slice(1, 8)) + foreach (var line in contents.Slice(1, 9)) { - if (string.IsNullOrEmpty(line) || string.IsNullOrWhiteSpace(line)) continue; + if (string.IsNullOrWhiteSpace(line)) continue; try { - var propertyName = GetPropertyName(line); - if (propertyName.Contains("type", StringComparison.Ordinal)) ContentInfo.ContentType = GetContentType(ParseJsonValue(line, "type"), this); - else if (propertyName.Contains("author", StringComparison.Ordinal)) ContentInfo.Authors.Add(ParseJsonValue(line, "author")); - else if (propertyName.Contains("email", StringComparison.Ordinal)) ContentInfo.Email = ParseJsonValue(line, "email"); - else if (propertyName.Contains("website", StringComparison.Ordinal)) ContentInfo.Website = ParseJsonValue(line, "website"); + ReadOnlySpan propertyName = GetPropertyName(line); + if (propertyName.Contains("type", StringComparison.Ordinal)) ContentInfo.ContentType = GetContentType(ParseJsonValue(line), this); + else if (propertyName.Contains("author", StringComparison.Ordinal)) ContentInfo.Authors.Add(ParseJsonValue(line)); + else if (propertyName.Contains("email", StringComparison.Ordinal)) ContentInfo.Email = ParseJsonValue(line); + else if (propertyName.Contains("website", StringComparison.Ordinal)) ContentInfo.Website = ParseJsonValue(line); } catch (Exception e) { - DPCommon.WriteToLog($"Failed to add metadata for file: {RelativePath} REASON: {e}"); + Logger.Error(e, "Failed to add metadata for file: {0}"); } } } - public static string ParseJsonValue(ReadOnlySpan jsonString, string propertyName) + private static string ParseJsonValue(ReadOnlySpan jsonString) { - // Substring via propertyName length + 6. var colIndex = jsonString.IndexOf(':'); if (colIndex == -1) return string.Empty; - var afterColString = jsonString.Slice(colIndex+1); + ReadOnlySpan afterColString = jsonString.Slice(colIndex + 1); var startSearchIndex = afterColString.IndexOf('"'); var lastQuoteIndex = afterColString.LastIndexOf('"'); @@ -62,13 +63,12 @@ public static string ParseJsonValue(ReadOnlySpan jsonString, string proper return afterColString.Slice(startSearchIndex + 1, lastQuoteIndex - startSearchIndex - 1).ToString(); } - // Not accurate but it's okay. - // throws error when msg is "", colonIndex returns -1 which results in out of index error. - public static ReadOnlySpan GetPropertyName(ReadOnlySpan msg) { + private static ReadOnlySpan GetPropertyName(ReadOnlySpan msg) + { var colonIndex = msg.IndexOf(':'); - var propertyName = msg.Slice(0,colonIndex-1); - propertyName.Trim('"').TrimStart(); - return propertyName; + if (colonIndex == -1) return string.Empty; + ReadOnlySpan propertyName = msg.Slice(0, colonIndex - 1); + return propertyName.Trim('"').TrimStart(); } diff --git a/src/DAZ_Installer.Core/DPDestinationDeterminer.cs b/src/DAZ_Installer.Core/DPDestinationDeterminer.cs new file mode 100644 index 0000000..8aa9b57 --- /dev/null +++ b/src/DAZ_Installer.Core/DPDestinationDeterminer.cs @@ -0,0 +1,238 @@ +using DAZ_Installer.IO; +using Serilog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.Core +{ + public class DPDestinationDeterminer : AbstractDestinationDeterminer + { + protected override ILogger Logger { get; set; } = Log.Logger.ForContext(); + + public DPDestinationDeterminer() { } + public DPDestinationDeterminer(ILogger logger) : base(logger) { } + + public override HashSet DetermineDestinations(DPArchive arc, DPProcessSettings settings) + { + // Handle Manifest first if a combo of both. + var filesToExtract = new HashSet(arc.Contents.Count); + HashSet contentFolders = new HashSet(4); + List>? destinations = null; + if (settings.InstallOption == InstallOptions.ManifestOnly || settings.InstallOption == InstallOptions.ManifestAndAuto) + contentFolders = SetContentFoldersFromManifest(arc, out destinations); + if (settings.InstallOption == InstallOptions.ManifestAndAuto || settings.InstallOption == InstallOptions.Automatic) + DetermineContentFolders(arc, contentFolders, settings); + UpdateRelativePaths(arc, settings); + + if (settings.InstallOption != InstallOptions.ManifestOnly) + DetermineViaFileSense(arc, settings, filesToExtract); + + // Determine manifest after file sense if manifest and auto. This way the manifest can override the file sense destinations. + if (settings.InstallOption == InstallOptions.ManifestOnly || settings.InstallOption == InstallOptions.ManifestAndAuto) + DetermineFromManifests(arc, destinations, settings, filesToExtract); + return filesToExtract; + } + + private HashSet SetContentFoldersFromManifest(DPArchive arc, out List> destinations) + { + HashSet contentFolders = new HashSet(4); + destinations = new List>(arc.ManifestFiles.Count); + foreach (var manifest in arc.ManifestFiles.Where(x => x.Extracted)) + { + try + { + var dest = manifest.GetManifestDestinations(); + destinations.Add(dest); + // The first folder after the "content" folder is the content folder. + foreach (var path in dest.Values) + { + var rootDir = PathHelper.GetRootDirectory(path, true); + if (string.IsNullOrEmpty(rootDir)) continue; + var result = arc.Folders.TryGetValue(PathHelper.NormalizePath(Path.Combine("Content", rootDir)), out var folder); + if (!result) + { + Logger.Warning("Could not find content folder {0} that was defined in manifest", rootDir, arc.FileName); + continue; + } + contentFolders.Add(folder); + folder.IsContentFolder = true; + } + } catch (Exception ex) + { + Logger.Error(ex, "Failed to get manifest destinations for {0}", manifest.FileName); + } + } + return contentFolders; + } + + // TODO: Update this function to determine content folders from the manifest. + // Or make this exclusive to auto mode. + private void DetermineContentFolders(DPArchive arc, HashSet ignoreSet, in DPProcessSettings settings) + { + // A content folder is a folder whose name is contained in the user's common content folders list + // or in their folder redirects map. + + + // Prepare sort so that the first elements in folders are the ones at root. + DPFolder[] folders = arc.Folders.Values.ToArray(); + var foldersKeys = new byte[folders.Length]; + + for (var i = 0; i < foldersKeys.Length; i++) + { + foldersKeys[i] = PathHelper.GetSubfoldersCount(folders[i].Path); + } + + // Elements at the beginning are folders at root levels. + Array.Sort(foldersKeys, folders); + + foreach (DPFolder? folder in folders) + { + if (ignoreSet.Contains(folder)) continue; + var folderName = Path.GetFileName(folder.Path); + var elgibleForContentFolderStatus = settings.ContentFolders.Contains(folderName) || + settings.ContentRedirectFolders.ContainsKey(folderName); + if (folder.Parent is null) + folder.IsContentFolder = elgibleForContentFolderStatus; + else + { + if (folder.Parent.IsContentFolder || folder.Parent.IsPartOfContentFolder) continue; + folder.IsContentFolder = elgibleForContentFolderStatus; + } + } + } + /// + /// Updates the relative paths of all files and folders in the archive relative to the content folder. + /// + /// The archive to check. + /// The settings to use. + private void UpdateRelativePaths(DPArchive arc, in DPProcessSettings settings) + { + foreach (DPFile content in arc.RootContents) + content.RelativePathToContentFolder = content.RelativeTargetPath = content.Path; + foreach (DPFolder folder in arc.Folders.Values) + folder.UpdateChildrenRelativePaths(settings); + } + + /// + /// This function returns the target path based on whether it is saving to it's destination or to a + /// temporary location, whether the has a relative path or not, and whether + /// the file's parent is in folderRedirects. + /// Additionally, there is which will be used for combining paths publicly; + /// however, this will be ignored if the parent name is in the user's folder redirects. + /// + /// The file to get a target path for. + /// The process settings to use. + /// Determines whether to get a target path saving to a temporary location. + /// The path to combine with instead of usual combining. + /// The target path for the specified file. + private string GetTargetPath(DPFile file, in DPProcessSettings settings, bool saveToTemp = false, string? overridePath = null) + { + var tmpLocation = Path.Combine(settings.TempPath, "DazProductInstaller"); + // file.RelativeTargetPath already substituted the content folder name with the redirect folder name. + var filePathPart = !string.IsNullOrEmpty(overridePath) ? overridePath : file.RelativeTargetPath; + + // This is the case for files determined by file sense but are not in a content folder + // (aka archives, since they are always extracted but usually never in a content folder). + // This would result in overridePath and RelativeTargetPath being empty. + if (string.IsNullOrEmpty(filePathPart)) filePathPart = file.FileName; + + if (file.Parent is null) + return Path.Combine(saveToTemp ? tmpLocation : settings.DestinationPath, filePathPart); + + var contentFolderName = GetContentFolderManifestString(file.Path); + var hasRedirect = settings.ContentRedirectFolders!.ContainsKey(contentFolderName); + // We need to + if (overridePath is not null && hasRedirect) + return Path.Combine(settings.DestinationPath, settings.ContentRedirectFolders[contentFolderName], filePathPart.Remove(0, contentFolderName.Length + 1)); + + // If the installation method includes Automatic, then relative target path calculations have been done prior to this function. + // Therefore, we can just use the relative target path. + return Path.Combine(saveToTemp ? tmpLocation : settings.DestinationPath, + filePathPart ?? file.Parent.CalculateChildRelativeTargetPath(file, settings)); + } + + private void DetermineFromManifests(DPArchive arc, List>? dests, in DPProcessSettings settings, HashSet filesToExtract) + { + if (dests is null) dests = arc!.ManifestFiles.Select(f => f.GetManifestDestinations()).ToList(); + if (settings.InstallOption != InstallOptions.ManifestAndAuto && settings.InstallOption != InstallOptions.ManifestOnly) return; + foreach (var manifestDestinations in dests) + { + foreach (DPFile file in arc.Contents.Values) + { + try + { + if (!manifestDestinations.ContainsKey(file.Path) || filesToExtract.Contains(file)) continue; + file.TargetPath = GetTargetPath(file, settings, overridePath: manifestDestinations[file.Path]); + filesToExtract.Add(file); + } + catch (Exception ex) + { + Logger.Error("Failed to determine file to extract: {0}", file.Path); + Logger.Debug("File information: {@0}", file); + } + } + } + } + + private void DetermineViaFileSense(DPArchive arc, in DPProcessSettings settings, HashSet filesToExtract) + { + + if (settings.InstallOption != InstallOptions.Automatic && settings.InstallOption != InstallOptions.ManifestAndAuto) return; + // Get contents where file was not extracted. + Dictionary.ValueCollection folders = arc.Folders.Values; + + foreach (DPFolder folder in folders) + { + if (!folder.IsContentFolder && !folder.IsPartOfContentFolder) continue; + // Update children's relative path. + folder.UpdateChildrenRelativePaths(settings); + + foreach (DPFile child in folder.Contents) + { + //Get destination path and update child destination path. + child.TargetPath = GetTargetPath(child, settings); + + filesToExtract.Add(child); + } + } + // Now hunt down all files in folders that aren't in content folders. + foreach (DPFolder folder in folders) + { + if (folder.IsContentFolder) continue; + // Add all archives to the inner archives to process for later processing. + foreach (DPFile file in folder.Contents) + { + if (file is not DPArchive nestedArc) continue; + nestedArc.TargetPath = GetTargetPath(nestedArc, settings, true); + // Add to queue. + filesToExtract.Add(nestedArc); + } + } + + // Hunt down all files in root content. + + foreach (DPFile content in arc.RootContents) + { + if (content is not DPArchive nestedArc) continue; + nestedArc.TargetPath = GetTargetPath(nestedArc, settings, true); + // Add to queue. + filesToExtract.Add(nestedArc); + } + } + + /// + /// A function that determines the content folder of the archive based off of the manifest. + /// It returns the folder name after the "Content/" folder in the manifest in the . + /// + /// The path that the manifest has for a file. + /// The content folder of the archive. + private string GetContentFolderManifestString(string path) + { + var segments = path.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + return segments.Length > 1 ? segments[1] : string.Empty; + } + } +} diff --git a/src/DAZ_Installer.Core/DPErrorArgs.cs b/src/DAZ_Installer.Core/DPErrorArgs.cs new file mode 100644 index 0000000..194d615 --- /dev/null +++ b/src/DAZ_Installer.Core/DPErrorArgs.cs @@ -0,0 +1,17 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents an exception and a file or + /// + public class DPErrorArgs : EventArgs + { + public readonly DPAbstractNode File; + public readonly Exception Ex; + + internal DPErrorArgs(DPAbstractNode file, Exception ex) : base() + { + File = file; + Ex = ex; + } + } +} diff --git a/src/DAZ_Installer.Core/DPEventHandler.cs b/src/DAZ_Installer.Core/DPEventHandler.cs new file mode 100644 index 0000000..9ce1660 --- /dev/null +++ b/src/DAZ_Installer.Core/DPEventHandler.cs @@ -0,0 +1,8 @@ +namespace DAZ_Installer.Core +{ + public delegate void DPProcessorEventHandler(DPProcessor sender, T args); + public delegate void DPArchiveEventHandler(DPArchive archive, T args); + public delegate void DPProcessorEventHandler(DPProcessor sender); + public delegate void DPArchiveEventHandler(DPArchive archive); + +} diff --git a/src/DAZ_Installer.Core/DPFile.cs b/src/DAZ_Installer.Core/DPFile.cs new file mode 100644 index 0000000..4ff21cf --- /dev/null +++ b/src/DAZ_Installer.Core/DPFile.cs @@ -0,0 +1,254 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.IO; +using Serilog; +using IOPath = System.IO.Path; +namespace DAZ_Installer.Core +{ + /// + /// A is a regular file that can be found in a or a . + /// + public class DPFile : DPAbstractNode + { + // Public static members + private static Dictionary enumPairs { get; } = new Dictionary(Enum.GetValues(typeof(ContentType)).Length); + public static readonly HashSet DAZFormats = new() { "duf", "dsa", "dse", "daz", "dsf", "dsb", "dson", "ds", "dsb", "djl", "dsx", "dsi", "dcb", "dbm", "dbc", "dbl", "dso", "dsd", "dsv" }; + public static readonly HashSet GeometryFormats = new() { "dae", "bvh", "fbx", "obj", "dso", "abc", "mdd", "mi", "u3d" }; + public static readonly HashSet MediaFormats = new() { "png", "jpg", "hdr", "hdri", "bmp", "gif", "webp", "eps", "raw", "tiff", "tif", "psd", "xcf", "jpeg", "cr2", "svg", "apng", "avif" }; + public static readonly HashSet DocumentFormats = new() { "txt", "pdf", "doc", "docx", "odt", "html", "ppt", "pptx", "xlsx", "xlsm", "xlsb", "rtf" }; + public static readonly HashSet OtherFormats = new() { "exe", "lib", "dll", "bat", "cmd" }; + public static readonly HashSet AcceptableImportFormats = new() { "rar", "zip", "7z", "001" }; + public List Tags { get; set; } = new List(0); + /// + /// The FileInfo object to use for moving, copying, and deleting files. Typically this is a . + /// + public IDPFileInfo? FileInfo { get; set; } + /// + /// The logger to use; typically this is of type . If you override this, make sure to use . + /// + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + #region Extraction Properties + // Properties that are generally used for extraction. + + /// + /// Determines whether the file has been extracted or not; this simply checks if is null or not. + /// This is because the extractor will not set until the file has been extracted. This does not necessarily + /// mean that the file has been extracted to the target path (eg. extracted to temp first). + /// If you wish to check if the file has been extracted to the target path, + /// use . + /// + /// + public bool Extracted => FileInfo != null; + /// + /// Determines whether the file has been extracted to the target path or not. + /// + public bool ExtractedToTarget => FileInfo != null && !string.IsNullOrEmpty(TargetPath) && FileInfo.Path == IOPath.GetFullPath(TargetPath); + /// + /// The FileInfo object that represents the file that is on disk. This should only be set during initialization + /// or by the extractor. + /// + #endregion + + // TO DO : Add get tags func. + // TO DO: Add static function to search for a property. + static DPFile() + { + foreach (var eName in Enum.GetNames(typeof(ContentType))) + { + var lowercasedName = eName.ToLower(); + enumPairs[lowercasedName] = (ContentType)Enum.Parse(typeof(ContentType), eName); + } + } + /// + /// A constructor that does nothing. + /// + public DPFile() { } + + public DPFile(string _path, DPArchive? arc, DPFolder? __parent) : base(_path, arc) + { + ArgumentNullException.ThrowIfNull(_path, nameof(_path)); + if (GetType() == typeof(DPFile)) Logger.Debug("Creating new DPFile for {0}", Path); + Parent = __parent; + InitializeTagsList(); + + if (arc is null) return; + AssociatedArchive = arc; + if (!arc.Contents.TryAdd(NormalizedPath, this)) + throw new Exception("File already exists in this archive."); + } + + public DPFile(string _path, DPArchive? arc, DPFolder? __parent, IDPFileInfo? fileInfo, ILogger logger) : this(_path, arc, __parent) + { + FileInfo = fileInfo; + Logger = logger; + } + + public static DPFile CreateNewFile(string path, DPArchive? arc, DPFolder? parent) + { + var ext = GetExtension(path); + if (ext == "dsf" || ext == "duf") + { + return new DPDazFile(path, arc!, parent); + } + else if (ext == "dsx") + { + return new DPDSXFile(path, arc!, parent); + } + else if (AcceptableImportFormats.Contains(ext)) + return new DPArchive(path, arc, parent); + return new DPFile(path, arc, parent); + } + + /// + /// Attempts to move the file to the given path. This simply calls to move + /// the file in file system space (not in archive space). Throws exceptions. + /// + /// The path to move to (must exist and have access to it). + public void MoveTo(string path) => FileInfo?.MoveTo(path, true); + /// + /// Attempts to delete the file in file system space (not archive space). This simply calls to delete the file. Throws exceptions. + /// + public void Delete() => FileInfo?.Delete(); + + /// + /// Updates the parent of the file (or archive). + /// + /// The folder that will be the new parent of the file (or archive). + protected override void UpdateParent(DPFolder? newParent) + { + // If we were null, but now we're not... + if (parent == null && newParent != null) + { + // Remove ourselves from root content of the working archive. + // AssociatedArchive shouldn't be null at the point. + AssociatedArchive!.RootContents.Remove(this); + + // Call the folder's addChild() to add ourselves to the children list. + newParent.AddChild(this); + parent = newParent; + } + else if (parent == null && newParent == null) + { + // If associated archive is null, then there are no parents to look for. + // This should only happen when the file is an archive to be processed/extracted. + // Any other DPFile should have an associated archive. + if (AssociatedArchive is null) + { + parent = null; + return; + } + // Try to find a parent. + DPFolder? potParent = AssociatedArchive.FindParent(this); + + // If we found a parent, then update it. This function will be called again. + if (potParent != null) Parent = potParent; + else + { + // Create a folder for us. + potParent = DPFolder.CreateFoldersForFile(Path, AssociatedArchive); + + // If we have successfully created a folder for us, then update it. This function will be called again. + if (potParent != null) Parent = potParent; + else // Otherwise, we are supposed to be at root. + { + parent = null; + if (!AssociatedArchive.RootContents.Contains(this)) + AssociatedArchive.RootContents.Add(this); + } + } + } + else if (parent != null && newParent != null) + { + // Remove ourselves from previous parent children. + parent.RemoveChild(this); + + // Add ourselves to new parent's children. + newParent.AddChild(this); + + parent = newParent; + } + else if (parent != null && newParent == null) + { + // Remove ourselves from previous parent's children. + parent.RemoveChild(this); + + // Add ourselves to the archive's root content list. + // AssoiciatedArchive should never be null at this point. + AssociatedArchive!.RootContents.Add(this); + parent = newParent; + } + } + + /// + /// Extracts the current file to . If the is not on disk, + /// then it will be extracted first. + /// + /// The extract settings to use. + /// Whether the extraction was successful or not. + public bool Extract(DPExtractSettings settings) + { + if (AssociatedArchive is null) return false; + return AssociatedArchive!.ExtractContent(this, settings.TempPath, settings.OverwriteFiles); + } + + /// + /// Extracts the current file to by setting to and extracting. + /// + /// The extract settings to use. + /// Whether the extraction was successful or not. + /// + public bool Extract(DPExtractSettings settings, string dest) + { + TargetPath = dest; + return Extract(settings); + } + + /// + /// Extracts the current file. If the file is not extracted, then it will be extracted. Otherwise, nothing will happen. + /// + /// The extract settings to use; only will be honored. + /// + public bool ExtractToTemp(DPExtractSettings settings) + { + if (AssociatedArchive is null) return false; + return AssociatedArchive.ExtractContentsToTemp(new DPExtractSettings(settings.TempPath, new[] { this }, archive: AssociatedArchive)).SuccessPercentage == 1; + } + + public static ContentType GetContentType(string type, DPFile dP) + { + if (!string.IsNullOrEmpty(type) && enumPairs.TryGetValue(type, out ContentType contentType)) + return contentType; + if (dP is null) return ContentType.DAZ_File; + if (GeometryFormats.Contains(dP.Ext)) + return ContentType.Geometry; + else if (MediaFormats.Contains(dP.Ext)) + return ContentType.Media; + else if (DocumentFormats.Contains(dP.Ext)) + return ContentType.Document; + else if (OtherFormats.Contains(dP.Ext)) + return ContentType.Program; + else if (DAZFormats.Contains(dP.Ext)) + return ContentType.DAZ_File; + + // The most obvious comment ever - implied else :\ + return ContentType.Unknown; + } + + public static bool ValidImportExtension(string ext) => AcceptableImportFormats.Contains(ext); + + /// + /// Adds the file name to the tags name. + /// + protected void InitializeTagsList() + { + var fileName = IOPath.GetFileName(Path); + var tokens = fileName.Split(' '); + Tags = new List(tokens.Length); + Tags.AddRange(tokens); + } + + } + +} diff --git a/src/DAZ_Installer.Core/DPFolder.cs b/src/DAZ_Installer.Core/DPFolder.cs new file mode 100644 index 0000000..b9ec8ea --- /dev/null +++ b/src/DAZ_Installer.Core/DPFolder.cs @@ -0,0 +1,258 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.IO; +using Serilog; +using IOPath = System.IO.Path; +namespace DAZ_Installer.Core +{ + public class DPFolder : DPAbstractNode + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + public override string NormalizedPath => PathHelper.NormalizePath(Path); + public List subfolders = new(); + private readonly Dictionary contents = new(); + public ICollection Contents => contents.Values; + public bool IsContentFolder { get; set; } + /// + /// Determined later in ProcessArchive(). + /// + public bool IsPartOfContentFolder => (Parent?.IsPartOfContentFolder ?? false) || (Parent?.IsContentFolder ?? false); + public DPFolder(string path, DPArchive arc, DPFolder? parent) : base(path, arc) + { + Logger.Debug("Creating folder for {Path}", path); + // ZipArchive returns folders with a trailing slash, so we need to remove it. + // Potentially others may do the same. + Path = PathHelper.CleanDirPath(path); + + //if (relativePathBase != null) + //{ + // relativePath = Path.GetRelativePath(path, relativePathBase); + //} + Parent = parent; + arc.Folders.TryAdd(Path, this); + + } + + /// + /// Create folder (and subfolders) for file. This is used when a file is added to the archive and the folder it is in does not exist. + /// This can occur when certain extractors discover files first rather than folders. + /// + /// + /// + /// + public static DPFolder CreateFoldersForFile(string dpFilePath, DPArchive associatedArchive) + { + var workingStr = PathHelper.Up(dpFilePath); + DPFolder firstFolder = null; + DPFolder previousFolder = null; + + // Continously get relative path. + while (workingStr != "") + { + if (associatedArchive.FindFolder(workingStr, out _)) + { + workingStr = PathHelper.Up(workingStr); + continue; + } + if (firstFolder == null) + { + firstFolder = new DPFolder(workingStr, associatedArchive, null); + previousFolder = firstFolder; + } + else + { + var workingParent = new DPFolder(workingStr, associatedArchive, previousFolder); + previousFolder = workingParent; + } + workingStr = PathHelper.Up(workingStr); + } + return firstFolder!; + } + + public void UpdateChildrenRelativePaths(DPProcessSettings settings) + { + DPFolder? contentFolder = IsContentFolder ? this : GetContentFolder(); + if (contentFolder is null) + { + Logger.Warning("Content folder was null, could not update relative paths for {Path}", Path); + return; + } + foreach (DPFile child in contents.Values) + { + // This prevents the code for running twice on a child that was previously processed when ManifestAndAuto is on. + if (!string.IsNullOrEmpty(child.RelativePathToContentFolder) && !string.IsNullOrEmpty(child.RelativeTargetPath)) + continue; + child.RelativePathToContentFolder = contentFolder.CalculateChildRelativePath(child); + child.RelativeTargetPath = contentFolder.CalculateChildRelativeTargetPath(child, settings); + } + } + + /// + /// Calculates the path of a child relative to this folder. + /// + /// The child of this folder. + /// A string representing the relative path of the child relative to this folder. + public string CalculateChildRelativePath(DPAbstractNode child) => PathHelper.GetRelativePathOfRelativeParent(child.Path, Path); + + /// + /// Calculates the target path of a child relative to this folder. Requires the settings object to + /// check if the folder name is in . + /// + /// The child of this folder. + /// The settings object in use. + /// A string representing the target path of the child relative to this folder. + public string CalculateChildRelativeTargetPath(DPAbstractNode child, DPProcessSettings settings) + { + // TODO: In Processor, make sure the ContentRedirectFolders is never null. + var containsKey = settings.ContentRedirectFolders.ContainsKey(FileName); + if (!IsContentFolder || !containsKey) return child.RelativePathToContentFolder!; + + var i = Path.LastIndexOf(PathHelper.GetSeperator(Path)); + var newPath = PathHelper.NormalizePath( + i != -1 ? string.Concat(Path.AsSpan(0, i + 1), settings.ContentRedirectFolders[FileName]) : settings.ContentRedirectFolders[FileName] + ); + var childNewPath = PathHelper.NormalizePath(child.Path); + i = childNewPath.IndexOf(Path); + if (i != -1) childNewPath = childNewPath.Remove(i, Path.Length).Insert(i, newPath); + + return PathHelper.GetRelativePathOfRelativeParent(childNewPath, newPath); + } + + public DPFolder? GetContentFolder() + { + if (Parent == null) return null; + DPFolder? workingFolder = this; + while (workingFolder != null && workingFolder.IsContentFolder == false) + { + workingFolder = workingFolder.Parent; + } + return workingFolder; + } + + /// + /// Handles the addition of the file to children property and subfolders property (if child is a DPFolder). + /// + /// DPFolder, DPArchive, DPFile + public void AddChild(DPAbstractNode child) + { + if (child is DPFolder folder) + { + subfolders.Add(folder); + return; + } + contents.TryAdd(child.Path, (DPFile)child); + } + + public void RemoveChild(DPAbstractNode child) + { + if (child.GetType() == typeof(DPFolder)) + { + var dpFolder = (DPFolder)child; + subfolders.Remove(dpFolder); + return; + } + contents.Remove(child.Path); + } + + public DPFolder? FindFolder(string _path) + { + if (Path == _path) return this; + else + { + foreach (DPFolder folder in subfolders) + { + DPFolder? result = folder.FindFolder(_path); + if (result != null) return result; + } + } + return null; + } + public static DPFolder[] FindChildFolders(string _path, DPFolder self) + { + var folderArr = new List(); + foreach (DPFolder folder in self.AssociatedArchive!.Folders.Values) + { + if (folder == self) continue; + // And make sure it only is one level up. + if (folder.Path.Contains(_path) && IOPath.GetFileName(_path) == IOPath.GetFileName(folder.Path) + && PathHelper.GetNumOfLevelsAbove(folder.Path, _path) == 1) + { + folderArr.Add(folder); + } + } + return folderArr.ToArray(); + } + /// + /// + /// This function removes and updates the root folders list instead of root contents list. + /// + /// The new parent for this folder. + + protected override void UpdateParent(DPFolder? newParent) + { + // If we were null, but now we're not... + if (parent == null && newParent != null) + { + // Remove ourselves from root folders list of the working archive. + try + { + AssociatedArchive!.RootFolders.Remove(this); + } + catch { } + + // Call the folder's addChild() to add ourselves to the children list. + newParent.AddChild(this); + parent = newParent; + } + else if (parent == null && newParent == null) + { + // Try to find a parent. + DPFolder? potParent = AssociatedArchive!.FindParent(this); + + // If we found a parent, then update it. This function will be called again. + if (potParent != null) + { + Parent = potParent; + } + else + { + // Otherwise, create a folder for us. + potParent = CreateFoldersForFile(Path, AssociatedArchive); + + // If we have successfully created a folder for us, then update it. This function will be called again. + if (potParent != null) Parent = potParent; + else + { // Otherwise, we are supposed to be at root. + parent = null; + if (!AssociatedArchive!.RootFolders.Contains(this)) + { + AssociatedArchive!.RootFolders.Add(this); + } + } + } + } + else if (parent != null && newParent != null) + { + // Remove ourselves from previous parent children. + parent.RemoveChild(this); + + // Add ourselves to new parent's children. + newParent.AddChild(this); + + parent = newParent; + } + else if (parent != null && newParent == null) + { + // Remove ourselves from previous parent's children. + parent.RemoveChild(this); + + // Add ourselves to the archive's root content list. + AssociatedArchive!.RootFolders.Add(this); + parent = newParent; + } + } + + + } +} diff --git a/src/DAZ_Installer.Core/DPProcessSettings.cs b/src/DAZ_Installer.Core/DPProcessSettings.cs new file mode 100644 index 0000000..4cbf2d5 --- /dev/null +++ b/src/DAZ_Installer.Core/DPProcessSettings.cs @@ -0,0 +1,85 @@ +namespace DAZ_Installer.Core +{ + /// + /// Options for how should determine which files + /// should be moved into the user's library. + /// + // TODO: Turn this into a flag, change "Automatic" to "File Sense" + // TODO: Flag 1 - Manifest, 2 - Auto, 3 - FallbackToAuto + public enum InstallOptions + { + /// + /// Will strictly read the manifest in the archive, if any, to determine + /// which files should be moved into the user's library. + /// If there is no manifest detected, then no files will be moved. + /// + ManifestOnly, + /// + /// Will rely on the manifest first then do "File Sense" by checking which files + /// are under a "Content Folder" defined in . + /// Additionally, any nested archive will be processed to find any potential products. Lastly, any files/folders + /// not under a content folder will not be moved unless it is defined in the manifest. + /// + ManifestAndAuto, + /// + /// Does "File Sense" by checking which files are under a "Content Folder" + /// defined in defined in . + /// Additionally, any nested archive will be processed to find any potential products. + /// Lastly, any files/folders not under a content folder will not be moved + /// unless it is defined in the manifest. + /// + Automatic + } + + /// + /// DPProcessSettings is used for extraction and processing operations. + /// + public struct DPProcessSettings + { + /// + /// The temporary path to use for operations. Null not allowed. + /// + public string TempPath = string.Empty; + /// + /// The final destination path to use for operations. Null not allowed. + /// + public string DestinationPath = string.Empty; + /// + /// The user-defined folder names to use as content folders. + /// If null, will use most common content folders. + /// + public HashSet? ContentFolders = null; + /// + /// The user-defined content redirect folders to redirect certain folder names to a content folder. + /// If , will use most common content redirect folders. + /// + public Dictionary? ContentRedirectFolders = null; + /// + /// The method the processor should use for determining which files should be moved into + /// the destination path or not. + /// + public InstallOptions InstallOption = InstallOptions.ManifestAndAuto; + /// + /// Determines whether the processor should overwrite files if it already exists in the user library. + /// Temp files will always be overwritten. + /// + public bool OverwriteFiles = true; + /// + /// Overrides the usual process of determining the destination of files and forces them to be moved to the specified destination in this dictionary. + /// This means that no matter what the file is, it will be moved to the specified destination. + /// + /// The file to force to a destination. + /// The destination to force the file to. + public Dictionary ForceFileToDest = new(0); + public DPProcessSettings(string temp, string dest, InstallOptions options, + HashSet? folders = null, Dictionary? redirects = null, bool overwriteFiles = true) + { + TempPath = temp; + DestinationPath = dest; + InstallOption = options; + ContentFolders = folders; + ContentRedirectFolders = redirects; + OverwriteFiles = overwriteFiles; + } + } +} diff --git a/src/DAZ_Installer.Core/DPProcessor.cs b/src/DAZ_Installer.Core/DPProcessor.cs new file mode 100644 index 0000000..ca87963 --- /dev/null +++ b/src/DAZ_Installer.Core/DPProcessor.cs @@ -0,0 +1,453 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +using DAZ_Installer.Core.Extraction; + +using DAZ_Installer.IO; +using Serilog; +using Serilog.Context; +using System.Collections.Immutable; +using System.IO.Compression; +using System.Text; +using System.Diagnostics.CodeAnalysis; + +namespace DAZ_Installer.Core +{ + // GOAL: Extract files through RAR. While it discovers files, add it to list. + // Then, deeply analyze each file; determine best approach; and execute best approach (or ask). + // Lastly, clean up. + public class DPProcessor + { + // SecureString - System.Security + // We use these variables in case the user changes the settings in mist of an extraction process + public static readonly ImmutableDictionary DefaultRedirects = ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase) + .AddRange(new KeyValuePair[]{ new("docs", "Documentation"), + new("Documents", "Documentation"), + new("Readme", "Documentation"), + new("ReadMe's", "Documentation"), + new("Readmes", "Documentation"), + new("Transport", "Vehicles"), + new("Scene", "Scenes")}); + public static readonly ImmutableHashSet DefaultContentFolders = ImmutableHashSet.Create(StringComparer.OrdinalIgnoreCase) + .Union(new[] {"aniBlocks", "Animals", "Architecture", "Camera Presets", "data", "DAZ Studio Tutorials", "Documentation", "Documents", + "Environments", "General", "Light Presets", "Lights", "People", "Presets", "Props", "Render Presets", "Render Settings", "Runtime", + "Scene Builder", "Scene Subsets", "Scenes", "Scripts", "Shader Presets", "Shaders", "Support", "Templates", "Textures", "Vehicles" }); + public DPProcessSettings CurrentProcessSettings { get; private set; } = new(); + public ILogger Logger { get; set; } = Log.Logger.ForContext(); + public AbstractFileSystem FileSystem { get; set; } = new DPFileSystem(); + public AbstractTagProvider TagProvider { get; set; } = new DPTagProvider(); + public AbstractDestinationDeterminer DestinationDeterminer { get; set; } = new DPDestinationDeterminer(); + private CancellationTokenSource CancellationTokenSource { get; set; } = new(); + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + private CancellationTokenSource ArchiveCancellationSource { get; set; } = new(); + private CancellationToken ArchiveCancellationToken { get; set; } = CancellationToken.None; + private bool ArchiveCancelled => ArchiveCancellationToken.IsCancellationRequested || CancellationToken.IsCancellationRequested; + public string TempLocation => Path.Combine(CurrentProcessSettings.TempPath, @"DazProductInstaller\"); + public string DestinationPath => CurrentProcessSettings.DestinationPath; + public DPArchive CurrentArchive { get; private set; } = null!; + public ProcessorState State { get => state; private set { state = value; StateChanged?.Invoke(); } } + + /// + /// An event that is invoked when a file that is being extracted, moved, or deleted throws an error. + /// + /// + public event DPProcessorEventHandler? FileError; + /// + /// An event that is invoked when a + /// + public event DPProcessorEventHandler? ProcessError; + public event DPProcessorEventHandler? ArchiveEnter; + public event DPProcessorEventHandler? ArchiveExit; + public event DPProcessorEventHandler? ExtractProgress; + public event DPProcessorEventHandler? MoveProgress; + public event Action? Finished; + public event Action? StateChanged; + + private ProcessorState state; + // public event FilePreMove + + /// + /// Emits the event and passes the arguments required. + /// References to determine whether should + /// cancel operations or not. + /// + private void EmitOnArchiveEnter() + { + Logger.Information("Entering archive {arc}", CurrentArchive.FileName); + if (ArchiveEnter is null) return; + var args = new DPArchiveEnterArgs(CurrentArchive); + ArchiveEnter.Invoke(this, args); + } + /// + /// Emits the event and passes the arguments required. + /// + /// Tell whether the archive had been successfully processed. + private void EmitOnArchiveExit(bool successfullyProcessed, DPExtractionReport? report) + { + if (successfullyProcessed) Logger.Information("Exiting archive {0} with success", CurrentArchive.FileName); + else Logger.Warning("Exiting archive {0} with failures", CurrentArchive.FileName); + Logger.Debug("Archive exit report: {@0}", report); + ArchiveExit?.Invoke(this, new DPArchiveExitArgs(CurrentArchive, report, successfullyProcessed)); + } + + private void EmitOnProcessError(DPProcessorErrorArgs args) + { + Logger.Error(args.Ex, args.Explaination); + if (ProcessError is null) return; + ProcessError.Invoke(this, args); + } + + private void EmitOnExtractionProgress(DPArchive _, DPExtractProgressArgs args) => ExtractProgress?.Invoke(this, args); + + private void processArchiveInternal([NotNull] DPArchive archiveFile, DPProcessSettings settings) + { + Stack archivesToProcess = new(); + Stack> parentArchives = new(); + + archivesToProcess.Push(archiveFile); + CancellationTokenSource = new(); + CancellationToken = CancellationTokenSource.Token; + while (archivesToProcess.TryPop(out DPArchive arc)) + { + CurrentArchive = arc!; + DPExtractionReport? report = null; + PopParentArchive(parentArchives); + ArchiveCancellationSource = new(); + ArchiveCancellationToken = ArchiveCancellationSource.Token; + EmitOnArchiveEnter(); + try + { + using (LogContext.PushProperty("Archive", arc.FileName)) + Logger.Information("Processing archive"); + var arcDebugInfo = new + { + Name = arc.FileName, + NestedArchive = arc.IsInnerArchive, + Path = arc.IsInnerArchive ? arc.Path : arc?.FileInfo?.Path, + Extractor = arc.Extractor?.GetType().Name, + ParentArchiveNestedArchive = arc?.AssociatedArchive?.IsInnerArchive, + ParentArchiveName = arc?.AssociatedArchive?.FileName, + ParentArchivePath = arc?.AssociatedArchive?.IsInnerArchive ?? false ? arc?.AssociatedArchive?.Path : + arc?.AssociatedArchive?.FileInfo?.Path, + ParentExtractor = arc?.AssociatedArchive?.Extractor?.GetType().Name, + }; + Logger.Debug("Archive that is about to be processed: {@Arc}", arcDebugInfo); + if (CancellationToken.IsCancellationRequested) { HandleEarlyExit(); return; } + + State = ProcessorState.Starting; + try + { + FileSystem.CreateDirectoryInfo(TempLocation).Create(); + } + catch (Exception e) + { + EmitOnProcessError(new DPProcessorErrorArgs(e, "Unable to create temp directory.") { Continuable = true }); + } + + State = ProcessorState.Peeking; + if (ArchiveCancelled) { HandleEarlyExit(); return; } + if (arc.Extractor is null) + { + EmitOnProcessError(new DPProcessorErrorArgs(null, "Unable to process archive. Potentially not an archive or archive is corrupted.")); + HandleEarlyExit(); + continue; + } + arc.Extractor.CancellationToken = ArchiveCancellationToken; + if (!tryCatch(() => arc!.PeekContents(), "Failed to peek into archive")) continue; + + // Check if we have enough room. + if (!HandleOnDestinationNotEnoughSpace()) + { + HandleEarlyExit(); + continue; + } + + State = ProcessorState.PreparingExtraction; + HashSet filesToExtract = null!; + if (!tryCatch(prepareOperations, "Failed to prepare for extraction")) continue; + if (ArchiveCancelled) { HandleEarlyExit(); continue; } + if (!tryCatch(() => filesToExtract = DestinationDeterminer.DetermineDestinations(arc, settings), "Failed to determine destinations for files")) continue; + + State = ProcessorState.Extracting; + var extractSettings = new DPExtractSettings() + { + TempPath = CurrentProcessSettings.TempPath, + Archive = arc, + FilesToExtract = filesToExtract, + OverwriteFiles = CurrentProcessSettings.OverwriteFiles, + CancelToken = ArchiveCancellationToken, + }; + + if (ArchiveCancelled) { HandleEarlyExit(); continue; } + if (!tryCatch(() => report = arc.ExtractContents(extractSettings), "Failed to extract contents for archive")) continue; + + // DPCommon.WriteToLog("We are done"); + Logger.Information("Analyzing the archive - fetching tags"); + State = ProcessorState.Analyzing; + if (!tryCatch(() => arc.Type = arc.DetermineArchiveType(), "Failed to analyze archive")) continue; + if (!tryCatch(() => TagProvider.GetTags(arc, settings), "Failed to get tags for archive")) continue; + + foreach (var subarc in arc.Subarchives.Where(x => x.Extracted)) + { + archivesToProcess.Push(subarc); + } + + // Create record. + parentArchives.Push(new Tuple(arc, report)); // TODO: Use method to determine whether an archive was successfully processed. + } + catch (Exception ex) + { + handleError(ex, "An unexpected error occured while processing archive."); + EmitOnArchiveExit(false, report); + } + } + + PopParentArchive(parentArchives); + + } + + // TODO: RetryArchive() + public void ProcessArchive(string filePath, DPProcessSettings settings) + { + validateProcessSettings(ref settings); + CurrentProcessSettings = settings; + FileSystem.Scope = setupScope(settings); + // Create new archive. + var archiveFile = DPArchive.CreateNewParentArchive(FileSystem.CreateFileInfo(filePath)); + CurrentArchive = archiveFile; + + processArchiveInternal(archiveFile, settings); + Finished?.Invoke(); + State = ProcessorState.Idle; + } + + /// + /// For testing. + /// + /// + /// + internal void ProcessArchive(DPArchive arc, DPProcessSettings settings) + { + validateProcessSettings(ref settings); + CurrentProcessSettings = settings; + FileSystem.Scope = setupScope(settings); + CurrentArchive = arc; + processArchiveInternal(arc, settings); + Finished?.Invoke(); + State = ProcessorState.Idle; + } + + /// + /// Validates the object and throws an exception for any non-nullable properties that are null + /// and defaults any nullable, null properties to their default values. + /// + /// The settings to check and manipulate (will modify if nullable, null properties are detected)./> + private static void validateProcessSettings(ref DPProcessSettings settings) + { + ArgumentNullException.ThrowIfNull(settings.DestinationPath, nameof(settings.DestinationPath)); + ArgumentNullException.ThrowIfNull(settings.TempPath, nameof(settings.TempPath)); + ArgumentNullException.ThrowIfNull(settings.ForceFileToDest, nameof(settings.ForceFileToDest)); + settings.ContentRedirectFolders ??= new(DefaultRedirects, StringComparer.OrdinalIgnoreCase); + settings.ContentFolders ??= new(DefaultContentFolders, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Creates a scope based off of DPProcessSettings. + /// + /// + private static DPFileScopeSettings setupScope(DPProcessSettings settings) + { + var filesToAllow = new List(settings.ForceFileToDest.Values); + return new DPFileScopeSettings(filesToAllow, new[] { settings.DestinationPath, settings.TempPath }, false, false, true, false); + } + + /// + /// Checks whether the destination has enough space for the archive. + /// + /// True if the destination has enough space, otherwise false. + /// An exception caused by creating the object. + private bool DestinationHasEnoughSpace() => (ulong)FileSystem.CreateDriveInfo(CurrentProcessSettings.DestinationPath).AvailableFreeSpace > CurrentArchive.TrueArchiveSize; + + /// + /// Checks whether the temp path has enough space for the archive. + /// + /// True if temp has enough space, otherwise false. + /// An exception caused by creating the object. + private bool TempHasEnoughSpace() => (ulong)FileSystem.CreateDriveInfo(CurrentProcessSettings.TempPath).AvailableFreeSpace > CurrentArchive.TrueArchiveSize; + + + // TODO: Clear temp needs to remove as much space as possible. It will error when we have file handles. + private void ClearTemp() + { + Logger.Information("Clearing temp location at {TempLocation}", TempLocation); + var tmpScope = FileSystem.Scope; + FileSystem.Scope = new DPFileScopeSettings(Array.Empty(), new[] { TempLocation }, false, throwOnPathTransversal: true); + IDPDirectoryInfo info = FileSystem.CreateDirectoryInfo(TempLocation); + if (!TryHelper.Try(() => info.Delete(true), out Exception? ex)) + Logger.Error(ex, "Failed to clear temp location"); + else Logger.Information("Cleared temp location"); + FileSystem.Scope = tmpScope; + } + + private void prepareOperations() + { + Logger.Information("Preparing operations"); + while (!ArchiveCancelled && !TempHasEnoughSpace()) + { + ClearTemp(); + if (TempHasEnoughSpace()) break; + Logger.Warning("Temp location does not have enough space after clearing temp, requesting for an action"); + // Requires user help. + var args = new DPProcessorErrorArgs(null, "Temp location does not have enough space") { Continuable = true }; + EmitOnProcessError(args); + } + if (CurrentArchive.Extractor != null) CurrentArchive.Extractor.ExtractProgress += EmitOnExtractionProgress; + else Logger.Warning("Extractor is null, cannot report extraction progress"); + ReadMetaFiles(CurrentProcessSettings); + } + + private void HandleEarlyExit() + { + State = ProcessorState.Idle; + CurrentArchive.Extractor.ExtractProgress -= EmitOnExtractionProgress; + EmitOnArchiveExit(false, null); + } + + private bool HandleOnDestinationNotEnoughSpace() + { + if (DestinationHasEnoughSpace()) return true; + while (!ArchiveCancelled && !DestinationHasEnoughSpace()) + { + var args = new DPProcessorErrorArgs(null, "Destination does not have enough space.") { Continuable = true }; + EmitOnProcessError(args); + } + return !ArchiveCancelled || DestinationHasEnoughSpace(); + } + + /// + /// Executes an action and emits the error and handles the early exit procedure if an exception occurs. + /// + /// The action to execute. + /// The additional explanation of the error + /// Whether the try-catch caught an exception or not. + private bool tryCatch(Action action, string errorMessage) + { + if (!TryHelper.Try(action, out Exception? ex)) + { + handleError(ex, errorMessage); + return false; + } + return true; + } + + private void handleError(Exception ex, string errorMessage) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, errorMessage)); + HandleEarlyExit(); + return; + } + + /// + /// Reads the files listed in . + /// + private void ReadMetaFiles(DPProcessSettings settings) + { + // Extract the DAZ Files that have not been extracted. + var extractSettings = new DPExtractSettings(settings.TempPath, + CurrentArchive!.DSXFiles.Where((f) => f.FileInfo is null || !f.FileInfo.Exists), + true, CurrentArchive); + if (ArchiveCancelled) return; + CurrentArchive.ExtractContentsToTemp(extractSettings); + Stream? stream = null!; + foreach (DPDSXFile file in CurrentArchive!.DSXFiles) + { + if (ArchiveCancelled) return; + using (LogContext.PushProperty("File", file.Path)) + // If it did not extract correctly we don't have acces, just skip it. + if (file.FileInfo is null || !file.FileInfo.Exists) + { + Logger.Warning("FileInfo was null or returned does not exist, skipping file to read meta data", file.Path); + Logger.Debug("FileInfo is null: {0}, FileInfo exists: {1}", file.FileInfo is null, file?.FileInfo?.Exists); + continue; + } + try + { + if (!file.FileInfo!.TryAndFixOpenRead(out stream, out Exception? ex)) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, "Failed to open read stream for file for reading meta")); + continue; + } + if (stream is null) + { + EmitOnProcessError(new DPProcessorErrorArgs(null, "OpenRead returned successful but also returned null stream, skipping meta read")); + continue; + } + if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) + { + // It is gzipped compressed. + stream.Seek(0, SeekOrigin.Begin); + using var gstream = new GZipStream(stream, CompressionMode.Decompress); + using var streamReader = new StreamReader(gstream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + else + { + // It is normal text. + stream.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(stream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + } + catch (Exception ex) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, $"Unable to read contents of {file.Path}")); + } + finally + { + stream?.Dispose(); + } + } + } + + /// + /// Cancels the processing of the archive. + /// + public void CancelProcessing() + { + try + { + CancellationTokenSource.Cancel(); + ArchiveCancellationSource.Cancel(); + } + catch (Exception ex) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, "Failed to cancel processing")); + } + } + /// + /// Cancels only the current archive. + /// + public void CancelCurrentArchive() + { + try + { + ArchiveCancellationSource.Cancel(); + } catch (Exception ex) + { + EmitOnProcessError(new DPProcessorErrorArgs(ex, "Failed to cancel current archive")); + } + } + private void PopParentArchive(Stack> s) + { + if (s.TryPop(out var parentArc)) + { + var temp = CurrentArchive; + var report = parentArc.Item2; + CurrentArchive = parentArc.Item1; + try { EmitOnArchiveExit(report.SuccessPercentage >= 0.1f, report); } catch { } + CurrentArchive = temp; + } + } + + } +} diff --git a/src/DAZ_Installer.Core/DPProcessorErrorArgs.cs b/src/DAZ_Installer.Core/DPProcessorErrorArgs.cs new file mode 100644 index 0000000..8ab6093 --- /dev/null +++ b/src/DAZ_Installer.Core/DPProcessorErrorArgs.cs @@ -0,0 +1,37 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents error arguments for errors in DPProcessor outside out extraction errors. + /// + public class DPProcessorErrorArgs : EventArgs + { + /// + /// The exception thrown, if any. + /// + public Exception? Ex { get; init; } + /// + /// The processor that threw the error. + /// + public DPProcessor Processor { get; internal set; } = null!; + /// + /// Additional information for the error, if any. + /// + public string Explaination { get; internal set; } = string.Empty; + /// + /// Represents whether the operation can be continued or not. Default is false. + /// + public bool Continuable { get; internal set; } = false; + /// + /// + /// + /// The exception thrown by the error, if any. + /// The additional explaination for the error/situation. + /// The corresponding archive the + internal DPProcessorErrorArgs(Exception? ex = null, string? explaination = null) : base() + { + Ex = ex; + if (explaination != null) + Explaination = explaination; + } + } +} diff --git a/src/DAZ_Installer.Core/DPProcessorState.cs b/src/DAZ_Installer.Core/DPProcessorState.cs new file mode 100644 index 0000000..9370bd8 --- /dev/null +++ b/src/DAZ_Installer.Core/DPProcessorState.cs @@ -0,0 +1,30 @@ +namespace DAZ_Installer.Core +{ + public enum ProcessorState + { + /// + /// The processor is idle and not doing anything. + /// + Idle, + /// + /// The processor is starting up and preparing to process an archive (including nested). + /// + Starting, + /// + /// The processor is determining which files to extract and to where. + /// + PreparingExtraction, + /// + /// The processor has identified files to extract and is extracting them. + /// + Extracting, + /// + /// The processor is currently reading the files to extract. + /// + Peeking, + /// + /// The processor is analyzing the files, fetching tags, reading metadata, etc. + /// + Analyzing, + } +} diff --git a/src/DAZ_Installer.Core/DPProductInfo.cs b/src/DAZ_Installer.Core/DPProductInfo.cs new file mode 100644 index 0000000..dc5962a --- /dev/null +++ b/src/DAZ_Installer.Core/DPProductInfo.cs @@ -0,0 +1,44 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +namespace DAZ_Installer.Core +{ + public struct DPProductInfo + { + /// + /// The name of the product. + /// + public string ProductName = string.Empty; + /// + /// The tags auto-generated from the archive. + /// + public HashSet Tags = new(3); + /// + /// The authors identified from the archive. + /// + public HashSet Authors = new(1); + /// + /// The SKU of the product. + /// + public string SKU = string.Empty; + + /// + /// Creates a new instance of . + /// + public DPProductInfo() { } + /// + /// Creates a new instance of . + /// + /// The product name observed from the archive. + /// The authors identified from the archive. + /// The SKU of the product. + /// Tags generated based on parsed archive contents. + public DPProductInfo(string productName, HashSet? authors = null, string? sku = null, HashSet? tags = null) + { + if (tags != null) Tags = tags; + if (authors != null) Authors = authors; + if (sku != null) SKU = sku; + ProductName = productName; + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.Core/DPTagProvider.cs b/src/DAZ_Installer.Core/DPTagProvider.cs new file mode 100644 index 0000000..96eff9c --- /dev/null +++ b/src/DAZ_Installer.Core/DPTagProvider.cs @@ -0,0 +1,173 @@ +using DAZ_Installer.IO; +using System.IO.Compression; +using DAZ_Installer.Core.Extraction; +using System.Text; +using Serilog.Context; + +namespace DAZ_Installer.Core +{ + /// + /// Provides tags for s and updates them in the archive. + /// + public class DPTagProvider : AbstractTagProvider + { + /// + public override HashSet GetTags(DPArchive arc, DPProcessSettings settings) + { + // First is always author. + // Next is folder names. + + // Read DAZ files to see if we can get some juicy information such as + // the author, content type, website, email, etc. + ReadContentFiles(arc, settings); + // Read the meta files to get the product name(s) and other information. + ReadMetaFiles(arc, settings); + var tagsSet = new HashSet(StringComparer.OrdinalIgnoreCase); + tagsSet.EnsureCapacity(arc.GetEstimateTagCount() + 5 + + (arc.Folders.Count * 2) + ((arc.Contents.Count - arc.Subarchives.Count) * 2)); + foreach (DPDazFile file in arc.DazFiles) + { + DPContentInfo contentInfo = file.ContentInfo; + if (contentInfo.Website.Length != 0) tagsSet.Add(contentInfo.Website); + if (contentInfo.Email.Length != 0) tagsSet.Add(contentInfo.Email); + if (contentInfo.ContentType != ContentType.Unknown) tagsSet.Add(contentInfo.ContentType.ToString()); + tagsSet.UnionWith(contentInfo.Authors); // <-- I think this can be omitted since arc.ProductInfo includes + // the authors already. + } + foreach (DPFile content in arc.Contents.Values) + { + if (content is DPArchive) continue; + tagsSet.UnionWith(Path.GetFileNameWithoutExtension(content.FileName).Split(' ')); + } + foreach (KeyValuePair folder in arc.Folders) + { + tagsSet.UnionWith(PathHelper.GetFileName(folder.Key).Split(' ')); + } + tagsSet.UnionWith(arc.ProductInfo.Authors); + tagsSet.UnionWith(DPArchive.RegexSplitName(Path.GetFileNameWithoutExtension(arc.FileName))); + if (arc.ProductInfo.SKU.Length != 0) tagsSet.Add(arc.ProductInfo.SKU); + if (!string.IsNullOrEmpty(arc.ProductName)) tagsSet.Add(arc.ProductName); + return arc.ProductInfo.Tags = tagsSet; + } + + /// + /// Reads files that have the extension .dsf and .duf after it has been extracted. + /// + private void ReadContentFiles(DPArchive arc, DPProcessSettings settings) + { + // Extract the DAZ Files that have not been extracted. + var extractSettings = new DPExtractSettings(settings.TempPath, + arc!.DazFiles.Where(f => f.FileInfo is null || !f.FileInfo.Exists), + true, arc); + if (extractSettings.FilesToExtract.Count > 0) arc.ExtractContentsToTemp(extractSettings); + Stream? stream = null; + // Read the contents of the files. + foreach (DPDazFile file in arc!.DazFiles.Where(f => f.FileInfo?.Exists ?? false)) + { + using (LogContext.PushProperty("File", file.Path)) + // If it did not extract correctly we don't have acces, just skip it. + if (file.FileInfo is null || !file.FileInfo.Exists) + { + Logger.Error("File does not exist on disk (or does not have access to it)."); + continue; + } + try + { + if (!file.FileInfo!.TryAndFixOpenRead(out stream, out Exception? ex)) + { + Logger.Error(ex, $"Failed to open read stream for file: {file.Path}"); + continue; + } + if (stream is null) + { + Logger.Error($"OpenRead returned successful but also returned null stream, skipping {file.Path}"); + continue; + } + if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) + { + // It is gzipped compressed. + stream.Seek(0, SeekOrigin.Begin); + using var gstream = new GZipStream(stream, CompressionMode.Decompress); + using var streamReader = new StreamReader(gstream, Encoding.UTF8, true); + file.ReadContents(streamReader); + } + else + { + // It is normal text. + stream.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(stream, Encoding.UTF8, true); + file.ReadContents(streamReader); + } + } + catch (Exception ex) + { + Logger.Error(ex, $"Unable to read contents of {file}"); + } + finally + { + stream?.Dispose(); + } + } + } + + /// + /// Reads the files listed in . + /// + private void ReadMetaFiles(DPArchive arc, DPProcessSettings settings) + { + // Extract the DAZ Files that have not been extracted. + var extractSettings = new DPExtractSettings(settings.TempPath, + arc!.DSXFiles.Where((f) => f.FileInfo is null || !f.FileInfo.Exists), + true, arc); + arc.ExtractContentsToTemp(extractSettings); + Stream? stream = null!; + foreach (DPDSXFile file in arc!.DSXFiles) + { + using (LogContext.PushProperty("File", file.Path)) + // If it did not extract correctly we don't have acces, just skip it. + if (file.FileInfo is null || !file.FileInfo.Exists) + { + Logger.Warning("FileInfo was null or returned does not exist, skipping file to read meta data", file.Path); + Logger.Debug("FileInfo is null: {0}, FileInfo exists: {1}", file.FileInfo is null, file?.FileInfo?.Exists); + continue; + } + try + { + if (!file.FileInfo!.TryAndFixOpenRead(out stream, out Exception? ex)) + { + Logger.Error(ex, $"Failed to open read stream for file for reading meta: {file.Path}"); + continue; + } + if (stream is null) + { + Logger.Error($"OpenRead returned successful but also returned null stream, skipping {file.Path}"); + continue; + } + if (stream.ReadByte() == 0x1F && stream.ReadByte() == 0x8B) + { + // It is gzipped compressed. + stream.Seek(0, SeekOrigin.Begin); + using var gstream = new GZipStream(stream, CompressionMode.Decompress); + using var streamReader = new StreamReader(gstream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + else + { + // It is normal text. + stream.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(stream, Encoding.UTF8, true); + file.CheckContents(streamReader); + } + } + catch (Exception ex) + { + Logger.Error(ex, $"Unable to read contents of {file.Path}"); + } + finally + { + stream?.Dispose(); + } + } + } + } +} diff --git a/src/DAZ_Installer.Core/DPTaskManager.cs b/src/DAZ_Installer.Core/DPTaskManager.cs new file mode 100644 index 0000000..fe0bcb3 --- /dev/null +++ b/src/DAZ_Installer.Core/DPTaskManager.cs @@ -0,0 +1,320 @@ +// This code is licensed under the Keep It Free License V1. +// You may find a full copy of this license at root project directory\LICENSE + +namespace DAZ_Installer.Core +{ + // Notes about Tasks, TaskFactory: + // (1) Token still works even after being disposed. + // (2) Source still semi-works after dispose call. Token is "disposed". + // (3) Tasks will continue to run unless you explicitly use the token and current task scheduler. + public struct DPTaskManager + { + public delegate void QueueAction(CancellationToken token); + public delegate void QueueAction(T arg1, CancellationToken token); + public delegate void QueueAction(T1 arg1, T2 arg2, CancellationToken token); + public delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, CancellationToken token); + public delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, CancellationToken token); + public delegate void QueueAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, CancellationToken token); + + + + private CancellationTokenSource _source; + private TaskFactory _taskFactory; + private CancellationToken _token; + private volatile Task? lastTask; + private object lockObj = new(); + // (3) Tasks will continue with continueWith() chain unless this is passed in. + private const TaskContinuationOptions _continuationOptions = TaskContinuationOptions.NotOnCanceled; + + // According to documentation, Scheduler may (and usually is) null, if null use Current property. + // Check each time by using property instead of raw field. + private TaskScheduler _scheduler => _taskFactory.Scheduler ?? TaskScheduler.Current; + + public DPTaskManager() + { + _source = new CancellationTokenSource(); + _token = _source.Token; + _taskFactory = new TaskFactory(_token); + lastTask = null; + } + + private void Reset() + { + lock (lockObj) + { + _source.Dispose(); + _source = new CancellationTokenSource(); + _token = _source.Token; + _taskFactory = new TaskFactory(_token); + lastTask = null; + } + } + + public void Stop() + { + lock (lockObj) + { + _source.Cancel(); + Reset(); + } + } + + public void StopAndWait() + { + lock (lockObj) + { + _source.Cancel(); + lastTask?.Wait(); + Reset(); + } + } + #region Queue methods + + public Task AddToQueue(Action action) + { + CancellationToken t = _token; + Task task; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(action, _token); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(), t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(QueueAction action) + { + CancellationToken t = _token; + Task task; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(t), t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(QueueAction action, T arg) + { + CancellationToken t = _token; + Task task; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg, t), t, _continuationOptions, _scheduler); + } + } + return task; + } + + + public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2) + { + CancellationToken t = _token; + Task task; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, t), t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, arg4, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, arg4, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(QueueAction action, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + CancellationToken t = _token; + Task task = lastTask; + + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => action(arg1, arg2, arg3, arg4, arg5, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => action(arg1, arg2, arg3, arg4, arg5, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + + public Task AddToQueue(Func func) + { + CancellationToken t = _token; + Task task = lastTask; + + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, T1 arg1) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, T1 arg1, T2 arg2) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, + T1 arg1, T2 arg2, T3 arg3) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, + T1 arg1, T2 arg2, T3 arg3, T4 arg4) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, arg4, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, arg4, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + public Task AddToQueue(Func func, + T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + { + CancellationToken t = _token; + Task task = lastTask; + lock (lockObj) + { + if (lastTask == null) + { + task = lastTask = Task.Factory.StartNew(() => func(arg1, arg2, arg3, arg4, arg5, t)); + } + else + { + task = lastTask = lastTask.ContinueWith((_) => func(arg1, arg2, arg3, arg4, arg5, t), + t, _continuationOptions, _scheduler); + } + } + return task; + } + #endregion + + + + } +} diff --git a/src/External/7za.exe b/src/DAZ_Installer.Core/External/7za.exe similarity index 100% rename from src/External/7za.exe rename to src/DAZ_Installer.Core/External/7za.exe diff --git a/src/External/RAR.cs b/src/DAZ_Installer.Core/External/RAR.cs similarity index 85% rename from src/External/RAR.cs rename to src/DAZ_Installer.Core/External/RAR.cs index df21cda..30221cf 100644 --- a/src/External/RAR.cs +++ b/src/DAZ_Installer.Core/External/RAR.cs @@ -1,13 +1,11 @@ -using System; -using System.IO; +using System.Collections; +using DAZ_Installer.Core.Extraction; using System.Runtime.InteropServices; -using System.Collections; -using DAZ_Installer.DP; namespace DAZ_Installer.External { // GC Collect - public class RAR : IDisposable + public class RAR : IRAR { #region Event Delegate Definitions @@ -15,27 +13,27 @@ public class RAR : IDisposable /// /// Represents the method that will handle data available events /// - public delegate void DataAvailableHandler(RAR sender, DataAvailableEventArgs e); + public delegate void DataAvailableHandler(IRAR sender, DataAvailableEventArgs e); /// /// Represents the method that will handle extraction progress events /// - public delegate void ExtractionProgressHandler(RAR sender, ExtractionProgressEventArgs e); + public delegate void ExtractionProgressHandler(IRAR sender, ExtractionProgressEventArgs e); /// /// Represents the method that will handle missing archive volume events /// - public delegate void MissingVolumeHandler(RAR sender, MissingVolumeEventArgs e); + public delegate void MissingVolumeHandler(IRAR sender, MissingVolumeEventArgs e); /// /// Represents the method that will handle new volume events /// - public delegate void NewVolumeHandler(RAR sender, NewVolumeEventArgs e); + public delegate void NewVolumeHandler(IRAR sender, NewVolumeEventArgs e); /// /// Represents the method that will handle new file notifications /// - public delegate void NewFileHandler(RAR sender, NewFileEventArgs e); + public delegate void NewFileHandler(IRAR sender, NewFileEventArgs e); /// /// Represents the method that will handle password required events /// - public delegate void PasswordRequiredHandler(RAR sender, PasswordRequiredEventArgs e); + public delegate void PasswordRequiredHandler(IRAR sender, PasswordRequiredEventArgs e); #endregion #region RAR DLL enumerations @@ -55,7 +53,7 @@ public enum OpenMode Extract = 1 } - internal enum RarError : uint + public enum RarError : uint { EndOfArchive = 10, InsufficientMemory = 11, @@ -70,25 +68,25 @@ internal enum RarError : uint BufferTooSmall = 20, UnknownError = 21, MissingPassword = 22, - InternalError = 23, + publicError = 23, BadPassword = 24 } - internal enum Operation : uint + public enum Operation : uint { Skip = 0, Test = 1, Extract = 2 } - internal enum VolumeMessage : uint + public enum VolumeMessage : uint { Ask = 0, Notify = 1 } [Flags] - internal enum ArchiveFlags : uint + public enum ArchiveFlags : uint { Volume = 0x1, // Volume attribute (archive volume) CommentPresent = 0x2, // Archive comment present @@ -101,7 +99,7 @@ internal enum ArchiveFlags : uint FirstVolume = 0x100 // 0x0100 - First volume (set only by RAR 3.0 and later) } - internal enum CallbackMessages : uint + public enum CallbackMessages : uint { VolumeChange = 0, ProcessData = 1, @@ -299,12 +297,12 @@ private static extern void RARSetPassword(IntPtr hArcData, #region Private fields private string archivePathName = string.Empty; - private IntPtr archiveHandle = new IntPtr(0); + private IntPtr archiveHandle = new(0); private bool retrieveComment = true; private string password = string.Empty; private string comment = string.Empty; private ArchiveFlags archiveFlags = 0; - private RARHeaderDataEx header = new RARHeaderDataEx(); + private RARHeaderDataEx header = new(); private string destinationPath = string.Empty; private RARFileInfo currentFile = null; private UNRARCallback callback = null; @@ -313,15 +311,9 @@ private static extern void RARSetPassword(IntPtr hArcData, #region Object lifetime procedures - public RAR() - { - callback = new UNRARCallback(RARCallback); - } + public RAR() => callback = new UNRARCallback(RARCallback); - public RAR(string archivePathName) : this() - { - this.archivePathName = archivePathName; - } + public RAR(string archivePathName) : this() => this.archivePathName = archivePathName; ~RAR() { @@ -350,51 +342,27 @@ public void Dispose() /// public string ArchivePathName { - get - { - return archivePathName; - } - set - { - archivePathName = value; - } + get => archivePathName; + set => archivePathName = value; } /// /// Archive comment /// - public string Comment - { - get - { - return comment; - } - } + public string Comment => comment; /// /// Current file being processed /// - public RARFileInfo CurrentFile - { - get - { - return currentFile; - } - } + public RARFileInfo CurrentFile => currentFile; /// /// Default destination path for extraction /// public string DestinationPath { - get - { - return destinationPath; - } - set - { - destinationPath = value; - } + get => destinationPath; + set => destinationPath = value; } /// @@ -402,10 +370,7 @@ public string DestinationPath /// public string Password { - get - { - return password; - } + get => password; set { password = value; @@ -416,7 +381,7 @@ public string Password /// /// Archive data /// - public RAROpenArchiveDataEx arcData; + public RAROpenArchiveDataEx ArchiveData { get; private set; } #endregion @@ -432,7 +397,7 @@ public void Close() return; // Close archive - int result = RARCloseArchive(archiveHandle); + var result = RARCloseArchive(archiveHandle); // Check result if (result != 0) @@ -481,7 +446,7 @@ public void Open(string archivePathName, OpenMode openMode) // Prepare extended open archive struct ArchivePathName = archivePathName; - RAROpenArchiveDataEx openStruct = new RAROpenArchiveDataEx(); + var openStruct = new RAROpenArchiveDataEx(); openStruct.Initialize(); openStruct.ArcName = this.archivePathName + "\0"; openStruct.ArcNameW = this.archivePathName + "\0"; @@ -499,7 +464,7 @@ public void Open(string archivePathName, OpenMode openMode) // Open archive handle = RAROpenArchiveEx(ref openStruct); - arcData = openStruct; + ArchiveData = openStruct; // Check for success if (openStruct.OpenResult != 0) { @@ -554,7 +519,7 @@ public bool ReadHeader() // Read next entry currentFile = null; - int result = RARReadHeaderEx(archiveHandle, ref header); + var result = RARReadHeaderEx(archiveHandle, ref header); // Check for error or end of archive if ((RarError)result == RarError.EndOfArchive) @@ -601,7 +566,7 @@ public bool ReadHeader() } catch (Exception e) { - DPCommon.WriteToLog(e); + // DPCommon.WriteToLog(e); // Headers are encrypted. // Close archive and try again. return false; @@ -618,14 +583,14 @@ public bool ReadHeader() /// public string[] ListFiles() { - ArrayList fileNames = new ArrayList(); + var fileNames = new ArrayList(); while (ReadHeader()) { if (!currentFile.IsDirectory) fileNames.Add(currentFile.FileName); Skip(); } - string[] files = new string[fileNames.Count]; + var files = new string[fileNames.Count]; fileNames.CopyTo(files); return files; } @@ -636,7 +601,7 @@ public string[] ListFiles() /// public void Skip() { - int result = RARProcessFileW(archiveHandle, (int)Operation.Skip, string.Empty, string.Empty); + var result = RARProcessFileW(archiveHandle, (int)Operation.Skip, string.Empty, string.Empty); // Check result if (result != 0) @@ -651,7 +616,7 @@ public void Skip() /// public void Test() { - int result = RARProcessFileW(archiveHandle, (int)Operation.Test, string.Empty, string.Empty); + var result = RARProcessFileW(archiveHandle, (int)Operation.Test, string.Empty, string.Empty); // Check result if (result != 0) @@ -664,30 +629,21 @@ public void Test() /// Extracts the current file to the default destination path /// /// - public void Extract() - { - Extract(destinationPath, string.Empty); - } + public void Extract() => Extract(destinationPath, string.Empty); /// /// Extracts the current file to a specified destination path and filename /// /// Path and name of extracted file /// - public void Extract(string destinationName) - { - Extract(string.Empty, destinationName); - } + public void Extract(string destinationName) => Extract(string.Empty, destinationName); /// /// Extracts the current file to a specified directory without renaming file /// /// /// - public void ExtractToDirectory(string destinationPath) - { - Extract(destinationPath, string.Empty); - } + public void ExtractToDirectory(string destinationPath) => Extract(destinationPath, string.Empty); #endregion @@ -695,7 +651,7 @@ public void ExtractToDirectory(string destinationPath) private void Extract(string destinationPath, string destinationName) { - int result = RARProcessFileW(archiveHandle, (int)Operation.Extract, destinationPath, destinationName); + var result = RARProcessFileW(archiveHandle, (int)Operation.Extract, destinationPath, destinationName); // Check result if (result != 0) @@ -706,12 +662,12 @@ private void Extract(string destinationPath, string destinationName) private DateTime FromMSDOSTime(uint dosTime) { - int day = 0; - int month = 0; - int year = 0; - int second = 0; - int hour = 0; - int minute = 0; + var day = 0; + var month = 0; + var year = 0; + var second = 0; + var hour = 0; + var minute = 0; ushort hiWord; ushort loWord; hiWord = (ushort)((dosTime & 0xFFFF0000) >> 16); @@ -755,7 +711,7 @@ private void ProcessFileError(int result) case RarError.MissingPassword: case RarError.BadPassword: throw new IOException("Password was incorrect."); - case RarError.InternalError: + case RarError.publicError: throw new IOException("Reference error."); } @@ -763,14 +719,15 @@ private void ProcessFileError(int result) private int RARCallback(uint msg, int UserData, IntPtr p1, int p2) { - string volume = string.Empty; - string newVolume = string.Empty; - int result = -1; + var volume = string.Empty; + var newVolume = string.Empty; + var result = -1; switch ((CallbackMessages)msg) { + case CallbackMessages.VolumeChange: case CallbackMessages.VolumeChangeAgain: - volume = Marshal.PtrToStringUni(p1); // Volume it was expecting. + volume = (CallbackMessages) msg == CallbackMessages.VolumeChange ? Marshal.PtrToStringUni(p1) : Marshal.PtrToStringUni(p1); if ((VolumeMessage)p2 == VolumeMessage.Notify) result = OnNewVolume(volume); else if ((VolumeMessage)p2 == VolumeMessage.Ask) @@ -783,12 +740,13 @@ private int RARCallback(uint msg, int UserData, IntPtr p1, int p2) if (newVolume != volume) { // Encode to Unicode. - var lilEndian = System.Text.Encoding.Unicode; + System.Text.Encoding lilEndian = System.Text.Encoding.Unicode; var bytes = lilEndian.GetBytes(newVolume + "\0"); - for (int i = 0; i < bytes.Length; i++) + for (var i = 0; i < bytes.Length; i++) { Marshal.WriteByte(p1, i, bytes[i]); } + Marshal.WriteByte(p1, newVolume.Length, 0); } result = 1; } @@ -816,21 +774,21 @@ protected virtual void OnNewFile() { if (NewFile != null) { - NewFileEventArgs e = new NewFileEventArgs(currentFile); + var e = new NewFileEventArgs(currentFile); NewFile(this, e); } } protected virtual int OnPasswordRequired(IntPtr p1, int p2) { - int result = -1; + var result = -1; if (PasswordRequired != null) { - PasswordRequiredEventArgs e = new PasswordRequiredEventArgs(); + var e = new PasswordRequiredEventArgs(); PasswordRequired(this, e); if (e.ContinueOperation && e.Password.Length > 0) { - for (int i = 0; i < e.Password.Length && i < p2; i++) + for (var i = 0; i < e.Password.Length && i < p2; i++) Marshal.WriteByte(p1, i, (byte)e.Password[i]); Marshal.WriteByte(p1, e.Password.Length, 0); result = 1; @@ -844,17 +802,17 @@ protected virtual int OnPasswordRequired(IntPtr p1, int p2) } protected virtual int OnPasswordRequiredLilE(IntPtr p1, int p2) { - int result = -1; + var result = -1; if (PasswordRequired != null) { - PasswordRequiredEventArgs e = new PasswordRequiredEventArgs(); + var e = new PasswordRequiredEventArgs(); PasswordRequired(this, e); if (e.ContinueOperation && e.Password.Length > 0) { - var lilEndian = System.Text.Encoding.Unicode; + System.Text.Encoding lilEndian = System.Text.Encoding.Unicode; var bytes = lilEndian.GetBytes(e.Password + "\0"); - for (int i = 0; i < bytes.Length && i < p2; i++) Marshal.WriteByte(p1, i, bytes[i]); + for (var i = 0; i < bytes.Length && i < p2; i++) Marshal.WriteByte(p1, i, bytes[i]); //Marshal.WriteByte(p1, e.Password.Length, (byte)18); result = 1; } @@ -868,21 +826,21 @@ protected virtual int OnPasswordRequiredLilE(IntPtr p1, int p2) protected virtual int OnDataAvailable(IntPtr p1, int p2) { - int result = 1; + var result = 1; if (currentFile != null) currentFile.BytesExtracted += p2; if (DataAvailable != null) { - byte[] data = new byte[p2]; + var data = new byte[p2]; Marshal.Copy(p1, data, 0, p2); - DataAvailableEventArgs e = new DataAvailableEventArgs(data); + var e = new DataAvailableEventArgs(data); DataAvailable(this, e); if (!e.ContinueOperation) result = -1; } if (ExtractionProgress != null && currentFile != null) { - ExtractionProgressEventArgs e = new ExtractionProgressEventArgs(); + var e = new ExtractionProgressEventArgs(); e.FileName = currentFile.FileName; e.FileSize = currentFile.UnpackedSize; e.BytesExtracted = currentFile.BytesExtracted; @@ -896,10 +854,10 @@ protected virtual int OnDataAvailable(IntPtr p1, int p2) protected virtual int OnNewVolume(string volume) { - int result = 1; + var result = 1; if (NewVolume != null) { - NewVolumeEventArgs e = new NewVolumeEventArgs(volume); + var e = new NewVolumeEventArgs(volume); NewVolume(this, e); if (!e.ContinueOperation) result = -1; @@ -909,10 +867,10 @@ protected virtual int OnNewVolume(string volume) protected virtual string OnMissingVolume(string volume) { - string result = string.Empty; + var result = string.Empty; if (MissingVolume != null) { - MissingVolumeEventArgs e = new MissingVolumeEventArgs(volume); + var e = new MissingVolumeEventArgs(volume); MissingVolume(this, e); if (e.ContinueOperation) result = e.VolumeName; @@ -930,10 +888,7 @@ public class NewVolumeEventArgs public string VolumeName; public bool ContinueOperation = true; - public NewVolumeEventArgs(string volumeName) - { - VolumeName = volumeName; - } + public NewVolumeEventArgs(string volumeName) => VolumeName = volumeName; } public class MissingVolumeEventArgs @@ -941,10 +896,7 @@ public class MissingVolumeEventArgs public string VolumeName; public bool ContinueOperation = false; - public MissingVolumeEventArgs(string volumeName) - { - VolumeName = volumeName; - } + public MissingVolumeEventArgs(string volumeName) => VolumeName = volumeName; } public class DataAvailableEventArgs @@ -952,10 +904,7 @@ public class DataAvailableEventArgs public readonly byte[] Data; public bool ContinueOperation = true; - public DataAvailableEventArgs(byte[] data) - { - Data = data; - } + public DataAvailableEventArgs(byte[] data) => Data = data; } public class PasswordRequiredEventArgs @@ -967,10 +916,7 @@ public class PasswordRequiredEventArgs public class NewFileEventArgs { public RARFileInfo fileInfo; - public NewFileEventArgs(RARFileInfo fileInfo) - { - this.fileInfo = fileInfo; - } + public NewFileEventArgs(RARFileInfo fileInfo) => this.fileInfo = fileInfo; } public class ExtractionProgressEventArgs diff --git a/src/DAZ_Installer.Core/Extraction/DP7zExtractor.cs b/src/DAZ_Installer.Core/Extraction/DP7zExtractor.cs new file mode 100644 index 0000000..0b6fb92 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DP7zExtractor.cs @@ -0,0 +1,454 @@ +using DAZ_Installer.IO; +using System.Diagnostics; +using Serilog; +using Serilog.Context; + + +namespace DAZ_Installer.Core.Extraction +{ + public class DP7zExtractor : DPAbstractExtractor + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + internal IProcessFactory Factory { get; init; } = new ProcessFactory(); + private struct Entity + { + public string Path; + public bool isDirectory; + + public bool IsEmpty => Path == null; + } + private bool _hasEncryptedFiles = false; + private bool _hasEncryptedHeader = false; + private volatile IProcess? _process = null; + private bool _processHasStarted = false; + private string _arcPassword = string.Empty; + + // Peek phase variables. + private bool _peekFinished = false; + + // Extract phase variables. + private bool _extractFinished = false; + private bool _extractEventEmitted = false; + + // Extract phase variables. + private bool _moveFinished = false; + private bool _movingEventEmitted = false; + + private bool _seekingFiles = false; + + private bool _stopListening = false; + + private Entity _lastEntity = new() { }; + private DPExtractionReport workingExtractionReport = null!; + private DPArchive workingArchive = null!; + + // Flag + private bool tempOnly = false; + private DPExtractSettings workingSettings => workingExtractionReport.Settings; + private string tempFolder => Path.Combine(workingSettings.TempPath, Path.GetFileNameWithoutExtension(workingArchive.FileName)); + + public DP7zExtractor() { } + /// + /// Constructor for testing + /// + internal DP7zExtractor(ILogger logger, IProcessFactory factory) => (Logger, Factory) = (logger, factory); + public override DPExtractionReport Extract(DPExtractSettings settings) + { + using var _ = LogContext.PushProperty("Archive", settings.Archive.FileName); + return extractInternal(settings, false); + } + + public override DPExtractionReport ExtractToTemp(DPExtractSettings settings) + { + using var _ = LogContext.PushProperty("Archive", settings.Archive.FileName); + return extractInternal(settings, true); + } + + public override void Peek(DPArchive archive) + { + using var _ = LogContext.PushProperty("Archive", archive.FileName); + Reset(); + Logger.Information("Preparing to peek"); + mode = Mode.Peek; + workingArchive = archive; + FileSystem = archive.FileSystem; + EmitOnPeeking(); + _process = Setup7ZProcess(); + if (StartProcess()) + { + var time = TimeSpan.FromSeconds(120); + if (!SpinWait.SpinUntil(() => _peekFinished || CancellationToken.IsCancellationRequested, time)) + handleError(archive, $"Peek timeout of {time.TotalSeconds} seconds exceeded.", null, null, null); + } + KillProcess(); + EmitOnPeekFinished(); + Logger.Information("Peek finished"); + } + + private bool StartProcess() + { + Logger.Information("Starting 7z process"); + try + { + _process!.Start(); + _processHasStarted = true; + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + _process.StandardInput.WriteLineAsync(_arcPassword); + } + catch (Exception ex) + { + handleError(workingArchive, "Failed to start 7z process", null, null, ex); + return false; + } + return true; + } + + /// + /// Attempts to kill the process, dispose of it, and set it to null. + /// + private void KillProcess() + { + if (_process is not null) Logger.Information("Killing 7z process"); + if (_process is not null && !_process.HasExited) + Logger.Warning("7z process is being killed while it is not exited"); + try + { + if (_process is null) return; + _process.Kill(true); + _process.Dispose(); + _process.OutputDataReceived -= Handle7zOutput; + _process.ErrorDataReceived -= Handle7zErrors; + } catch (Exception ex) + { + Logger.Error(ex, "Failed to kill 7z process"); + } + _process = null; + } + + /// + /// Creates a new 7z process object depending on the current mode. + /// If the current mode is Peek, then it will tell 7z to list contents. + /// Otherwise, it will tell 7z to extract contents. + /// + /// A 7z process. + private IProcess Setup7ZProcess() + { + var process = Factory.Create(); + process.StartInfo.FileName = "7za.exe"; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardInput = true; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.CreateNoWindow = true; + process.EnableRaisingEvents = true; + process.OutputDataReceived += Handle7zOutput; + process.ErrorDataReceived += Handle7zErrors; + + if (mode == Mode.Peek) + process.StartInfo.ArgumentList.Add("l"); + else + process.StartInfo.ArgumentList.Add("x"); + process.StartInfo.ArgumentList.Add("-aoa"); // Overwrite existing files w/o prompt. + process.StartInfo.ArgumentList.Add("-slt"); // Show technical information. + // process.StartInfo.ArgumentList.Add("-bb1"); // Show names of processed files in log. + + process.StartInfo.ArgumentList.Add(workingArchive.FileInfo!.Path); + return process; + } + + private void Handle7zErrors(string? data) + { + if (_stopListening) return; + Logger.Debug("7z error output: {output}", data ?? "null"); + if (data is null) _peekFinished = true; + ReadOnlySpan msg = data; + + if (msg.Contains("Can not open encrypted archive. Wrong password?")) + handleError(workingArchive, DPArchiveErrorArgs.EncryptedArchiveExplanation, null, null, null); + // DPCommon.WriteToLog($"Handle 7z errors called! Msg: {e.Data}"); + } + + /// + /// Handles the appropriate action when receiving data from StandardOutput. + /// + private void Handle7zOutput(string? data) + { + using var _ = LogContext.PushProperty("Archive", workingArchive.FileName); + + if (CancellationToken.IsCancellationRequested || _stopListening) return; + Logger.Debug("7z output: {output}", data ?? "null"); + + var isEncrypted = _hasEncryptedFiles || _hasEncryptedHeader; + if (data == null) + { + if (mode == Mode.Peek) _peekFinished = true; + else _extractFinished = true; + + // Finalize the last 7z content. + if (data is null && !_lastEntity.IsEmpty) + { + FinalizeEntity(); + _lastEntity = new Entity { }; + } + if (tempOnly) finalizeTempOnlyOperation(); + return; + } + if (data.Contains("Enter password (will not be echoed):")) + { + handleEncryptedArchive(true); + return; + } + ReadOnlySpan msg = data; + if (mode == Mode.Peek) + { + if (msg.StartsWith("----------")) _seekingFiles = true; + if (!_seekingFiles) return; + + if (msg.StartsWith("Path")) + { + if (!_lastEntity.IsEmpty) FinalizeEntity(); + _lastEntity = new Entity { Path = msg.Slice(7).ToString() }; + } + else if (msg.StartsWith("Size")) + { + if (ulong.TryParse(msg.Slice(7), out var size)) + workingArchive.TrueArchiveSize += size; + } + else if (msg.StartsWith("Attributes")) + { + ReadOnlySpan attributes = msg.Slice("Attributes = ".Length); + _lastEntity.isDirectory = attributes.Contains("D"); + } + else if (msg.StartsWith("Encrypted = +")) handleEncryptedArchive(); + else if (msg.Contains("Errors:")) + Logger.Warning("7z reported errors while peeking"); + } + else + { + // Only check if everything really did extract if + var ok = msg.StartsWith("Everything is Ok"); + var errors = msg.Contains("Errors"); + if (!ok && !errors) return; + if (errors) Logger.Warning("7z reported errors while extracting"); + _extractFinished = true; + if (!tempOnly) MoveFiles(); + } + } + + /// + /// Emits an error due to encrypted archive and stops processing outputs. + /// + /// Set whether should be set to true or not. + private void handleEncryptedArchive(bool encryptedHeader = false) + { + handleError(workingArchive, DPArchiveErrorArgs.EncryptedFilesExplanation, null, null, null); + _extractFinished = _peekFinished = _moveFinished = _stopListening = true; + if (encryptedHeader) _hasEncryptedHeader = true; + else _hasEncryptedFiles = true; + } + + /// + /// Relocates the files from the temporary directory to their final destination. It also updates their file info. + /// + private void MoveFiles() + { + // Let anyone know that we are beginning to move files. + Logger.Information("Preparing to move files to destination"); + EmitOnMoving(); + _movingEventEmitted = true; + try + { + var i = 0UL; + var count = workingSettings.FilesToExtract.Count; + // For 7z specifically, we need to verify that the files were actually extracted and update their file info at the same time. + UpdateFileInfos(); + foreach (DPFile file in workingSettings.FilesToExtract) + { + CancellationToken.ThrowIfCancellationRequested(); + EmitOnMoveProgress(workingArchive, new DPExtractProgressArgs((byte)((float)i / count), workingArchive, file)); + IDPFileInfo? fileInfo = file.FileInfo; + // If the file did not extract to temp or it no longer exists (or we don't have access permissions), then we can't move it so we skip it. + if (fileInfo is null || !fileInfo.Exists) + { + Logger.Warning("{file} skipped due to failed extraction or insufficient permissions", file.Path); + Logger.Debug("FileInfo is null: {0} | FileInfo.Exists: {1}", fileInfo is null, fileInfo?.Exists); + continue; + } + // Create the directories needed so moving the file can be successful. + var targetDir = FileSystem.CreateDirectoryInfo(Path.GetDirectoryName(file.TargetPath)); + if (!targetDir.Exists && !targetDir.TryCreate()) + { + handleError(workingArchive, $"Failed to create directory for {file.Path}", file, workingExtractionReport, null); + continue; + } + if (fileInfo.TryAndFixMoveTo(file.TargetPath, true, out var ex)) workingExtractionReport.ExtractedFiles.Add(file); + else handleError(workingArchive, "Failed to move file to target location", file, workingExtractionReport, ex); + } + } + catch (Exception ex) + { + handleError(workingArchive, "An unknown error occured while attempting to move files to target location", null, null, ex); + } finally + { + _moveFinished = true; + } + } + + private void finalizeTempOnlyOperation() + { + UpdateFileInfos(); + foreach (DPFile file in workingSettings.FilesToExtract) + { + if (file.FileInfo is not null) workingExtractionReport.ExtractedFiles.Add(file); + else workingExtractionReport.ErroredFiles.Add(file, "Failed to extract file to temp directory"); + } + } + + /// + /// Resets the state of the extractor. + /// + private void Reset() + { + _moveFinished = _extractFinished = _movingEventEmitted = _extractEventEmitted = + _peekFinished = _seekingFiles = _hasEncryptedFiles = _hasEncryptedHeader = + _stopListening = false; + _lastEntity = new Entity { }; + KillProcess(); + _processHasStarted = false; + workingExtractionReport = null!; + workingArchive = null!; + tempOnly = false; + } + + /// + /// Updates all of the file infos in the archive that did extract. This needs to be called after the extraction is finished. + /// Since 7z extracts ALL files in the directory, we need to update all of the file infos. For example, if a file that originally wasn't + /// supposed to be moved to the user's library (e.g. unusual content folder) and now they want to move it, we can simply move the file + /// from the disk instead of extracting the entire archive again. + /// + private void UpdateFileInfos() + { + try + { + CancellationToken.ThrowIfCancellationRequested(); + foreach (DPFile file in workingArchive.Contents.Values) + { + if (file.AssociatedArchive != workingArchive) + { + handleError(workingArchive, string.Format(DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, file.Path), file, workingExtractionReport, null); + Log.Debug("File {0} Associated Archive: {1}", file.FileName, file.AssociatedArchive?.Path); + continue; + } + var fileInfo = FileSystem.CreateFileInfo(Path.Combine(tempFolder, file.Path)); + if (fileInfo.Exists) file.FileInfo = fileInfo; // Set the file info even if we did not extract it to it's final dest. + else handleError(workingArchive, $"{file.Path} did not extract successfully", file, workingExtractionReport, null); + } + } + catch (Exception ex) + { + handleError(workingArchive, "Failed to update file infos in working archive", null, null, ex); + } + } + + /// + /// FinalizeEntity indicates that the last entity is finished and can make a or a + /// It has to be done this way because 7z seperates the attributes within each line and the the callback is called + /// for each line passed in. + /// + private void FinalizeEntity() + { + if (_lastEntity.isDirectory) + // Setting DPFolder to null will automatically create parent folders if they don't exist or + // automatically add the folder to the parent folder if it does exist. + new DPFolder(_lastEntity.Path, workingArchive, null); + else + DPFile.CreateNewFile(_lastEntity.Path, workingArchive, null); + } + + private DPExtractionReport StartExtractionProcess(string tempFolder, bool tempOnly = false) + { + try + { + EmitOnExtracting(); + _process = Setup7ZProcess(); + _process.StartInfo.ArgumentList.Add("-o" + tempFolder); + if (CancellationToken.IsCancellationRequested) return workingExtractionReport; + if (!StartProcess()) return workingExtractionReport; + var time = TimeSpan.FromSeconds(120); + var extractSuccessful = false; + if (!(extractSuccessful = SpinWait.SpinUntil(() => _extractFinished || CancellationToken.IsCancellationRequested, time))) + handleError(workingArchive, $"Extraction timeout of {time.TotalSeconds} seconds exceeded.", null, null, null); + EmitOnExtractFinished(); + _extractEventEmitted = true; + Logger.Information("Extract finished"); + if (tempOnly || !extractSuccessful || CancellationToken.IsCancellationRequested) return workingExtractionReport; + if (!SpinWait.SpinUntil(() => _moveFinished || CancellationToken.IsCancellationRequested, time)) + handleError(workingArchive, $"Move timeout of {time.TotalSeconds} seconds exceeded.", null, null, null); + } + finally + { + KillProcess(); + if (!_extractEventEmitted) EmitOnExtractFinished(); + if (_movingEventEmitted) + { + EmitOnMoveFinished(); + Logger.Information("Move finished"); + } + } + + return workingExtractionReport; + } + + /// + /// Logs the error, emits the error event, and adds the file to the report if it is not null. + /// + private void handleError(DPArchive arc, string msg, DPFile? file, DPExtractionReport? report, Exception? ex) + { + using var _ = LogContext.PushProperty("Archive", arc.FileName); + Logger.Error(ex, msg); + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, ex, msg)); + if (file is not null && report is not null) + report.ErroredFiles.Add(file, msg); + } + + + private DPExtractionReport extractInternal(DPExtractSettings settings, bool extractToTemp) + { + Reset(); + Logger.Information(extractToTemp ? "Preparing to extract to temp" : "Preparing to extract"); + Logger.Debug("Extract(settings) = \n{@settings}", settings); + FileSystem = settings.Archive.FileSystem; + DPArchive archive = workingArchive = settings.Archive; + tempOnly = extractToTemp; + CancellationToken = settings.CancelToken; + if (archive.Contents.Count == 0) + { + Logger.Information("Archive Contents length was 0, now peeking..."); + Peek(archive); + } + mode = Mode.Extract; + // TODO: Log warning if process was interrupted while extracting. + workingExtractionReport = new DPExtractionReport() + { + ExtractedFiles = new(settings.FilesToExtract.Count), + Settings = settings + }; + if (CancellationToken.IsCancellationRequested) return workingExtractionReport; + if (archive.FileInfo is null || !archive.FileInfo.Exists) + { + handleError(archive, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation, null, null, null); + return workingExtractionReport; + } + var tempDir = FileSystem.CreateDirectoryInfo(tempFolder); + if (!tempDir.Exists && !TryHelper.Try(() => tempDir.Create(), out var ex)) + { + handleError(archive, "Failed to create required temp directories for extraction operations", null, null, ex); + return workingExtractionReport; + } + StartExtractionProcess(tempFolder, extractToTemp); + return workingExtractionReport; + } + + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPAbstractExtractor.cs b/src/DAZ_Installer.Core/Extraction/DPAbstractExtractor.cs new file mode 100644 index 0000000..e53f8ae --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPAbstractExtractor.cs @@ -0,0 +1,198 @@ +using DAZ_Installer.IO; +using Serilog; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// An abstract class for all extractors. + /// + public abstract class DPAbstractExtractor + { + /// + /// The logger to use for this extractor, if any. + /// + public virtual ILogger Logger { get; set; } = Log.Logger.ForContext(); + /// + /// The context to use for moving and (potentially) extracting files. + /// WARNING: Context may not be used to the full extent. For example, 7z requires all files be extracted to a temp location first, then moved to the final destination. + /// However, moving files is guaranteed to use this context to it's full extent. WinZip and RAR files extract directly to the final destination, so the context is used to it's full extent. + /// + public AbstractFileSystem FileSystem { get; protected set; } = new DPFileSystem(); + /// + /// The cancellation token to use for the extraction. By default, it is . + /// + public CancellationToken CancellationToken { get; set; } = CancellationToken.None; + /// + /// The current mode of the archive file; describes whether the archive is peeking, extracting, or moving files. + /// + protected enum Mode + { + /// + /// The archive is discovering files and folders. + /// + Peek, + /// + /// The archive is extracting files to a location. + /// + Extract, + /// + /// The archive is moving files from (usually) temp to its final destination. + /// + Moving + } + protected Mode mode; + /// + /// An event that is fired when an error occurs during extraction. + /// + public event DPArchiveEventHandler? ArchiveErrored; + /// + /// An event that is fired when the progress of extraction changes. + /// + public event DPArchiveEventHandler? ExtractProgress; + /// + /// An event that is fired when the progress of moving the files (from temp to dest) changes. + /// + public event DPArchiveEventHandler? MoveProgress; + /// + /// An event that is fired when the extractor is beginning to extract files, for some extractors, it may + /// need to extract files to a temporary location first. Check the event to know + /// when the files are being moved to the final destination. + /// + public event Action? Extracting; + /// + /// An event that is fired when the extractor is beginning to peek files. + /// + public event Action? Peeking; + /// + /// An event that is fired when the extractor is moving files to the final destination. This event may not + /// be invoked for some extractors. This event only occurs when the extractor is moving files from a temporary + /// location. + /// + public event Action? Moving; + /// + /// An event that is fired when the extractor is finished peeking files. + /// This event is ALWAYS invoked if, and only if, the Peeking event was invoked prior. + /// + public event Action? PeekFinished; + /// + /// An event that is fired when the extractor is finished extracting files. + /// This event is ALWAYS invoked if, and only if, the Extracting event was invoked prior. + /// + public event Action? ExtractFinished; + /// + /// An event that is fired when the extractor is finished moving files. + /// This event is ALWAYS invoked if, and only if, the Moving event was invoked prior. + /// + public event Action? MoveFinished; + /// + /// Attempts to extract files specifed in . + /// If the archive has no children, then the file will be peeked first via . + /// After that, the files will be extracted and a report will be returned when finished. + /// + /// The settings to use for extraction. + /// An extraction report indicating what files successfully extracted, what errored, etc. + public abstract DPExtractionReport Extract(DPExtractSettings settings); + /// + /// An optimized version of that extracts files to the temporary location defined + /// by . It does NOT extract to . + /// If the archive has no children, then the file will be peeked first + /// via . After that, the files will be extracted and a report will be returned when finished. + /// + /// Settings to use for extraction. + /// + public abstract DPExtractionReport ExtractToTemp(DPExtractSettings settings); + /// + /// Checks to see if the archive exists (and accessible) and if it is a valid archive. It will create a new instance + /// of for you, peeks the archive through and attempts + /// to extract via . + /// After that, the files will be extracted and a report will be returned when finished. + /// ADDITIONAL NOTE: will be overwritten to the new archive that is created. + /// + /// The settings to use for extraction. + /// The archive that you wish to extract from. + /// An extraction report indicating what files successfully extracted, what errored, etc. + public DPExtractionReport Extract(DPExtractSettings settings, IDPFileInfo archive) + { + ValidateArchive(archive); + var arc = DPArchive.CreateNewParentArchive(archive); + settings.Archive = arc; + return Extract(settings); + } + /// + /// Seeks the archive for it's contents and creates file objects and folders as children of . + /// You can check the contents found through . + /// + /// The archive you wish to seek files for. + public abstract void Peek(DPArchive archive); + /// + /// Checks to see if archive exists (and accessible) and if it is a valid archive. It will create a new + /// instance of for you, peeks the archive through , + /// and returns the instance. + /// + /// The archive that you wish to peek. + /// An archive object that is ready for extraction. + public DPArchive Peek(IDPFileInfo archive) + { + ValidateArchive(archive); + var arc = DPArchive.CreateNewParentArchive(archive); + Peek(arc); + return arc; + } + /// + /// Validates the archive to make sure it exists and is a valid archive. + /// + /// The archive that you wish to validate. + /// The archive does not exist or application does not have access to it. + /// The file is not an 7z, rar, or winzip archive after checking its file signature. + protected virtual void ValidateArchive(IDPFileInfo archive) + { + if (!archive.Exists) throw new FileNotFoundException("The archive file does not exist.", archive.Path); + if (DPArchive.DetermineArchiveFormatPrecise(archive.OpenRead(), true) == ArchiveFormat.Unknown) + throw new ArgumentException("The archive file is not a valid archive.", archive.Path); + } + /// + /// Invoke the event. + /// + protected virtual void EmitOnExtracting() => Extracting?.Invoke(); + /// + /// Invoke the event. + /// + protected virtual void EmitOnPeeking() => Peeking?.Invoke(); + /// + /// Invoke the event. + /// + protected virtual void EmitOnMoving() => Moving?.Invoke(); + /// + /// Invoke the event. + /// + /// The archive whose progress has changed. + /// The extraction args. + protected virtual void EmitOnExtractionProgress(DPArchive arc, DPExtractProgressArgs args) => ExtractProgress?.Invoke(arc, args); + /// + /// Invoke the event. + /// + /// The archive whose progress has changed. + /// The extraction args. + protected virtual void EmitOnMoveProgress(DPArchive arc, DPExtractProgressArgs args) => MoveProgress?.Invoke(arc, args); + /// + /// Invoke the event. + /// + /// The archive whose progress has changed. + /// The error args. + protected virtual void EmitOnArchiveError(DPArchive arc, DPArchiveErrorArgs args) => ArchiveErrored?.Invoke(arc, args); + /// + /// Invoke the event. + /// + protected virtual void EmitOnPeekFinished() => PeekFinished?.Invoke(); + /// + /// Invoke the event. + /// + protected virtual void EmitOnExtractFinished() => ExtractFinished?.Invoke(); + /// + /// Invoke the event. + /// + protected virtual void EmitOnMoveFinished() => MoveFinished?.Invoke(); + + public DPAbstractExtractor() { } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPExtractProgressArgs.cs b/src/DAZ_Installer.Core/Extraction/DPExtractProgressArgs.cs new file mode 100644 index 0000000..e49a0fa --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPExtractProgressArgs.cs @@ -0,0 +1,29 @@ +namespace DAZ_Installer.Core +{ + /// + /// Represents the extraction progress arguments. + /// + public class DPExtractProgressArgs : EventArgs + { + /// + /// Percentage of the total extraction progress. + /// + public readonly byte ExtractionPercentage = 0; + /// + /// The archive that is currently extracting files. + /// + public readonly DPArchive Archive; + /// + /// The file that is currently being extracted from archive. Sometimes this is null. This can occur + /// when the archive has just finished the extraction process. + /// + public readonly DPAbstractNode? File; + + internal DPExtractProgressArgs(byte percent, DPArchive archive, DPAbstractNode? file) : base() + { + ExtractionPercentage = percent; + Archive = archive; + File = file; + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPExtractSettings.cs b/src/DAZ_Installer.Core/Extraction/DPExtractSettings.cs new file mode 100644 index 0000000..559f989 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPExtractSettings.cs @@ -0,0 +1,44 @@ +namespace DAZ_Installer.Core.Extraction +{ + /// + /// The extract settings for the archive to use. + /// + [Serializable] + public struct DPExtractSettings + { + /// + /// The temporary path to use for operations. Null not allowed. + /// This is used if the archive needs to be extracted to a temporary location before being moved to the destination. + /// + public string TempPath = string.Empty; + /// + /// Determines whether the extractor should overwrite files if it already exists in the user library. + /// Temp files will always be overwritten. + /// + public bool OverwriteFiles = true; + /// + /// A collection of files to extract. Files in this collection MUST BE IN . + /// + /// The file from the archive to extract. + public HashSet FilesToExtract = new(0); + /// + /// An archive to extract from. This can be implicitly set by . + /// All files in must be in this archive. + /// Or in other words, the of all + /// files in must be this archive. + /// + public DPArchive Archive = null!; + /// + /// The cancellation token to use for the extraction. This setting will update . + /// + public CancellationToken CancelToken = CancellationToken.None; + + public DPExtractSettings(string? temp, IEnumerable filesToExtract, bool overwriteFiles = true, DPArchive? archive = null) + { + TempPath = temp ?? string.Empty; + OverwriteFiles = overwriteFiles; + FilesToExtract = new HashSet(filesToExtract); + Archive = archive ?? filesToExtract.FirstOrDefault()?.AssociatedArchive ?? throw new ArgumentException("No archive provided and no files to extract provided."); + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPExtractionReport.cs b/src/DAZ_Installer.Core/Extraction/DPExtractionReport.cs new file mode 100644 index 0000000..4a428f2 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPExtractionReport.cs @@ -0,0 +1,28 @@ +namespace DAZ_Installer.Core.Extraction +{ + /// + /// A report that is generated after the extraction process, even if the process has been interrupted. + /// + public record DPExtractionReport + { + /// + /// Files that have been successfully extracted. + /// + public List ExtractedFiles = new(0); + /// + /// The settings used for the extraction. + /// + public DPExtractSettings Settings; + /// + /// Errors that occurred while attempting to extract files. This may be empty if an error occured internally + /// before the extractor could extract the files. + /// + /// The file that errored while attempting to extract it. + /// The error message. + public Dictionary ErroredFiles = new(0); + /// + /// The percentage of files that successfully extracted. + /// + public float SuccessPercentage => (float)ExtractedFiles.Count / (ExtractedFiles.Count + ErroredFiles.Count); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPRARExtractor.cs b/src/DAZ_Installer.Core/Extraction/DPRARExtractor.cs new file mode 100644 index 0000000..715fc00 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPRARExtractor.cs @@ -0,0 +1,318 @@ +using DAZ_Installer.External; +using DAZ_Installer.IO; +using Serilog; +using Serilog.Context; + +namespace DAZ_Installer.Core.Extraction +{ + public class DPRARExtractor : DPAbstractExtractor + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + internal IRARFactory Factory { get; init; } = new RARFactory(); + private Session? session = null; + private class Session + { + public DPExtractSettings settings; + public DPExtractionReport report = new(); + public bool tempOnly = false; + } + + public DPRARExtractor() { } + /// + /// Constructor used for testing + /// + /// The factory to use for creating handlers. + internal DPRARExtractor(ILogger logger, IRARFactory factory) => (Logger, Factory) = (logger, factory); + #region Event Methods + + private void HandleMissingVolume(IRAR sender, MissingVolumeEventArgs e) + { + var msg = $"{sender.CurrentFile.FileName} is missing volume : {e.VolumeName}."; + Logger.Warning(msg); + var args = new DPArchiveErrorArgs(session.settings.Archive, null, msg); + EmitOnArchiveError(session.settings.Archive, args); + } + + + public void HandleNewFile(IRAR sender, NewFileEventArgs e) + { + try + { + if (e.fileInfo.IsDirectory) + { + if (session.settings.Archive.FolderExists(e.fileInfo.FileName)) return; + var f = new DPFolder(e.fileInfo.FileName, session.settings.Archive, null); + Logger.Debug("Discovered new directory: {0}", f.FileName); + } + else + { + var f = DPFile.CreateNewFile(e.fileInfo.FileName, session.settings.Archive, null); + Logger.Debug("Discovered new file: {0}", f.FileName); + } + } catch (Exception ex) + { + Logger.Error(ex, "Unexpected error occurred while handling new file: {0}", e.fileInfo.FileName); + } + } + + + #endregion + #region Override Methods + public override DPExtractionReport ExtractToTemp(DPExtractSettings settings) + { + Logger.Information("Extracting to temp"); + session = new Session() { settings = settings, report = new DPExtractionReport(), tempOnly = true }; + try + { + return Extract(settings); + } catch { throw; } + finally { session = null; } + } + public override DPExtractionReport Extract(DPExtractSettings settings) + { + using var _ = LogContext.PushProperty("Archive", settings.Archive.FileName); + Logger.Information("Preparing to extract"); + mode = Mode.Extract; + EmitOnExtracting(); + FileSystem = settings.Archive.FileSystem; + DPArchive arc = settings.Archive; + session ??= new Session() { report = new DPExtractionReport(), settings = settings }; + CancellationToken = settings.CancelToken; + + var report = new DPExtractionReport() + { + Settings = settings, + ExtractedFiles = new(settings.FilesToExtract.Count), + }; + session.report = report; + try + { + CancellationToken.ThrowIfCancellationRequested(); + if (arc.Contents.Count == 0) Peek(arc); + + if (arc.FileInfo is null || !arc.FileInfo.Exists) + { + handleError(arc, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation, report, null, null); + Logger.Debug("FileInfo is null: {0} | FileInfo.Exists: {1}", arc.FileInfo is null, arc.FileInfo?.Exists); + return report; + } + + using (var RARHandler = Factory.Create(arc.FileInfo.Path)) + { + try + { + // TODO: Update destination path. + RARHandler.Open(RAR.OpenMode.Extract); + var flags = (RAR.ArchiveFlags)RARHandler.ArchiveData.Flags; + var isFirstVolume = flags.HasFlag(RAR.ArchiveFlags.FirstVolume); + var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); + + if (isVolume && !isFirstVolume) + handleError(arc, "The archive is not the first volume out of a multi-volume archive. Only input the first volume", null, null, null); + for (var i = 0; i < settings.FilesToExtract.Count && RARHandler.ReadHeader(); i++) + { + CancellationToken.ThrowIfCancellationRequested(); + var arcHasFile = arc.Contents.TryGetValue(PathHelper.NormalizePath(RARHandler.CurrentFile.FileName), out var file); + if (!RARHandler.CurrentFile.IsDirectory && arcHasFile && file!.AssociatedArchive == arc && settings.FilesToExtract.Contains(file)) + { + if (!ExtractFile(RARHandler, settings, report)) + RARHandler.Skip(); + EmitOnExtractionProgress(settings.Archive, new DPExtractProgressArgs((byte)((float)i / settings.FilesToExtract.Count), arc, file)); + } + else + { + if (arcHasFile && file!.AssociatedArchive != arc) + handleError(arc, string.Format(DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, file.Path), report, file, null); + RARHandler.Skip(); + i--; + } + + } + RARHandler.Close(); + } + catch (OperationCanceledException) { } + catch (Exception e) + { + handleError(arc, "An unexpected error occured while processing the archive", null, null, e); + } + } + } + catch (Exception ex) + { + handleError(arc, "An unexpected error occurred before attempting to process the archive.", null, null, ex); + } finally + { + EmitOnExtractFinished(); + } + + return report; + } + public override void Peek(DPArchive arc) + { + using var _ = LogContext.PushProperty("Archive", arc.FileName); + Logger.Information("Preparing to peek"); + mode = Mode.Peek; + FileSystem = arc.FileSystem; + EmitOnPeeking(); + try + { + CancellationToken.ThrowIfCancellationRequested(); + + if (arc.FileInfo is null || !arc.FileInfo.Exists) + { + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, null, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation)); + EmitOnPeekFinished(); + return; + } + using var RARHandler = Factory.Create(arc.FileInfo.Path); + RARHandler.MissingVolume += HandleMissingVolume; + RARHandler.NewFile += HandleNewFile; + session = new Session() { report = new DPExtractionReport(), settings = new() { Archive = arc, } }; + + // TODO: Can we remove this? + RARHandler.DestinationPath = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(arc.Path)); + // Create path and see if it exists. + var dir = FileSystem.CreateDirectoryInfo(RARHandler.DestinationPath); + if (!dir.PreviewCreate()) Logger.Warning("The current destination directory is not whitelisted and will not be created"); + dir.TryCreate(); + + RARHandler.Open(RAR.OpenMode.List); + var flags = (RAR.ArchiveFlags)RARHandler.ArchiveData.Flags; + var isFirstVolume = flags.HasFlag(RAR.ArchiveFlags.FirstVolume); + var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); + + if (isVolume && !isFirstVolume) + { + handleError(arc, "The archive is not the first volume out of a multi-volume archive. Only input the first volume.", null, null, null); + return; + } + + while (RARHandler.ReadHeader()) + { + CancellationToken.ThrowIfCancellationRequested(); + if (RARHandler.CurrentFile.IsDirectory) continue; + TestFile(RARHandler, arc); + } + RARHandler.Close(); + } + catch (OperationCanceledException) { } + catch (Exception e) + { + handleError(arc, "An unexpected error occured while processing the archive.", null, null, e); + } finally + { + EmitOnPeekFinished(); + } + } + #endregion + private bool ExtractFile(IRAR handler, DPExtractSettings settings, DPExtractionReport report) + { + var fileName = handler.CurrentFile.FileName; + DPArchive arc = settings.Archive; + + // Means that archive was modified while we were extracting. + if (!arc.Contents.TryGetValue(PathHelper.NormalizePath(fileName), out var file)) + { + handleError(arc, DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, report, file, null); + return false; + } + IDPFileInfo? fileInfo = null!; + EXTRACT: + try + { + var targetPath = session!.tempOnly ? Path.Combine(settings.TempPath, + Path.GetFileNameWithoutExtension(arc.FileName), + fileName) : file.TargetPath; + handler.DestinationPath = Path.GetDirectoryName(targetPath)!; + // Create folders for the destination path if needed. + var dir = FileSystem.CreateDirectoryInfo(handler.DestinationPath); + if (!dir.Exists && !dir.TryCreate()) + { + handleError(arc, DPArchiveErrorArgs.UnauthorizedAccessExplanation, report, file, null); + return false; + } + + fileInfo ??= FileSystem.CreateFileInfo(targetPath); + if (!FileSystem.Scope.IsFilePathWhitelisted(targetPath)) + throw new OutOfScopeException(targetPath); + handler.Extract(targetPath); + + // Only update if we didn't error. + file.FileInfo = fileInfo; + report.ExtractedFiles.Add(file); + } + catch (IOException e) + { + var msg = string.Empty; + if (e.Message == "File CRC Error" || e.Message == "File could not be opened.") + { + var flags = (RAR.ArchiveFlags)handler.ArchiveData.Flags; + var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); + var continuesNext = handler.CurrentFile.ContinuedOnNext; + var isEncrypted = handler.CurrentFile.encrypted; + msg = (!isVolume || !continuesNext) && !isEncrypted ? "Archive could be corrupt (or simply not an RAR file)." : string.Empty; + handleError(arc, msg, report, file, e); + } + // Check to see if we are attempting to overwrite a file that we don't have access to (ex: hidden/read-only/anti-virus/user no access). + else if (e.Message == "File write error." || e.Message == "File read error." || e.Message == "File could not be opened.") + { + fileInfo ??= FileSystem.CreateFileInfo(file.TargetPath); + if (fileInfo.TryAndFixOpenRead(out var stream, out Exception? ex)) + { + stream?.Dispose(); + goto EXTRACT; + } + else + { + msg = fileInfo.Exists ? "File could not be extracted and cannot overwrite existing file on disk due to file permissions." : + DPArchiveErrorArgs.UnauthorizedAccessAfterExplanation; + handleError(arc, msg, report, file, new AggregateException(e, ex)); + } + } + return false; + } catch (OutOfScopeException e) + { + handleError(arc, $"Extractor attempted to extract file to a destination that was not specified: {file.TargetPath}", report, file, e); + return false; + } catch (Exception e) + { + handleError(arc, "An unexpected error occured while processing the archive.", report, file, e); + return false; + } + return true; + } + + private bool TestFile(IRAR handler, DPArchive arc) + { + try + { + // I'm not sure if UnpackedSize returns negative if the file is partial. + arc.TrueArchiveSize += (ulong)Math.Max(0, handler.CurrentFile.UnpackedSize); + handler.Test(); + } + catch (IOException e) + { + if (e.Message == "File CRC Error" || e.Message == "File could not be opened.") + { + var flags = (RAR.ArchiveFlags)handler.ArchiveData.Flags; + var isVolume = flags.HasFlag(RAR.ArchiveFlags.Volume); + var continuesNext = handler.CurrentFile.ContinuedOnNext; + var isEncrypted = handler.CurrentFile.encrypted; + var msg = (!isVolume || !continuesNext) && !isEncrypted ? "Archive could be corrupt (or simply not an RAR file)." : string.Empty; + var associatedDPFile = arc.Contents.TryGetValue(PathHelper.NormalizePath(handler.CurrentFile.FileName), out var file) ? file : null; + handleError(arc, msg, null, associatedDPFile, e); + return false; + } + } + return true; + } + private void handleError(DPArchive arc, string msg, DPExtractionReport? report, DPFile? file, Exception? e) + { + using var _ = LogContext.PushProperty("Archive", arc.FileName); + Logger.Error(e, msg); + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, e, msg)); + if (file is not null && report is not null) + report.ErroredFiles.Add(file, msg); + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/DPZipExtractor.cs b/src/DAZ_Installer.Core/Extraction/DPZipExtractor.cs new file mode 100644 index 0000000..40a7531 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/DPZipExtractor.cs @@ -0,0 +1,226 @@ + +using DAZ_Installer.IO; +using Serilog; +using Serilog.Context; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// An extractor for zip archives. + /// + public class DPZipExtractor : DPAbstractExtractor + { + public override ILogger Logger { get; set; } = Log.Logger.ForContext(); + internal virtual IZipArchiveFactory Factory { get; init; } = new ZipArchiveWrapperFactory(); + private bool tempOnly = false; + + public DPZipExtractor() { } + /// + /// Constructor used for testing + /// + /// The logger to use, if any. + /// The factory to use if any. + internal DPZipExtractor(ILogger logger, IZipArchiveFactory factory) => (Logger, Factory) = (logger, factory); + + public override DPExtractionReport Extract(DPExtractSettings settings) + { + using var _ = LogContext.PushProperty("Archive", settings.Archive.FileName); + Logger.Information("Preparing to extract"); + Logger.Debug("Extract(settings) = \n{@Settings}", settings); + + // Set up the extraction report to return in case of any issues. + var e = new DPExtractionReport() + { + ExtractedFiles = new(settings.FilesToExtract?.Count ?? 0), + Settings = settings + }; + try + { + // Reset any variables if needed. + FileSystem = settings.Archive.FileSystem; + // Peek into the archive if needed. + DPArchive arc = settings.Archive; + if (arc.Contents.Count == 0) Peek(arc); + + var max = settings.FilesToExtract.Count; + + if (CancellationToken.IsCancellationRequested) return e; + + // Check if the archive is on disk or we have access to it. + if (arc.FileInfo is null || !arc.FileInfo.Exists) + { + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, null, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation)); + return e; + } + // Let listeners know that we are beginning to extract. + EmitOnExtracting(); + try + { + // Create the zip archive. + using var zipArc = Factory.Create(arc.FileInfo.OpenRead()); + + // Loop through all the files to extract and attempt to extract them. + var i = 0; + foreach (DPFile file in settings.FilesToExtract) + { + CancellationToken.ThrowIfCancellationRequested(); + // Check if the file is part of this archive, if not, emit an error and continue. + if (file.AssociatedArchive != settings.Archive) + { + HandleError(arc, file, e, null, string.Format(DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, file.Path)); + Log.Debug("File {0} Associated Archive: {1}", file.FileName, file.AssociatedArchive?.Path); + continue; + } + // Extract the file. + ExtractFile(zipArc.GetEntry(file.Path), file, settings, e); + HandleProgressionZIP(file, ++i, max); + } + } + catch (Exception ex) + { + HandleError(arc, null, e, ex, "An unknown error occured while attempting to extract the archive"); + } + } catch (Exception ex) + { + Logger.Error(ex, "An unknown error occured while attempting to extract the archive"); + } finally + { + EmitOnExtractFinished(); + Logger.Information("Finished extracting"); + } + return e; + } + + public override DPExtractionReport ExtractToTemp(DPExtractSettings settings) + { + tempOnly = true; + try + { + return Extract(settings); + } catch + { + throw; + } finally + { + tempOnly = false; + } + } + + public override void Peek(DPArchive arc) + { + using var _ = LogContext.PushProperty("Archive", arc.FileName); + Logger.Information("Preparing to peek"); + // Emit that we are peeking. + EmitOnPeeking(); + // Reset any variables if needed. + arc.TrueArchiveSize = 0; + FileSystem = arc.FileSystem; + + try + { + if (CancellationToken.IsCancellationRequested) return; + + if (arc.FileInfo is null || !arc.FileInfo.Exists) + { + HandleError(arc, null, null, null, DPArchiveErrorArgs.ArchiveDoesNotExistOrNoAccessExplanation); + Logger.Debug("FileInfo is null: {0} | FileInfo.Exists: {1}", arc.FileInfo is null, arc.FileInfo?.Exists); + return; + } + + using var zipArc = Factory.Create(arc.FileInfo.OpenRead()); + foreach (var entry in zipArc.Entries) + { + CancellationToken.ThrowIfCancellationRequested(); + if (string.IsNullOrEmpty(entry.Name) && !arc.FolderExists(entry.FullName)) + // Set folder to null to let it automatically generate subfolders. + new DPFolder(entry.FullName, arc, null); + else DPFile.CreateNewFile(entry.FullName, arc, null); + arc.TrueArchiveSize += (ulong)Math.Max(0, entry.Length); + } + } catch (Exception ex) + { + HandleError(arc, null, null, ex, "An unknown error occured while attempting to peek the archive"); + } finally + { + EmitOnPeekFinished(); + Logger.Information("Finished peeking"); + } + } + + private void ExtractFile(IZipArchiveEntry? entry, DPFile file, DPExtractSettings settings, DPExtractionReport report) + { + var tryAgain = false; + DPArchive arc = settings.Archive; + if (entry is null) + { + HandleError(arc, file, report, null, string.Format(DPArchiveErrorArgs.FileNotPartOfArchiveErrorFormat, file.FileName)); + return; + } + + var expectedPath = tempOnly ? Path.Combine(settings.TempPath, Path.GetFileNameWithoutExtension(arc.Path), entry.Name) : file.TargetPath; + if (string.IsNullOrWhiteSpace(expectedPath)) + { + HandleError(arc, file, report, null, "Cannot perform extraction on empty target path"); + return; + } + IDPFileInfo? fileInfo = null!; + EXTRACT: + try + { + var dirInfo = FileSystem.CreateDirectoryInfo(Path.GetDirectoryName(expectedPath)!); + if (!dirInfo.Exists && !dirInfo.TryCreate()) Logger.Warning("Failed to create directory for {0}", dirInfo.Path); + // Extract the file and create the file info if it successfully extracted. + fileInfo ??= FileSystem.CreateFileInfo(expectedPath); + if (!FileSystem.Scope.IsFilePathWhitelisted(expectedPath)) + { + HandleError(arc, file, report, null, $"{expectedPath} is not whitelisted."); + return; + } + entry.ExtractToFile(expectedPath, settings.OverwriteFiles); + file.FileInfo = fileInfo; + report.ExtractedFiles.Add(file); + } + catch (IOException e) + { + if (e.Message.StartsWith("The file ") && e.Message.EndsWith("already exists")) + HandleError(arc, file, report, e, "Attempted to overwrite a file that exists but user chose not to overwrite files"); + else HandleError(arc, file, report, e, "Unknown error occurred while extracting file"); + } + // Note: System.UnauthorizedAccessException can occur when zip is attempting to overwrite a hidden and/or read-only file. + catch (UnauthorizedAccessException e) + { + if (tryAgain) + { + HandleError(arc, file, report, e, DPArchiveErrorArgs.UnauthorizedAccessAfterExplanation); + return; + } + if (!TryHelper.TryFixFilePermissions(fileInfo ??= FileSystem.CreateFileInfo(expectedPath), out Exception? ex)) + HandleError(arc, file, report, new AggregateException(e, ex), DPArchiveErrorArgs.UnauthorizedAccessExplanation); + // Try it again. + tryAgain = true; + goto EXTRACT; + } + catch (Exception e) + { + HandleError(arc, file, report, e, "Unknown error occurred while extracting file"); + } + } + + private void HandleProgressionZIP(DPFile file, int i, int max) + { + i = Math.Min(i, max); + var percentComplete = (float)i / max; + var progress = (byte)Math.Floor(percentComplete * 100); + DPArchive arc = file.AssociatedArchive!; + EmitOnExtractionProgress(arc, new DPExtractProgressArgs(progress, arc, file)); + } + + private void HandleError(DPArchive arc, DPFile? file, DPExtractionReport? report, Exception? e, string msg) + { + Logger.Error(e, msg); + EmitOnArchiveError(arc, new DPArchiveErrorArgs(arc, e, msg)); + if (file is not null && report is not null) + report.ErroredFiles.TryAdd(file, msg); + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Factories/ProcessFactory.cs b/src/DAZ_Installer.Core/Extraction/Factories/ProcessFactory.cs new file mode 100644 index 0000000..1d044d7 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Factories/ProcessFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal class ProcessFactory : IProcessFactory + { + public IProcess Create() => new ProcessWrapper(); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Factories/RARFactory.cs b/src/DAZ_Installer.Core/Extraction/Factories/RARFactory.cs new file mode 100644 index 0000000..e712940 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Factories/RARFactory.cs @@ -0,0 +1,9 @@ +using DAZ_Installer.External; + +namespace DAZ_Installer.Core.Extraction +{ + internal class RARFactory : IRARFactory + { + public IRAR Create(string arcPath) => new RAR(arcPath); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Factories/ZipArchiveWrapperFactory.cs b/src/DAZ_Installer.Core/Extraction/Factories/ZipArchiveWrapperFactory.cs new file mode 100644 index 0000000..d0cd26c --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Factories/ZipArchiveWrapperFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal class ZipArchiveWrapperFactory : IZipArchiveFactory + { + public IZipArchive Create(Stream stream) => new ZipArchiveWrapper(stream); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IProcess.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IProcess.cs new file mode 100644 index 0000000..a299661 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IProcess.cs @@ -0,0 +1,42 @@ +using System.Diagnostics; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// Interface for the class. + /// + internal interface IProcess : IDisposable + { + /// + StreamWriter StandardInput { get; } + /// + ProcessStartInfo StartInfo { get; set; } + /// + /// Occurs each time an application writes a line to its redirected stream. + /// /// Returns the line written. Returns if stream ended. + /// + event Action? OutputDataReceived; + /// + /// Occurs each time an application writes a line to its redirected stream. + /// Returns the line written. Returns if stream ended. + /// + event Action? ErrorDataReceived; + /// + event Action Exited; + /// + bool HasExited { get; } + /// + bool EnableRaisingEvents { get; set; } + /// + void Start(); + /// + void Kill(bool entireProcessTree); + /// + void BeginOutputReadLine(); + /// + void BeginErrorReadLine(); + /// + bool WaitForExit(int milliseconds); + + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IProcessFactory.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IProcessFactory.cs new file mode 100644 index 0000000..fe44644 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IProcessFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal interface IProcessFactory + { + IProcess Create(); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IRAR.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IRAR.cs new file mode 100644 index 0000000..89ad088 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IRAR.cs @@ -0,0 +1,39 @@ +using DAZ_Installer.External; +using static DAZ_Installer.External.RAR; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// An interface for the class. + /// + public interface IRAR : IDisposable + { + /// + event MissingVolumeHandler MissingVolume; + /// + event NewFileHandler NewFile; + /// + event PasswordRequiredHandler PasswordRequired; + /// + event ExtractionProgressHandler ExtractionProgress; + + /// + RARFileInfo CurrentFile { get; } + /// + RAR.RAROpenArchiveDataEx ArchiveData { get; } + /// + string DestinationPath { get; set; } + /// + void Close(); + /// + void Open(RAR.OpenMode mode); + /// + bool ReadHeader(); + /// + void Skip(); + /// + void Test(); + /// + void Extract(string destinationName); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IRARFactory.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IRARFactory.cs new file mode 100644 index 0000000..b056447 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IRARFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal interface IRARFactory + { + IRAR Create(string arcPath); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchive.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchive.cs new file mode 100644 index 0000000..6abd4da --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchive.cs @@ -0,0 +1,17 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction +{ + /// + internal interface IZipArchive : IDisposable + { + /// + IReadOnlyCollection Entries { get; } + /// + ZipArchiveMode Mode { get; } + /// + IZipArchiveEntry CreateEntry(string entryName); + /// + IZipArchiveEntry? GetEntry(string entryName); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveEntry.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveEntry.cs new file mode 100644 index 0000000..88ec251 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveEntry.cs @@ -0,0 +1,29 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction +{ + /// + internal interface IZipArchiveEntry + { + /// + IZipArchive Archive { get; } + /// + string Name { get; } + /// + string FullName { get; } + /// + long Length { get; } + /// + long CompressedLength { get; } + /// + DateTimeOffset LastWriteTime { get; set; } + /// + void Delete(); + /// + void ExtractToFile(string destinationFileName); + /// + void ExtractToFile(string destinationFileName, bool overwrite); + /// + Stream Open(); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveFactory.cs b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveFactory.cs new file mode 100644 index 0000000..c46a1ee --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Interfaces/IZipArchiveFactory.cs @@ -0,0 +1,7 @@ +namespace DAZ_Installer.Core.Extraction +{ + internal interface IZipArchiveFactory + { + IZipArchive Create(Stream stream); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Wrappers/ProcessWrapper.cs b/src/DAZ_Installer.Core/Extraction/Wrappers/ProcessWrapper.cs new file mode 100644 index 0000000..60d5fdf --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Wrappers/ProcessWrapper.cs @@ -0,0 +1,45 @@ +using System.Diagnostics; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// An implementation of that wraps the class. + /// + internal class ProcessWrapper : IProcess + { + private readonly Process process = new(); + public event Action? OutputDataReceived + { + add => process.OutputDataReceived += (_, e) => value?.Invoke(e.Data); + remove => process.OutputDataReceived -= (_, e) => value?.Invoke(e.Data); + } + public event Action? ErrorDataReceived + { + add => process.ErrorDataReceived += (_, e) => value?.Invoke(e.Data); + remove => process.ErrorDataReceived -= (_, e) => value?.Invoke(e.Data); + } + public event Action Exited + { + add => process.Exited += (_, __) => value?.Invoke(); + remove => process.Exited -= (_, __) => value?.Invoke(); + } + public StreamWriter StandardInput => process.StandardInput; + public bool HasExited => process.HasExited; + public bool EnableRaisingEvents { get => process.EnableRaisingEvents; set => process.EnableRaisingEvents = value; } + public ProcessStartInfo StartInfo { get => process.StartInfo; set => process.StartInfo = value; } + + internal ProcessWrapper() { } + + public void BeginErrorReadLine() => process.BeginErrorReadLine(); + public void BeginOutputReadLine() => process.BeginOutputReadLine(); + public void Kill(bool entireProcessTree) => process.Kill(entireProcessTree); + public void Start() => process.Start(); + public bool WaitForExit(int milliseconds) => process.WaitForExit(milliseconds); + + public void Dispose() + { + process.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveEntryWrapper.cs b/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveEntryWrapper.cs new file mode 100644 index 0000000..3caf9c1 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveEntryWrapper.cs @@ -0,0 +1,36 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// Wrapper for + /// + internal class ZipArchiveEntryWrapper : IZipArchiveEntry + { + private readonly ZipArchiveEntry _entry; + + internal ZipArchiveEntryWrapper(ZipArchiveEntry entry) => _entry = entry; + /// + public string Name => _entry.Name; + /// + public string FullName => _entry.FullName; + /// + public long Length => _entry.Length; + /// + public DateTimeOffset LastWriteTime { get => _entry.LastWriteTime; set => _entry.LastWriteTime = value; } + /// + public IZipArchive Archive => new ZipArchiveWrapper(_entry.Archive); + /// + public long CompressedLength => _entry.CompressedLength; + /// + public void Delete() => _entry.Delete(); + /// + public void ExtractToFile(string destinationFileName) => _entry.ExtractToFile(destinationFileName); + /// + public void ExtractToFile(string destinationFileName, bool overwrite) => _entry.ExtractToFile(destinationFileName, overwrite); + /// + public Stream Open() => _entry.Open(); + /// + public override string ToString() => _entry.ToString(); + } +} diff --git a/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveWrapper.cs b/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveWrapper.cs new file mode 100644 index 0000000..d671398 --- /dev/null +++ b/src/DAZ_Installer.Core/Extraction/Wrappers/ZipArchiveWrapper.cs @@ -0,0 +1,32 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction +{ + /// + /// Wrapper for + /// + internal class ZipArchiveWrapper : IZipArchive + { + readonly ZipArchive archive; + + internal ZipArchiveWrapper(ZipArchive archive) => this.archive = archive; + + public ZipArchiveWrapper(Stream stream) => archive = new(stream); + public IReadOnlyCollection Entries => archive.Entries.Select(x => new ZipArchiveEntryWrapper(x)).ToList(); + + public ZipArchiveMode Mode => archive.Mode; + + public IZipArchiveEntry CreateEntry(string entryName) => new ZipArchiveEntryWrapper(archive.CreateEntry(entryName)); + public void Dispose() + { + archive.Dispose(); + GC.SuppressFinalize(this); + } + + public IZipArchiveEntry? GetEntry(string entryName) + { + var entry = archive.GetEntry(entryName); + return entry is null ? null : new ZipArchiveEntryWrapper(entry); + } + } +} diff --git a/src/Libs/UnRAR.dll b/src/DAZ_Installer.Core/Libs/UnRAR.dll similarity index 100% rename from src/Libs/UnRAR.dll rename to src/DAZ_Installer.Core/Libs/UnRAR.dll diff --git a/src/DAZ_Installer.Core/Properties/AssemblyInfo1.cs b/src/DAZ_Installer.Core/Properties/AssemblyInfo1.cs new file mode 100644 index 0000000..16dc5f9 --- /dev/null +++ b/src/DAZ_Installer.Core/Properties/AssemblyInfo1.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// In SDK-style projects such as this one, several assembly attributes that were historically +// defined in this file are now automatically added during build and populated with +// values defined in project properties. For details of which attributes are included +// and how to customise this process see: https://aka.ms/assembly-info-properties + + +// Setting ComVisible to false makes the types in this assembly not visible to COM +// components. If you need to access a type in this assembly from COM, set the ComVisible +// attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM. + +[assembly: Guid("b8a29ead-4696-4dde-bdd7-0aa54fa23802")] +[assembly: InternalsVisibleTo("DAZ_Installer.CoreTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // For generating mocks with NSubstitute diff --git a/src/DAZ_Installer.CoreTests/DAZ_Installer.CoreTests.csproj b/src/DAZ_Installer.CoreTests/DAZ_Installer.CoreTests.csproj new file mode 100644 index 0000000..9ad68b4 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DAZ_Installer.CoreTests.csproj @@ -0,0 +1,59 @@ + + + + net6.0 + enable + enable + + false + true + x64 + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + diff --git a/src/DAZ_Installer.CoreTests/DPDSXFileTests.cs b/src/DAZ_Installer.CoreTests/DPDSXFileTests.cs new file mode 100644 index 0000000..1a1a675 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPDSXFileTests.cs @@ -0,0 +1,77 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DAZ_Installer.IO.Fakes; + +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPDSXFileTests + { + internal const string SupplementContent = + @" + + + + "; + + internal const string SupportContent = + @" + + + + + + + + + + + + + "; + + StreamReader SetupStreamReader(string str) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(str); + writer.Flush(); + stream.Position = 0; + return new StreamReader(stream); + } + + [TestMethod] + public void CheckContentsTest_Supplement() + { + var f = new DPDSXFile("doesnt matter", new(), null); + var a = f.AssociatedArchive; + using var sr = SetupStreamReader(SupplementContent); + + f.CheckContents(sr); + + Assert.AreEqual("Test Product", a.ProductInfo.ProductName); + Assert.AreEqual("Test Product", a.ProductName); + } + + [TestMethod] + public void CheckContentsTest_Support() + { + var f = new DPDSXFile("doesnt matter", new(), null); + var a = f.AssociatedArchive; + using var sr = SetupStreamReader(SupportContent); + + f.CheckContents(sr); + + Assert.AreEqual("82114", f.ContentInfo.ID); + Assert.AreEqual("82114", a.ProductInfo.SKU); + CollectionAssert.AreEqual(new[] { "TheRealSolly", "Sollybean" }, a.ProductInfo.Authors.ToArray()); + CollectionAssert.AreEqual(new[] { "TheRealSolly", "Sollybean" }, f.ContentInfo.Authors.ToArray()); + + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/DPDazFileTests.cs b/src/DAZ_Installer.CoreTests/DPDazFileTests.cs new file mode 100644 index 0000000..de988a2 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPDazFileTests.cs @@ -0,0 +1,61 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; + +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPDazFileTests + { + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + + StreamReader SetupStreamReader(string str) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(str); + writer.Flush(); + stream.Position = 0; + return new StreamReader(stream); + } + [TestMethod] + public void ReadContentsTest() + { + // Arrange + var f = new DPDazFile("doesnt matter", new DPArchive(), null); + const string DSFContents = + @"{ + ""file_version"" : ""0.6.0.0"", + ""asset_info"" : { + ""id"" : ""/data/data.dsf"", + ""type"" : ""prop"", + ""contributor"" : { + ""author"" : ""TheRealSolly"", + ""email"" : ""solomon1blount@gmail.com"", + ""website"" : ""www.thesolomonchronicles.com"" + }, + ""revision"" : ""1.0"", + ""modified"" : ""2020-12-06T00:04:11Z"" + } + }"; + + // Act + using var sr = SetupStreamReader(DSFContents); + f.ReadContents(sr); + + // Assert + Assert.AreEqual("TheRealSolly", f.ContentInfo.Authors[0]); + Assert.AreEqual(ContentType.Prop, f.ContentInfo.ContentType); + Assert.AreEqual("www.thesolomonchronicles.com", f.ContentInfo.Website); + Assert.AreEqual("solomon1blount@gmail.com", f.ContentInfo.Email); + } + } +} diff --git a/src/DAZ_Installer.CoreTests/DPDestinationDeterminerTests.cs b/src/DAZ_Installer.CoreTests/DPDestinationDeterminerTests.cs new file mode 100644 index 0000000..4e7f170 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPDestinationDeterminerTests.cs @@ -0,0 +1,434 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Moq; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.IO; +using DAZ_Installer.CoreTests.Extraction; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using static DAZ_Installer.Core.Tests.Helpers.DPDestinationDeterminerTestHelpers; +using System.Reflection; +using System.IO; +using System.Xml.Linq; + +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPDestinationDeterminerTests + { + // Supplement and Manifest should not be included. + public static readonly string[] DefaultManifestPaths = new[] { "Manifest.dsx", "Supplement.dsx", "Content/data/TheReaolSolly/a.txt", "Content/docs/TheRealSolly/b.txt" }; + public static readonly string[] DefaultNonDazPaths = new[] { "data/TheRealSolly/a.txt", "docs/TheRealSolly/b.txt", "should not be included.txt" }; + private static readonly MockOptions DazArchiveOpts = new(); + private static readonly MockOptions NonDazOpts = new() { paths = DefaultNonDazPaths }; + private static readonly DPProcessSettings ManifestProcessSettings = new("Z:/", + "Z:/", + InstallOptions.ManifestOnly, + DPProcessor.DefaultContentFolders.ToHashSet(), + new Dictionary(DPProcessor.DefaultRedirects)); + private static readonly DPProcessSettings AutoProcessSettings = ManifestProcessSettings with { InstallOption = InstallOptions.Automatic }; + private static readonly DPProcessSettings BothProcessSettings = ManifestProcessSettings with { InstallOption = InstallOptions.ManifestAndAuto }; + + // TODO: Test DPDestinationDeterminer when a folder is not included in the content redirect folders map. + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + public struct MockOptions + { + public bool partialFileInfo = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFileSystem = true; + public string[] paths = DefaultManifestPaths; + + public MockOptions() { } + } + + private static DPArchive NewMockedArchive(MockOptions options, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock fakeFileSystem) + { + var fs = new Mock(DPFileScopeSettings.All) { CallBase = options.partialFileSystem }; + fs.Object.PartialMock = options.partialFileSystem; + fakeFileSystem = fs; + fakeFileInfo = new Mock("Z:/test.zip") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fs.Object, null) { CallBase = options.partialFileInfo }; + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, Mock.Of()); + foreach (var file in options.paths) + { + var dpFile = DPFile.CreateNewFile(file, arc, null); + var fi = fakeFileSystem.Object.CreateFileInfo(file); + dpFile.FileInfo = fi; + Exception? ex = null; + Mock.Get(fi).Setup(x => x.TryAndFixOpenRead(out It.Ref.IsAny, out ex)) + .Callback((out Stream s, out Exception ex) => + { + s = DPArchiveTestHelpers.DetermineFileStream(dpFile, arc, arc.Contents.Values.Where(x => x.Parent is not null).Select(x => x.Path)); + ex = null; + }) + .Returns(true); + } + return arc; + } + + [TestMethod] + public void DetermineDestinationsTest_ManifestDAZArchive() + { + var tc = new { + Paths = DefaultManifestPaths, + Settings = ManifestProcessSettings, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_ManifestNonDAZArchive() + { + var tc = new + { + Paths = DefaultNonDazPaths, + Settings = ManifestProcessSettings, + Options = NonDazOpts, + ExpectedTargetPaths = new string[] { }, + ExpectedRelativePathsTargetPaths = new string[] { }, + ExpectedRelativePathToContentFolders = new string[] { }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + var expected = new HashSet(0); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + } + + [TestMethod] + public void DetermineDestinationsTest_AutoDAZArchive() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = AutoProcessSettings, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_AutoNonDAZArchive() + { + var tc = new + { + Paths = DefaultNonDazPaths, + Settings = AutoProcessSettings, + Options = NonDazOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheRealSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheRealSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheRealSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "data", "docs" }, + }; + + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_BothDAZArchive() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = BothProcessSettings, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_BothNonDAZArchive() + { + var tc = new + { + Paths = DefaultNonDazPaths, + Settings = BothProcessSettings, + Options = NonDazOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheRealSolly/a.txt", "Z:/Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheRealSolly/a.txt", "Documentation/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheRealSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "data", "docs" }, + }; + + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoRedirectsDAZManifest() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = ManifestProcessSettings with { ContentRedirectFolders = new() }, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/docs/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoRedirectsDAZManifestAuto() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = AutoProcessSettings with { ContentRedirectFolders = new() }, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet( new[] { arc.Contents[PathHelper.NormalizePath("Content/data/TheReaolSolly/a.txt")] }); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoRedirectsDAZManifestBoth() + { + var tc = new + { + Paths = DefaultManifestPaths, + Settings = BothProcessSettings with { ContentRedirectFolders = new() }, + Options = DazArchiveOpts, + ExpectedTargetPaths = new[] { "Z:/data/TheReaolSolly/a.txt", "Z:/docs/TheRealSolly/b.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/TheReaolSolly/a.txt", "docs/TheRealSolly/b.txt" }, + ExpectedContentFoldersMarked = new[] { "Content/data", "Content/docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_ContentFolderTree() + { + var paths = new string[] { "data/data/a.txt", "docs/docs/b.txt", "data/data/docs/docs/c.txt" }; + var tc = new + { + Paths = paths, + Settings = AutoProcessSettings, + Options = new MockOptions() { paths = paths }, + ExpectedTargetPaths = new[] { "Z:/data/data/a.txt", "Z:/Documentation/docs/b.txt", "Z:/data/data/docs/docs/c.txt" }, + ExpectedRelativePathsTargetPaths = new[] { "data/data/a.txt", "Documentation/docs/b.txt", "data/data/docs/docs/c.txt" }, + ExpectedRelativePathToContentFolders = new[] { "data/data/a.txt", "docs/docs/b.txt", "data/data/docs/docs/c.txt" }, + ExpectedContentFoldersMarked = new[] { "data", "docs" }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(arc.Contents.Values.Where(x => x.Parent is not null)); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoDirs() + { + var paths = new string[] { "a.txt", "b.txt", "c.txt" }; + var tc = new + { + Paths = paths, + Settings = AutoProcessSettings, + Options = new MockOptions() { paths = paths }, + ExpectedTargetPaths = new string[] { }, + ExpectedRelativePathsTargetPaths = new string[] { }, + ExpectedRelativePathToContentFolders = new string[] { }, + ExpectedContentFoldersMarked = new string[] { }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(0); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + + [TestMethod] + public void DetermineDestinationsTest_NoDirsEmptyContentFolderSet() + { + var paths = new string[] { "a.txt", "b.txt", "c.txt" }; + var tc = new + { + Paths = paths, + Settings = AutoProcessSettings with { ContentFolders = new HashSet() { "" } }, + Options = new MockOptions() { paths = paths }, + ExpectedTargetPaths = new string[] { }, + ExpectedRelativePathsTargetPaths = new string[] { }, + ExpectedRelativePathToContentFolders = new string[] { }, + ExpectedContentFoldersMarked = new string[] { }, + }; + var arc = NewMockedArchive(tc.Options, out var _, out _, out _); + var determiner = new DPDestinationDeterminer(); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/"); + + var actual = determiner.DetermineDestinations(arc, tc.Settings); + + // Everything under the "Contents" folder should be extracted. + var expected = new HashSet(0); + AssertDestinations(expected, actual, "Z:/"); + AssertTargetPaths(actual, tc.ExpectedTargetPaths); + + // Additional assertions for ExpectedRelativePathsTargetPaths, ExpectedRelativePathToContentFolders, and ExpectedContentFoldersMarked + AssertRelativePaths(arc, tc.ExpectedRelativePathsTargetPaths, tc.ExpectedRelativePathToContentFolders); + AssertContentFolders(arc, tc.ExpectedContentFoldersMarked); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/DPProcessorTests.cs b/src/DAZ_Installer.CoreTests/DPProcessorTests.cs new file mode 100644 index 0000000..67aa50f --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPProcessorTests.cs @@ -0,0 +1,271 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Serilog; +using DAZ_Installer.Core.Extraction; +using Moq; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; + +#pragma warning disable 618 +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPProcessorTests + { + static IEnumerable DefaultContents => DPProcessorTestHelpers.DefaultContents; + static readonly DPProcessSettings DefaultProcessSettings = new("A:/", "B:/", InstallOptions.ManifestAndAuto); + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + + [TestMethod] + public void ProcessArchiveTest() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } } + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + p.ProcessArchive(a, DefaultProcessSettings); + + DPProcessorTestHelpers.AssertCommon(p); + //CollectionAssert.Contains(a.ProductInfo.Tags.ToArray(), new[] { "Gentlemen's Library", "TheRealSolly", "solomon1blount@gmail.com", "www.thesolomonchronicles.com", a.FileName }); + } + + [TestMethod] + public void ProcessArchiveTest_AfterCancelledProcess() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } }, + ExpectedArchiveCount = 2, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + var cancelledOnce = false; + p.ArchiveEnter += (_, __) => + { + if (!cancelledOnce) p.CancelProcessing(); + cancelledOnce = true; + }; + + p.ProcessArchive(a, DefaultProcessSettings); + p.ProcessArchive(a, DefaultProcessSettings); + + + DPProcessorTestHelpers.AssertCommon(p, Times.AtLeast(1)); + //CollectionAssert.Contains(a.ProductInfo.Tags.ToArray(), new[] { "Gentlemen's Library", "TheRealSolly", "solomon1blount@gmail.com", "www.thesolomonchronicles.com", a.FileName }); + } + + [TestMethod] + public void ProcessArchiveTest_AfterProcess() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } }, + ExpectedArchiveCount = 2, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + p.ProcessArchive(a, DefaultProcessSettings); + p.ProcessArchive(a, DefaultProcessSettings); + + + DPProcessorTestHelpers.AssertCommon(p, Times.Exactly(2)); + //CollectionAssert.Contains(a.ProductInfo.Tags.ToArray(), new[] { "Gentlemen's Library", "TheRealSolly", "solomon1blount@gmail.com", "www.thesolomonchronicles.com", a.FileName }); + } + + [TestMethod] + public void ProcessArchiveTest_AfterProcessError() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } }, + ExpectedArchiveCount = 2, + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + var calledOnce = false; + var mock = new Mock(new FakeFileSystem(), "A:/") { CallBase = true }.Object; + fs.Setup(x => x.CreateDriveInfo(It.IsRegex(@"A:/"))).Returns(() => + { + if (!calledOnce) + { + calledOnce = true; + throw new Exception("CreateDrive-a-doo"); + } + return mock; + }); + p.ProcessArchive(a, DefaultProcessSettings); + p.ProcessArchive(a, DefaultProcessSettings); + + + DPProcessorTestHelpers.AssertCommon(p, Times.Once()); + //CollectionAssert.Contains(a.ProductInfo.Tags.ToArray(), new[] { "Gentlemen's Library", "TheRealSolly", "solomon1blount@gmail.com", "www.thesolomonchronicles.com", a.FileName }); + } + + [TestMethod] + [DataRow("null", "null")] + [DataRow("null", "A:/")] + [DataRow("", "A:/")] + [DataRow("", "null")] + [DataRow("A:/", "null")] + + public void ProcessArchiveTest_InvalidProcessSettings(string? temp, string? dest) + { + if (temp == "null") temp = null; + if (dest == "null") dest = null; + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var _, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(DefaultContents, a); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectArchiveProcessed = new() { { a.FileName, DPProcessorTestHelpers.CreateExtractionReport(settings, Enumerable.Empty(), DPProcessorTestHelpers.CalculateExpectedFiles(DefaultContents)) } } + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + + try + { + p.ProcessArchive(a, new DPProcessSettings(temp, dest, InstallOptions.Automatic)); + Assert.Fail("Expected exception, got none."); + } catch { } + } + + [TestMethod] + public void ProcessArchiveTest_PeekError() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + e.Setup(x => x.Peek(It.IsAny())).Throws(new Exception("Peek-a-boo")); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + p.ProcessArchive(a, DefaultProcessSettings); + } + [TestMethod] + public void ProcessArchiveTest_ExtractError() + { + Func extractFunc = () => throw new Exception("Extract-a-doo"); + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions with { ExtractFunc = extractFunc }, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + p.ProcessArchive(a, DefaultProcessSettings); + } + [TestMethod] + public void ProcessArchiveTest_ExtractToTempError() + { + Func extractFunc = () => throw new Exception("Extract-a-doo"); + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions with { ExtractToTempFunc = extractFunc }, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + p.ProcessArchive(a, DefaultProcessSettings); + } + [TestMethod] + public void ProcessArchiveTest_OutOfStorage() + { + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + var fakeDriveInfo = new Mock(fs.Object, "N:/"); + fs.Setup(x => x.CreateDriveInfo(It.IsAny())).Returns(fakeDriveInfo.Object); + fakeDriveInfo.Object.AvailableFreeSpace = 0; + p.ProcessError += (_, __) => p.CancelCurrentArchive(); + p.ProcessArchive(a, DefaultProcessSettings); + } + + [TestMethod] + public void ProcessArchiveTest_OutOfStorageFixedButExtractError() + { + Func extractErrorFunc = () => throw new Exception("no u"); + var a = DPProcessorTestHelpers.NewMockedArchive(DPProcessorTestHelpers.DefaultMockOptions with { ExtractFunc = extractErrorFunc }, out var e, out _, out _, out var fs); + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out _, out _); + var ao = new DPProcessorTestHelpers.AssertOptions() + { + ExpectedProcessErrorCount = 1, + }; + DPProcessorTestHelpers.AttachCommonEventHandlers(p, ao); + p.ArchiveExit += (_, p) => Assert.IsFalse(p.Processed); + var fakeDriveInfo = new Mock(fs.Object, "N:/"); + fakeDriveInfo.Object.AvailableFreeSpace = 0; + fs.Setup(x => x.CreateDriveInfo(It.IsAny())).Returns(() => + { + if (fakeDriveInfo.Object.AvailableFreeSpace == 0) fakeDriveInfo.SetupProperty(x => x.AvailableFreeSpace, long.MaxValue); + return fakeDriveInfo.Object; + }); + p.ProcessArchive(a, DefaultProcessSettings); + } + + [TestMethod] + public void ProcessArchiveOrderTest() + { + var opts = DPProcessorTestHelpers.DefaultMockOptions with { paths = Enumerable.Empty() }; + var a = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out var ff, out var fs); + a.Path = ff.Object.FullName = "a.rar"; + var b = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out ff, out _); + b.Path = ff.Object.FullName = "b.rar"; + var c = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out ff, out _); + c.Path = ff.Object.FullName = "c.rar"; + var d = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out ff, out _); + d.Path = ff.Object.FullName = "d.rar"; + var e = DPProcessorTestHelpers.NewMockedArchive(opts, out var _, out _, out ff, out _); + e.Path = ff.Object.FullName = "e.rar"; + a.Subarchives.AddRange(new[] { b, c }); + b.Subarchives.Add(d); + c.Subarchives.Add(e); + + var p = DPProcessorTestHelpers.SetupProcessor(a, fs.Object, out var _, out var _); + var settings = DPProcessorTestHelpers.CreateExtractSettings(Enumerable.Empty(), a); + Queue expectedOrder = new(new[] { a, c, e, b, d }); + Queue expectedOrderClone = new(expectedOrder); + + p.ArchiveEnter += (_, p) => Assert.AreEqual(p.Archive, expectedOrder.Dequeue()); + p.ArchiveExit += (_, p) => Assert.AreEqual(p.Archive, expectedOrderClone.Dequeue()); + + p.ProcessArchive(a, DefaultProcessSettings); + + Assert.IsTrue(expectedOrder.Count == 0); + Assert.IsTrue(expectedOrderClone.Count == 0); + } + + // Invalid Process Settings + // + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/DPTagProviderTests.cs b/src/DAZ_Installer.CoreTests/DPTagProviderTests.cs new file mode 100644 index 0000000..6986671 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/DPTagProviderTests.cs @@ -0,0 +1,188 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client; +using Serilog; +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.IO.Fakes; +using Moq; +using DAZ_Installer.CoreTests.Extraction; + +namespace DAZ_Installer.Core.Tests +{ + [TestClass] + public class DPTagProviderTests + { + public static IEnumerable DefaultContents => new string[] { "Manifest.dsx", "Supplement.dsx", "Contents/a.txt", "Contents/b.txt", "Contents/Documents/c.png", "Contents/Documents/d.txt", "Contents/e.duf", "Contents/f.duf", "bullshit.png" }; + public static MockOptions DefaultMockOptions => new(); + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + } + + public struct MockOptions + { + public bool partialFileInfo = true; + public bool partialRAR = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFakeFileSystem = true; + public IEnumerable paths = DefaultContents; + public Func? ExtractToTempFunc = null; + public Func? ExtractFunc = null; + + public MockOptions() { } + } + public static DPArchive NewMockedArchive(MockOptions options, out Mock extractor, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock fakeFileSystem) + { + var fs = new Mock() { CallBase = options.partialFakeFileSystem }; + fakeFileSystem = fs; + fakeFileInfo = new Mock("Z:/test.rar") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fakeFileSystem.Object, null) { CallBase = options.partialDPFileInfo }; + extractor = new Mock(); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor.Object); + extractor.Setup(x => x.ExtractToTemp(It.IsAny())).Returns((DPExtractSettings x) => + { + return options.ExtractToTempFunc is null ? handleExtract(x, fs.Object) : options.ExtractToTempFunc(); + }); + extractor.Setup(x => x.Extract(It.IsAny())).Returns((DPExtractSettings x) => + { + return options.ExtractFunc is null ? handleExtract(x, fs.Object) : options.ExtractFunc(); + }); + SetupEntities(options.paths, arc); + extractor.Object.Extract(new DPExtractSettings("Z:/", Enumerable.Empty(), true, arc)); + return arc; + } + + private static void SetupEntities(IEnumerable paths, DPArchive arc) + { + foreach (var path in paths) + { + if (string.IsNullOrEmpty(Path.GetFileName(path))) new DPFolder(path, arc, null); + else DPFile.CreateNewFile(path, arc, null); + } + } + + private static void UpdateFileInfos(DPExtractSettings settings, FakeFileSystem system) + { + foreach (var file in settings.Archive.Contents.Values) + { + var path = string.IsNullOrEmpty(file.TargetPath) ? Path.Combine(settings.TempPath, file.Path) : file.TargetPath; + file.FileInfo = system.CreateFileInfo(path); + var mockFileInfo = Mock.Get(file.FileInfo); + var stream = DPArchiveTestHelpers.DetermineFileStream(file, settings.Archive); + Exception? ex = null; + mockFileInfo.Setup(x => x.TryAndFixOpenRead(out It.Ref.IsAny, out ex)) + .Callback((out Stream s, out Exception ex) => + { + s = DPArchiveTestHelpers.DetermineFileStream(file, settings.Archive); + ex = null; + }) + .Returns(true); + } + } + + private static DPExtractionReport handleExtract(DPExtractSettings settings, FakeFileSystem fs) + { + UpdateFileInfos(settings, fs); + return new DPExtractionReport() + { + ErroredFiles = new(0), + ExtractedFiles = settings.FilesToExtract.ToList(), + Settings = settings + }; + } + + private static void AssertTagsEqual(DPArchive arc, HashSet actual) + { + // Make sure that all of the ProductInfo tags are in the actual tags. + foreach (var tag in arc.DazFiles.Where(x => x.Extracted)) + { + var ci = tag.ContentInfo; + Assert.IsTrue(actual.Contains(ci.ContentType.ToString()), "Content type not found in Tags."); + Assert.IsTrue(actual.Contains(ci.Email), "Email not found in Tags."); + Assert.IsTrue(actual.Contains(ci.Website), "Website not found in Tags."); + foreach (var author in ci.Authors) + Assert.IsTrue(actual.Contains(author), "Author not found in Tags."); + } + foreach (var s in DPArchive.RegexSplitName(arc.ProductInfo.ProductName)) + { + Assert.IsTrue(actual.Contains(s)); + } + } + + [TestMethod] + public void GetTagsTest() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + } + + [TestMethod] + public void GetTagsTest_UnderscoreName() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + fi.Object.FullName = "Z:/i_am_leg.rar"; + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + Assert.IsTrue(result.Contains("i") && result.Contains("am") && result.Contains("leg"), "Underlined name not found in Tags."); + } + + [TestMethod] + public void GetTagsTest_DashName() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + fi.Object.FullName = "Z:/i-am-leg.rar"; + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + Assert.IsTrue(result.Contains("i") && result.Contains("am") && result.Contains("leg"), "Underlined name not found in Tags."); + } + + [TestMethod] + public void GetTagsTest_PlusName() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + fi.Object.FullName = "Z:/i+am+leg.rar"; + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + Assert.IsTrue(result.Contains("i") && result.Contains("am") && result.Contains("leg"), "Underlined name not found in Tags."); + } + + [TestMethod] + public void GetTagsTest_MixedName() + { + var arc = NewMockedArchive(DefaultMockOptions, out var e, out var dpfi, out var fi, out var fs); + fi.Object.FullName = "Z:/a+b-c_d.rar"; + var tp = new DPTagProvider(); + + var result = tp.GetTags(arc, new DPProcessSettings("Z:/", "Z:/", InstallOptions.Automatic)); + + AssertTagsEqual(arc, result); + Assert.IsTrue(result.Contains("a") && result.Contains("b") && result.Contains("c") && result.Contains("d"), "Underlined name not found in Tags."); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/DP7zExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/DP7zExtractorTests.cs new file mode 100644 index 0000000..cffbee0 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/DP7zExtractorTests.cs @@ -0,0 +1,540 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO; +using DAZ_Installer.IO.Fakes; +using Moq; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Microsoft.VisualStudio.TestPlatform.Utilities; + +#pragma warning disable 618 +namespace DAZ_Installer.Core.Extraction.Tests +{ + [TestClass] + public class DP7zExtractorTests + { + /// + /// A factory that returns a mocked fake archive with the default contents. + /// + static IProcessFactory DefaultFactory { get; set; } = null!; + static string[] DefaultContents => DPArchiveTestHelpers.DefaultContents; + static MockOptions DefaultOptions = new(); + static MockOptions DefaultPeekOptions = new(); + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + + var factMockObj = new Mock(); + factMockObj.Setup(x => x.Create()).Returns(SetupFakeProcess(DefaultContents).Object); + DefaultFactory = factMockObj.Object; + + DefaultPeekOptions.peek = false; + } + + private struct MockOptions + { + public bool partialFileInfo = true; + public bool partialFakeProcess = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFileSystem = true; + public string[] paths = DefaultContents; + public bool peek = true; + + public MockOptions() { } + } + + private readonly record struct MockOutputs (DP7zExtractor extractor, Mock fakeProcess, Mock fakeDPFileInfo, Mock fakeFileInfo, Mock fakeFileSystem, Mock factory); + + private static DPArchive NewMockedArchive(MockOptions options, out MockOutputs mockOutputs) + { + var fakeProcess = SetupFakeProcess(options.paths); + var fakeFileInfo = new Mock("Z:/test.7z") { CallBase = options.partialFileInfo }; + var fakeFileSystem = new Mock(DPFileScopeSettings.All) { CallBase = options.partialFileSystem }; + var fakeDPFileInfo = new Mock(fakeFileInfo.Object, fakeFileSystem.Object, null) { CallBase = options.partialDPFileInfo }; + var factory = new Mock(); + factory.Setup(x => x.Create()).Returns(fakeProcess.Object); + var extractor = new DP7zExtractor(Log.Logger.ForContext(), factory.Object); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor); + mockOutputs = new MockOutputs(extractor, fakeProcess, fakeDPFileInfo, fakeFileInfo, fakeFileSystem, factory); + if (!options.peek) return arc; + extractor.Peek(arc); + fakeProcess.Object.OutputEnumerable = new[] { "Everything is Ok" }; + return arc; + } + + /// + /// Returns a new and sets up the to contain the given paths. + /// + /// The entries to add to the fake archive. + internal static Mock SetupFakeProcess(IEnumerable paths, bool partial = true) + { + var l = new List(); + foreach (var p in paths) FakeProcess.GetLinesForEntity(p, l); + var proc = partial ? new Mock() { CallBase = true } : new Mock(); + proc.Object.OutputEnumerable = l; + return proc; + } + + public static DPExtractionReport RunAndAssertExtractEvents(DPAbstractExtractor extractor, DPExtractSettings settings, bool toTemp = false) + { + bool extracting = false, moving = false; + extractor.Extracting += () => + { + if (extracting) + Assert.Fail("Extracting event was raised more than once"); + extracting = true; + }; + extractor.Moving += () => + { + if (moving && !toTemp) + Assert.Fail("Moving event was raised more than once"); + else if (!moving && toTemp) Assert.Fail("Moving event was called when extracting to temp"); + moving = true; + }; + bool extractFinished = false, moveFinished = false; + extractor.ExtractFinished += () => + { + if (extractFinished) + Assert.Fail("ExtractFinished event was raised more than once"); + extractFinished = true; + }; + extractor.MoveFinished += () => + { + if (moveFinished && !toTemp) + Assert.Fail("MoveFinished event was raised more than once"); + else if (!moveFinished && toTemp) Assert.Fail("MoveFinished event was called when extracting to temp"); + moveFinished = true; + }; + var report = toTemp ? extractor.ExtractToTemp(settings) : extractor.Extract(settings); + Assert.IsTrue(extracting, "Extracting event was not raised"); + Assert.IsTrue(extractFinished, "ExtractFinished event was not raised"); + if (!toTemp) + { + Assert.IsTrue(moving, "Moving event was not raised"); + Assert.IsTrue(moveFinished, "MoveFinished event was not raised"); + } + return report; + } + + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, Path.GetFileNameWithoutExtension(arc.FileName), file.Path); + } + + [TestMethod] + public void DP7zExtractorTest() + { + var l = Mock.Of(); + var e = new DP7zExtractor(l, DefaultFactory); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(DefaultFactory, e.Factory); + } + + [TestMethod] + public void ExtractTest() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_PartialExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + var skippedFile = arc.Contents.Values.ElementAt(1); + var successFiles = arc.Contents.Values.Except(new[] { skippedFile }).ToList(); + var settings = new DPExtractSettings("Z:/temp", successFiles, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = successFiles, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_QuitsOnArcNotExists() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + var arcDPFileInfo = outputs.fakeDPFileInfo; + arcDPFileInfo.Setup(x => x.Exists).Returns(false); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + e.Extracting += () => Assert.Fail("Extracting event was raised"); + var report = e.Extract(settings); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_NoContents() + { + var arc = NewMockedArchive(new MockOptions() { paths = Array.Empty() }, out var outputs); + var e = outputs.extractor; + var proc = outputs.fakeProcess; + proc.Object.OutputEnumerable = new[] {null, "Everything is Ok"}; + var settings = new DPExtractSettings("Z:/temp", Array.Empty(), archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_ArcFileInfoOpenFail() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var arcDPFileInfo, out var _, out var _, out var _); + arcDPFileInfo.Setup(x => x.OpenRead()).Throws(new Exception("Something went wrong")); + + var settings = new DPExtractSettings("Z:/temp", new DPFile[] { new(), new(), new() }, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_FileNotPartOfArchive() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + arc.Contents.Values.First().AssociatedArchive = new DPArchive(); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var successFiles = arc.Contents.Values.Skip(1).ToList(); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = successFiles, + ErroredFiles = new() { { arc.Contents.Values.First(), "" } }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, successFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + } + [TestMethod] + public void ExtractTest_FilesNotWhitelisted() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var arcDPFileInfo, out var _, out var _, out var _); + e.Peek(arc); + outputs.fakeFileSystem.SetupProperty(x => x.Scope, DPFileScopeSettings.None); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = new(0), + ErroredFiles = arc.Contents.Values.ToDictionary(x => x, x => ""), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_UnexpectedExtractErrorEverythingFine() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + + var e = outputs.extractor; + var proc = outputs.fakeProcess; + proc.Object.ErrorEnumerable = new[] { "i am a teapot" }; + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.Extract(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractError() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + settings.FilesToExtract.Add(null); + e.Extract(settings); + settings.FilesToExtract.Remove(null); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractToTemp(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, settings.TempPath); // This should not matter, but will reuse this for testing purposes for AssertExtractorSetPathsCorrectly + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, settings.TempPath); // ExtractToTemp does not require TargetPaths + arc.ExtractContents(settings); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest_EmitsWithErrors() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var fakeProcess, out var _, out var _, out var _, out var _); + fakeProcess.Object.ErrorEnumerable = new[] { "Can not open encrypted archive. Wrong password? You silly goose." }; + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual("Can not open encrypted archive. Wrong password? You silly goose.", e.Explaination); + } + [TestMethod] + public void PeekTest_StartProcessFails() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var fakeProcess, out var _, out var _, out var _, out var _); + fakeProcess.Setup(x => x.Start()).Throws(new Exception("Something went wrong")); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual("Failed to start 7z process", e.Explaination); + } + + [TestMethod] + public void PeekTest_Encrypted() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + outputs.Deconstruct(out var e, out var fakeProcess, out var _, out var _, out var _, out var _); + var l = new List(); + FakeProcess.GetLinesForEntity("encrypted_something.jpg", l); + l.Insert(3, "Encrypted = +"); + fakeProcess.Object.OutputEnumerable = fakeProcess.Object.OutputEnumerable.Concat(l); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual(DPArchiveErrorArgs.EncryptedFilesExplanation, e.Explaination); + } + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + cts.Cancel(true); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = e.Extract(settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_CancelledDuringExtractOp() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + outputs.fakeProcess.Object.OutputDataReceived += _ => cts.Cancel(true); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_CancelledDuringMoveOp() + { + var arc = NewMockedArchive(DefaultOptions, out var outputs); + var e = outputs.extractor; + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + e.MoveProgress += (_, __) => cts.Cancel(true); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void PeekTest_CancelledBeforeOp() + { + var arc = NewMockedArchive(DefaultOptions with { peek = false }, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + e.CancellationToken = new(true); + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(0, arc.Contents.Count); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void PeekTest_CancelledDuringOp() + { + var arc = NewMockedArchive(DefaultOptions with { peek = false }, out var outputs); + outputs.Deconstruct(out var e, out var _, out var _, out var _, out var _, out var _); + CancellationTokenSource cts = new(); + e.CancellationToken = cts.Token; + outputs.fakeProcess.Object.OutputDataReceived += _ => cts.Cancel(true); + + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(0, arc.Contents.Count); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/DPRARExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/DPRARExtractorTests.cs new file mode 100644 index 0000000..1c4bc08 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/DPRARExtractorTests.cs @@ -0,0 +1,461 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using Moq; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using DAZ_Installer.External; +using System.Runtime.CompilerServices; +#pragma warning disable 618 +namespace DAZ_Installer.Core.Extraction.Tests +{ + [TestClass] + public class DPRARExtractorTests + { + /// + /// A factory that returns a mocked fake archive with the default contents. + /// + static IRARFactory DefaultFactory { get; set; } = null!; + static string[] DefaultContents => DPArchiveTestHelpers.DefaultContents; + static MockOptions DefaultOptions = new(); + static MockOptions DefaultPeekOptions = new(); + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + + var mock = new Mock(); + mock.Setup(x => x.Create(It.IsAny())).Returns(SetupFakeRAR(DefaultContents).Object); + DefaultFactory = mock.Object; + DefaultPeekOptions.peek = false; + } + + private struct MockOptions + { + public bool partialFileInfo = true; + public bool partialRAR = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFileSystem = true; + public string[] paths = DefaultContents; + public bool peek = true; + + public MockOptions() { } + } + + private static DPArchive SetupArchiveWithPartiallyFakedDependencies(MockOptions options, out DPRARExtractor extractor, out Mock fakeRAR, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock factory, out Mock fakeFileSystem) + { + var fs = new Mock(DPFileScopeSettings.All) { CallBase = options.partialFileSystem }; + fakeFileSystem = fs; + factory = new Mock(); + var aFakeRAR = SetupFakeRAR(options.paths, options.partialRAR); + fakeRAR = aFakeRAR; + fakeFileInfo = new Mock("Z:/test.rar") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fakeFileSystem.Object, null) { CallBase = options.partialDPFileInfo }; + factory.Setup(x => x.Create(It.IsAny())).Returns(() => + { + aFakeRAR.Object.Disposed = false; + return aFakeRAR.Object; + }); + extractor = new DPRARExtractor(Log.Logger.ForContext(), factory.Object); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor); + if (!options.peek) return arc; + extractor.Peek(arc); + return arc; + } + + /// + /// Returns a new and sets up the to contain the given paths. + /// + /// The entries to add to the fake archive. + internal static Mock SetupFakeRAR(IEnumerable paths, bool partial = true) + { + var l = new List(); + foreach (var p in paths) + l.Add(FakeRAR.CreateFileInfoForEntity(p)); + return partial ? new Mock(l) { CallBase = true } : new Mock(l); + } + + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, Path.GetFileNameWithoutExtension(arc.FileName), file.Path); + } + + [TestMethod] + public void DPRARExtractorTest() + { + var l = Mock.Of(); + var e = new DPRARExtractor(l, DefaultFactory); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(DefaultFactory, e.Factory); + } + + [TestMethod] + public void ExtractTest() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_QuitsOnArcNotExists() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out var arcDPFileInfo, out _, out _, out _); + arcDPFileInfo.SetupGet(x => x.Exists).Returns(false); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_NoContents() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(new MockOptions() { paths = Array.Empty() }, out var e, out var proc, out _, out _, out _, out _); + var settings = new DPExtractSettings("Z:/temp", Array.Empty(), archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_RAROpenFail() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var fakeRAR, out _, out _, out var factory, out _); + fakeRAR.Setup(x => x.Open(It.IsAny())).Throws(new Exception("no no no... we no do that here")); + + var settings = new DPExtractSettings("Z:/temp", new DPFile[] { new(), new(), new() }, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_FileNotPartOfArchive() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + arc.Contents.Values.First().AssociatedArchive = null; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var successFiles = arc.Contents.Values.Skip(1).ToList(); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = successFiles, + ErroredFiles = new() { { arc.Contents.Values.First(), "" } }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, successFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + } + [TestMethod] + public void ExtractTest_PartialExtract() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + var skippedFile = arc.Contents.Values.ElementAt(1); + var successFiles = arc.Contents.Values.Except(new[] { skippedFile }).ToList(); + var settings = new DPExtractSettings("Z:/temp", successFiles, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = successFiles, + ErroredFiles = new(0), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, successFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + } + + [TestMethod] + public void ExtractTest_FilesNotWhitelisted() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out var fs); + fs.Object.Scope = DPFileScopeSettings.None; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = new(0), + ErroredFiles = arc.Contents.Values.ToDictionary(x => x, x => ""), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_UnexpectedExtractErrorEverythingFine() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var proc, out _, out _, out _, out _); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtract() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.Extract(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractError() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var fakeArc, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + settings.FilesToExtract.Add(null); + e.Extract(settings); + settings.FilesToExtract.Remove(null); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractToTemp(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, settings.TempPath); // This should not matter, but will reuse this for testing purposes for AssertExtractorSetPathsCorrectly + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, settings.TempPath); // ExtractToTemp does not require TargetPaths + arc.ExtractContents(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var _, out _, out _, out _, out _); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest_EmitsWithErrors() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var fakeRAR, out _, out _, out _, out _); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual("Can not open encrypted archive. Wrong password? You silly goose.", e.Explaination); + } + [TestMethod] + public void PeekTest_StartProcessFails() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var fakeRAR, out _, out _, out _, out _); + fakeRAR.Setup(x => x.ReadHeader()).Throws(new Exception("Something went wrong")); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual("Failed to start 7z process", e.Explaination); + } + + [TestMethod] + public void PeekTest_Encrypted() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var fakeRAR, out _, out _, out _, out _); + var l = new List(); + fakeRAR.Object.FilesEnumerable.MoveNext(); + fakeRAR.Object.FilesEnumerable.Current.encrypted = true; + fakeRAR.Object.FilesEnumerable.Reset(); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + e.ArchiveErrored += (s, e) => Assert.AreEqual(DPArchiveErrorArgs.EncryptedFilesExplanation, e.Explaination); + } + + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + cts.Cancel(true); + + // Testing Extract() here: + var report = e.Extract(settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledDuringOp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + CancellationTokenSource cts = new(); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc) { CancelToken = cts.Token }; + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractProgress += (s, e) => cts.Cancel(true); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + + [TestMethod] + public void PeekTest_CancelledBeforeOp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var _, out _, out _, out _, out _); + e.CancellationToken = new(true); + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + Assert.AreEqual(0, arc.Contents.Count); + } + + [TestMethod] + public void PeekTest_CancelledDuringOp() + { + var arc = SetupArchiveWithPartiallyFakedDependencies(DefaultPeekOptions, out var e, out var r, out _, out _, out _, out _); + r.Setup(x => x.ReadHeader()).Callback(() => e.CancellationToken = new(true)); + + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + Assert.AreEqual(0, arc.Contents.Count); + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/DPZipExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/DPZipExtractorTests.cs new file mode 100644 index 0000000..9b2f70e --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/DPZipExtractorTests.cs @@ -0,0 +1,513 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Moq; +using Serilog; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.Core.Extraction.Fakes; + +#pragma warning disable CS0618 // Obsolete is for production code, not testing code. +namespace DAZ_Installer.Core.Extraction.Tests +{ + [TestClass] + public class DPZipExtractorTests + { + /// + /// A factory that returns a mocked fake archive with the default contents. + /// + static IZipArchiveFactory DefaultFactory { get; set; } = null!; + static string[] DefaultContents => DPArchiveTestHelpers.DefaultContents; + static MockOptions DefaultOptions = new(); + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .CreateLogger(); + var mock = new Mock(); + mock.Setup(m => m.Create(It.IsAny())).Returns(() => SetupFakeArchiveAndEntries(DefaultContents).Object); + DefaultFactory = mock.Object; + } + + private struct MockOptions + { + public bool partialFileInfo = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFileSystem = true; + public string[] paths = DefaultContents; + public bool peek = true; + + public MockOptions() { } + } + + private static DPArchive NewMockedArchive(MockOptions options, out DPZipExtractor extractor, out Mock fakeArc, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock factory, out Mock fakeFileSystem) + { + var fs = new Mock(DPFileScopeSettings.All) { CallBase = options.partialFileSystem }; + fakeFileSystem = fs; + fakeArc = SetupFakeArchiveAndEntries(options.paths, options.partialZipArchiveEntry); + fakeFileInfo = new Mock("Z:/test.zip") { CallBase = options.partialFileInfo }; + fakeDPFileInfo = new Mock(fakeFileInfo.Object, fs.Object, null) { CallBase = options.partialFileInfo }; + factory = new Mock(); + factory.Setup(m => m.Create(It.IsAny())).Returns(fakeArc.Object); + extractor = new DPZipExtractor(Log.Logger.ForContext(), factory.Object); + var arc = new DPArchive(string.Empty, Log.Logger.ForContext(), fakeDPFileInfo.Object, extractor); + if (options.peek) extractor.Peek(arc); + return arc; + } + + /// + /// Returns a new and sets up mocked s for the specified paths. + /// + /// The entries to add to the fake archive. + /// Whether to partially mock the entries, default is true. + /// A new with partially mocked s. + internal static Mock SetupFakeArchiveAndEntries(IEnumerable paths, bool partial = true) + { + var arc = new Mock() { CallBase = true }; + foreach (var path in paths) + { + var entryMock = new Mock(arc.Object, Stream.Null) { CallBase = partial }; + if (!partial) entryMock.SetupGet(x => x.FullName).Returns(path); + else entryMock.Object.FullName = path; + if (!partial) entryMock.SetupGet(x => x.Name).Returns(Path.GetFileName(path)); + arc.Object.PathToEntries.Add(entryMock.Object.FullName, entryMock.Object); + } + return arc; + } + + [TestMethod] + public void DPZipExtractorTest() + { + var l = Mock.Of(); + var e = new DPZipExtractor(l, DefaultFactory); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(DefaultFactory, e.Factory); + } + + [TestMethod] + public void ExtractTest() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_PartialExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var skippedFile = arc.Contents.Values.ElementAt(1); + var successFiles = arc.Contents.Values.Except(new[] { skippedFile }).ToList(); + var settings = new DPExtractSettings("Z:/temp", successFiles, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(successFiles), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_QuitsOnArcNotExists() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out var arcDPFileInfo, out _, out _, out _); + arcDPFileInfo.SetupGet(x => x.Exists).Returns(false); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.Extracting += () => Assert.Fail("Extracting event was raised"); + + // Testing Extract() here: + var report = e.Extract(settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_NoContents() + { + var arc = NewMockedArchive(new MockOptions() { paths = Array.Empty() }, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", Array.Empty(), archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_ArcFileInfoOpenFail() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out var arcDPFileInfo, out _, out _, out _); + arcDPFileInfo.Setup(x => x.OpenRead()).Throws(new IOException()); + + var settings = new DPExtractSettings("Z:/temp", new DPFile[] {new(), new(), new()}, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_FileNotPartOfArchive() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + arc.Contents.Values.First().AssociatedArchive = new DPArchive(); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { + ExtractedFiles = arc.Contents.Values.Skip(1).ToList(), + ErroredFiles = new() { { arc.Contents.Values.First(), "" } }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_FilesNotWhitelisted() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out var fs); + e.Peek(arc); + fs.Object.Scope = DPFileScopeSettings.None; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = new(0), + ErroredFiles = arc.Contents.Values.ToDictionary(x => x, x => ""), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_RetrySuccessUnauthorizedFileException() + { + var arc = NewMockedArchive(new() { partialZipArchiveEntry = false }, out var e, out var zipArc, out _, out _, out _, out var _); + + var firstEntityName = zipArc.Object.PathToEntries.Values.Where(x => !string.IsNullOrEmpty(x.Name)).First().FullName; + var subEntity = new Mock(zipArc.Object, Stream.Null) { CallBase = true }; + var calledOnce = false; + subEntity.Object.FullName = firstEntityName; + subEntity.Setup(x => x.ExtractToFile(It.IsAny(), It.IsAny())).Callback(() => + { + if (calledOnce) return; + calledOnce = true; + Log.Logger.Information("Throwing UnauthorizedAccessException"); + throw new UnauthorizedAccessException(); + }); + zipArc.Object.PathToEntries[firstEntityName] = subEntity.Object; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = arc.Contents.Values.ToList(), + ErroredFiles = new(0), + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_RetryFailUnauthorizedFileException() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var zipArc, out _, out _, out _, out _); + + var firstEntityName = zipArc.Object.PathToEntries.Values.Where(x => !string.IsNullOrEmpty(x.Name)).First().FullName; + var subEntity = new Mock(); + subEntity.Setup(x => x.ExtractToFile(It.IsAny(), It.IsAny())).Throws(new UnauthorizedAccessException()); + zipArc.Object.PathToEntries[firstEntityName] = subEntity.Object; + + var file = arc.Contents[PathHelper.NormalizePath(firstEntityName)]; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = arc.Contents.Values.Where(x => x != file).ToList(), + ErroredFiles = new() { { file, ""} }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, expectedReport.ExtractedFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(expectedReport.ExtractedFiles); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_UnexpectedExtractError() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var zipArc, out _, out _, out _, out _); + + var firstEntityName = zipArc.Object.PathToEntries.Values.Where(x => !string.IsNullOrEmpty(x.Name)).First().FullName; + var subEntity = new Mock(); + zipArc.Object.PathToEntries[firstEntityName] = null; + + var file = arc.Contents[PathHelper.NormalizePath(firstEntityName)]; + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() + { + ExtractedFiles = arc.Contents.Values.Where(x => x != file).ToList(), + ErroredFiles = new() { { file, "" } }, + Settings = settings + }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, expectedReport.ExtractedFiles.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(expectedReport.ExtractedFiles); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.Extract(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractError() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var fakeArc, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + settings.FilesToExtract.Add(null); + e.Extract(settings); + settings.FilesToExtract.Remove(null); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractToTemp(settings); + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abcd/"); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPathsForTemp(arc, settings.TempPath); // This should not matter, but will reuse this for testing purposes for AssertExtractorSetPathsCorrectly + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPathsForTemp(arc, settings.TempPath); // ExtractToTemp does not require TargetPaths, it will do + arc.ExtractContents(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest() + { + var e = new DPZipExtractor(Log.Logger, DefaultFactory); + var dpFileInfo = new FakeDPFileInfo(new FakeFileInfo("Z:/test.zip", null), new(), null); + var arc = new DPArchive(string.Empty, Log.Logger, dpFileInfo, e); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPArchiveTestHelpers.AssertDefaultContents(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, DefaultContents); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest_EmitsWithErrors() + { + var erroringFactory = new Mock(); + erroringFactory.Setup(erroringFactory => erroringFactory.Create(It.IsAny())).Throws(new Exception("Test exception")); + var e = new DPZipExtractor(Log.Logger, erroringFactory.Object); + var dpFileInfo = new FakeDPFileInfo(new FakeFileInfo("Z:/test.zip", null), new(), null); + var arc = new DPArchive(string.Empty, Log.Logger, dpFileInfo, e); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + } + [TestMethod] + public void PeekTest_SeekingErrorStopsSeeking() + { + var erroringFactory = new Mock(); + erroringFactory.Setup(erroringFactory => erroringFactory.Create(It.IsAny())).Throws(new Exception("Test exception")); + var e = new DPZipExtractor(Log.Logger, erroringFactory.Object); + var dpFileInfo = new FakeDPFileInfo(new FakeFileInfo("Z:/test.zip", null), new(), null); + var arc = new DPArchive(string.Empty, Log.Logger, dpFileInfo, e); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + } + + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.CancellationToken = new(true); + + // Testing Extract() here: + var report = e.Extract(settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_CancelledDuringOp() + { + var arc = NewMockedArchive(DefaultOptions, out var e, out var _, out _, out _, out _, out _); + + var settings = new DPExtractSettings("Z:/temp", arc.Contents.Values, archive: arc); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, "Z:/abc/"); + e.ExtractProgress += (_, __) => e.CancellationToken = new(true); + + // Testing Extract() here: + var report = e.Extract(settings); + DPArchiveTestHelpers.AssertReport(expectedReport, report); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest_CancelledBeforeOp() + { + var arc = NewMockedArchive(DefaultOptions with { peek = false }, out var e, out var a, out _, out _, out _, out _); + e.CancellationToken = new(true); + + // Testing Peek() here: + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + Assert.AreEqual(0, arc.Contents.Count); + } + + [TestMethod] + public void PeekTest_CancelledDuringOp() + { + var arc = NewMockedArchive(DefaultOptions with { peek = false }, out var e, out var a, out _, out _, out _, out _); + a.Setup(x => x.Entries).Callback(() => e.CancellationToken = new(true)); + + // Testing Peek() here: + e.Peek(arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + Assert.AreEqual(0, arc.Contents.Count); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeProcess.cs b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeProcess.cs new file mode 100644 index 0000000..8e1df89 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeProcess.cs @@ -0,0 +1,94 @@ +using System.Diagnostics; + +namespace DAZ_Installer.Core.Extraction.Fakes +{ + /// + /// A fake process that implements and all implemented properties and methods are virtual. + /// + internal class FakeProcess : IProcess + { + public virtual StreamWriter StandardInput => StreamWriter.Null; + public virtual ProcessStartInfo StartInfo { get; set; } = new(); + public virtual bool HasExited { get; set; } + public virtual bool EnableRaisingEvents { get; set; } = false; + + public virtual event Action? OutputDataReceived; + public virtual event Action? ErrorDataReceived; + public virtual event Action? Exited; + + public virtual IEnumerable OutputEnumerable { get; set; } = Enumerable.Empty(); + public virtual IEnumerable ErrorEnumerable { get; set; } = Enumerable.Empty(); + public virtual IEnumerator? OutputEnumerator { get; set; } = null; + public virtual IEnumerator? ErrorEnumerator { get; set; } = null; + /// + /// Begins raising the event. + /// It will emit error data to the event via . + /// Additionally, if any values are provided, it will stop until the next call to . + /// This method is virtual. + /// + public virtual void BeginErrorReadLine() + { + if (!EnableRaisingEvents) throw new Exception("EnableRaisingEvents must be true to use this method."); + ErrorEnumerator ??= ErrorEnumerable.GetEnumerator(); + while (ErrorEnumerator.MoveNext()) + { + var line = ErrorEnumerator.Current; + ErrorDataReceived?.Invoke(line); + if (line is null) return; + } + ErrorEnumerator = null; + ErrorDataReceived?.Invoke(null); + HasExited = true; + } + /// + /// Begins raising the event. + /// It will emit error data to the event via . + /// Additionally, if any values are provided, it will stop until the next call to . + /// + /// This method is virtual. + /// + public virtual void BeginOutputReadLine() + { + if (!EnableRaisingEvents) throw new Exception("EnableRaisingEvents must be true to use this method."); + OutputEnumerator ??= OutputEnumerable.GetEnumerator(); + while (OutputEnumerator.MoveNext()) + { + var line = OutputEnumerator.Current; + OutputDataReceived?.Invoke(line); + if (line is null) return; + } + OutputEnumerator = null; + OutputDataReceived?.Invoke(null); + } + /// + /// Does nothing. + /// + public virtual void Dispose() { } + /// + /// Sets to true. + /// + /// does nothing + public virtual void Kill(bool entireProcessTree) => HasExited = true; + /// + /// Does nothing. + /// + public virtual void Start() { } + public virtual bool WaitForExit(int milliseconds) => SpinWait.SpinUntil(() => HasExited, milliseconds); + /// + /// Returns an array of strings that represent 7z output. At [0] = path, [1] = size, [2] = attributes, [3] = separator. + /// + /// The path to get 7z info from. + /// The lines of output for the entity. + public static void GetLinesForEntity(string entity, in List listToAddStringsTo) + { + var fname = Path.GetFileName(entity); + listToAddStringsTo.AddRange(new[] + { + "----------", + "Path = " + entity, + "Size = " + 1, + "Attributes = " + (string.IsNullOrEmpty(fname) ? "D" : ""), + }); + } + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeRAR.cs b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeRAR.cs new file mode 100644 index 0000000..36b62fc --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeRAR.cs @@ -0,0 +1,143 @@ +using DAZ_Installer.External; + +namespace DAZ_Installer.Core.Extraction.Fakes +{ + internal class FakeRAR : IRAR + { + public IEnumerator FilesEnumerable; + public virtual bool Disposed { get; set; } = false; + public virtual bool Closed { get; set; } = true; + /// + /// ActionCalled is a variable used to determine if after a call to , + /// that either , , or was called. + /// + public virtual bool ActionCalled { get; set; } = true; + public virtual RAR.OpenMode Mode { get; set; } = RAR.OpenMode.List; + + public virtual event RAR.MissingVolumeHandler? MissingVolume; + public virtual event RAR.NewFileHandler? NewFile; + public virtual event RAR.PasswordRequiredHandler? PasswordRequired; + public virtual event RAR.ExtractionProgressHandler? ExtractionProgress; + + internal FakeRAR(IEnumerable files) => FilesEnumerable = files.GetEnumerator(); + + public virtual RARFileInfo CurrentFile => FilesEnumerable.Current; + + /// + /// Returns the value at . + /// + public RAR.RAROpenArchiveDataEx ArchiveData => ArchiveDataToReturn; + public RAR.RAROpenArchiveDataEx ArchiveDataToReturn = new() + { + ArcName = "test.rar", + ArcNameW = "test.rar", + OpenMode = (uint)RAR.OpenMode.List, + Flags = (uint)(RAR.ArchiveFlags.FirstVolume | RAR.ArchiveFlags.Volume), + }; + + public virtual string DestinationPath { get; set; } = string.Empty; + + /// + /// Sets the flag to true. + /// + public virtual void Close() => Closed = true; + /// + /// Sets the flag to true. + /// + public virtual void Dispose() => Disposed = true; + /// + /// Changes destination path and throws if or is true. + /// + /// The destination to extract to. + /// + /// + public virtual void Extract(string destinationName) + { + _ = throwIfDisposed() && throwIfClosed(); + if (Mode != RAR.OpenMode.Extract) throw new InvalidOperationException("Archive is not open for extraction."); + if (CurrentFile is null) throw new InvalidOperationException("No file is selected."); + if (CurrentFile.encrypted) throw new IOException("File could not be opened."); // do not change this err message or type. + DestinationPath = Path.GetDirectoryName(destinationName) ?? string.Empty; + ActionCalled = true; + } + /// + /// Resets the enumerator and sets the flag to false. Throws if is true or is false + /// + /// The mode to use + /// + /// + public virtual void Open(RAR.OpenMode mode) + { + throwIfDisposed(); + if (!Closed) throw new InvalidOperationException("Archive is already open."); + Closed = false; + ActionCalled = true; + Mode = mode; + FilesEnumerable.Reset(); + } + /// + /// Moves the enumerator to the next element in the archive. + /// + /// Whether there are any more elements in the archive. + /// if it is. Otherwise, . + /// + /// + /// + public virtual bool ReadHeader() + { + var a = throwIfDisposed() || throwIfClosed() || throwIfActionNotCalled() || FilesEnumerable.MoveNext(); + if (!a) return a; + if (Mode == RAR.OpenMode.List) + NewFile?.Invoke(this, new NewFileEventArgs(FilesEnumerable.Current)); + ActionCalled = false; + return true; + + } + /// + /// Checks if disposed or closed. If not, throws an exception. + /// + /// + /// + public virtual void Skip() + { + _ = throwIfDisposed() || throwIfClosed(); + ActionCalled = true; + } + /// + /// Throws an exception if is null or or is true. + /// + /// + /// + public virtual void Test() + { + _ = throwIfDisposed() && throwIfClosed(); + ArgumentNullException.ThrowIfNull(FilesEnumerable.Current); + ActionCalled = true; + } + + /// + /// Throws an exception if is true. Always returns false. + /// + /// + private bool throwIfDisposed() => Disposed ? throw new ObjectDisposedException(nameof(FakeRAR)) : false; + /// + /// Throws an exception if is true. Always returns false. + /// + /// + private bool throwIfClosed() => Closed ? throw new InvalidOperationException("Archive is closed.") : false; + + /// + /// Throws an exception if is false and the current mode is set to . Always returns false. + /// + /// + private bool throwIfActionNotCalled() => ActionCalled || Mode != RAR.OpenMode.Extract ? false : + throw new InvalidOperationException("Archive is corrupt."); + + internal static RARFileInfo CreateFileInfoForEntity(string path) => new() + { + UnpackedSize = 1, + FileName = path, + IsDirectory = string.IsNullOrEmpty(Path.GetFileName(path)) + }; + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchive.cs b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchive.cs new file mode 100644 index 0000000..2dfaf97 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchive.cs @@ -0,0 +1,45 @@ +using System.IO.Compression; + +namespace DAZ_Installer.Core.Extraction.Fakes +{ + internal class FakeZipArchive : IZipArchive + { + public Dictionary PathToEntries = new(); + public FakeZipArchive() { } + public FakeZipArchive(IEnumerable entries) + { + foreach (IZipArchiveEntry entry in entries) + { + PathToEntries.Add(entry.FullName, entry); + } + } + public FakeZipArchive(Dictionary dict) => PathToEntries = dict; + public FakeZipArchive(IEnumerable paths) + { + foreach (var path in paths) + { + var entry = new FakeZipArchiveEntry(this, null) + { + FullName = path, + }; + PathToEntries.Add(path, entry); + } + } + + public virtual IReadOnlyCollection Entries => PathToEntries.Values; + + public virtual ZipArchiveMode Mode { get; set; } + + public virtual IZipArchiveEntry CreateEntry(string entryName) => new FakeZipArchiveEntry(this, null); + /// + /// The name of the archive + /// + /// The entry to find. + /// + public virtual IZipArchiveEntry? GetEntry(string entryName) => PathToEntries.TryGetValue(entryName, out IZipArchiveEntry? entry) ? entry : null; + /// + /// Does nothing. + /// + public virtual void Dispose() { } + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchiveEntry.cs b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchiveEntry.cs new file mode 100644 index 0000000..9833b7f --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Fakes/FakeZipArchiveEntry.cs @@ -0,0 +1,27 @@ +namespace DAZ_Installer.Core.Extraction.Fakes +{ + internal class FakeZipArchiveEntry : IZipArchiveEntry + { + Stream stream = Stream.Null; + public virtual IZipArchive Archive { get; set; } + + public virtual string Name => Path.GetFileName(FullName); + + public virtual string FullName { get; set; } = string.Empty; + + public virtual long Length { get; set; } = 0; + + public virtual long CompressedLength { get; set; } = 0; + + public virtual DateTimeOffset LastWriteTime { get; set; } = new DateTimeOffset(); + public FakeZipArchiveEntry(IZipArchive Archive, Stream? stream) + { + this.Archive = Archive; + this.stream = stream ?? Stream.Null; + } + public virtual void Delete() { } + public virtual void ExtractToFile(string destinationFileName) { } + public virtual void ExtractToFile(string destinationFileName, bool overwrite) { } + public virtual Stream Open() => stream; + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Helpers/DPArchiveTestHelpers.cs b/src/DAZ_Installer.CoreTests/Extraction/Helpers/DPArchiveTestHelpers.cs new file mode 100644 index 0000000..3f61b95 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Helpers/DPArchiveTestHelpers.cs @@ -0,0 +1,203 @@ +using DAZ_Installer.Core; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using Moq; +using DAZ_Installer.IO; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.Core.Extraction; +using Serilog; +using System.Text; +using System.Xml; + +namespace DAZ_Installer.CoreTests.Extraction +{ + internal static class DPArchiveTestHelpers + { + /// + /// The default folders and files to add to an archive. + /// + internal static readonly string[] DefaultContents = new[] { "Contents/", "Contents/a.txt", "b.txt", "Contents/A/c.png" }; + + /// + /// Asserts whether the contents of the archive are as expected. + /// + /// The archive to check. + internal static void AssertDefaultContents(DPArchive arc) + { + Assert.AreEqual(3, arc.Contents.Count, "Archive contents count does not match"); + Assert.AreEqual(1, arc.RootFolders.Count, "Archive root folders count does not match"); + Assert.AreEqual(2, arc.Folders.Count, "Archive folders count does not match"); + Assert.AreEqual(1, arc.RootContents.Count, "Archive root contents count does not match"); + } + + /// + /// Asserts whether the paths of the entities in the archive are as expected (or whether it was added in successfully). + /// + /// The archive to check. + /// The entities to check. + public static void AssertExtractorSetPathsCorrectly(DPArchive arc, IEnumerable entities) + { + foreach (var entity in entities) + { + if (!arc.Contents.ContainsKey(PathHelper.NormalizePath(entity)) && !arc.Folders.ContainsKey(PathHelper.NormalizePath(entity))) + Assert.Fail("Extractor did not correctly set entity paths"); + } + } + + /// + /// Sets up peeking events for the extractor and asserts whether they were raised correctly by peeking into the specified archive. + /// + /// The extractor to use. + /// The archive for to peek into. + public static void RunAndAssertPeekEvents(DPAbstractExtractor extractor, DPArchive arc) + { + bool peeked = false; + extractor.Peeking += () => + { + if (peeked) + Assert.Fail("Peeking event was raised more than once"); + peeked = true; + }; + bool peekFinished = false; + extractor.PeekFinished += () => + { + if (peekFinished) + Assert.Fail("PeekFinished event was raised more than once"); + peekFinished = true; + }; + extractor.Peek(arc); + Assert.IsTrue(peeked, "Peeking event was not raised"); + Assert.IsTrue(peekFinished, "PeekFinished event was not raised"); + } + + /// + /// Sets up peeking events for the extractor and asserts whether they were raised correctly by peeking into the specified archive. + /// + /// The extractor to use. + /// The archive for to peek into. + public static DPExtractionReport RunAndAssertExtractEvents(DPAbstractExtractor extractor, DPExtractSettings settings, bool toTemp = false) + { + bool extracting = false; + extractor.Extracting += () => + { + if (extracting) + Assert.Fail("Extracting event was raised more than once"); + extracting = true; + }; + bool extractFinished = false; + extractor.ExtractFinished += () => + { + if (extractFinished) + Assert.Fail("ExtractFinished event was raised more than once"); + extractFinished = true; + }; + var report = toTemp ? extractor.ExtractToTemp(settings) : extractor.Extract(settings); + Assert.IsTrue(extracting, "Extracting event was not raised"); + Assert.IsTrue(extractFinished, "ExtractFinished event was not raised"); + return report; + } + + public static void AssertExtractFileInfosCorrectlySet(IEnumerable expectedFilesExtracted) + { + foreach (var file in expectedFilesExtracted) + { + Assert.IsNotNull(file.FileInfo, $"{file.FileName}'s FileInfo is null, want not null"); + Assert.AreEqual(PathHelper.NormalizePath(file.TargetPath), + PathHelper.NormalizePath(file.FileInfo!.Path), + $"{file}'s FileInfo's Path does not match TargetPath"); + } + } + + public static void AssertReport(DPExtractionReport want, DPExtractionReport got) + { + CollectionAssert.AreEqual(want.ExtractedFiles, got.ExtractedFiles, "Reports' extracted files are not equal"); + CollectionAssert.AreEqual(want.ErroredFiles.Keys, got.ErroredFiles.Keys, "Reports' errored file keys are not equal"); + Assert.AreEqual(want.Settings, got.Settings, "Reports' settings are not equal"); + } + + public static void SetupTargetPaths(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, arc.FileName, file.FileName); + } + + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, file.FileName); + } + + public static Stream CreateMetadataStream(DPFile file, + string type = "wearable", + string author = "TheRealSolly", + string email = "solomon1blount@gmail.com", + string website = "www.thesolomonchronicles.com") + { + var stream = new MemoryStream(); + var json = $@" + {{ + ""file_version"" : ""0.6.0.0"", + ""asset_info"" : {{ + ""id"" : ""/{file.Path}"", + ""type"" : ""{type}"", + ""contributor"" : {{ + ""author"" : ""{author}"", + ""email"" : ""{email}"", + ""website"" : ""{website}"" + }} + }} + }}"; + var sw = new StreamWriter(stream); + sw.Write(json); + sw.Flush(); + stream.Position = 0; + return stream; + + } + + public static Stream CreateManifestStream(DPArchive arc, IEnumerable files) + { + var stream = new MemoryStream(); + var doc = new XmlDocument(); + + var header = doc.CreateElement("DAZInstallManifest"); + header.SetAttribute("VERSION", "0.1"); + doc.AppendChild(header); + + foreach (var file in files) + { + var node = doc.CreateElement("File"); + node.SetAttribute("TARGET", "Content"); + node.SetAttribute("ACTION", "Install"); + node.SetAttribute("VALUE", file); + + header.AppendChild(node); + } + doc.Save(stream); + stream.Position = 0; + return stream; + } + + public static Stream CreateSupplementStream(string productName = "Gentlemen's Library") + { + string supplementStr = "" + + " " + + $" " + + " " + + " " + + ""; + + MemoryStream stream = new(Encoding.ASCII.GetBytes(supplementStr)); + stream.Position = 0; + return stream; + } + public static Stream DetermineFileStream(DPFile file, DPArchive arc, IEnumerable? pathsForManifest = null) + { + if (file.FileName == "Manifest.dsx") return CreateManifestStream(arc, pathsForManifest ?? arc.Contents.Values.Select(x => x.Path)); + else if (file.FileName == "Supplement.dsx") return CreateSupplementStream(); + else if (DPFile.DAZFormats.Contains(file.Ext)) return CreateMetadataStream(file); + return Stream.Null; + } + + } +} diff --git a/src/DAZ_Installer.CoreTests/Extraction/Integration/DP7zExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/Integration/DP7zExtractorTests.cs new file mode 100644 index 0000000..640255d --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Integration/DP7zExtractorTests.cs @@ -0,0 +1,433 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO; +using DAZ_Installer.IO.Fakes; +using Moq; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using DAZ_Installer.Core.Integration; +using System.Diagnostics; + +#pragma warning disable 618 +namespace DAZ_Installer.Core.Extraction.Integration.Tests +{ + [TestClass] + public class DP7zExtractorTests + { + public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "DAZ_Installer.CoreTests", "Extraction", "Integration", "Test Subjects"); + public static readonly string RegularArchivePath = Path.Combine(TempPath, "regular.7z"); + public static readonly string SolidArchivePath = Path.Combine(TempPath, "solid.7z"); + public static readonly string EncryptedArchivePath = Path.Combine(TempPath, "encrypted.7z"); + public static readonly string EncryptedHeadersArchivePath = Path.Combine(TempPath, "encrypted.7z"); + public static readonly string MultiVolumeArchivePathInit = Path.Combine(TempPath, "multivolume.7z"); + public static readonly string MultiVolumeArchivePath = Path.Combine(TempPath, "multivolume.7z.001"); + public static readonly string MultiVolumeArchivePath2 = Path.Combine(TempPath, "multivolume.7z.002"); + public static readonly string ArchiveContentsPath = Path.Combine(TempPath, "Archive Contents"); + public static List ArchiveContents = new(5); + // f DRY all my homies copypasta + public static readonly string ExtractPath = Path.Combine(Path.GetTempPath(), "DAZ_InstallerTests", "Extract"); + public static readonly string TestSubjectsPath = Path.Combine(Environment.CurrentDirectory, "Integration", "Test Subjects"); + public static readonly DPFileScopeSettings DefaultScope = new(Enumerable.Empty(), new[] { ExtractPath }, false); + public static readonly DPFileSystem FileSystem = new DPFileSystem(DefaultScope); + public static readonly ProcessStartInfo StartInfoTemplate = new() { FileName = "7za.exe", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; + public static readonly List DefaultOpts = new() + { + "-t7z", // Set archive type to 7z + "-mx0", // Set compression method to simply copy. + "-ms=off" // Set solid mode to off. + }; + public static IEnumerable ArchiveEnumerable => new[] + { + new[] { RegularArchivePath }, + new[] { SolidArchivePath }, + new[] { MultiVolumeArchivePath }, + }; + public static IEnumerable EncryptedArchiveEnumerable => new[] + { + new[] { EncryptedArchivePath }, + new[] { EncryptedHeadersArchivePath }, + }; + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Verbose() + .CreateLogger(); + + ArchiveContents = DPIntegrationArchiveHelpers.CreateArchiveContents(ArchiveContentsPath); + Create7zArchiveOnDisk(RegularArchivePath, ArchiveContentsPath, DefaultOpts); + Create7zArchiveOnDisk(SolidArchivePath, ArchiveContentsPath, DefaultOpts.SkipLast(1)); + Create7zArchiveOnDisk(MultiVolumeArchivePathInit, ArchiveContentsPath, DefaultOpts.Append("-v6m")); + if (!File.Exists(MultiVolumeArchivePath2)) throw new Exception("Failed to create multivolume archive"); + Create7zArchiveOnDisk(EncryptedArchivePath, ArchiveContentsPath, DefaultOpts.Append("-pPASSWORD")); + Create7zArchiveOnDisk(EncryptedHeadersArchivePath, ArchiveContentsPath, DefaultOpts.Append("-pPASSWORD") + .Append("-mhe")); + Directory.CreateDirectory(ExtractPath); + } + [ClassCleanup] + public static void ClassCleanup() + { + Directory.Delete(TempPath, true); + Directory.Delete(ExtractPath, true); + } + + [TestCleanup] + public void TestCleanup() + { + Directory.Delete(ExtractPath, true); + } + + [TestInitialize] + public void TestInitialize() + { + Directory.CreateDirectory(ExtractPath); + } + + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, Path.GetFileNameWithoutExtension(arc.FileName), file.Path); + } + + public static DPExtractionReport RunAndAssertExtractEvents(DPAbstractExtractor extractor, DPExtractSettings settings, bool toTemp = false) + { + bool extracting = false, moving = false; + extractor.Extracting += () => + { + if (extracting) + Assert.Fail("Extracting event was raised more than once"); + extracting = true; + }; + extractor.Moving += () => + { + if (moving && !toTemp) + Assert.Fail("Moving event was raised more than once"); + else if (!moving && toTemp) Assert.Fail("Moving event was called when extracting to temp"); + moving = true; + }; + bool extractFinished = false, moveFinished = false; + extractor.ExtractFinished += () => + { + if (extractFinished) + Assert.Fail("ExtractFinished event was raised more than once"); + extractFinished = true; + }; + extractor.MoveFinished += () => + { + if (moveFinished && !toTemp) + Assert.Fail("MoveFinished event was raised more than once"); + else if (!moveFinished && toTemp) Assert.Fail("MoveFinished event was called when extracting to temp"); + moveFinished = true; + }; + var report = toTemp ? extractor.ExtractToTemp(settings) : extractor.Extract(settings); + Assert.IsTrue(extracting, "Extracting event was not raised"); + Assert.IsTrue(extractFinished, "ExtractFinished event was not raised"); + if (!toTemp) + { + Assert.IsTrue(moving, "Moving event was not raised"); + Assert.IsTrue(moveFinished, "MoveFinished event was not raised"); + } + return report; + } + + public static void Create7zArchiveOnDisk(string savePath, string contentsPath, IEnumerable appendedArgs) + { + if (string.IsNullOrWhiteSpace(contentsPath)) throw new ArgumentException("No content path was provided", nameof(contentsPath)); + if (!contentsPath.EndsWith('*')) contentsPath = Path.Join(contentsPath, "*"); + var opts = string.Join(' ', appendedArgs); + var args = $"a {opts} \"{savePath}\" \"{contentsPath}\""; + var proc = new Process() { StartInfo = new() { + FileName = "7za.exe", + Arguments = args, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + proc.Start(); + proc.WaitForExit(); + + Log.Information("Create7zArchiveOnDisk() args: {args}", args); + Log.Information("7z output: {output}", proc.StandardOutput.ReadToEnd()); + if (!proc.StandardError.EndOfStream) Log.Error(proc.StandardError.ReadToEnd()); + + if (opts.Contains("-v")) return; + + // Check if the file exists, if it doesn't throw an error. + if (!File.Exists(savePath)) + throw new Exception($"Failed to create 7z archive on disk at {savePath}"); + } + + [TestMethod] + public void DP7zExtractorTest() + { + var l = Mock.Of(); + var f = new ProcessFactory(); + var e = new DP7zExtractor(l, f); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(f, e.Factory); + } + + [TestMethod] + [DynamicData(nameof(ArchiveEnumerable), DynamicDataSourceType.Property)] + public void ExtractTest(string path) + { + var fi = FileSystem.CreateFileInfo(path); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [Timeout(6000)] + [DynamicData(nameof(EncryptedArchiveEnumerable), DynamicDataSourceType.Property)] + public void ExtractTest_Encrypted(string path) + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(5)); + var fi = FileSystem.CreateFileInfo(path); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values, archive: arc); + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here + bool encryptedFilesShowed = false; + e.ArchiveErrored += (a, b) => + { + Assert.IsFalse(encryptedFilesShowed, "Encrypted files event called more than once."); + if (b.Explaination == DPArchiveErrorArgs.EncryptedFilesExplanation) encryptedFilesShowed = true; + }; + var report = e.Extract(settings); + + Assert.IsTrue(encryptedFilesShowed, "Encrypted files event was not called."); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_FilesNotWhitelisted() + { + var scope = new DPFileScopeSettings(Array.Empty(), new[] { Path.Combine(ExtractPath, "regular") }, true); + var fi = new DPFileSystem(scope).CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = arc.Contents.Values.ToDictionary(x => x, x => ""), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtract() + { + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.Extract(settings); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.ExtractToTemp(settings); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + [DynamicData(nameof(ArchiveEnumerable), DynamicDataSourceType.Property)] + public void ExtractToTempTest(string path) + { + var fi = FileSystem.CreateFileInfo(path); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, ExtractPath); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, ExtractPath); + e.Extract(settings); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [DynamicData(nameof(ArchiveEnumerable), DynamicDataSourceType.Property)] + public void PeekTest(string path) + { + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var fi = FileSystem.CreateFileInfo(path); + var arc = new DPArchive(fi); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [DynamicData(nameof(EncryptedArchiveEnumerable), DynamicDataSourceType.Property)] + public void PeekTest_Encrypted(string path) + { + var e = new DP7zExtractor(); + var fi = FileSystem.CreateFileInfo(path); + var arc = new DPArchive(fi); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + e.CancellationToken = new CancellationToken(true); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledDuringExtractOp() + { + var processFactory = new Mock(); + var proc = new ProcessWrapper(); + proc.OutputDataReceived += _ => + processFactory.SetupSequence(x => x.Create()).Returns(new ProcessWrapper()) + .Returns(proc); + + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var cts = new CancellationTokenSource(); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()) { CancellationToken = cts.Token }; + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + proc.OutputDataReceived += _ => cts.Cancel(true); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_CancelledDuringMoveOp() + { + var processFactory = new Mock(); + var proc = new ProcessWrapper(); + proc.OutputDataReceived += _ => + processFactory.SetupSequence(x => x.Create()).Returns(new ProcessWrapper()) + .Returns(proc); + + var fi = FileSystem.CreateFileInfo(RegularArchivePath); + var cts = new CancellationTokenSource(); + var e = new DP7zExtractor(Log.Logger, new ProcessFactory()) { CancellationToken = cts.Token }; + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.MoveProgress += (_, __) => cts.Cancel(true); + + // Testing Extract() here: + var report = RunAndAssertExtractEvents(e, settings); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/Integration/DPRARExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/Integration/DPRARExtractorTests.cs new file mode 100644 index 0000000..bb11dc2 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Integration/DPRARExtractorTests.cs @@ -0,0 +1,222 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using Moq; +using Serilog; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using DAZ_Installer.External; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Castle.DynamicProxy.Generators; +#pragma warning disable 618 +namespace DAZ_Installer.Core.Extraction.Integration.Tests +{ + [TestClass] + public class DPRARExtractorTests + { + public static readonly string ExtractPath = Path.Combine(Path.GetTempPath(), "DAZ_InstallerTests", "Extract"); + public static readonly string TestSubjectsPath = Path.Combine(Environment.CurrentDirectory, "Integration", "Test Subjects"); + public static readonly DPFileScopeSettings DefaultScope = new(Enumerable.Empty(), new[] { ExtractPath }, false); + public static readonly DPFileSystem FileSystem = new DPFileSystem(DefaultScope); + + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .MinimumLevel.Information() + .CreateLogger(); + MSTestLogger.LogMessage("Class initializing..."); + if (!Directory.Exists(TestSubjectsPath)) throw new DirectoryNotFoundException(TestSubjectsPath); + } + + [TestMethod] + public void DPRARExtractorTest() + { + var l = Mock.Of(); + var f = new RARFactory(); + var e = new DPRARExtractor(l, f); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(f, e.Factory); + } + + [TestCleanup] + public void Cleanup() + { + MSTestLogger.LogMessage("Cleaning up..."); + Directory.Delete(ExtractPath, true); + } + + [TestInitialize] + public void Initialize() + { + MSTestLogger.LogMessage("Initializing..."); + Directory.CreateDirectory(ExtractPath); + Console.WriteLine(Environment.CurrentDirectory); + Console.WriteLine(Directory.Exists(TestSubjectsPath)); + } + + // This is different than the one from DPArchiveTestHelpers. + public static void SetupTargetPathsForTemp(DPArchive arc, string basePath) + { + foreach (var file in arc.Contents.Values) + file.TargetPath = Path.Combine(basePath, Path.GetFileNameWithoutExtension(arc.FileName), file.Path); + } + + [TestMethod] + [DataRow("Test.rar"), DataRow("Test_split.part1.rar"), DataRow("Test_split_solid.part1.rar")] + public void ExtractTest(string path) + { + List fileData = new(65536); + var factory = new Mock(); + using var r2 = new RAR(Path.Combine(TestSubjectsPath, path)); + RAR.DataAvailableHandler fileDataFunc = (s, e) => + { + if (!s.CurrentFile.FileName.Contains("random_image") && !path.Contains("Test_split")) return; + fileData.AddRange(e.Data); + }; + r2.DataAvailable += fileDataFunc; + factory.SetupSequence(x => x.Create(It.IsAny())).Returns(new RAR(Path.Combine(TestSubjectsPath, path))).Returns(r2); + var e = new DPRARExtractor(Log.Logger, factory.Object); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, path)); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [DataRow("Test.rar")] + public void ExtractTest_PartialExtract(string path) + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, path)); + var arc = new DPArchive(fi); + arc.PeekContents(); + var skippedFile = arc.Contents.Values.ElementAt(1); + var successFiles = arc.Contents.Values.Except(new[] { skippedFile }).ToList(); + var settings = new DPExtractSettings(ExtractPath, successFiles); + var expectedReport = new DPExtractionReport() { ExtractedFiles = successFiles, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(successFiles); + + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtract() + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, "Test.rar")); + var arc = new DPArchive(fi); + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.Extract(settings); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, "Test.rar")); + var arc = new DPArchive(fi); + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + e.ExtractToTemp(settings); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, "Test.rar")); + var arc = new DPArchive(fi); + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, ExtractPath); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, "Test.rar")); + var arc = new DPArchive(fi); + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + SetupTargetPathsForTemp(arc, ExtractPath); + e.ExtractToTemp(settings); + + //// Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + [DataRow("Test.rar"), DataRow("Test_split.part1.rar"), DataRow("Test_split_solid.part1.rar")] + + public void PeekTest(string path) + { + var e = new DPRARExtractor(); + var fi = FileSystem.CreateFileInfo(Path.Combine(TestSubjectsPath, path)); + var arc = new DPArchive(fi); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Extraction/Integration/DPZipExtractorTests.cs b/src/DAZ_Installer.CoreTests/Extraction/Integration/DPZipExtractorTests.cs new file mode 100644 index 0000000..809ee43 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Extraction/Integration/DPZipExtractorTests.cs @@ -0,0 +1,230 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MSTestLogger = Microsoft.VisualStudio.TestTools.UnitTesting.Logging.Logger; +using Moq; +using Serilog; +using DAZ_Installer.IO.Fakes; +using DAZ_Installer.IO; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.Core.Extraction.Fakes; +using DAZ_Installer.Core.Integration; +using System.IO; +using System.IO.Compression; + +#pragma warning disable CS0618 // Obsolete is for production code, not testing code. +namespace DAZ_Installer.Core.Extraction.Integration.Tests +{ + [TestClass] + public class DPZipExtractorTests + { + public static readonly string TempPath = Path.Combine(Path.GetTempPath(), "DAZ_Installer.CoreTests", "Extraction", "Integration", "Test Subjects"); + public static readonly string ArchivePath = Path.Combine(TempPath, "Test Archive.zip"); + public static readonly string ArchiveContentsPath = Path.Combine(TempPath, "Archive Contents"); + public static readonly string ExtractPath = Path.Combine(Path.GetTempPath(), "DAZ_InstallerTests", "Extract"); + public static readonly DPFileScopeSettings DefaultScope = new(Enumerable.Empty(), new[] { ExtractPath }, false); + public static readonly DPFileSystem FileSystem = new DPFileSystem(DefaultScope); + public static List ArchiveContents = new(5); + + /// + /// A factory that returns a mocked fake archive with the default contents. + /// + [ClassInitialize] + public static void ClassInitialize(TestContext _) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Sink(new MSTestLoggerSink(SerilogLoggerConstants.LoggerTemplate, MSTestLogger.LogMessage)) + .CreateLogger(); + ArchiveContents = DPIntegrationArchiveHelpers.CreateArchiveContents(ArchiveContentsPath); + ZipFile.CreateFromDirectory(ArchiveContentsPath, ArchivePath, CompressionLevel.NoCompression, false); + } + + [ClassCleanup] + public static void ClassCleanup() + { + Directory.Delete(TempPath, true); + Directory.Delete(ExtractPath, true); + } + + [TestCleanup] + public void TestCleanup() + { + Directory.Delete(ExtractPath, true); + } + + [TestInitialize] + public void TestInitialize() + { + Directory.CreateDirectory(ExtractPath); + } + + [TestMethod] + public void DPZipExtractorTest() + { + var l = Mock.Of(); + var f = Mock.Of(); + var e = new DPZipExtractor(l, f); + Assert.AreEqual(l, e.Logger); + Assert.AreEqual(f, e.Factory); + } + + [TestMethod] + public void ExtractTest() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtract() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + arc.Extract(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_AfterExtractTemp() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + arc.ExtractToTemp(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledBeforeOp() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + e.CancellationToken = new CancellationToken(true); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(0), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + // Testing Extract() here: + // TODO: ExtractFinish should be called once Extracting event is emitted. + var report = e.Extract(settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void ExtractTest_CancelledDuringOp() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var cts = new CancellationTokenSource(); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()) { CancellationToken = cts.Token }; + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + e.ExtractProgress += (_, _) => cts.Cancel(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(1) { arc.Contents.First().Value }, ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPaths(arc, ExtractPath); + + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + + [TestMethod] + public void ExtractToTempTest() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPathsForTemp(arc, ExtractPath); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + + } + [TestMethod] + public void ExtractToTempTest_AfterExtract() + { + var fi = FileSystem.CreateFileInfo(ArchivePath); + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var arc = new DPArchive(fi) { Extractor = e }; + arc.PeekContents(); + var settings = new DPExtractSettings(ExtractPath, arc.Contents.Values); + var expectedReport = new DPExtractionReport() { ExtractedFiles = new(arc.Contents.Values), ErroredFiles = new(0), Settings = settings }; + DPArchiveTestHelpers.SetupTargetPathsForTemp(arc, ExtractPath); + arc.ExtractToTemp(settings); + + // Testing Extract() here: + var report = DPArchiveTestHelpers.RunAndAssertExtractEvents(e, settings, true); + + DPArchiveTestHelpers.AssertReport(expectedReport, report); + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractFileInfosCorrectlySet(arc.Contents.Values); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + + [TestMethod] + public void PeekTest() + { + var e = new DPZipExtractor(Log.Logger, new ZipArchiveWrapperFactory()); + var fi = FileSystem.CreateFileInfo(ArchivePath); + var arc = new DPArchive(fi); + + // Testing Peek() here: + DPArchiveTestHelpers.RunAndAssertPeekEvents(e, arc); + + DPIntegrationArchiveHelpers.AssertDefaultContentsNonDAZ(arc); + DPArchiveTestHelpers.AssertExtractorSetPathsCorrectly(arc, arc.Contents.Values.Select(x => x.Path)); + Assert.AreEqual(arc.FileSystem, e.FileSystem); + } + } +} \ No newline at end of file diff --git a/src/DAZ_Installer.CoreTests/Helpers/DPDestinationDeterminerTestHelpers.cs b/src/DAZ_Installer.CoreTests/Helpers/DPDestinationDeterminerTestHelpers.cs new file mode 100644 index 0000000..890bf0e --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Helpers/DPDestinationDeterminerTestHelpers.cs @@ -0,0 +1,80 @@ +using DAZ_Installer.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DAZ_Installer.Core.Tests.Helpers +{ + internal static class DPDestinationDeterminerTestHelpers + { + public static void AssertDestinations(HashSet expected, HashSet actual, string basePath) + { + Assert.AreEqual(expected.Count, actual.Count, "Destinations' count are not the same."); + foreach (var file in expected) + { + Assert.IsTrue(actual.Contains(file), "Actual destinations does not contain expected file."); + } + } + + /// + /// Asserts that all files in the archive have the correct s. + /// + /// The actual determined destinations from + /// + /// + /// The expected paths to be + public static void AssertTargetPaths(HashSet actual, IEnumerable expectedPaths) + { + var expectedHashSet = new HashSet(expectedPaths.Select(x => PathHelper.NormalizePath(x))); + foreach (var file in actual) + { + Assert.IsTrue(expectedHashSet.Contains(PathHelper.NormalizePath(file.TargetPath)), $"File {file.FileName} has incorrect TargetPath."); + } + } + + /// + /// Asserts that all files in the archive have the correct s + /// and paths. + /// + /// The archive to check. + /// The expected RelativeTargetPaths. + /// The expected RelativePathToContentFolder paths. + public static void AssertRelativePaths(DPArchive arc, IEnumerable expectedRTPaths, IEnumerable expectedRPTCFPaths) + { + var normalizedExpectedRTPaths = expectedRTPaths.Select(x => PathHelper.NormalizePath(x)); + var normalizedExpectedRPTCFPaths = expectedRPTCFPaths.Select(x => PathHelper.NormalizePath(x)); + var foundRTs = new HashSet(arc.Contents.Values.Select(x => x.RelativeTargetPath)); + var foundRPTCFs = new HashSet(arc.Contents.Values.Select(x => x.RelativePathToContentFolder)); + + foreach (var path in normalizedExpectedRTPaths) + { + Assert.IsTrue(foundRTs.Contains(path), $"Expected RelativeTargetPath {path} is not found."); + } + foreach (var path in normalizedExpectedRPTCFPaths) + { + Assert.IsTrue(foundRPTCFs.Contains(path), $"Expected RelativePathToContentFolder {path} is not found."); + } + } + + /// + /// Asserts that all files in the archive have the correct s + /// + /// The archive to check. + /// The expected absolute content folders paths. + public static void AssertContentFolders(DPArchive arc, IEnumerable expectedContentFolderPaths) + { + var expectedContentFolderHashSet = new HashSet(expectedContentFolderPaths.Select(x => PathHelper.NormalizePath(x))); + var contentFolders = arc.Folders.Values.Where(f => f.IsContentFolder); + Assert.AreEqual(expectedContentFolderHashSet.Count, contentFolders.Count(), "Content folder count is not the same."); + foreach (var folder in contentFolders) + { + Assert.IsTrue(expectedContentFolderHashSet.Contains(folder.NormalizedPath), $"Folder {folder.NormalizedPath} is not marked as content folder."); + } + } + + + } +} diff --git a/src/DAZ_Installer.CoreTests/Helpers/DPProcessorTestHelpers.cs b/src/DAZ_Installer.CoreTests/Helpers/DPProcessorTestHelpers.cs new file mode 100644 index 0000000..10e77a7 --- /dev/null +++ b/src/DAZ_Installer.CoreTests/Helpers/DPProcessorTestHelpers.cs @@ -0,0 +1,193 @@ +using DAZ_Installer.Core.Extraction; +using DAZ_Installer.CoreTests.Extraction; +using DAZ_Installer.IO; +using DAZ_Installer.IO.Fakes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Serilog; +using System.Text; +using System.Text.Json; +using System.Xml; + +namespace DAZ_Installer.Core.Tests +{ + [Obsolete("For testing purposes only.")] + internal class DPProcessorTestHelpers + { + /// + /// Contains the DefaultContents from and the manifest and supplement."/> + /// + public static IEnumerable DefaultContents => new string[] { "Manifest.dsx", "Supplement.dsx", "Contents/a.txt", "Contents/b.txt", "Contents/Documents/c.png", "Contents/Documents/d.txt", "Contents/e.duf", "Contents/f.duf", "bullshit.png" }; + public static MockOptions DefaultMockOptions => new(); + public static AssertOptions DefaultAssertOptions => new(); + public struct MockOptions + { + public bool partialFileInfo = true; + public bool partialRAR = true; + public bool partialDPFileInfo = true; + public bool partialZipArchiveEntry = true; + public bool partialFakeFileSystem = true; + public IEnumerable paths = DefaultContents; + public Func? ExtractToTempFunc = null; + public Func? ExtractFunc = null; + + public MockOptions() { } + } + public static DPArchive NewMockedArchive(MockOptions options, out Mock extractor, out Mock fakeDPFileInfo, out Mock fakeFileInfo, out Mock fakeFileSystem) + { + var fs = new Mock() { CallBase = options.partialFakeFileSystem }; + fakeFileSystem = fs; + fakeFileInfo = new Mock
+ /// RARFileInfo info = e.fileInfo; + /// info.FileName == Path // returns true + ///
This function removes and updates the root folders list instead of root contents list.