From 396ae57f050ef25a45f1aefd58937e4926400d67 Mon Sep 17 00:00:00 2001 From: Rene Lergner Date: Thu, 25 Oct 2018 22:35:49 +0200 Subject: [PATCH] Initial commit - WPinternals 2.6 --- 7zip/Common/CRC.cs | 55 + 7zip/Common/CommandLineParser.cs | 274 + 7zip/Common/InBuffer.cs | 72 + 7zip/Common/OutBuffer.cs | 47 + 7zip/Compress/LZ/IMatchFinder.cs | 24 + 7zip/Compress/LZ/LzBinTree.cs | 367 + 7zip/Compress/LZ/LzInWindow.cs | 132 + 7zip/Compress/LZ/LzOutWindow.cs | 110 + 7zip/Compress/LZMA/LzmaBase.cs | 76 + 7zip/Compress/LZMA/LzmaDecoder.cs | 398 + 7zip/Compress/LZMA/LzmaEncoder.cs | 1480 +++ 7zip/Compress/RangeCoder/RangeCoder.cs | 234 + 7zip/Compress/RangeCoder/RangeCoderBit.cs | 117 + 7zip/Compress/RangeCoder/RangeCoderBitTree.cs | 157 + 7zip/ICoder.cs | 157 + App.xaml | 112 + App.xaml.cs | 94 + CommandLine.cs | 1744 +++ DiscUtils/Block.cs | 42 + DiscUtils/BlockCache.cs | 138 + DiscUtils/BlockCacheSettings.cs | 77 + DiscUtils/BlockCacheStatistics.cs | 79 + DiscUtils/BlockCacheStream.cs | 472 + DiscUtils/BootConfig/ApplicationImageType.cs | 55 + DiscUtils/BootConfig/ApplicationType.cs | 80 + DiscUtils/BootConfig/BaseStorage.cs | 75 + DiscUtils/BootConfig/BcdObject.cs | 365 + DiscUtils/BootConfig/BooleanElementValue.cs | 54 + DiscUtils/BootConfig/DeviceAndPathRecord.cs | 58 + DiscUtils/BootConfig/DeviceElementValue.cs | 114 + DiscUtils/BootConfig/DeviceRecord.cs | 84 + .../BootConfig/DiscUtilsRegistryStorage.cs | 162 + DiscUtils/BootConfig/Element.cs | 390 + DiscUtils/BootConfig/ElementClass.cs | 55 + DiscUtils/BootConfig/ElementFormat.cs | 70 + DiscUtils/BootConfig/ElementValue.cs | 138 + DiscUtils/BootConfig/GuidElementValue.cs | 49 + DiscUtils/BootConfig/GuidListElementValue.cs | 60 + DiscUtils/BootConfig/InheritType.cs | 54 + DiscUtils/BootConfig/IntegerElementValue.cs | 63 + .../BootConfig/IntegerListElementValue.cs | 82 + DiscUtils/BootConfig/ObjectType.cs | 50 + DiscUtils/BootConfig/PartitionRecord.cs | 149 + DiscUtils/BootConfig/Store.cs | 168 + DiscUtils/BootConfig/StringElementValue.cs | 44 + DiscUtils/BootConfig/WellKnownElement.cs | 618 ++ DiscUtils/Buffer.cs | 120 + DiscUtils/BufferStream.cs | 218 + DiscUtils/BuilderBufferExtent.cs | 77 + DiscUtils/BuilderExtent.cs | 69 + DiscUtils/BuilderSparseStreamExtent.cs | 74 + DiscUtils/BuiltStream.cs | 327 + DiscUtils/ChsAddress.cs | 112 + DiscUtils/ClusterMap.cs | 128 + DiscUtils/Compression/Adler32.cs | 93 + DiscUtils/Compression/BZip2BlockDecoder.cs | 144 + .../Compression/BZip2CombinedHuffmanTrees.cs | 135 + DiscUtils/Compression/BZip2DecoderStream.cs | 349 + DiscUtils/Compression/BZip2Randomizer.cs | 124 + DiscUtils/Compression/BZip2RleStream.cs | 161 + DiscUtils/Compression/BigEndianBitStream.cs | 95 + DiscUtils/Compression/BitStream.cs | 60 + DiscUtils/Compression/BlockCompressor.cs | 62 + DiscUtils/Compression/CompressionResult.cs | 50 + DiscUtils/Compression/DataBlockTransform.cs | 70 + DiscUtils/Compression/HuffmanTree.cs | 109 + .../Compression/InverseBurrowsWheeler.cs | 97 + DiscUtils/Compression/MoveToFront.cs | 70 + DiscUtils/Compression/ZlibStream.cs | 237 + DiscUtils/ConcatStream.cs | 289 + DiscUtils/Crc32.cs | 43 + DiscUtils/Crc32Algorithm.cs | 47 + DiscUtils/Crc32BigEndian.cs | 96 + DiscUtils/Crc32LittleEndian.cs | 100 + DiscUtils/DiscDirectoryInfo.cs | 186 + DiscUtils/DiscFileInfo.cs | 205 + DiscUtils/DiscFileLocator.cs | 92 + DiscUtils/DiscFileSystem.cs | 499 + DiscUtils/DiscFileSystemChecker.cs | 76 + DiscUtils/DiscFileSystemInfo.cs | 226 + DiscUtils/DiscFileSystemOptions.cs | 56 + DiscUtils/DiskImageBuilder.cs | 144 + DiscUtils/DiskImageFileSpecification.cs | 56 + DiscUtils/Fat/ClusterReader.cs | 94 + DiscUtils/Fat/ClusterStream.cs | 475 + DiscUtils/Fat/Directory.cs | 568 + DiscUtils/Fat/DirectoryEntry.cs | 223 + DiscUtils/Fat/FatAttributes.cs | 37 + DiscUtils/Fat/FatBuffer.cs | 262 + DiscUtils/Fat/FatFileStream.cs | 140 + DiscUtils/Fat/FatFileSystem.cs | 2016 ++++ DiscUtils/Fat/FatFileSystemOptions.cs | 73 + DiscUtils/Fat/FatType.cs | 50 + DiscUtils/Fat/FileAllocationTable.cs | 101 + DiscUtils/Fat/FileName.cs | 212 + DiscUtils/Fat/FileSystemFactory.cs | 46 + DiscUtils/FileLocator.cs | 61 + DiscUtils/FileSystemInfo.cs | 90 + DiscUtils/FileSystemParameters.cs | 58 + DiscUtils/FloppyDiskType.cs | 45 + DiscUtils/GenericDiskAdapterType.cs | 40 + DiscUtils/Geometry.cs | 497 + DiscUtils/GeometryTranslation.cs | 50 + DiscUtils/IBuffer.cs | 118 + DiscUtils/IByteArraySerializable.cs | 50 + DiscUtils/IClusterBasedFileSystem.cs | 85 + DiscUtils/IDiagnosticTraceable.cs | 39 + DiscUtils/IFileSystem.cs | 354 + DiscUtils/IMappedBuffer.cs | 29 + DiscUtils/IWindowsFileSystem.cs | 129 + DiscUtils/InvalidFileSystemException.cs | 71 + .../LogicalDiskManager/ComponentRecord.cs | 64 + DiscUtils/LogicalDiskManager/Database.cs | 177 + .../LogicalDiskManager/DatabaseHeader.cs | 92 + .../LogicalDiskManager/DatabaseRecord.cs | 157 + .../LogicalDiskManager/DiskGroupRecord.cs | 49 + DiscUtils/LogicalDiskManager/DiskRecord.cs | 47 + DiscUtils/LogicalDiskManager/DynamicDisk.cs | 146 + .../LogicalDiskManager/DynamicDiskGroup.cs | 319 + .../LogicalDiskManager/DynamicDiskManager.cs | 153 + .../DynamicDiskManagerFactory.cs | 53 + DiscUtils/LogicalDiskManager/DynamicVolume.cs | 76 + .../LogicalDiskManager/ExtentMergeType.cs | 31 + DiscUtils/LogicalDiskManager/ExtentRecord.cs | 60 + DiscUtils/LogicalDiskManager/PrivateHeader.cs | 88 + DiscUtils/LogicalDiskManager/RecordType.cs | 34 + DiscUtils/LogicalDiskManager/TocBlock.cs | 71 + DiscUtils/LogicalDiskManager/VolumeRecord.cs | 77 + DiscUtils/LogicalVolumeFactory.cs | 33 + DiscUtils/LogicalVolumeFactoryAttribute.cs | 31 + DiscUtils/LogicalVolumeInfo.cs | 149 + DiscUtils/MappedStream.cs | 83 + DiscUtils/MirrorStream.cs | 175 + DiscUtils/Ntfs/AttributeDefinitionRecord.cs | 86 + DiscUtils/Ntfs/AttributeDefinitions.cs | 143 + DiscUtils/Ntfs/AttributeList.cs | 140 + DiscUtils/Ntfs/AttributeListRecord.cs | 145 + DiscUtils/Ntfs/AttributeRecord.cs | 199 + DiscUtils/Ntfs/AttributeReference.cs | 130 + DiscUtils/Ntfs/AttributeType.cs | 116 + DiscUtils/Ntfs/BiosParameterBlock.cs | 219 + DiscUtils/Ntfs/Bitmap.cs | 223 + DiscUtils/Ntfs/ClusterBitmap.cs | 270 + DiscUtils/Ntfs/ClusterStream.cs | 51 + DiscUtils/Ntfs/CompressedClusterStream.cs | 257 + DiscUtils/Ntfs/CookedDataRun.cs | 85 + DiscUtils/Ntfs/CookedDataRuns.cs | 321 + DiscUtils/Ntfs/DataRun.cs | 173 + DiscUtils/Ntfs/Directory.cs | 291 + DiscUtils/Ntfs/DirectoryEntry.cs | 75 + DiscUtils/Ntfs/File.cs | 1279 +++ DiscUtils/Ntfs/FileNameRecord.cs | 210 + DiscUtils/Ntfs/FileRecord.cs | 468 + DiscUtils/Ntfs/FileRecordReference.cs | 123 + DiscUtils/Ntfs/FileSystemFactory.cs | 46 + DiscUtils/Ntfs/FixupRecordBase.cs | 190 + DiscUtils/Ntfs/GenericFixupRecord.cs | 59 + DiscUtils/Ntfs/Index.cs | 485 + DiscUtils/Ntfs/IndexBlock.cs | 114 + DiscUtils/Ntfs/IndexEntry.cs | 193 + DiscUtils/Ntfs/IndexHeader.cs | 58 + DiscUtils/Ntfs/IndexNode.cs | 588 + DiscUtils/Ntfs/IndexRoot.cs | 297 + DiscUtils/Ntfs/IndexView.cs | 160 + DiscUtils/Ntfs/Internals/AttributeFlags.cs | 53 + .../Ntfs/Internals/AttributeListAttribute.cs | 67 + .../Ntfs/Internals/AttributeListEntry.cs | 93 + DiscUtils/Ntfs/Internals/EntryState.cs | 56 + DiscUtils/Ntfs/Internals/EntryStates.cs | 56 + DiscUtils/Ntfs/Internals/FileNameAttribute.cs | 145 + DiscUtils/Ntfs/Internals/GenericAttribute.cs | 117 + DiscUtils/Ntfs/Internals/MasterFileTable.cs | 154 + .../Internals/MasterFileTableAttribute.cs | 10 + .../Ntfs/Internals/MasterFileTableEntry.cs | 135 + .../Internals/MasterFileTableEntryFlags.cs | 58 + .../Ntfs/Internals/MasterFileTableRecord.cs | 71 + .../Internals/MasterFileTableRecordFlags.cs | 36 + .../Internals/MasterFileTableReference.cs | 103 + .../Ntfs/Internals/NtfsFileAttributes.cs | 113 + DiscUtils/Ntfs/Internals/NtfsNamespace.cs | 56 + .../Internals/StandardInformationAttribute.cs | 146 + DiscUtils/Ntfs/Internals/UnknownAttribute.cs | 32 + DiscUtils/Ntfs/LZNT1.cs | 314 + DiscUtils/Ntfs/LzWindowDictionary.cs | 142 + DiscUtils/Ntfs/MasterFileTable.cs | 522 + DiscUtils/Ntfs/NewFileOptions.cs | 60 + DiscUtils/Ntfs/NonResidentAttributeBuffer.cs | 353 + DiscUtils/Ntfs/NonResidentAttributeRecord.cs | 401 + DiscUtils/Ntfs/NonResidentDataBuffer.cs | 169 + DiscUtils/Ntfs/NtfsAttribute.cs | 414 + DiscUtils/Ntfs/NtfsAttributeBuffer.cs | 198 + DiscUtils/Ntfs/NtfsContext.cs | 261 + DiscUtils/Ntfs/NtfsFileStream.cs | 224 + DiscUtils/Ntfs/NtfsFileSystem.cs | 2440 ++++ DiscUtils/Ntfs/NtfsFileSystemChecker.cs | 618 ++ DiscUtils/Ntfs/NtfsFormatOptions.cs | 46 + DiscUtils/Ntfs/NtfsFormatter.cs | 341 + DiscUtils/Ntfs/NtfsOptions.cs | 141 + DiscUtils/Ntfs/NtfsStream.cs | 122 + DiscUtils/Ntfs/NtfsTransaction.cs | 66 + DiscUtils/Ntfs/ObjectId.cs | 53 + DiscUtils/Ntfs/ObjectIdRecord.cs | 64 + DiscUtils/Ntfs/ObjectIds.cs | 125 + DiscUtils/Ntfs/Quotas.cs | 227 + DiscUtils/Ntfs/RawClusterStream.cs | 368 + DiscUtils/Ntfs/ReparsePointRecord.cs | 69 + DiscUtils/Ntfs/ReparsePoints.cs | 125 + DiscUtils/Ntfs/ResidentAttributeRecord.cs | 177 + DiscUtils/Ntfs/SecurityDescriptor.cs | 177 + DiscUtils/Ntfs/SecurityDescriptorRecord.cs | 77 + DiscUtils/Ntfs/SecurityDescriptors.cs | 383 + DiscUtils/Ntfs/ShortFileNameOption.cs | 49 + DiscUtils/Ntfs/SparseClusterStream.cs | 91 + DiscUtils/Ntfs/StandardInformation.cs | 157 + DiscUtils/Ntfs/StructuredNtfsAttribute.cs | 105 + DiscUtils/Ntfs/UpperCase.cs | 101 + DiscUtils/Ntfs/VolumeInformation.cs | 86 + DiscUtils/Ntfs/VolumeInformationFlags.cs | 40 + DiscUtils/Ntfs/VolumeName.cs | 68 + DiscUtils/Numbers.cs | 258 + DiscUtils/ObjectCache.cs | 150 + DiscUtils/Ownership.cs | 44 + .../Partitions/BiosExtendedPartitionTable.cs | 117 + DiscUtils/Partitions/BiosPartitionInfo.cs | 136 + DiscUtils/Partitions/BiosPartitionRecord.cs | 166 + DiscUtils/Partitions/BiosPartitionTable.cs | 712 ++ DiscUtils/Partitions/BiosPartitionTypes.cs | 157 + .../Partitions/BiosPartitionedDiskBuilder.cs | 169 + .../DefaultPartitionTableFactory.cs | 55 + DiscUtils/Partitions/GptEntry.cs | 112 + DiscUtils/Partitions/GptHeader.cs | 144 + DiscUtils/Partitions/GuidPartitionInfo.cs | 120 + DiscUtils/Partitions/GuidPartitionTable.cs | 651 ++ DiscUtils/Partitions/GuidPartitionTypes.cs | 94 + DiscUtils/Partitions/PartitionInfo.cs | 95 + DiscUtils/Partitions/PartitionTable.cs | 208 + DiscUtils/Partitions/PartitionTableFactory.cs | 33 + .../PartitionTableFactoryAttribute.cs | 31 + .../Partitions/WellKnownPartitionType.cs | 55 + DiscUtils/PhysicalVolumeInfo.cs | 241 + DiscUtils/PumpProgressEventArgs.cs | 52 + DiscUtils/Range.cs | 140 + DiscUtils/Raw/Disk.cs | 223 + DiscUtils/Raw/DiskFactory.cs | 81 + DiscUtils/Raw/DiskImageFile.cs | 258 + DiscUtils/Registry/Bin.cs | 217 + DiscUtils/Registry/BinHeader.cs | 69 + DiscUtils/Registry/Cell.cs | 90 + DiscUtils/Registry/HiveHeader.cs | 141 + DiscUtils/Registry/KeyNodeCell.cs | 126 + DiscUtils/Registry/ListCell.cs | 74 + .../Registry/RegistryCorruptException.cs | 70 + DiscUtils/Registry/RegistryHive.cs | 404 + DiscUtils/Registry/RegistryKey.cs | 853 ++ DiscUtils/Registry/RegistryKeyFlags.cs | 113 + DiscUtils/Registry/RegistryValue.cs | 304 + DiscUtils/Registry/RegistryValueType.cs | 90 + DiscUtils/Registry/SecurityCell.cs | 112 + DiscUtils/Registry/SubKeyHashedListCell.cs | 320 + DiscUtils/Registry/SubKeyIndirectListCell.cs | 310 + DiscUtils/Registry/ValueCell.cs | 116 + DiscUtils/Registry/ValueFlags.cs | 47 + DiscUtils/ReparsePoint.cs | 62 + DiscUtils/Sizes.cs | 33 + DiscUtils/SnapshotStream.cs | 423 + DiscUtils/SparseMemoryBuffer.cs | 228 + DiscUtils/SparseMemoryStream.cs | 52 + DiscUtils/SparseStream.cs | 349 + DiscUtils/StreamBuilder.cs | 76 + DiscUtils/StreamExtent.cs | 512 + DiscUtils/StreamPump.cs | 270 + DiscUtils/StripedStream.cs | 229 + DiscUtils/SubBuffer.cs | 175 + DiscUtils/SubStream.cs | 217 + DiscUtils/Tuple.cs | 50 + DiscUtils/Tuple_2.cs | 77 + DiscUtils/Tuple_3.cs | 87 + DiscUtils/UnixFileType.cs | 72 + DiscUtils/Utilities.cs | 1106 ++ DiscUtils/Vfs/IVfsDirectory.cs | 63 + DiscUtils/Vfs/IVfsFile.cs | 90 + DiscUtils/Vfs/IVfsSymlink.cs | 39 + DiscUtils/Vfs/VfsContext.cs | 31 + DiscUtils/Vfs/VfsDirEntry.cs | 131 + DiscUtils/Vfs/VfsFileSystem.cs | 772 ++ DiscUtils/Vfs/VfsFileSystemFacade.cs | 541 + DiscUtils/Vfs/VfsFileSystemFactory.cs | 63 + .../Vfs/VfsFileSystemFactoryAttribute.cs | 36 + DiscUtils/Vfs/VfsFileSystemInfo.cs | 96 + DiscUtils/Vfs/VfsReadOnlyFileSystem.cs | 169 + DiscUtils/VirtualDisk.cs | 736 ++ DiscUtils/VirtualDiskClass.cs | 50 + DiscUtils/VirtualDiskExtent.cs | 88 + DiscUtils/VirtualDiskFactory.cs | 54 + DiscUtils/VirtualDiskFactoryAttribute.cs | 49 + DiscUtils/VirtualDiskLayer.cs | 142 + DiscUtils/VirtualDiskParameters.cs | 70 + DiscUtils/VirtualDiskTransport.cs | 52 + DiscUtils/VirtualDiskTransportAttribute.cs | 42 + DiscUtils/VirtualDiskTypeInfo.cs | 67 + DiscUtils/VolumeInfo.cs | 76 + DiscUtils/VolumeManager.cs | 321 + DiscUtils/WindowsFileInformation.cs | 58 + DiscUtils/WrappingMappedStream.cs | 178 + DiscUtils/ZeroStream.cs | 155 + FilePickerControl.xaml | 41 + FilePickerControl.xaml.cs | 448 + FolderSelectDialog.cs | 338 + HelperClasses.cs | 2239 ++++ LICENSE | 21 + Logo-Small.png | Bin 0 -> 10176 bytes Logo.png | Bin 0 -> 76655 bytes Models/ByteOperations.cs | 381 + Models/FFU.cs | 449 + Models/GPT.cs | 644 ++ Models/LZMA.cs | 287 + Models/LumiaDownloadModel.cs | 478 + Models/MassStorage.cs | 461 + Models/NativeMethods.cs | 315 + Models/NokiaFlashModel.cs | 1150 ++ Models/NokiaPhoneModel.cs | 347 + Models/PatchEngine.cs | 597 + Models/Privileges.cs | 667 ++ Models/QualcommDownload.cs | 151 + Models/QualcommFlasher.cs | 226 + Models/QualcommLoader.cs | 109 + Models/QualcommPartition.cs | 166 + Models/QualcommSahara.cs | 313 + Models/QualcommSerial.cs | 349 + Models/SBL1.cs | 113 + Models/SBL2.cs | 54 + Models/SBL3.cs | 87 + Models/UEFI.cs | 591 + PatchDefinitions.xml | 4314 ++++++++ Properties/AssemblyInfo.cs | 55 + Properties/Resources.Designer.cs | 63 + Properties/Resources.resx | 117 + Properties/Settings.Designer.cs | 26 + Properties/Settings.settings | 7 + SB | Bin 0 -> 736 bytes SBMSM | Bin 0 -> 807 bytes Terminal.cs | 68 + TestCode.cs | 81 + ViewModels/AboutViewModel.cs | 43 + ViewModels/BackupTargetSelectionViewModel.cs | 216 + ViewModels/BackupViewModel.cs | 353 + ViewModels/BusyViewModel.cs | 192 + ViewModels/ContextViewModel.cs | 153 + ViewModels/DisclaimerAndNdaViewModel.cs | 71 + ViewModels/DisclaimerViewModel.cs | 72 + ViewModels/DownloadsViewModel.cs | 871 ++ ViewModels/DumpRomTargetSelectionViewModel.cs | 232 + ViewModels/DumpRomViewModel.cs | 169 + ViewModels/GettingStartedViewModel.cs | 47 + .../LumiaFlashRomSourceSelectionViewModel.cs | 262 + ViewModels/LumiaFlashRomViewModel.cs | 657 ++ ViewModels/LumiaInfoViewModel.cs | 87 + ViewModels/LumiaModeViewModel.cs | 121 + ViewModels/LumiaUnlockBootViewModel.cs | 1652 +++ ...LumiaUnlockRootTargetSelectionViewModel.cs | 198 + ViewModels/LumiaUnlockRootViewModel.cs | 243 + ViewModels/LumiaV2UnlockBootViewModel.cs | 2988 +++++ ViewModels/MainViewModel.cs | 580 + ViewModels/MessageViewModel.cs | 93 + ViewModels/NokiaFlashViewModel.cs | 440 + ViewModels/NokiaLabelViewModel.cs | 290 + ViewModels/NokiaMassStorageViewModel.cs | 45 + ViewModels/NokiaModeFlashViewModel.cs | 123 + ViewModels/NokiaModeLabelViewModel.cs | 58 + ViewModels/NokiaModeMassStorageViewModel.cs | 63 + ViewModels/NokiaModeNormalViewModel.cs | 55 + ViewModels/NokiaNormalViewModel.cs | 237 + ViewModels/NotImplementedViewModel.cs | 39 + ViewModels/PhoneNotifierViewModel.cs | 424 + ViewModels/RegistrationViewModel.cs | 154 + ViewModels/RestoreSourceSelectionViewModel.cs | 208 + ViewModels/RestoreViewModel.cs | 198 + ViewModels/SwitchModeViewModel.cs | 669 ++ Views/About.xaml | 100 + Views/About.xaml.cs | 52 + Views/BackupView.xaml | 112 + Views/BackupView.xaml.cs | 57 + Views/BootRestoreResourcesView.xaml | 118 + Views/BootRestoreResourcesView.xaml.cs | 62 + Views/BusyView.xaml | 53 + Views/BusyView.xaml.cs | 39 + Views/ContextView.xaml | 48 + Views/ContextView.xaml.cs | 35 + Views/DisclaimerAndNdaView.xaml | 87 + Views/DisclaimerAndNdaView.xaml.cs | 35 + Views/DisclaimerView.xaml | 58 + Views/DisclaimerView.xaml.cs | 35 + Views/DumpRomView.xaml | 83 + Views/DumpRomView.xaml.cs | 76 + Views/Empty.xaml | 71 + Views/Empty.xaml.cs | 99 + Views/FlashResourcesView.xaml | 178 + Views/FlashResourcesView.xaml.cs | 56 + Views/FlashRomView.xaml | 160 + Views/FlashRomView.xaml.cs | 63 + Views/GettingStartedView.xaml | 896 ++ Views/GettingStartedView.xaml.cs | 88 + Views/LumiaDownloadView.xaml | 240 + Views/LumiaDownloadView.xaml.cs | 59 + Views/LumiaUndoRootTargetSelectionView.xaml | 73 + .../LumiaUndoRootTargetSelectionView.xaml.cs | 52 + Views/LumiaUnlockRootTargetSelectionView.xaml | 119 + ...LumiaUnlockRootTargetSelectionView.xaml.cs | 66 + Views/MainWindow.xaml | 231 + Views/MainWindow.xaml.cs | 76 + Views/MessageView.xaml | 48 + Views/MessageView.xaml.cs | 35 + Views/NokiaFlashView.xaml | 227 + Views/NokiaFlashView.xaml.cs | 54 + Views/NokiaLabelView.xaml | 110 + Views/NokiaLabelView.xaml.cs | 47 + Views/NokiaMassStorageView.xaml | 66 + Views/NokiaMassStorageView.xaml.cs | 47 + Views/NokiaModeFlashView.xaml | 87 + Views/NokiaModeFlashView.xaml.cs | 51 + Views/NokiaModeLabelView.xaml | 86 + Views/NokiaModeLabelView.xaml.cs | 51 + Views/NokiaModeMassStorageView.xaml | 61 + Views/NokiaModeMassStorageView.xaml.cs | 51 + Views/NokiaModeNormalView.xaml | 86 + Views/NokiaModeNormalView.xaml.cs | 51 + Views/NokiaNormalView.xaml | 139 + Views/NokiaNormalView.xaml.cs | 52 + Views/NotImplementedView.xaml | 33 + Views/NotImplementedView.xaml.cs | 35 + Views/RegistrationView.xaml | 71 + Views/RegistrationView.xaml.cs | 35 + Views/RestoreView.xaml | 80 + Views/RestoreView.xaml.cs | 59 + Views/StartupWindow.xaml | 30 + Views/StartupWindow.xaml.cs | 67 + WPinternals.csproj | 1276 +++ WPinternals.csproj.user | 117 + WPinternals.ico | Bin 0 -> 65156 bytes WPinternals.sln | 151 + WPinternalsConfig.cs | 345 + WinUSBNet/API/APIException.cs | 51 + WinUSBNet/API/DeviceDetails.cs | 26 + WinUSBNet/API/DeviceManagement.cs | 368 + WinUSBNet/API/DeviceManagementAPI.cs | 181 + WinUSBNet/API/FileAPI.cs | 39 + WinUSBNet/API/WinUSBDevice.cs | 456 + WinUSBNet/API/WinUSBDeviceAPI.cs | 202 + WinUSBNet/DeviceNotifyHook.cs | 169 + WinUSBNet/USB.cs | 80 + WinUSBNet/USBAsyncResult.cs | 136 + WinUSBNet/USBDevice.cs | 826 ++ WinUSBNet/USBDeviceDescriptor.cs | 135 + WinUSBNet/USBDeviceInfo.cs | 97 + WinUSBNet/USBException.cs | 40 + WinUSBNet/USBInterface.cs | 152 + WinUSBNet/USBInterfaceCollection.cs | 160 + WinUSBNet/USBNotifier.cs | 240 + WinUSBNet/USBPipe.cs | 448 + WinUSBNet/USBPipeCollection.cs | 135 + WinUSBNet/USBPipePolicy.cs | 181 + aerobusy.gif | Bin 0 -> 11900 bytes app.config | 12 + app.manifest | 11 + packages.config | 4 + .../Newtonsoft.Json.9.0.1.nupkg | Bin 0 -> 1603487 bytes .../Newtonsoft.Json.9.0.1.nuspec | 50 + .../lib/net20/Newtonsoft.Json.dll | Bin 0 -> 483840 bytes .../lib/net20/Newtonsoft.Json.xml | 9793 +++++++++++++++++ .../lib/net35/Newtonsoft.Json.dll | Bin 0 -> 447488 bytes .../lib/net35/Newtonsoft.Json.xml | 8922 +++++++++++++++ .../lib/net40/Newtonsoft.Json.dll | Bin 0 -> 489472 bytes .../lib/net40/Newtonsoft.Json.xml | 9229 ++++++++++++++++ .../lib/net45/Newtonsoft.Json.dll | Bin 0 -> 526336 bytes .../lib/net45/Newtonsoft.Json.xml | 9229 ++++++++++++++++ .../lib/netstandard1.0/Newtonsoft.Json.dll | Bin 0 -> 468480 bytes .../lib/netstandard1.0/Newtonsoft.Json.xml | 8756 +++++++++++++++ .../Newtonsoft.Json.dll | Bin 0 -> 419840 bytes .../Newtonsoft.Json.xml | 8409 ++++++++++++++ .../Newtonsoft.Json.dll | Bin 0 -> 468480 bytes .../Newtonsoft.Json.xml | 8756 +++++++++++++++ .../Newtonsoft.Json.9.0.1/tools/install.ps1 | 116 + packages/repositories.config | 4 + 483 files changed, 159677 insertions(+) create mode 100644 7zip/Common/CRC.cs create mode 100644 7zip/Common/CommandLineParser.cs create mode 100644 7zip/Common/InBuffer.cs create mode 100644 7zip/Common/OutBuffer.cs create mode 100644 7zip/Compress/LZ/IMatchFinder.cs create mode 100644 7zip/Compress/LZ/LzBinTree.cs create mode 100644 7zip/Compress/LZ/LzInWindow.cs create mode 100644 7zip/Compress/LZ/LzOutWindow.cs create mode 100644 7zip/Compress/LZMA/LzmaBase.cs create mode 100644 7zip/Compress/LZMA/LzmaDecoder.cs create mode 100644 7zip/Compress/LZMA/LzmaEncoder.cs create mode 100644 7zip/Compress/RangeCoder/RangeCoder.cs create mode 100644 7zip/Compress/RangeCoder/RangeCoderBit.cs create mode 100644 7zip/Compress/RangeCoder/RangeCoderBitTree.cs create mode 100644 7zip/ICoder.cs create mode 100644 App.xaml create mode 100644 App.xaml.cs create mode 100644 CommandLine.cs create mode 100644 DiscUtils/Block.cs create mode 100644 DiscUtils/BlockCache.cs create mode 100644 DiscUtils/BlockCacheSettings.cs create mode 100644 DiscUtils/BlockCacheStatistics.cs create mode 100644 DiscUtils/BlockCacheStream.cs create mode 100644 DiscUtils/BootConfig/ApplicationImageType.cs create mode 100644 DiscUtils/BootConfig/ApplicationType.cs create mode 100644 DiscUtils/BootConfig/BaseStorage.cs create mode 100644 DiscUtils/BootConfig/BcdObject.cs create mode 100644 DiscUtils/BootConfig/BooleanElementValue.cs create mode 100644 DiscUtils/BootConfig/DeviceAndPathRecord.cs create mode 100644 DiscUtils/BootConfig/DeviceElementValue.cs create mode 100644 DiscUtils/BootConfig/DeviceRecord.cs create mode 100644 DiscUtils/BootConfig/DiscUtilsRegistryStorage.cs create mode 100644 DiscUtils/BootConfig/Element.cs create mode 100644 DiscUtils/BootConfig/ElementClass.cs create mode 100644 DiscUtils/BootConfig/ElementFormat.cs create mode 100644 DiscUtils/BootConfig/ElementValue.cs create mode 100644 DiscUtils/BootConfig/GuidElementValue.cs create mode 100644 DiscUtils/BootConfig/GuidListElementValue.cs create mode 100644 DiscUtils/BootConfig/InheritType.cs create mode 100644 DiscUtils/BootConfig/IntegerElementValue.cs create mode 100644 DiscUtils/BootConfig/IntegerListElementValue.cs create mode 100644 DiscUtils/BootConfig/ObjectType.cs create mode 100644 DiscUtils/BootConfig/PartitionRecord.cs create mode 100644 DiscUtils/BootConfig/Store.cs create mode 100644 DiscUtils/BootConfig/StringElementValue.cs create mode 100644 DiscUtils/BootConfig/WellKnownElement.cs create mode 100644 DiscUtils/Buffer.cs create mode 100644 DiscUtils/BufferStream.cs create mode 100644 DiscUtils/BuilderBufferExtent.cs create mode 100644 DiscUtils/BuilderExtent.cs create mode 100644 DiscUtils/BuilderSparseStreamExtent.cs create mode 100644 DiscUtils/BuiltStream.cs create mode 100644 DiscUtils/ChsAddress.cs create mode 100644 DiscUtils/ClusterMap.cs create mode 100644 DiscUtils/Compression/Adler32.cs create mode 100644 DiscUtils/Compression/BZip2BlockDecoder.cs create mode 100644 DiscUtils/Compression/BZip2CombinedHuffmanTrees.cs create mode 100644 DiscUtils/Compression/BZip2DecoderStream.cs create mode 100644 DiscUtils/Compression/BZip2Randomizer.cs create mode 100644 DiscUtils/Compression/BZip2RleStream.cs create mode 100644 DiscUtils/Compression/BigEndianBitStream.cs create mode 100644 DiscUtils/Compression/BitStream.cs create mode 100644 DiscUtils/Compression/BlockCompressor.cs create mode 100644 DiscUtils/Compression/CompressionResult.cs create mode 100644 DiscUtils/Compression/DataBlockTransform.cs create mode 100644 DiscUtils/Compression/HuffmanTree.cs create mode 100644 DiscUtils/Compression/InverseBurrowsWheeler.cs create mode 100644 DiscUtils/Compression/MoveToFront.cs create mode 100644 DiscUtils/Compression/ZlibStream.cs create mode 100644 DiscUtils/ConcatStream.cs create mode 100644 DiscUtils/Crc32.cs create mode 100644 DiscUtils/Crc32Algorithm.cs create mode 100644 DiscUtils/Crc32BigEndian.cs create mode 100644 DiscUtils/Crc32LittleEndian.cs create mode 100644 DiscUtils/DiscDirectoryInfo.cs create mode 100644 DiscUtils/DiscFileInfo.cs create mode 100644 DiscUtils/DiscFileLocator.cs create mode 100644 DiscUtils/DiscFileSystem.cs create mode 100644 DiscUtils/DiscFileSystemChecker.cs create mode 100644 DiscUtils/DiscFileSystemInfo.cs create mode 100644 DiscUtils/DiscFileSystemOptions.cs create mode 100644 DiscUtils/DiskImageBuilder.cs create mode 100644 DiscUtils/DiskImageFileSpecification.cs create mode 100644 DiscUtils/Fat/ClusterReader.cs create mode 100644 DiscUtils/Fat/ClusterStream.cs create mode 100644 DiscUtils/Fat/Directory.cs create mode 100644 DiscUtils/Fat/DirectoryEntry.cs create mode 100644 DiscUtils/Fat/FatAttributes.cs create mode 100644 DiscUtils/Fat/FatBuffer.cs create mode 100644 DiscUtils/Fat/FatFileStream.cs create mode 100644 DiscUtils/Fat/FatFileSystem.cs create mode 100644 DiscUtils/Fat/FatFileSystemOptions.cs create mode 100644 DiscUtils/Fat/FatType.cs create mode 100644 DiscUtils/Fat/FileAllocationTable.cs create mode 100644 DiscUtils/Fat/FileName.cs create mode 100644 DiscUtils/Fat/FileSystemFactory.cs create mode 100644 DiscUtils/FileLocator.cs create mode 100644 DiscUtils/FileSystemInfo.cs create mode 100644 DiscUtils/FileSystemParameters.cs create mode 100644 DiscUtils/FloppyDiskType.cs create mode 100644 DiscUtils/GenericDiskAdapterType.cs create mode 100644 DiscUtils/Geometry.cs create mode 100644 DiscUtils/GeometryTranslation.cs create mode 100644 DiscUtils/IBuffer.cs create mode 100644 DiscUtils/IByteArraySerializable.cs create mode 100644 DiscUtils/IClusterBasedFileSystem.cs create mode 100644 DiscUtils/IDiagnosticTraceable.cs create mode 100644 DiscUtils/IFileSystem.cs create mode 100644 DiscUtils/IMappedBuffer.cs create mode 100644 DiscUtils/IWindowsFileSystem.cs create mode 100644 DiscUtils/InvalidFileSystemException.cs create mode 100644 DiscUtils/LogicalDiskManager/ComponentRecord.cs create mode 100644 DiscUtils/LogicalDiskManager/Database.cs create mode 100644 DiscUtils/LogicalDiskManager/DatabaseHeader.cs create mode 100644 DiscUtils/LogicalDiskManager/DatabaseRecord.cs create mode 100644 DiscUtils/LogicalDiskManager/DiskGroupRecord.cs create mode 100644 DiscUtils/LogicalDiskManager/DiskRecord.cs create mode 100644 DiscUtils/LogicalDiskManager/DynamicDisk.cs create mode 100644 DiscUtils/LogicalDiskManager/DynamicDiskGroup.cs create mode 100644 DiscUtils/LogicalDiskManager/DynamicDiskManager.cs create mode 100644 DiscUtils/LogicalDiskManager/DynamicDiskManagerFactory.cs create mode 100644 DiscUtils/LogicalDiskManager/DynamicVolume.cs create mode 100644 DiscUtils/LogicalDiskManager/ExtentMergeType.cs create mode 100644 DiscUtils/LogicalDiskManager/ExtentRecord.cs create mode 100644 DiscUtils/LogicalDiskManager/PrivateHeader.cs create mode 100644 DiscUtils/LogicalDiskManager/RecordType.cs create mode 100644 DiscUtils/LogicalDiskManager/TocBlock.cs create mode 100644 DiscUtils/LogicalDiskManager/VolumeRecord.cs create mode 100644 DiscUtils/LogicalVolumeFactory.cs create mode 100644 DiscUtils/LogicalVolumeFactoryAttribute.cs create mode 100644 DiscUtils/LogicalVolumeInfo.cs create mode 100644 DiscUtils/MappedStream.cs create mode 100644 DiscUtils/MirrorStream.cs create mode 100644 DiscUtils/Ntfs/AttributeDefinitionRecord.cs create mode 100644 DiscUtils/Ntfs/AttributeDefinitions.cs create mode 100644 DiscUtils/Ntfs/AttributeList.cs create mode 100644 DiscUtils/Ntfs/AttributeListRecord.cs create mode 100644 DiscUtils/Ntfs/AttributeRecord.cs create mode 100644 DiscUtils/Ntfs/AttributeReference.cs create mode 100644 DiscUtils/Ntfs/AttributeType.cs create mode 100644 DiscUtils/Ntfs/BiosParameterBlock.cs create mode 100644 DiscUtils/Ntfs/Bitmap.cs create mode 100644 DiscUtils/Ntfs/ClusterBitmap.cs create mode 100644 DiscUtils/Ntfs/ClusterStream.cs create mode 100644 DiscUtils/Ntfs/CompressedClusterStream.cs create mode 100644 DiscUtils/Ntfs/CookedDataRun.cs create mode 100644 DiscUtils/Ntfs/CookedDataRuns.cs create mode 100644 DiscUtils/Ntfs/DataRun.cs create mode 100644 DiscUtils/Ntfs/Directory.cs create mode 100644 DiscUtils/Ntfs/DirectoryEntry.cs create mode 100644 DiscUtils/Ntfs/File.cs create mode 100644 DiscUtils/Ntfs/FileNameRecord.cs create mode 100644 DiscUtils/Ntfs/FileRecord.cs create mode 100644 DiscUtils/Ntfs/FileRecordReference.cs create mode 100644 DiscUtils/Ntfs/FileSystemFactory.cs create mode 100644 DiscUtils/Ntfs/FixupRecordBase.cs create mode 100644 DiscUtils/Ntfs/GenericFixupRecord.cs create mode 100644 DiscUtils/Ntfs/Index.cs create mode 100644 DiscUtils/Ntfs/IndexBlock.cs create mode 100644 DiscUtils/Ntfs/IndexEntry.cs create mode 100644 DiscUtils/Ntfs/IndexHeader.cs create mode 100644 DiscUtils/Ntfs/IndexNode.cs create mode 100644 DiscUtils/Ntfs/IndexRoot.cs create mode 100644 DiscUtils/Ntfs/IndexView.cs create mode 100644 DiscUtils/Ntfs/Internals/AttributeFlags.cs create mode 100644 DiscUtils/Ntfs/Internals/AttributeListAttribute.cs create mode 100644 DiscUtils/Ntfs/Internals/AttributeListEntry.cs create mode 100644 DiscUtils/Ntfs/Internals/EntryState.cs create mode 100644 DiscUtils/Ntfs/Internals/EntryStates.cs create mode 100644 DiscUtils/Ntfs/Internals/FileNameAttribute.cs create mode 100644 DiscUtils/Ntfs/Internals/GenericAttribute.cs create mode 100644 DiscUtils/Ntfs/Internals/MasterFileTable.cs create mode 100644 DiscUtils/Ntfs/Internals/MasterFileTableAttribute.cs create mode 100644 DiscUtils/Ntfs/Internals/MasterFileTableEntry.cs create mode 100644 DiscUtils/Ntfs/Internals/MasterFileTableEntryFlags.cs create mode 100644 DiscUtils/Ntfs/Internals/MasterFileTableRecord.cs create mode 100644 DiscUtils/Ntfs/Internals/MasterFileTableRecordFlags.cs create mode 100644 DiscUtils/Ntfs/Internals/MasterFileTableReference.cs create mode 100644 DiscUtils/Ntfs/Internals/NtfsFileAttributes.cs create mode 100644 DiscUtils/Ntfs/Internals/NtfsNamespace.cs create mode 100644 DiscUtils/Ntfs/Internals/StandardInformationAttribute.cs create mode 100644 DiscUtils/Ntfs/Internals/UnknownAttribute.cs create mode 100644 DiscUtils/Ntfs/LZNT1.cs create mode 100644 DiscUtils/Ntfs/LzWindowDictionary.cs create mode 100644 DiscUtils/Ntfs/MasterFileTable.cs create mode 100644 DiscUtils/Ntfs/NewFileOptions.cs create mode 100644 DiscUtils/Ntfs/NonResidentAttributeBuffer.cs create mode 100644 DiscUtils/Ntfs/NonResidentAttributeRecord.cs create mode 100644 DiscUtils/Ntfs/NonResidentDataBuffer.cs create mode 100644 DiscUtils/Ntfs/NtfsAttribute.cs create mode 100644 DiscUtils/Ntfs/NtfsAttributeBuffer.cs create mode 100644 DiscUtils/Ntfs/NtfsContext.cs create mode 100644 DiscUtils/Ntfs/NtfsFileStream.cs create mode 100644 DiscUtils/Ntfs/NtfsFileSystem.cs create mode 100644 DiscUtils/Ntfs/NtfsFileSystemChecker.cs create mode 100644 DiscUtils/Ntfs/NtfsFormatOptions.cs create mode 100644 DiscUtils/Ntfs/NtfsFormatter.cs create mode 100644 DiscUtils/Ntfs/NtfsOptions.cs create mode 100644 DiscUtils/Ntfs/NtfsStream.cs create mode 100644 DiscUtils/Ntfs/NtfsTransaction.cs create mode 100644 DiscUtils/Ntfs/ObjectId.cs create mode 100644 DiscUtils/Ntfs/ObjectIdRecord.cs create mode 100644 DiscUtils/Ntfs/ObjectIds.cs create mode 100644 DiscUtils/Ntfs/Quotas.cs create mode 100644 DiscUtils/Ntfs/RawClusterStream.cs create mode 100644 DiscUtils/Ntfs/ReparsePointRecord.cs create mode 100644 DiscUtils/Ntfs/ReparsePoints.cs create mode 100644 DiscUtils/Ntfs/ResidentAttributeRecord.cs create mode 100644 DiscUtils/Ntfs/SecurityDescriptor.cs create mode 100644 DiscUtils/Ntfs/SecurityDescriptorRecord.cs create mode 100644 DiscUtils/Ntfs/SecurityDescriptors.cs create mode 100644 DiscUtils/Ntfs/ShortFileNameOption.cs create mode 100644 DiscUtils/Ntfs/SparseClusterStream.cs create mode 100644 DiscUtils/Ntfs/StandardInformation.cs create mode 100644 DiscUtils/Ntfs/StructuredNtfsAttribute.cs create mode 100644 DiscUtils/Ntfs/UpperCase.cs create mode 100644 DiscUtils/Ntfs/VolumeInformation.cs create mode 100644 DiscUtils/Ntfs/VolumeInformationFlags.cs create mode 100644 DiscUtils/Ntfs/VolumeName.cs create mode 100644 DiscUtils/Numbers.cs create mode 100644 DiscUtils/ObjectCache.cs create mode 100644 DiscUtils/Ownership.cs create mode 100644 DiscUtils/Partitions/BiosExtendedPartitionTable.cs create mode 100644 DiscUtils/Partitions/BiosPartitionInfo.cs create mode 100644 DiscUtils/Partitions/BiosPartitionRecord.cs create mode 100644 DiscUtils/Partitions/BiosPartitionTable.cs create mode 100644 DiscUtils/Partitions/BiosPartitionTypes.cs create mode 100644 DiscUtils/Partitions/BiosPartitionedDiskBuilder.cs create mode 100644 DiscUtils/Partitions/DefaultPartitionTableFactory.cs create mode 100644 DiscUtils/Partitions/GptEntry.cs create mode 100644 DiscUtils/Partitions/GptHeader.cs create mode 100644 DiscUtils/Partitions/GuidPartitionInfo.cs create mode 100644 DiscUtils/Partitions/GuidPartitionTable.cs create mode 100644 DiscUtils/Partitions/GuidPartitionTypes.cs create mode 100644 DiscUtils/Partitions/PartitionInfo.cs create mode 100644 DiscUtils/Partitions/PartitionTable.cs create mode 100644 DiscUtils/Partitions/PartitionTableFactory.cs create mode 100644 DiscUtils/Partitions/PartitionTableFactoryAttribute.cs create mode 100644 DiscUtils/Partitions/WellKnownPartitionType.cs create mode 100644 DiscUtils/PhysicalVolumeInfo.cs create mode 100644 DiscUtils/PumpProgressEventArgs.cs create mode 100644 DiscUtils/Range.cs create mode 100644 DiscUtils/Raw/Disk.cs create mode 100644 DiscUtils/Raw/DiskFactory.cs create mode 100644 DiscUtils/Raw/DiskImageFile.cs create mode 100644 DiscUtils/Registry/Bin.cs create mode 100644 DiscUtils/Registry/BinHeader.cs create mode 100644 DiscUtils/Registry/Cell.cs create mode 100644 DiscUtils/Registry/HiveHeader.cs create mode 100644 DiscUtils/Registry/KeyNodeCell.cs create mode 100644 DiscUtils/Registry/ListCell.cs create mode 100644 DiscUtils/Registry/RegistryCorruptException.cs create mode 100644 DiscUtils/Registry/RegistryHive.cs create mode 100644 DiscUtils/Registry/RegistryKey.cs create mode 100644 DiscUtils/Registry/RegistryKeyFlags.cs create mode 100644 DiscUtils/Registry/RegistryValue.cs create mode 100644 DiscUtils/Registry/RegistryValueType.cs create mode 100644 DiscUtils/Registry/SecurityCell.cs create mode 100644 DiscUtils/Registry/SubKeyHashedListCell.cs create mode 100644 DiscUtils/Registry/SubKeyIndirectListCell.cs create mode 100644 DiscUtils/Registry/ValueCell.cs create mode 100644 DiscUtils/Registry/ValueFlags.cs create mode 100644 DiscUtils/ReparsePoint.cs create mode 100644 DiscUtils/Sizes.cs create mode 100644 DiscUtils/SnapshotStream.cs create mode 100644 DiscUtils/SparseMemoryBuffer.cs create mode 100644 DiscUtils/SparseMemoryStream.cs create mode 100644 DiscUtils/SparseStream.cs create mode 100644 DiscUtils/StreamBuilder.cs create mode 100644 DiscUtils/StreamExtent.cs create mode 100644 DiscUtils/StreamPump.cs create mode 100644 DiscUtils/StripedStream.cs create mode 100644 DiscUtils/SubBuffer.cs create mode 100644 DiscUtils/SubStream.cs create mode 100644 DiscUtils/Tuple.cs create mode 100644 DiscUtils/Tuple_2.cs create mode 100644 DiscUtils/Tuple_3.cs create mode 100644 DiscUtils/UnixFileType.cs create mode 100644 DiscUtils/Utilities.cs create mode 100644 DiscUtils/Vfs/IVfsDirectory.cs create mode 100644 DiscUtils/Vfs/IVfsFile.cs create mode 100644 DiscUtils/Vfs/IVfsSymlink.cs create mode 100644 DiscUtils/Vfs/VfsContext.cs create mode 100644 DiscUtils/Vfs/VfsDirEntry.cs create mode 100644 DiscUtils/Vfs/VfsFileSystem.cs create mode 100644 DiscUtils/Vfs/VfsFileSystemFacade.cs create mode 100644 DiscUtils/Vfs/VfsFileSystemFactory.cs create mode 100644 DiscUtils/Vfs/VfsFileSystemFactoryAttribute.cs create mode 100644 DiscUtils/Vfs/VfsFileSystemInfo.cs create mode 100644 DiscUtils/Vfs/VfsReadOnlyFileSystem.cs create mode 100644 DiscUtils/VirtualDisk.cs create mode 100644 DiscUtils/VirtualDiskClass.cs create mode 100644 DiscUtils/VirtualDiskExtent.cs create mode 100644 DiscUtils/VirtualDiskFactory.cs create mode 100644 DiscUtils/VirtualDiskFactoryAttribute.cs create mode 100644 DiscUtils/VirtualDiskLayer.cs create mode 100644 DiscUtils/VirtualDiskParameters.cs create mode 100644 DiscUtils/VirtualDiskTransport.cs create mode 100644 DiscUtils/VirtualDiskTransportAttribute.cs create mode 100644 DiscUtils/VirtualDiskTypeInfo.cs create mode 100644 DiscUtils/VolumeInfo.cs create mode 100644 DiscUtils/VolumeManager.cs create mode 100644 DiscUtils/WindowsFileInformation.cs create mode 100644 DiscUtils/WrappingMappedStream.cs create mode 100644 DiscUtils/ZeroStream.cs create mode 100644 FilePickerControl.xaml create mode 100644 FilePickerControl.xaml.cs create mode 100644 FolderSelectDialog.cs create mode 100644 HelperClasses.cs create mode 100644 LICENSE create mode 100644 Logo-Small.png create mode 100644 Logo.png create mode 100644 Models/ByteOperations.cs create mode 100644 Models/FFU.cs create mode 100644 Models/GPT.cs create mode 100644 Models/LZMA.cs create mode 100644 Models/LumiaDownloadModel.cs create mode 100644 Models/MassStorage.cs create mode 100644 Models/NativeMethods.cs create mode 100644 Models/NokiaFlashModel.cs create mode 100644 Models/NokiaPhoneModel.cs create mode 100644 Models/PatchEngine.cs create mode 100644 Models/Privileges.cs create mode 100644 Models/QualcommDownload.cs create mode 100644 Models/QualcommFlasher.cs create mode 100644 Models/QualcommLoader.cs create mode 100644 Models/QualcommPartition.cs create mode 100644 Models/QualcommSahara.cs create mode 100644 Models/QualcommSerial.cs create mode 100644 Models/SBL1.cs create mode 100644 Models/SBL2.cs create mode 100644 Models/SBL3.cs create mode 100644 Models/UEFI.cs create mode 100644 PatchDefinitions.xml create mode 100644 Properties/AssemblyInfo.cs create mode 100644 Properties/Resources.Designer.cs create mode 100644 Properties/Resources.resx create mode 100644 Properties/Settings.Designer.cs create mode 100644 Properties/Settings.settings create mode 100644 SB create mode 100644 SBMSM create mode 100644 Terminal.cs create mode 100644 TestCode.cs create mode 100644 ViewModels/AboutViewModel.cs create mode 100644 ViewModels/BackupTargetSelectionViewModel.cs create mode 100644 ViewModels/BackupViewModel.cs create mode 100644 ViewModels/BusyViewModel.cs create mode 100644 ViewModels/ContextViewModel.cs create mode 100644 ViewModels/DisclaimerAndNdaViewModel.cs create mode 100644 ViewModels/DisclaimerViewModel.cs create mode 100644 ViewModels/DownloadsViewModel.cs create mode 100644 ViewModels/DumpRomTargetSelectionViewModel.cs create mode 100644 ViewModels/DumpRomViewModel.cs create mode 100644 ViewModels/GettingStartedViewModel.cs create mode 100644 ViewModels/LumiaFlashRomSourceSelectionViewModel.cs create mode 100644 ViewModels/LumiaFlashRomViewModel.cs create mode 100644 ViewModels/LumiaInfoViewModel.cs create mode 100644 ViewModels/LumiaModeViewModel.cs create mode 100644 ViewModels/LumiaUnlockBootViewModel.cs create mode 100644 ViewModels/LumiaUnlockRootTargetSelectionViewModel.cs create mode 100644 ViewModels/LumiaUnlockRootViewModel.cs create mode 100644 ViewModels/LumiaV2UnlockBootViewModel.cs create mode 100644 ViewModels/MainViewModel.cs create mode 100644 ViewModels/MessageViewModel.cs create mode 100644 ViewModels/NokiaFlashViewModel.cs create mode 100644 ViewModels/NokiaLabelViewModel.cs create mode 100644 ViewModels/NokiaMassStorageViewModel.cs create mode 100644 ViewModels/NokiaModeFlashViewModel.cs create mode 100644 ViewModels/NokiaModeLabelViewModel.cs create mode 100644 ViewModels/NokiaModeMassStorageViewModel.cs create mode 100644 ViewModels/NokiaModeNormalViewModel.cs create mode 100644 ViewModels/NokiaNormalViewModel.cs create mode 100644 ViewModels/NotImplementedViewModel.cs create mode 100644 ViewModels/PhoneNotifierViewModel.cs create mode 100644 ViewModels/RegistrationViewModel.cs create mode 100644 ViewModels/RestoreSourceSelectionViewModel.cs create mode 100644 ViewModels/RestoreViewModel.cs create mode 100644 ViewModels/SwitchModeViewModel.cs create mode 100644 Views/About.xaml create mode 100644 Views/About.xaml.cs create mode 100644 Views/BackupView.xaml create mode 100644 Views/BackupView.xaml.cs create mode 100644 Views/BootRestoreResourcesView.xaml create mode 100644 Views/BootRestoreResourcesView.xaml.cs create mode 100644 Views/BusyView.xaml create mode 100644 Views/BusyView.xaml.cs create mode 100644 Views/ContextView.xaml create mode 100644 Views/ContextView.xaml.cs create mode 100644 Views/DisclaimerAndNdaView.xaml create mode 100644 Views/DisclaimerAndNdaView.xaml.cs create mode 100644 Views/DisclaimerView.xaml create mode 100644 Views/DisclaimerView.xaml.cs create mode 100644 Views/DumpRomView.xaml create mode 100644 Views/DumpRomView.xaml.cs create mode 100644 Views/Empty.xaml create mode 100644 Views/Empty.xaml.cs create mode 100644 Views/FlashResourcesView.xaml create mode 100644 Views/FlashResourcesView.xaml.cs create mode 100644 Views/FlashRomView.xaml create mode 100644 Views/FlashRomView.xaml.cs create mode 100644 Views/GettingStartedView.xaml create mode 100644 Views/GettingStartedView.xaml.cs create mode 100644 Views/LumiaDownloadView.xaml create mode 100644 Views/LumiaDownloadView.xaml.cs create mode 100644 Views/LumiaUndoRootTargetSelectionView.xaml create mode 100644 Views/LumiaUndoRootTargetSelectionView.xaml.cs create mode 100644 Views/LumiaUnlockRootTargetSelectionView.xaml create mode 100644 Views/LumiaUnlockRootTargetSelectionView.xaml.cs create mode 100644 Views/MainWindow.xaml create mode 100644 Views/MainWindow.xaml.cs create mode 100644 Views/MessageView.xaml create mode 100644 Views/MessageView.xaml.cs create mode 100644 Views/NokiaFlashView.xaml create mode 100644 Views/NokiaFlashView.xaml.cs create mode 100644 Views/NokiaLabelView.xaml create mode 100644 Views/NokiaLabelView.xaml.cs create mode 100644 Views/NokiaMassStorageView.xaml create mode 100644 Views/NokiaMassStorageView.xaml.cs create mode 100644 Views/NokiaModeFlashView.xaml create mode 100644 Views/NokiaModeFlashView.xaml.cs create mode 100644 Views/NokiaModeLabelView.xaml create mode 100644 Views/NokiaModeLabelView.xaml.cs create mode 100644 Views/NokiaModeMassStorageView.xaml create mode 100644 Views/NokiaModeMassStorageView.xaml.cs create mode 100644 Views/NokiaModeNormalView.xaml create mode 100644 Views/NokiaModeNormalView.xaml.cs create mode 100644 Views/NokiaNormalView.xaml create mode 100644 Views/NokiaNormalView.xaml.cs create mode 100644 Views/NotImplementedView.xaml create mode 100644 Views/NotImplementedView.xaml.cs create mode 100644 Views/RegistrationView.xaml create mode 100644 Views/RegistrationView.xaml.cs create mode 100644 Views/RestoreView.xaml create mode 100644 Views/RestoreView.xaml.cs create mode 100644 Views/StartupWindow.xaml create mode 100644 Views/StartupWindow.xaml.cs create mode 100644 WPinternals.csproj create mode 100644 WPinternals.csproj.user create mode 100644 WPinternals.ico create mode 100644 WPinternals.sln create mode 100644 WPinternalsConfig.cs create mode 100644 WinUSBNet/API/APIException.cs create mode 100644 WinUSBNet/API/DeviceDetails.cs create mode 100644 WinUSBNet/API/DeviceManagement.cs create mode 100644 WinUSBNet/API/DeviceManagementAPI.cs create mode 100644 WinUSBNet/API/FileAPI.cs create mode 100644 WinUSBNet/API/WinUSBDevice.cs create mode 100644 WinUSBNet/API/WinUSBDeviceAPI.cs create mode 100644 WinUSBNet/DeviceNotifyHook.cs create mode 100644 WinUSBNet/USB.cs create mode 100644 WinUSBNet/USBAsyncResult.cs create mode 100644 WinUSBNet/USBDevice.cs create mode 100644 WinUSBNet/USBDeviceDescriptor.cs create mode 100644 WinUSBNet/USBDeviceInfo.cs create mode 100644 WinUSBNet/USBException.cs create mode 100644 WinUSBNet/USBInterface.cs create mode 100644 WinUSBNet/USBInterfaceCollection.cs create mode 100644 WinUSBNet/USBNotifier.cs create mode 100644 WinUSBNet/USBPipe.cs create mode 100644 WinUSBNet/USBPipeCollection.cs create mode 100644 WinUSBNet/USBPipePolicy.cs create mode 100644 aerobusy.gif create mode 100644 app.config create mode 100644 app.manifest create mode 100644 packages.config create mode 100644 packages/Newtonsoft.Json.9.0.1/Newtonsoft.Json.9.0.1.nupkg create mode 100644 packages/Newtonsoft.Json.9.0.1/Newtonsoft.Json.9.0.1.nuspec create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/net20/Newtonsoft.Json.dll create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/net20/Newtonsoft.Json.xml create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/net35/Newtonsoft.Json.dll create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/net35/Newtonsoft.Json.xml create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/net40/Newtonsoft.Json.dll create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/net40/Newtonsoft.Json.xml create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/net45/Newtonsoft.Json.dll create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/net45/Newtonsoft.Json.xml create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/netstandard1.0/Newtonsoft.Json.dll create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/netstandard1.0/Newtonsoft.Json.xml create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.dll create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.xml create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/portable-net45+wp80+win8+wpa81/Newtonsoft.Json.dll create mode 100644 packages/Newtonsoft.Json.9.0.1/lib/portable-net45+wp80+win8+wpa81/Newtonsoft.Json.xml create mode 100644 packages/Newtonsoft.Json.9.0.1/tools/install.ps1 create mode 100644 packages/repositories.config diff --git a/7zip/Common/CRC.cs b/7zip/Common/CRC.cs new file mode 100644 index 0000000..62bb847 --- /dev/null +++ b/7zip/Common/CRC.cs @@ -0,0 +1,55 @@ +// Common/CRC.cs + +namespace SevenZip +{ + class CRC + { + public static readonly uint[] Table; + + static CRC() + { + Table = new uint[256]; + const uint kPoly = 0xEDB88320; + for (uint i = 0; i < 256; i++) + { + uint r = i; + for (int j = 0; j < 8; j++) + if ((r & 1) != 0) + r = (r >> 1) ^ kPoly; + else + r >>= 1; + Table[i] = r; + } + } + + uint _value = 0xFFFFFFFF; + + public void Init() { _value = 0xFFFFFFFF; } + + public void UpdateByte(byte b) + { + _value = Table[(((byte)(_value)) ^ b)] ^ (_value >> 8); + } + + public void Update(byte[] data, uint offset, uint size) + { + for (uint i = 0; i < size; i++) + _value = Table[(((byte)(_value)) ^ data[offset + i])] ^ (_value >> 8); + } + + public uint GetDigest() { return _value ^ 0xFFFFFFFF; } + + static uint CalculateDigest(byte[] data, uint offset, uint size) + { + CRC crc = new CRC(); + // crc.Init(); + crc.Update(data, offset, size); + return crc.GetDigest(); + } + + static bool VerifyDigest(uint digest, byte[] data, uint offset, uint size) + { + return (CalculateDigest(data, offset, size) == digest); + } + } +} diff --git a/7zip/Common/CommandLineParser.cs b/7zip/Common/CommandLineParser.cs new file mode 100644 index 0000000..b46f6f2 --- /dev/null +++ b/7zip/Common/CommandLineParser.cs @@ -0,0 +1,274 @@ +// CommandLineParser.cs + +using System; +using System.Collections; + +namespace SevenZip.CommandLineParser +{ + public enum SwitchType + { + Simple, + PostMinus, + LimitedPostString, + UnLimitedPostString, + PostChar + } + + public class SwitchForm + { + public string IDString; + public SwitchType Type; + public bool Multi; + public int MinLen; + public int MaxLen; + public string PostCharSet; + + public SwitchForm(string idString, SwitchType type, bool multi, + int minLen, int maxLen, string postCharSet) + { + IDString = idString; + Type = type; + Multi = multi; + MinLen = minLen; + MaxLen = maxLen; + PostCharSet = postCharSet; + } + public SwitchForm(string idString, SwitchType type, bool multi, int minLen): + this(idString, type, multi, minLen, 0, "") + { + } + public SwitchForm(string idString, SwitchType type, bool multi): + this(idString, type, multi, 0) + { + } + } + + public class SwitchResult + { + public bool ThereIs; + public bool WithMinus; + public ArrayList PostStrings = new ArrayList(); + public int PostCharIndex; + public SwitchResult() + { + ThereIs = false; + } + } + + public class Parser + { + public ArrayList NonSwitchStrings = new ArrayList(); + SwitchResult[] _switches; + + public Parser(int numSwitches) + { + _switches = new SwitchResult[numSwitches]; + for (int i = 0; i < numSwitches; i++) + _switches[i] = new SwitchResult(); + } + + bool ParseString(string srcString, SwitchForm[] switchForms) + { + int len = srcString.Length; + if (len == 0) + return false; + int pos = 0; + if (!IsItSwitchChar(srcString[pos])) + return false; + while (pos < len) + { + if (IsItSwitchChar(srcString[pos])) + pos++; + const int kNoLen = -1; + int matchedSwitchIndex = 0; + int maxLen = kNoLen; + for (int switchIndex = 0; switchIndex < _switches.Length; switchIndex++) + { + int switchLen = switchForms[switchIndex].IDString.Length; + if (switchLen <= maxLen || pos + switchLen > len) + continue; + if (String.Compare(switchForms[switchIndex].IDString, 0, + srcString, pos, switchLen, true) == 0) + { + matchedSwitchIndex = switchIndex; + maxLen = switchLen; + } + } + if (maxLen == kNoLen) + throw new Exception("maxLen == kNoLen"); + SwitchResult matchedSwitch = _switches[matchedSwitchIndex]; + SwitchForm switchForm = switchForms[matchedSwitchIndex]; + if ((!switchForm.Multi) && matchedSwitch.ThereIs) + throw new Exception("switch must be single"); + matchedSwitch.ThereIs = true; + pos += maxLen; + int tailSize = len - pos; + SwitchType type = switchForm.Type; + switch (type) + { + case SwitchType.PostMinus: + { + if (tailSize == 0) + matchedSwitch.WithMinus = false; + else + { + matchedSwitch.WithMinus = (srcString[pos] == kSwitchMinus); + if (matchedSwitch.WithMinus) + pos++; + } + break; + } + case SwitchType.PostChar: + { + if (tailSize < switchForm.MinLen) + throw new Exception("switch is not full"); + string charSet = switchForm.PostCharSet; + const int kEmptyCharValue = -1; + if (tailSize == 0) + matchedSwitch.PostCharIndex = kEmptyCharValue; + else + { + int index = charSet.IndexOf(srcString[pos]); + if (index < 0) + matchedSwitch.PostCharIndex = kEmptyCharValue; + else + { + matchedSwitch.PostCharIndex = index; + pos++; + } + } + break; + } + case SwitchType.LimitedPostString: + case SwitchType.UnLimitedPostString: + { + int minLen = switchForm.MinLen; + if (tailSize < minLen) + throw new Exception("switch is not full"); + if (type == SwitchType.UnLimitedPostString) + { + matchedSwitch.PostStrings.Add(srcString.Substring(pos)); + return true; + } + String stringSwitch = srcString.Substring(pos, minLen); + pos += minLen; + for (int i = minLen; i < switchForm.MaxLen && pos < len; i++, pos++) + { + char c = srcString[pos]; + if (IsItSwitchChar(c)) + break; + stringSwitch += c; + } + matchedSwitch.PostStrings.Add(stringSwitch); + break; + } + } + } + return true; + + } + + public void ParseStrings(SwitchForm[] switchForms, string[] commandStrings) + { + int numCommandStrings = commandStrings.Length; + bool stopSwitch = false; + for (int i = 0; i < numCommandStrings; i++) + { + string s = commandStrings[i]; + if (stopSwitch) + NonSwitchStrings.Add(s); + else + if (s == kStopSwitchParsing) + stopSwitch = true; + else + if (!ParseString(s, switchForms)) + NonSwitchStrings.Add(s); + } + } + + public SwitchResult this[int index] { get { return _switches[index]; } } + + public static int ParseCommand(CommandForm[] commandForms, string commandString, + out string postString) + { + for (int i = 0; i < commandForms.Length; i++) + { + string id = commandForms[i].IDString; + if (commandForms[i].PostStringMode) + { + if (commandString.IndexOf(id) == 0) + { + postString = commandString.Substring(id.Length); + return i; + } + } + else + if (commandString == id) + { + postString = ""; + return i; + } + } + postString = ""; + return -1; + } + + static bool ParseSubCharsCommand(int numForms, CommandSubCharsSet[] forms, + string commandString, ArrayList indices) + { + indices.Clear(); + int numUsedChars = 0; + for (int i = 0; i < numForms; i++) + { + CommandSubCharsSet charsSet = forms[i]; + int currentIndex = -1; + int len = charsSet.Chars.Length; + for (int j = 0; j < len; j++) + { + char c = charsSet.Chars[j]; + int newIndex = commandString.IndexOf(c); + if (newIndex >= 0) + { + if (currentIndex >= 0) + return false; + if (commandString.IndexOf(c, newIndex + 1) >= 0) + return false; + currentIndex = j; + numUsedChars++; + } + } + if (currentIndex == -1 && !charsSet.EmptyAllowed) + return false; + indices.Add(currentIndex); + } + return (numUsedChars == commandString.Length); + } + const char kSwitchID1 = '-'; + const char kSwitchID2 = '/'; + + const char kSwitchMinus = '-'; + const string kStopSwitchParsing = "--"; + + static bool IsItSwitchChar(char c) + { + return (c == kSwitchID1 || c == kSwitchID2); + } + } + + public class CommandForm + { + public string IDString = ""; + public bool PostStringMode = false; + public CommandForm(string idString, bool postStringMode) + { + IDString = idString; + PostStringMode = postStringMode; + } + } + + class CommandSubCharsSet + { + public string Chars = ""; + public bool EmptyAllowed = false; + } +} diff --git a/7zip/Common/InBuffer.cs b/7zip/Common/InBuffer.cs new file mode 100644 index 0000000..9c47c73 --- /dev/null +++ b/7zip/Common/InBuffer.cs @@ -0,0 +1,72 @@ +// InBuffer.cs + +namespace SevenZip.Buffer +{ + public class InBuffer + { + byte[] m_Buffer; + uint m_Pos; + uint m_Limit; + uint m_BufferSize; + System.IO.Stream m_Stream; + bool m_StreamWasExhausted; + ulong m_ProcessedSize; + + public InBuffer(uint bufferSize) + { + m_Buffer = new byte[bufferSize]; + m_BufferSize = bufferSize; + } + + public void Init(System.IO.Stream stream) + { + m_Stream = stream; + m_ProcessedSize = 0; + m_Limit = 0; + m_Pos = 0; + m_StreamWasExhausted = false; + } + + public bool ReadBlock() + { + if (m_StreamWasExhausted) + return false; + m_ProcessedSize += m_Pos; + int aNumProcessedBytes = m_Stream.Read(m_Buffer, 0, (int)m_BufferSize); + m_Pos = 0; + m_Limit = (uint)aNumProcessedBytes; + m_StreamWasExhausted = (aNumProcessedBytes == 0); + return (!m_StreamWasExhausted); + } + + + public void ReleaseStream() + { + // m_Stream.Close(); + m_Stream = null; + } + + public bool ReadByte(byte b) // check it + { + if (m_Pos >= m_Limit) + if (!ReadBlock()) + return false; + b = m_Buffer[m_Pos++]; + return true; + } + + public byte ReadByte() + { + // return (byte)m_Stream.ReadByte(); + if (m_Pos >= m_Limit) + if (!ReadBlock()) + return 0xFF; + return m_Buffer[m_Pos++]; + } + + public ulong GetProcessedSize() + { + return m_ProcessedSize + m_Pos; + } + } +} diff --git a/7zip/Common/OutBuffer.cs b/7zip/Common/OutBuffer.cs new file mode 100644 index 0000000..c205aa6 --- /dev/null +++ b/7zip/Common/OutBuffer.cs @@ -0,0 +1,47 @@ +// OutBuffer.cs + +namespace SevenZip.Buffer +{ + public class OutBuffer + { + byte[] m_Buffer; + uint m_Pos; + uint m_BufferSize; + System.IO.Stream m_Stream; + ulong m_ProcessedSize; + + public OutBuffer(uint bufferSize) + { + m_Buffer = new byte[bufferSize]; + m_BufferSize = bufferSize; + } + + public void SetStream(System.IO.Stream stream) { m_Stream = stream; } + public void FlushStream() { m_Stream.Flush(); } + public void CloseStream() { m_Stream.Close(); } + public void ReleaseStream() { m_Stream = null; } + + public void Init() + { + m_ProcessedSize = 0; + m_Pos = 0; + } + + public void WriteByte(byte b) + { + m_Buffer[m_Pos++] = b; + if (m_Pos >= m_BufferSize) + FlushData(); + } + + public void FlushData() + { + if (m_Pos == 0) + return; + m_Stream.Write(m_Buffer, 0, (int)m_Pos); + m_Pos = 0; + } + + public ulong GetProcessedSize() { return m_ProcessedSize + m_Pos; } + } +} diff --git a/7zip/Compress/LZ/IMatchFinder.cs b/7zip/Compress/LZ/IMatchFinder.cs new file mode 100644 index 0000000..30fab86 --- /dev/null +++ b/7zip/Compress/LZ/IMatchFinder.cs @@ -0,0 +1,24 @@ +// IMatchFinder.cs + +using System; + +namespace SevenZip.Compression.LZ +{ + interface IInWindowStream + { + void SetStream(System.IO.Stream inStream); + void Init(); + void ReleaseStream(); + Byte GetIndexByte(Int32 index); + UInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit); + UInt32 GetNumAvailableBytes(); + } + + interface IMatchFinder : IInWindowStream + { + void Create(UInt32 historySize, UInt32 keepAddBufferBefore, + UInt32 matchMaxLen, UInt32 keepAddBufferAfter); + UInt32 GetMatches(UInt32[] distances); + void Skip(UInt32 num); + } +} diff --git a/7zip/Compress/LZ/LzBinTree.cs b/7zip/Compress/LZ/LzBinTree.cs new file mode 100644 index 0000000..7a9ca20 --- /dev/null +++ b/7zip/Compress/LZ/LzBinTree.cs @@ -0,0 +1,367 @@ +// LzBinTree.cs + +using System; + +namespace SevenZip.Compression.LZ +{ + public class BinTree : InWindow, IMatchFinder + { + UInt32 _cyclicBufferPos; + UInt32 _cyclicBufferSize = 0; + UInt32 _matchMaxLen; + + UInt32[] _son; + UInt32[] _hash; + + UInt32 _cutValue = 0xFF; + UInt32 _hashMask; + UInt32 _hashSizeSum = 0; + + bool HASH_ARRAY = true; + + const UInt32 kHash2Size = 1 << 10; + const UInt32 kHash3Size = 1 << 16; + const UInt32 kBT2HashSize = 1 << 16; + const UInt32 kStartMaxLen = 1; + const UInt32 kHash3Offset = kHash2Size; + const UInt32 kEmptyHashValue = 0; + const UInt32 kMaxValForNormalize = ((UInt32)1 << 31) - 1; + + UInt32 kNumHashDirectBytes = 0; + UInt32 kMinMatchCheck = 4; + UInt32 kFixHashSize = kHash2Size + kHash3Size; + + public void SetType(int numHashBytes) + { + HASH_ARRAY = (numHashBytes > 2); + if (HASH_ARRAY) + { + kNumHashDirectBytes = 0; + kMinMatchCheck = 4; + kFixHashSize = kHash2Size + kHash3Size; + } + else + { + kNumHashDirectBytes = 2; + kMinMatchCheck = 2 + 1; + kFixHashSize = 0; + } + } + + public new void SetStream(System.IO.Stream stream) { base.SetStream(stream); } + public new void ReleaseStream() { base.ReleaseStream(); } + + public new void Init() + { + base.Init(); + for (UInt32 i = 0; i < _hashSizeSum; i++) + _hash[i] = kEmptyHashValue; + _cyclicBufferPos = 0; + ReduceOffsets(-1); + } + + public new void MovePos() + { + if (++_cyclicBufferPos >= _cyclicBufferSize) + _cyclicBufferPos = 0; + base.MovePos(); + if (_pos == kMaxValForNormalize) + Normalize(); + } + + public new Byte GetIndexByte(Int32 index) { return base.GetIndexByte(index); } + + public new UInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit) + { return base.GetMatchLen(index, distance, limit); } + + public new UInt32 GetNumAvailableBytes() { return base.GetNumAvailableBytes(); } + + public void Create(UInt32 historySize, UInt32 keepAddBufferBefore, + UInt32 matchMaxLen, UInt32 keepAddBufferAfter) + { + if (historySize > kMaxValForNormalize - 256) + throw new Exception(); + _cutValue = 16 + (matchMaxLen >> 1); + + UInt32 windowReservSize = (historySize + keepAddBufferBefore + + matchMaxLen + keepAddBufferAfter) / 2 + 256; + + base.Create(historySize + keepAddBufferBefore, matchMaxLen + keepAddBufferAfter, windowReservSize); + + _matchMaxLen = matchMaxLen; + + UInt32 cyclicBufferSize = historySize + 1; + if (_cyclicBufferSize != cyclicBufferSize) + _son = new UInt32[(_cyclicBufferSize = cyclicBufferSize) * 2]; + + UInt32 hs = kBT2HashSize; + + if (HASH_ARRAY) + { + hs = historySize - 1; + hs |= (hs >> 1); + hs |= (hs >> 2); + hs |= (hs >> 4); + hs |= (hs >> 8); + hs >>= 1; + hs |= 0xFFFF; + if (hs > (1 << 24)) + hs >>= 1; + _hashMask = hs; + hs++; + hs += kFixHashSize; + } + if (hs != _hashSizeSum) + _hash = new UInt32[_hashSizeSum = hs]; + } + + public UInt32 GetMatches(UInt32[] distances) + { + UInt32 lenLimit; + if (_pos + _matchMaxLen <= _streamPos) + lenLimit = _matchMaxLen; + else + { + lenLimit = _streamPos - _pos; + if (lenLimit < kMinMatchCheck) + { + MovePos(); + return 0; + } + } + + UInt32 offset = 0; + UInt32 matchMinPos = (_pos > _cyclicBufferSize) ? (_pos - _cyclicBufferSize) : 0; + UInt32 cur = _bufferOffset + _pos; + UInt32 maxLen = kStartMaxLen; // to avoid items for len < hashSize; + UInt32 hashValue, hash2Value = 0, hash3Value = 0; + + if (HASH_ARRAY) + { + UInt32 temp = CRC.Table[_bufferBase[cur]] ^ _bufferBase[cur + 1]; + hash2Value = temp & (kHash2Size - 1); + temp ^= ((UInt32)(_bufferBase[cur + 2]) << 8); + hash3Value = temp & (kHash3Size - 1); + hashValue = (temp ^ (CRC.Table[_bufferBase[cur + 3]] << 5)) & _hashMask; + } + else + hashValue = _bufferBase[cur] ^ ((UInt32)(_bufferBase[cur + 1]) << 8); + + UInt32 curMatch = _hash[kFixHashSize + hashValue]; + if (HASH_ARRAY) + { + UInt32 curMatch2 = _hash[hash2Value]; + UInt32 curMatch3 = _hash[kHash3Offset + hash3Value]; + _hash[hash2Value] = _pos; + _hash[kHash3Offset + hash3Value] = _pos; + if (curMatch2 > matchMinPos) + if (_bufferBase[_bufferOffset + curMatch2] == _bufferBase[cur]) + { + distances[offset++] = maxLen = 2; + distances[offset++] = _pos - curMatch2 - 1; + } + if (curMatch3 > matchMinPos) + if (_bufferBase[_bufferOffset + curMatch3] == _bufferBase[cur]) + { + if (curMatch3 == curMatch2) + offset -= 2; + distances[offset++] = maxLen = 3; + distances[offset++] = _pos - curMatch3 - 1; + curMatch2 = curMatch3; + } + if (offset != 0 && curMatch2 == curMatch) + { + offset -= 2; + maxLen = kStartMaxLen; + } + } + + _hash[kFixHashSize + hashValue] = _pos; + + UInt32 ptr0 = (_cyclicBufferPos << 1) + 1; + UInt32 ptr1 = (_cyclicBufferPos << 1); + + UInt32 len0, len1; + len0 = len1 = kNumHashDirectBytes; + + if (kNumHashDirectBytes != 0) + { + if (curMatch > matchMinPos) + { + if (_bufferBase[_bufferOffset + curMatch + kNumHashDirectBytes] != + _bufferBase[cur + kNumHashDirectBytes]) + { + distances[offset++] = maxLen = kNumHashDirectBytes; + distances[offset++] = _pos - curMatch - 1; + } + } + } + + UInt32 count = _cutValue; + + while(true) + { + if(curMatch <= matchMinPos || count-- == 0) + { + _son[ptr0] = _son[ptr1] = kEmptyHashValue; + break; + } + UInt32 delta = _pos - curMatch; + UInt32 cyclicPos = ((delta <= _cyclicBufferPos) ? + (_cyclicBufferPos - delta) : + (_cyclicBufferPos - delta + _cyclicBufferSize)) << 1; + + UInt32 pby1 = _bufferOffset + curMatch; + UInt32 len = Math.Min(len0, len1); + if (_bufferBase[pby1 + len] == _bufferBase[cur + len]) + { + while(++len != lenLimit) + if (_bufferBase[pby1 + len] != _bufferBase[cur + len]) + break; + if (maxLen < len) + { + distances[offset++] = maxLen = len; + distances[offset++] = delta - 1; + if (len == lenLimit) + { + _son[ptr1] = _son[cyclicPos]; + _son[ptr0] = _son[cyclicPos + 1]; + break; + } + } + } + if (_bufferBase[pby1 + len] < _bufferBase[cur + len]) + { + _son[ptr1] = curMatch; + ptr1 = cyclicPos + 1; + curMatch = _son[ptr1]; + len1 = len; + } + else + { + _son[ptr0] = curMatch; + ptr0 = cyclicPos; + curMatch = _son[ptr0]; + len0 = len; + } + } + MovePos(); + return offset; + } + + public void Skip(UInt32 num) + { + do + { + UInt32 lenLimit; + if (_pos + _matchMaxLen <= _streamPos) + lenLimit = _matchMaxLen; + else + { + lenLimit = _streamPos - _pos; + if (lenLimit < kMinMatchCheck) + { + MovePos(); + continue; + } + } + + UInt32 matchMinPos = (_pos > _cyclicBufferSize) ? (_pos - _cyclicBufferSize) : 0; + UInt32 cur = _bufferOffset + _pos; + + UInt32 hashValue; + + if (HASH_ARRAY) + { + UInt32 temp = CRC.Table[_bufferBase[cur]] ^ _bufferBase[cur + 1]; + UInt32 hash2Value = temp & (kHash2Size - 1); + _hash[hash2Value] = _pos; + temp ^= ((UInt32)(_bufferBase[cur + 2]) << 8); + UInt32 hash3Value = temp & (kHash3Size - 1); + _hash[kHash3Offset + hash3Value] = _pos; + hashValue = (temp ^ (CRC.Table[_bufferBase[cur + 3]] << 5)) & _hashMask; + } + else + hashValue = _bufferBase[cur] ^ ((UInt32)(_bufferBase[cur + 1]) << 8); + + UInt32 curMatch = _hash[kFixHashSize + hashValue]; + _hash[kFixHashSize + hashValue] = _pos; + + UInt32 ptr0 = (_cyclicBufferPos << 1) + 1; + UInt32 ptr1 = (_cyclicBufferPos << 1); + + UInt32 len0, len1; + len0 = len1 = kNumHashDirectBytes; + + UInt32 count = _cutValue; + while (true) + { + if (curMatch <= matchMinPos || count-- == 0) + { + _son[ptr0] = _son[ptr1] = kEmptyHashValue; + break; + } + + UInt32 delta = _pos - curMatch; + UInt32 cyclicPos = ((delta <= _cyclicBufferPos) ? + (_cyclicBufferPos - delta) : + (_cyclicBufferPos - delta + _cyclicBufferSize)) << 1; + + UInt32 pby1 = _bufferOffset + curMatch; + UInt32 len = Math.Min(len0, len1); + if (_bufferBase[pby1 + len] == _bufferBase[cur + len]) + { + while (++len != lenLimit) + if (_bufferBase[pby1 + len] != _bufferBase[cur + len]) + break; + if (len == lenLimit) + { + _son[ptr1] = _son[cyclicPos]; + _son[ptr0] = _son[cyclicPos + 1]; + break; + } + } + if (_bufferBase[pby1 + len] < _bufferBase[cur + len]) + { + _son[ptr1] = curMatch; + ptr1 = cyclicPos + 1; + curMatch = _son[ptr1]; + len1 = len; + } + else + { + _son[ptr0] = curMatch; + ptr0 = cyclicPos; + curMatch = _son[ptr0]; + len0 = len; + } + } + MovePos(); + } + while (--num != 0); + } + + void NormalizeLinks(UInt32[] items, UInt32 numItems, UInt32 subValue) + { + for (UInt32 i = 0; i < numItems; i++) + { + UInt32 value = items[i]; + if (value <= subValue) + value = kEmptyHashValue; + else + value -= subValue; + items[i] = value; + } + } + + void Normalize() + { + UInt32 subValue = _pos - _cyclicBufferSize; + NormalizeLinks(_son, _cyclicBufferSize * 2, subValue); + NormalizeLinks(_hash, _hashSizeSum, subValue); + ReduceOffsets((Int32)subValue); + } + + public void SetCutValue(UInt32 cutValue) { _cutValue = cutValue; } + } +} diff --git a/7zip/Compress/LZ/LzInWindow.cs b/7zip/Compress/LZ/LzInWindow.cs new file mode 100644 index 0000000..f1974ce --- /dev/null +++ b/7zip/Compress/LZ/LzInWindow.cs @@ -0,0 +1,132 @@ +// LzInWindow.cs + +using System; + +namespace SevenZip.Compression.LZ +{ + public class InWindow + { + public Byte[] _bufferBase = null; // pointer to buffer with data + System.IO.Stream _stream; + UInt32 _posLimit; // offset (from _buffer) of first byte when new block reading must be done + bool _streamEndWasReached; // if (true) then _streamPos shows real end of stream + + UInt32 _pointerToLastSafePosition; + + public UInt32 _bufferOffset; + + public UInt32 _blockSize; // Size of Allocated memory block + public UInt32 _pos; // offset (from _buffer) of curent byte + UInt32 _keepSizeBefore; // how many BYTEs must be kept in buffer before _pos + UInt32 _keepSizeAfter; // how many BYTEs must be kept buffer after _pos + public UInt32 _streamPos; // offset (from _buffer) of first not read byte from Stream + + public void MoveBlock() + { + UInt32 offset = (UInt32)(_bufferOffset) + _pos - _keepSizeBefore; + // we need one additional byte, since MovePos moves on 1 byte. + if (offset > 0) + offset--; + + UInt32 numBytes = (UInt32)(_bufferOffset) + _streamPos - offset; + + // check negative offset ???? + for (UInt32 i = 0; i < numBytes; i++) + _bufferBase[i] = _bufferBase[offset + i]; + _bufferOffset -= offset; + } + + public virtual void ReadBlock() + { + if (_streamEndWasReached) + return; + while (true) + { + int size = (int)((0 - _bufferOffset) + _blockSize - _streamPos); + if (size == 0) + return; + int numReadBytes = _stream.Read(_bufferBase, (int)(_bufferOffset + _streamPos), size); + if (numReadBytes == 0) + { + _posLimit = _streamPos; + UInt32 pointerToPostion = _bufferOffset + _posLimit; + if (pointerToPostion > _pointerToLastSafePosition) + _posLimit = (UInt32)(_pointerToLastSafePosition - _bufferOffset); + + _streamEndWasReached = true; + return; + } + _streamPos += (UInt32)numReadBytes; + if (_streamPos >= _pos + _keepSizeAfter) + _posLimit = _streamPos - _keepSizeAfter; + } + } + + void Free() { _bufferBase = null; } + + public void Create(UInt32 keepSizeBefore, UInt32 keepSizeAfter, UInt32 keepSizeReserv) + { + _keepSizeBefore = keepSizeBefore; + _keepSizeAfter = keepSizeAfter; + UInt32 blockSize = keepSizeBefore + keepSizeAfter + keepSizeReserv; + if (_bufferBase == null || _blockSize != blockSize) + { + Free(); + _blockSize = blockSize; + _bufferBase = new Byte[_blockSize]; + } + _pointerToLastSafePosition = _blockSize - keepSizeAfter; + } + + public void SetStream(System.IO.Stream stream) { _stream = stream; } + public void ReleaseStream() { _stream = null; } + + public void Init() + { + _bufferOffset = 0; + _pos = 0; + _streamPos = 0; + _streamEndWasReached = false; + ReadBlock(); + } + + public void MovePos() + { + _pos++; + if (_pos > _posLimit) + { + UInt32 pointerToPostion = _bufferOffset + _pos; + if (pointerToPostion > _pointerToLastSafePosition) + MoveBlock(); + ReadBlock(); + } + } + + public Byte GetIndexByte(Int32 index) { return _bufferBase[_bufferOffset + _pos + index]; } + + // index + limit have not to exceed _keepSizeAfter; + public UInt32 GetMatchLen(Int32 index, UInt32 distance, UInt32 limit) + { + if (_streamEndWasReached) + if ((_pos + index) + limit > _streamPos) + limit = _streamPos - (UInt32)(_pos + index); + distance++; + // Byte *pby = _buffer + (size_t)_pos + index; + UInt32 pby = _bufferOffset + _pos + (UInt32)index; + + UInt32 i; + for (i = 0; i < limit && _bufferBase[pby + i] == _bufferBase[pby + i - distance]; i++); + return i; + } + + public UInt32 GetNumAvailableBytes() { return _streamPos - _pos; } + + public void ReduceOffsets(Int32 subValue) + { + _bufferOffset += (UInt32)subValue; + _posLimit -= (UInt32)subValue; + _pos -= (UInt32)subValue; + _streamPos -= (UInt32)subValue; + } + } +} diff --git a/7zip/Compress/LZ/LzOutWindow.cs b/7zip/Compress/LZ/LzOutWindow.cs new file mode 100644 index 0000000..84914f0 --- /dev/null +++ b/7zip/Compress/LZ/LzOutWindow.cs @@ -0,0 +1,110 @@ +// LzOutWindow.cs + +namespace SevenZip.Compression.LZ +{ + public class OutWindow + { + byte[] _buffer = null; + uint _pos; + uint _windowSize = 0; + uint _streamPos; + System.IO.Stream _stream; + + public uint TrainSize = 0; + + public void Create(uint windowSize) + { + if (_windowSize != windowSize) + { + // System.GC.Collect(); + _buffer = new byte[windowSize]; + } + _windowSize = windowSize; + _pos = 0; + _streamPos = 0; + } + + public void Init(System.IO.Stream stream, bool solid) + { + ReleaseStream(); + _stream = stream; + if (!solid) + { + _streamPos = 0; + _pos = 0; + TrainSize = 0; + } + } + + public bool Train(System.IO.Stream stream) + { + long len = stream.Length; + uint size = (len < _windowSize) ? (uint)len : _windowSize; + TrainSize = size; + stream.Position = len - size; + _streamPos = _pos = 0; + while (size > 0) + { + uint curSize = _windowSize - _pos; + if (size < curSize) + curSize = size; + int numReadBytes = stream.Read(_buffer, (int)_pos, (int)curSize); + if (numReadBytes == 0) + return false; + size -= (uint)numReadBytes; + _pos += (uint)numReadBytes; + _streamPos += (uint)numReadBytes; + if (_pos == _windowSize) + _streamPos = _pos = 0; + } + return true; + } + + public void ReleaseStream() + { + Flush(); + _stream = null; + } + + public void Flush() + { + uint size = _pos - _streamPos; + if (size == 0) + return; + _stream.Write(_buffer, (int)_streamPos, (int)size); + if (_pos >= _windowSize) + _pos = 0; + _streamPos = _pos; + } + + public void CopyBlock(uint distance, uint len) + { + uint pos = _pos - distance - 1; + if (pos >= _windowSize) + pos += _windowSize; + for (; len > 0; len--) + { + if (pos >= _windowSize) + pos = 0; + _buffer[_pos++] = _buffer[pos++]; + if (_pos >= _windowSize) + Flush(); + } + } + + public void PutByte(byte b) + { + _buffer[_pos++] = b; + if (_pos >= _windowSize) + Flush(); + } + + public byte GetByte(uint distance) + { + uint pos = _pos - distance - 1; + if (pos >= _windowSize) + pos += _windowSize; + return _buffer[pos]; + } + } +} diff --git a/7zip/Compress/LZMA/LzmaBase.cs b/7zip/Compress/LZMA/LzmaBase.cs new file mode 100644 index 0000000..8447a2a --- /dev/null +++ b/7zip/Compress/LZMA/LzmaBase.cs @@ -0,0 +1,76 @@ +// LzmaBase.cs + +namespace SevenZip.Compression.LZMA +{ + internal abstract class Base + { + public const uint kNumRepDistances = 4; + public const uint kNumStates = 12; + + // static byte []kLiteralNextStates = {0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 4, 5}; + // static byte []kMatchNextStates = {7, 7, 7, 7, 7, 7, 7, 10, 10, 10, 10, 10}; + // static byte []kRepNextStates = {8, 8, 8, 8, 8, 8, 8, 11, 11, 11, 11, 11}; + // static byte []kShortRepNextStates = {9, 9, 9, 9, 9, 9, 9, 11, 11, 11, 11, 11}; + + public struct State + { + public uint Index; + public void Init() { Index = 0; } + public void UpdateChar() + { + if (Index < 4) Index = 0; + else if (Index < 10) Index -= 3; + else Index -= 6; + } + public void UpdateMatch() { Index = (uint)(Index < 7 ? 7 : 10); } + public void UpdateRep() { Index = (uint)(Index < 7 ? 8 : 11); } + public void UpdateShortRep() { Index = (uint)(Index < 7 ? 9 : 11); } + public bool IsCharState() { return Index < 7; } + } + + public const int kNumPosSlotBits = 6; + public const int kDicLogSizeMin = 0; + // public const int kDicLogSizeMax = 30; + // public const uint kDistTableSizeMax = kDicLogSizeMax * 2; + + public const int kNumLenToPosStatesBits = 2; // it's for speed optimization + public const uint kNumLenToPosStates = 1 << kNumLenToPosStatesBits; + + public const uint kMatchMinLen = 2; + + public static uint GetLenToPosState(uint len) + { + len -= kMatchMinLen; + if (len < kNumLenToPosStates) + return len; + return (uint)(kNumLenToPosStates - 1); + } + + public const int kNumAlignBits = 4; + public const uint kAlignTableSize = 1 << kNumAlignBits; + public const uint kAlignMask = (kAlignTableSize - 1); + + public const uint kStartPosModelIndex = 4; + public const uint kEndPosModelIndex = 14; + public const uint kNumPosModels = kEndPosModelIndex - kStartPosModelIndex; + + public const uint kNumFullDistances = 1 << ((int)kEndPosModelIndex / 2); + + public const uint kNumLitPosStatesBitsEncodingMax = 4; + public const uint kNumLitContextBitsMax = 8; + + public const int kNumPosStatesBitsMax = 4; + public const uint kNumPosStatesMax = (1 << kNumPosStatesBitsMax); + public const int kNumPosStatesBitsEncodingMax = 4; + public const uint kNumPosStatesEncodingMax = (1 << kNumPosStatesBitsEncodingMax); + + public const int kNumLowLenBits = 3; + public const int kNumMidLenBits = 3; + public const int kNumHighLenBits = 8; + public const uint kNumLowLenSymbols = 1 << kNumLowLenBits; + public const uint kNumMidLenSymbols = 1 << kNumMidLenBits; + public const uint kNumLenSymbols = kNumLowLenSymbols + kNumMidLenSymbols + + (1 << kNumHighLenBits); + public const uint kMatchMaxLen = kMatchMinLen + kNumLenSymbols - 1; + } +} diff --git a/7zip/Compress/LZMA/LzmaDecoder.cs b/7zip/Compress/LZMA/LzmaDecoder.cs new file mode 100644 index 0000000..00bfe63 --- /dev/null +++ b/7zip/Compress/LZMA/LzmaDecoder.cs @@ -0,0 +1,398 @@ +// LzmaDecoder.cs + +using System; + +namespace SevenZip.Compression.LZMA +{ + using RangeCoder; + + public class Decoder : ICoder, ISetDecoderProperties // ,System.IO.Stream + { + class LenDecoder + { + BitDecoder m_Choice = new BitDecoder(); + BitDecoder m_Choice2 = new BitDecoder(); + BitTreeDecoder[] m_LowCoder = new BitTreeDecoder[Base.kNumPosStatesMax]; + BitTreeDecoder[] m_MidCoder = new BitTreeDecoder[Base.kNumPosStatesMax]; + BitTreeDecoder m_HighCoder = new BitTreeDecoder(Base.kNumHighLenBits); + uint m_NumPosStates = 0; + + public void Create(uint numPosStates) + { + for (uint posState = m_NumPosStates; posState < numPosStates; posState++) + { + m_LowCoder[posState] = new BitTreeDecoder(Base.kNumLowLenBits); + m_MidCoder[posState] = new BitTreeDecoder(Base.kNumMidLenBits); + } + m_NumPosStates = numPosStates; + } + + public void Init() + { + m_Choice.Init(); + for (uint posState = 0; posState < m_NumPosStates; posState++) + { + m_LowCoder[posState].Init(); + m_MidCoder[posState].Init(); + } + m_Choice2.Init(); + m_HighCoder.Init(); + } + + public uint Decode(RangeCoder.Decoder rangeDecoder, uint posState) + { + if (m_Choice.Decode(rangeDecoder) == 0) + return m_LowCoder[posState].Decode(rangeDecoder); + else + { + uint symbol = Base.kNumLowLenSymbols; + if (m_Choice2.Decode(rangeDecoder) == 0) + symbol += m_MidCoder[posState].Decode(rangeDecoder); + else + { + symbol += Base.kNumMidLenSymbols; + symbol += m_HighCoder.Decode(rangeDecoder); + } + return symbol; + } + } + } + + class LiteralDecoder + { + struct Decoder2 + { + BitDecoder[] m_Decoders; + public void Create() { m_Decoders = new BitDecoder[0x300]; } + public void Init() { for (int i = 0; i < 0x300; i++) m_Decoders[i].Init(); } + + public byte DecodeNormal(RangeCoder.Decoder rangeDecoder) + { + uint symbol = 1; + do + symbol = (symbol << 1) | m_Decoders[symbol].Decode(rangeDecoder); + while (symbol < 0x100); + return (byte)symbol; + } + + public byte DecodeWithMatchByte(RangeCoder.Decoder rangeDecoder, byte matchByte) + { + uint symbol = 1; + do + { + uint matchBit = (uint)(matchByte >> 7) & 1; + matchByte <<= 1; + uint bit = m_Decoders[((1 + matchBit) << 8) + symbol].Decode(rangeDecoder); + symbol = (symbol << 1) | bit; + if (matchBit != bit) + { + while (symbol < 0x100) + symbol = (symbol << 1) | m_Decoders[symbol].Decode(rangeDecoder); + break; + } + } + while (symbol < 0x100); + return (byte)symbol; + } + } + + Decoder2[] m_Coders; + int m_NumPrevBits; + int m_NumPosBits; + uint m_PosMask; + + public void Create(int numPosBits, int numPrevBits) + { + if (m_Coders != null && m_NumPrevBits == numPrevBits && + m_NumPosBits == numPosBits) + return; + m_NumPosBits = numPosBits; + m_PosMask = ((uint)1 << numPosBits) - 1; + m_NumPrevBits = numPrevBits; + uint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits); + m_Coders = new Decoder2[numStates]; + for (uint i = 0; i < numStates; i++) + m_Coders[i].Create(); + } + + public void Init() + { + uint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits); + for (uint i = 0; i < numStates; i++) + m_Coders[i].Init(); + } + + uint GetState(uint pos, byte prevByte) + { return ((pos & m_PosMask) << m_NumPrevBits) + (uint)(prevByte >> (8 - m_NumPrevBits)); } + + public byte DecodeNormal(RangeCoder.Decoder rangeDecoder, uint pos, byte prevByte) + { return m_Coders[GetState(pos, prevByte)].DecodeNormal(rangeDecoder); } + + public byte DecodeWithMatchByte(RangeCoder.Decoder rangeDecoder, uint pos, byte prevByte, byte matchByte) + { return m_Coders[GetState(pos, prevByte)].DecodeWithMatchByte(rangeDecoder, matchByte); } + }; + + LZ.OutWindow m_OutWindow = new LZ.OutWindow(); + RangeCoder.Decoder m_RangeDecoder = new RangeCoder.Decoder(); + + BitDecoder[] m_IsMatchDecoders = new BitDecoder[Base.kNumStates << Base.kNumPosStatesBitsMax]; + BitDecoder[] m_IsRepDecoders = new BitDecoder[Base.kNumStates]; + BitDecoder[] m_IsRepG0Decoders = new BitDecoder[Base.kNumStates]; + BitDecoder[] m_IsRepG1Decoders = new BitDecoder[Base.kNumStates]; + BitDecoder[] m_IsRepG2Decoders = new BitDecoder[Base.kNumStates]; + BitDecoder[] m_IsRep0LongDecoders = new BitDecoder[Base.kNumStates << Base.kNumPosStatesBitsMax]; + + BitTreeDecoder[] m_PosSlotDecoder = new BitTreeDecoder[Base.kNumLenToPosStates]; + BitDecoder[] m_PosDecoders = new BitDecoder[Base.kNumFullDistances - Base.kEndPosModelIndex]; + + BitTreeDecoder m_PosAlignDecoder = new BitTreeDecoder(Base.kNumAlignBits); + + LenDecoder m_LenDecoder = new LenDecoder(); + LenDecoder m_RepLenDecoder = new LenDecoder(); + + LiteralDecoder m_LiteralDecoder = new LiteralDecoder(); + + uint m_DictionarySize; + uint m_DictionarySizeCheck; + + uint m_PosStateMask; + + public Decoder() + { + m_DictionarySize = 0xFFFFFFFF; + for (int i = 0; i < Base.kNumLenToPosStates; i++) + m_PosSlotDecoder[i] = new BitTreeDecoder(Base.kNumPosSlotBits); + } + + void SetDictionarySize(uint dictionarySize) + { + if (m_DictionarySize != dictionarySize) + { + m_DictionarySize = dictionarySize; + m_DictionarySizeCheck = Math.Max(m_DictionarySize, 1); + uint blockSize = Math.Max(m_DictionarySizeCheck, (1 << 12)); + m_OutWindow.Create(blockSize); + } + } + + void SetLiteralProperties(int lp, int lc) + { + if (lp > 8) + throw new InvalidParamException(); + if (lc > 8) + throw new InvalidParamException(); + m_LiteralDecoder.Create(lp, lc); + } + + void SetPosBitsProperties(int pb) + { + if (pb > Base.kNumPosStatesBitsMax) + throw new InvalidParamException(); + uint numPosStates = (uint)1 << pb; + m_LenDecoder.Create(numPosStates); + m_RepLenDecoder.Create(numPosStates); + m_PosStateMask = numPosStates - 1; + } + + bool _solid = false; + void Init(System.IO.Stream inStream, System.IO.Stream outStream) + { + m_RangeDecoder.Init(inStream); + m_OutWindow.Init(outStream, _solid); + + uint i; + for (i = 0; i < Base.kNumStates; i++) + { + for (uint j = 0; j <= m_PosStateMask; j++) + { + uint index = (i << Base.kNumPosStatesBitsMax) + j; + m_IsMatchDecoders[index].Init(); + m_IsRep0LongDecoders[index].Init(); + } + m_IsRepDecoders[i].Init(); + m_IsRepG0Decoders[i].Init(); + m_IsRepG1Decoders[i].Init(); + m_IsRepG2Decoders[i].Init(); + } + + m_LiteralDecoder.Init(); + for (i = 0; i < Base.kNumLenToPosStates; i++) + m_PosSlotDecoder[i].Init(); + // m_PosSpecDecoder.Init(); + for (i = 0; i < Base.kNumFullDistances - Base.kEndPosModelIndex; i++) + m_PosDecoders[i].Init(); + + m_LenDecoder.Init(); + m_RepLenDecoder.Init(); + m_PosAlignDecoder.Init(); + } + + public void Code(System.IO.Stream inStream, System.IO.Stream outStream, + Int64 inSize, Int64 outSize, ICodeProgress progress) + { + Init(inStream, outStream); + + Base.State state = new Base.State(); + state.Init(); + uint rep0 = 0, rep1 = 0, rep2 = 0, rep3 = 0; + + UInt64 nowPos64 = 0; + UInt64 outSize64 = (UInt64)outSize; + if (nowPos64 < outSize64) + { + if (m_IsMatchDecoders[state.Index << Base.kNumPosStatesBitsMax].Decode(m_RangeDecoder) != 0) + throw new DataErrorException(); + state.UpdateChar(); + byte b = m_LiteralDecoder.DecodeNormal(m_RangeDecoder, 0, 0); + m_OutWindow.PutByte(b); + nowPos64++; + } + while (nowPos64 < outSize64) + { + // UInt64 next = Math.Min(nowPos64 + (1 << 18), outSize64); + // while(nowPos64 < next) + { + uint posState = (uint)nowPos64 & m_PosStateMask; + if (m_IsMatchDecoders[(state.Index << Base.kNumPosStatesBitsMax) + posState].Decode(m_RangeDecoder) == 0) + { + byte b; + byte prevByte = m_OutWindow.GetByte(0); + if (!state.IsCharState()) + b = m_LiteralDecoder.DecodeWithMatchByte(m_RangeDecoder, + (uint)nowPos64, prevByte, m_OutWindow.GetByte(rep0)); + else + b = m_LiteralDecoder.DecodeNormal(m_RangeDecoder, (uint)nowPos64, prevByte); + m_OutWindow.PutByte(b); + state.UpdateChar(); + nowPos64++; + } + else + { + uint len; + if (m_IsRepDecoders[state.Index].Decode(m_RangeDecoder) == 1) + { + if (m_IsRepG0Decoders[state.Index].Decode(m_RangeDecoder) == 0) + { + if (m_IsRep0LongDecoders[(state.Index << Base.kNumPosStatesBitsMax) + posState].Decode(m_RangeDecoder) == 0) + { + state.UpdateShortRep(); + m_OutWindow.PutByte(m_OutWindow.GetByte(rep0)); + nowPos64++; + continue; + } + } + else + { + UInt32 distance; + if (m_IsRepG1Decoders[state.Index].Decode(m_RangeDecoder) == 0) + { + distance = rep1; + } + else + { + if (m_IsRepG2Decoders[state.Index].Decode(m_RangeDecoder) == 0) + distance = rep2; + else + { + distance = rep3; + rep3 = rep2; + } + rep2 = rep1; + } + rep1 = rep0; + rep0 = distance; + } + len = m_RepLenDecoder.Decode(m_RangeDecoder, posState) + Base.kMatchMinLen; + state.UpdateRep(); + } + else + { + rep3 = rep2; + rep2 = rep1; + rep1 = rep0; + len = Base.kMatchMinLen + m_LenDecoder.Decode(m_RangeDecoder, posState); + state.UpdateMatch(); + uint posSlot = m_PosSlotDecoder[Base.GetLenToPosState(len)].Decode(m_RangeDecoder); + if (posSlot >= Base.kStartPosModelIndex) + { + int numDirectBits = (int)((posSlot >> 1) - 1); + rep0 = ((2 | (posSlot & 1)) << numDirectBits); + if (posSlot < Base.kEndPosModelIndex) + rep0 += BitTreeDecoder.ReverseDecode(m_PosDecoders, + rep0 - posSlot - 1, m_RangeDecoder, numDirectBits); + else + { + rep0 += (m_RangeDecoder.DecodeDirectBits( + numDirectBits - Base.kNumAlignBits) << Base.kNumAlignBits); + rep0 += m_PosAlignDecoder.ReverseDecode(m_RangeDecoder); + } + } + else + rep0 = posSlot; + } + if (rep0 >= m_OutWindow.TrainSize + nowPos64 || rep0 >= m_DictionarySizeCheck) + { + if (rep0 == 0xFFFFFFFF) + break; + throw new DataErrorException(); + } + m_OutWindow.CopyBlock(rep0, len); + nowPos64 += len; + } + } + } + m_OutWindow.Flush(); + m_OutWindow.ReleaseStream(); + m_RangeDecoder.ReleaseStream(); + } + + public void SetDecoderProperties(byte[] properties) + { + if (properties.Length < 5) + throw new InvalidParamException(); + int lc = properties[0] % 9; + int remainder = properties[0] / 9; + int lp = remainder % 5; + int pb = remainder / 5; + if (pb > Base.kNumPosStatesBitsMax) + throw new InvalidParamException(); + UInt32 dictionarySize = 0; + for (int i = 0; i < 4; i++) + dictionarySize += ((UInt32)(properties[1 + i])) << (i * 8); + SetDictionarySize(dictionarySize); + SetLiteralProperties(lp, lc); + SetPosBitsProperties(pb); + } + + public bool Train(System.IO.Stream stream) + { + _solid = true; + return m_OutWindow.Train(stream); + } + + /* + public override bool CanRead { get { return true; }} + public override bool CanWrite { get { return true; }} + public override bool CanSeek { get { return true; }} + public override long Length { get { return 0; }} + public override long Position + { + get { return 0; } + set { } + } + public override void Flush() { } + public override int Read(byte[] buffer, int offset, int count) + { + return 0; + } + public override void Write(byte[] buffer, int offset, int count) + { + } + public override long Seek(long offset, System.IO.SeekOrigin origin) + { + return 0; + } + public override void SetLength(long value) {} + */ + } +} diff --git a/7zip/Compress/LZMA/LzmaEncoder.cs b/7zip/Compress/LZMA/LzmaEncoder.cs new file mode 100644 index 0000000..6dc2708 --- /dev/null +++ b/7zip/Compress/LZMA/LzmaEncoder.cs @@ -0,0 +1,1480 @@ +// LzmaEncoder.cs + +using System; + +namespace SevenZip.Compression.LZMA +{ + using RangeCoder; + + public class Encoder : ICoder, ISetCoderProperties, IWriteCoderProperties + { + enum EMatchFinderType + { + BT2, + BT4, + }; + + const UInt32 kIfinityPrice = 0xFFFFFFF; + + static Byte[] g_FastPos = new Byte[1 << 11]; + + static Encoder() + { + const Byte kFastSlots = 22; + int c = 2; + g_FastPos[0] = 0; + g_FastPos[1] = 1; + for (Byte slotFast = 2; slotFast < kFastSlots; slotFast++) + { + UInt32 k = ((UInt32)1 << ((slotFast >> 1) - 1)); + for (UInt32 j = 0; j < k; j++, c++) + g_FastPos[c] = slotFast; + } + } + + static UInt32 GetPosSlot(UInt32 pos) + { + if (pos < (1 << 11)) + return g_FastPos[pos]; + if (pos < (1 << 21)) + return (UInt32)(g_FastPos[pos >> 10] + 20); + return (UInt32)(g_FastPos[pos >> 20] + 40); + } + + static UInt32 GetPosSlot2(UInt32 pos) + { + if (pos < (1 << 17)) + return (UInt32)(g_FastPos[pos >> 6] + 12); + if (pos < (1 << 27)) + return (UInt32)(g_FastPos[pos >> 16] + 32); + return (UInt32)(g_FastPos[pos >> 26] + 52); + } + + Base.State _state = new Base.State(); + Byte _previousByte; + UInt32[] _repDistances = new UInt32[Base.kNumRepDistances]; + + void BaseInit() + { + _state.Init(); + _previousByte = 0; + for (UInt32 i = 0; i < Base.kNumRepDistances; i++) + _repDistances[i] = 0; + } + + const int kDefaultDictionaryLogSize = 22; + const UInt32 kNumFastBytesDefault = 0x20; + + class LiteralEncoder + { + public struct Encoder2 + { + BitEncoder[] m_Encoders; + + public void Create() { m_Encoders = new BitEncoder[0x300]; } + + public void Init() { for (int i = 0; i < 0x300; i++) m_Encoders[i].Init(); } + + public void Encode(RangeCoder.Encoder rangeEncoder, byte symbol) + { + uint context = 1; + for (int i = 7; i >= 0; i--) + { + uint bit = (uint)((symbol >> i) & 1); + m_Encoders[context].Encode(rangeEncoder, bit); + context = (context << 1) | bit; + } + } + + public void EncodeMatched(RangeCoder.Encoder rangeEncoder, byte matchByte, byte symbol) + { + uint context = 1; + bool same = true; + for (int i = 7; i >= 0; i--) + { + uint bit = (uint)((symbol >> i) & 1); + uint state = context; + if (same) + { + uint matchBit = (uint)((matchByte >> i) & 1); + state += ((1 + matchBit) << 8); + same = (matchBit == bit); + } + m_Encoders[state].Encode(rangeEncoder, bit); + context = (context << 1) | bit; + } + } + + public uint GetPrice(bool matchMode, byte matchByte, byte symbol) + { + uint price = 0; + uint context = 1; + int i = 7; + if (matchMode) + { + for (; i >= 0; i--) + { + uint matchBit = (uint)(matchByte >> i) & 1; + uint bit = (uint)(symbol >> i) & 1; + price += m_Encoders[((1 + matchBit) << 8) + context].GetPrice(bit); + context = (context << 1) | bit; + if (matchBit != bit) + { + i--; + break; + } + } + } + for (; i >= 0; i--) + { + uint bit = (uint)(symbol >> i) & 1; + price += m_Encoders[context].GetPrice(bit); + context = (context << 1) | bit; + } + return price; + } + } + + Encoder2[] m_Coders; + int m_NumPrevBits; + int m_NumPosBits; + uint m_PosMask; + + public void Create(int numPosBits, int numPrevBits) + { + if (m_Coders != null && m_NumPrevBits == numPrevBits && m_NumPosBits == numPosBits) + return; + m_NumPosBits = numPosBits; + m_PosMask = ((uint)1 << numPosBits) - 1; + m_NumPrevBits = numPrevBits; + uint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits); + m_Coders = new Encoder2[numStates]; + for (uint i = 0; i < numStates; i++) + m_Coders[i].Create(); + } + + public void Init() + { + uint numStates = (uint)1 << (m_NumPrevBits + m_NumPosBits); + for (uint i = 0; i < numStates; i++) + m_Coders[i].Init(); + } + + public Encoder2 GetSubCoder(UInt32 pos, Byte prevByte) + { return m_Coders[((pos & m_PosMask) << m_NumPrevBits) + (uint)(prevByte >> (8 - m_NumPrevBits))]; } + } + + class LenEncoder + { + RangeCoder.BitEncoder _choice = new RangeCoder.BitEncoder(); + RangeCoder.BitEncoder _choice2 = new RangeCoder.BitEncoder(); + RangeCoder.BitTreeEncoder[] _lowCoder = new RangeCoder.BitTreeEncoder[Base.kNumPosStatesEncodingMax]; + RangeCoder.BitTreeEncoder[] _midCoder = new RangeCoder.BitTreeEncoder[Base.kNumPosStatesEncodingMax]; + RangeCoder.BitTreeEncoder _highCoder = new RangeCoder.BitTreeEncoder(Base.kNumHighLenBits); + + public LenEncoder() + { + for (UInt32 posState = 0; posState < Base.kNumPosStatesEncodingMax; posState++) + { + _lowCoder[posState] = new RangeCoder.BitTreeEncoder(Base.kNumLowLenBits); + _midCoder[posState] = new RangeCoder.BitTreeEncoder(Base.kNumMidLenBits); + } + } + + public void Init(UInt32 numPosStates) + { + _choice.Init(); + _choice2.Init(); + for (UInt32 posState = 0; posState < numPosStates; posState++) + { + _lowCoder[posState].Init(); + _midCoder[posState].Init(); + } + _highCoder.Init(); + } + + public void Encode(RangeCoder.Encoder rangeEncoder, UInt32 symbol, UInt32 posState) + { + if (symbol < Base.kNumLowLenSymbols) + { + _choice.Encode(rangeEncoder, 0); + _lowCoder[posState].Encode(rangeEncoder, symbol); + } + else + { + symbol -= Base.kNumLowLenSymbols; + _choice.Encode(rangeEncoder, 1); + if (symbol < Base.kNumMidLenSymbols) + { + _choice2.Encode(rangeEncoder, 0); + _midCoder[posState].Encode(rangeEncoder, symbol); + } + else + { + _choice2.Encode(rangeEncoder, 1); + _highCoder.Encode(rangeEncoder, symbol - Base.kNumMidLenSymbols); + } + } + } + + public void SetPrices(UInt32 posState, UInt32 numSymbols, UInt32[] prices, UInt32 st) + { + UInt32 a0 = _choice.GetPrice0(); + UInt32 a1 = _choice.GetPrice1(); + UInt32 b0 = a1 + _choice2.GetPrice0(); + UInt32 b1 = a1 + _choice2.GetPrice1(); + UInt32 i = 0; + for (i = 0; i < Base.kNumLowLenSymbols; i++) + { + if (i >= numSymbols) + return; + prices[st + i] = a0 + _lowCoder[posState].GetPrice(i); + } + for (; i < Base.kNumLowLenSymbols + Base.kNumMidLenSymbols; i++) + { + if (i >= numSymbols) + return; + prices[st + i] = b0 + _midCoder[posState].GetPrice(i - Base.kNumLowLenSymbols); + } + for (; i < numSymbols; i++) + prices[st + i] = b1 + _highCoder.GetPrice(i - Base.kNumLowLenSymbols - Base.kNumMidLenSymbols); + } + }; + + const UInt32 kNumLenSpecSymbols = Base.kNumLowLenSymbols + Base.kNumMidLenSymbols; + + class LenPriceTableEncoder : LenEncoder + { + UInt32[] _prices = new UInt32[Base.kNumLenSymbols << Base.kNumPosStatesBitsEncodingMax]; + UInt32 _tableSize; + UInt32[] _counters = new UInt32[Base.kNumPosStatesEncodingMax]; + + public void SetTableSize(UInt32 tableSize) { _tableSize = tableSize; } + + public UInt32 GetPrice(UInt32 symbol, UInt32 posState) + { + return _prices[posState * Base.kNumLenSymbols + symbol]; + } + + void UpdateTable(UInt32 posState) + { + SetPrices(posState, _tableSize, _prices, posState * Base.kNumLenSymbols); + _counters[posState] = _tableSize; + } + + public void UpdateTables(UInt32 numPosStates) + { + for (UInt32 posState = 0; posState < numPosStates; posState++) + UpdateTable(posState); + } + + public new void Encode(RangeCoder.Encoder rangeEncoder, UInt32 symbol, UInt32 posState) + { + base.Encode(rangeEncoder, symbol, posState); + if (--_counters[posState] == 0) + UpdateTable(posState); + } + } + + const UInt32 kNumOpts = 1 << 12; + class Optimal + { + public Base.State State; + + public bool Prev1IsChar; + public bool Prev2; + + public UInt32 PosPrev2; + public UInt32 BackPrev2; + + public UInt32 Price; + public UInt32 PosPrev; + public UInt32 BackPrev; + + public UInt32 Backs0; + public UInt32 Backs1; + public UInt32 Backs2; + public UInt32 Backs3; + + public void MakeAsChar() { BackPrev = 0xFFFFFFFF; Prev1IsChar = false; } + public void MakeAsShortRep() { BackPrev = 0; ; Prev1IsChar = false; } + public bool IsShortRep() { return (BackPrev == 0); } + }; + Optimal[] _optimum = new Optimal[kNumOpts]; + LZ.IMatchFinder _matchFinder = null; + RangeCoder.Encoder _rangeEncoder = new RangeCoder.Encoder(); + + RangeCoder.BitEncoder[] _isMatch = new RangeCoder.BitEncoder[Base.kNumStates << Base.kNumPosStatesBitsMax]; + RangeCoder.BitEncoder[] _isRep = new RangeCoder.BitEncoder[Base.kNumStates]; + RangeCoder.BitEncoder[] _isRepG0 = new RangeCoder.BitEncoder[Base.kNumStates]; + RangeCoder.BitEncoder[] _isRepG1 = new RangeCoder.BitEncoder[Base.kNumStates]; + RangeCoder.BitEncoder[] _isRepG2 = new RangeCoder.BitEncoder[Base.kNumStates]; + RangeCoder.BitEncoder[] _isRep0Long = new RangeCoder.BitEncoder[Base.kNumStates << Base.kNumPosStatesBitsMax]; + + RangeCoder.BitTreeEncoder[] _posSlotEncoder = new RangeCoder.BitTreeEncoder[Base.kNumLenToPosStates]; + + RangeCoder.BitEncoder[] _posEncoders = new RangeCoder.BitEncoder[Base.kNumFullDistances - Base.kEndPosModelIndex]; + RangeCoder.BitTreeEncoder _posAlignEncoder = new RangeCoder.BitTreeEncoder(Base.kNumAlignBits); + + LenPriceTableEncoder _lenEncoder = new LenPriceTableEncoder(); + LenPriceTableEncoder _repMatchLenEncoder = new LenPriceTableEncoder(); + + LiteralEncoder _literalEncoder = new LiteralEncoder(); + + UInt32[] _matchDistances = new UInt32[Base.kMatchMaxLen * 2 + 2]; + + UInt32 _numFastBytes = kNumFastBytesDefault; + UInt32 _longestMatchLength; + UInt32 _numDistancePairs; + + UInt32 _additionalOffset; + + UInt32 _optimumEndIndex; + UInt32 _optimumCurrentIndex; + + bool _longestMatchWasFound; + + UInt32[] _posSlotPrices = new UInt32[1 << (Base.kNumPosSlotBits + Base.kNumLenToPosStatesBits)]; + UInt32[] _distancesPrices = new UInt32[Base.kNumFullDistances << Base.kNumLenToPosStatesBits]; + UInt32[] _alignPrices = new UInt32[Base.kAlignTableSize]; + UInt32 _alignPriceCount; + + UInt32 _distTableSize = (kDefaultDictionaryLogSize * 2); + + int _posStateBits = 2; + UInt32 _posStateMask = (4 - 1); + int _numLiteralPosStateBits = 0; + int _numLiteralContextBits = 3; + + UInt32 _dictionarySize = (1 << kDefaultDictionaryLogSize); + UInt32 _dictionarySizePrev = 0xFFFFFFFF; + UInt32 _numFastBytesPrev = 0xFFFFFFFF; + + Int64 nowPos64; + bool _finished; + System.IO.Stream _inStream; + + EMatchFinderType _matchFinderType = EMatchFinderType.BT4; + bool _writeEndMark = false; + + bool _needReleaseMFStream; + + void Create() + { + if (_matchFinder == null) + { + LZ.BinTree bt = new LZ.BinTree(); + int numHashBytes = 4; + if (_matchFinderType == EMatchFinderType.BT2) + numHashBytes = 2; + bt.SetType(numHashBytes); + _matchFinder = bt; + } + _literalEncoder.Create(_numLiteralPosStateBits, _numLiteralContextBits); + + if (_dictionarySize == _dictionarySizePrev && _numFastBytesPrev == _numFastBytes) + return; + _matchFinder.Create(_dictionarySize, kNumOpts, _numFastBytes, Base.kMatchMaxLen + 1); + _dictionarySizePrev = _dictionarySize; + _numFastBytesPrev = _numFastBytes; + } + + public Encoder() + { + for (int i = 0; i < kNumOpts; i++) + _optimum[i] = new Optimal(); + for (int i = 0; i < Base.kNumLenToPosStates; i++) + _posSlotEncoder[i] = new RangeCoder.BitTreeEncoder(Base.kNumPosSlotBits); + } + + void SetWriteEndMarkerMode(bool writeEndMarker) + { + _writeEndMark = writeEndMarker; + } + + void Init() + { + BaseInit(); + _rangeEncoder.Init(); + + uint i; + for (i = 0; i < Base.kNumStates; i++) + { + for (uint j = 0; j <= _posStateMask; j++) + { + uint complexState = (i << Base.kNumPosStatesBitsMax) + j; + _isMatch[complexState].Init(); + _isRep0Long[complexState].Init(); + } + _isRep[i].Init(); + _isRepG0[i].Init(); + _isRepG1[i].Init(); + _isRepG2[i].Init(); + } + _literalEncoder.Init(); + for (i = 0; i < Base.kNumLenToPosStates; i++) + _posSlotEncoder[i].Init(); + for (i = 0; i < Base.kNumFullDistances - Base.kEndPosModelIndex; i++) + _posEncoders[i].Init(); + + _lenEncoder.Init((UInt32)1 << _posStateBits); + _repMatchLenEncoder.Init((UInt32)1 << _posStateBits); + + _posAlignEncoder.Init(); + + _longestMatchWasFound = false; + _optimumEndIndex = 0; + _optimumCurrentIndex = 0; + _additionalOffset = 0; + } + + void ReadMatchDistances(out UInt32 lenRes, out UInt32 numDistancePairs) + { + lenRes = 0; + numDistancePairs = _matchFinder.GetMatches(_matchDistances); + if (numDistancePairs > 0) + { + lenRes = _matchDistances[numDistancePairs - 2]; + if (lenRes == _numFastBytes) + lenRes += _matchFinder.GetMatchLen((int)lenRes - 1, _matchDistances[numDistancePairs - 1], + Base.kMatchMaxLen - lenRes); + } + _additionalOffset++; + } + + + void MovePos(UInt32 num) + { + if (num > 0) + { + _matchFinder.Skip(num); + _additionalOffset += num; + } + } + + UInt32 GetRepLen1Price(Base.State state, UInt32 posState) + { + return _isRepG0[state.Index].GetPrice0() + + _isRep0Long[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0(); + } + + UInt32 GetPureRepPrice(UInt32 repIndex, Base.State state, UInt32 posState) + { + UInt32 price; + if (repIndex == 0) + { + price = _isRepG0[state.Index].GetPrice0(); + price += _isRep0Long[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1(); + } + else + { + price = _isRepG0[state.Index].GetPrice1(); + if (repIndex == 1) + price += _isRepG1[state.Index].GetPrice0(); + else + { + price += _isRepG1[state.Index].GetPrice1(); + price += _isRepG2[state.Index].GetPrice(repIndex - 2); + } + } + return price; + } + + UInt32 GetRepPrice(UInt32 repIndex, UInt32 len, Base.State state, UInt32 posState) + { + UInt32 price = _repMatchLenEncoder.GetPrice(len - Base.kMatchMinLen, posState); + return price + GetPureRepPrice(repIndex, state, posState); + } + + UInt32 GetPosLenPrice(UInt32 pos, UInt32 len, UInt32 posState) + { + UInt32 price; + UInt32 lenToPosState = Base.GetLenToPosState(len); + if (pos < Base.kNumFullDistances) + price = _distancesPrices[(lenToPosState * Base.kNumFullDistances) + pos]; + else + price = _posSlotPrices[(lenToPosState << Base.kNumPosSlotBits) + GetPosSlot2(pos)] + + _alignPrices[pos & Base.kAlignMask]; + return price + _lenEncoder.GetPrice(len - Base.kMatchMinLen, posState); + } + + UInt32 Backward(out UInt32 backRes, UInt32 cur) + { + _optimumEndIndex = cur; + UInt32 posMem = _optimum[cur].PosPrev; + UInt32 backMem = _optimum[cur].BackPrev; + do + { + if (_optimum[cur].Prev1IsChar) + { + _optimum[posMem].MakeAsChar(); + _optimum[posMem].PosPrev = posMem - 1; + if (_optimum[cur].Prev2) + { + _optimum[posMem - 1].Prev1IsChar = false; + _optimum[posMem - 1].PosPrev = _optimum[cur].PosPrev2; + _optimum[posMem - 1].BackPrev = _optimum[cur].BackPrev2; + } + } + UInt32 posPrev = posMem; + UInt32 backCur = backMem; + + backMem = _optimum[posPrev].BackPrev; + posMem = _optimum[posPrev].PosPrev; + + _optimum[posPrev].BackPrev = backCur; + _optimum[posPrev].PosPrev = cur; + cur = posPrev; + } + while (cur > 0); + backRes = _optimum[0].BackPrev; + _optimumCurrentIndex = _optimum[0].PosPrev; + return _optimumCurrentIndex; + } + + UInt32[] reps = new UInt32[Base.kNumRepDistances]; + UInt32[] repLens = new UInt32[Base.kNumRepDistances]; + + + UInt32 GetOptimum(UInt32 position, out UInt32 backRes) + { + if (_optimumEndIndex != _optimumCurrentIndex) + { + UInt32 lenRes = _optimum[_optimumCurrentIndex].PosPrev - _optimumCurrentIndex; + backRes = _optimum[_optimumCurrentIndex].BackPrev; + _optimumCurrentIndex = _optimum[_optimumCurrentIndex].PosPrev; + return lenRes; + } + _optimumCurrentIndex = _optimumEndIndex = 0; + + UInt32 lenMain, numDistancePairs; + if (!_longestMatchWasFound) + { + ReadMatchDistances(out lenMain, out numDistancePairs); + } + else + { + lenMain = _longestMatchLength; + numDistancePairs = _numDistancePairs; + _longestMatchWasFound = false; + } + + UInt32 numAvailableBytes = _matchFinder.GetNumAvailableBytes() + 1; + if (numAvailableBytes < 2) + { + backRes = 0xFFFFFFFF; + return 1; + } + if (numAvailableBytes > Base.kMatchMaxLen) + numAvailableBytes = Base.kMatchMaxLen; + + UInt32 repMaxIndex = 0; + UInt32 i; + for (i = 0; i < Base.kNumRepDistances; i++) + { + reps[i] = _repDistances[i]; + repLens[i] = _matchFinder.GetMatchLen(0 - 1, reps[i], Base.kMatchMaxLen); + if (repLens[i] > repLens[repMaxIndex]) + repMaxIndex = i; + } + if (repLens[repMaxIndex] >= _numFastBytes) + { + backRes = repMaxIndex; + UInt32 lenRes = repLens[repMaxIndex]; + MovePos(lenRes - 1); + return lenRes; + } + + if (lenMain >= _numFastBytes) + { + backRes = _matchDistances[numDistancePairs - 1] + Base.kNumRepDistances; + MovePos(lenMain - 1); + return lenMain; + } + + Byte currentByte = _matchFinder.GetIndexByte(0 - 1); + Byte matchByte = _matchFinder.GetIndexByte((Int32)(0 - _repDistances[0] - 1 - 1)); + + if (lenMain < 2 && currentByte != matchByte && repLens[repMaxIndex] < 2) + { + backRes = (UInt32)0xFFFFFFFF; + return 1; + } + + _optimum[0].State = _state; + + UInt32 posState = (position & _posStateMask); + + _optimum[1].Price = _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0() + + _literalEncoder.GetSubCoder(position, _previousByte).GetPrice(!_state.IsCharState(), matchByte, currentByte); + _optimum[1].MakeAsChar(); + + UInt32 matchPrice = _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1(); + UInt32 repMatchPrice = matchPrice + _isRep[_state.Index].GetPrice1(); + + if (matchByte == currentByte) + { + UInt32 shortRepPrice = repMatchPrice + GetRepLen1Price(_state, posState); + if (shortRepPrice < _optimum[1].Price) + { + _optimum[1].Price = shortRepPrice; + _optimum[1].MakeAsShortRep(); + } + } + + UInt32 lenEnd = ((lenMain >= repLens[repMaxIndex]) ? lenMain : repLens[repMaxIndex]); + + if(lenEnd < 2) + { + backRes = _optimum[1].BackPrev; + return 1; + } + + _optimum[1].PosPrev = 0; + + _optimum[0].Backs0 = reps[0]; + _optimum[0].Backs1 = reps[1]; + _optimum[0].Backs2 = reps[2]; + _optimum[0].Backs3 = reps[3]; + + UInt32 len = lenEnd; + do + _optimum[len--].Price = kIfinityPrice; + while (len >= 2); + + for (i = 0; i < Base.kNumRepDistances; i++) + { + UInt32 repLen = repLens[i]; + if (repLen < 2) + continue; + UInt32 price = repMatchPrice + GetPureRepPrice(i, _state, posState); + do + { + UInt32 curAndLenPrice = price + _repMatchLenEncoder.GetPrice(repLen - 2, posState); + Optimal optimum = _optimum[repLen]; + if (curAndLenPrice < optimum.Price) + { + optimum.Price = curAndLenPrice; + optimum.PosPrev = 0; + optimum.BackPrev = i; + optimum.Prev1IsChar = false; + } + } + while (--repLen >= 2); + } + + UInt32 normalMatchPrice = matchPrice + _isRep[_state.Index].GetPrice0(); + + len = ((repLens[0] >= 2) ? repLens[0] + 1 : 2); + if (len <= lenMain) + { + UInt32 offs = 0; + while (len > _matchDistances[offs]) + offs += 2; + for (; ; len++) + { + UInt32 distance = _matchDistances[offs + 1]; + UInt32 curAndLenPrice = normalMatchPrice + GetPosLenPrice(distance, len, posState); + Optimal optimum = _optimum[len]; + if (curAndLenPrice < optimum.Price) + { + optimum.Price = curAndLenPrice; + optimum.PosPrev = 0; + optimum.BackPrev = distance + Base.kNumRepDistances; + optimum.Prev1IsChar = false; + } + if (len == _matchDistances[offs]) + { + offs += 2; + if (offs == numDistancePairs) + break; + } + } + } + + UInt32 cur = 0; + + while (true) + { + cur++; + if (cur == lenEnd) + return Backward(out backRes, cur); + UInt32 newLen; + ReadMatchDistances(out newLen, out numDistancePairs); + if (newLen >= _numFastBytes) + { + _numDistancePairs = numDistancePairs; + _longestMatchLength = newLen; + _longestMatchWasFound = true; + return Backward(out backRes, cur); + } + position++; + UInt32 posPrev = _optimum[cur].PosPrev; + Base.State state; + if (_optimum[cur].Prev1IsChar) + { + posPrev--; + if (_optimum[cur].Prev2) + { + state = _optimum[_optimum[cur].PosPrev2].State; + if (_optimum[cur].BackPrev2 < Base.kNumRepDistances) + state.UpdateRep(); + else + state.UpdateMatch(); + } + else + state = _optimum[posPrev].State; + state.UpdateChar(); + } + else + state = _optimum[posPrev].State; + if (posPrev == cur - 1) + { + if (_optimum[cur].IsShortRep()) + state.UpdateShortRep(); + else + state.UpdateChar(); + } + else + { + UInt32 pos; + if (_optimum[cur].Prev1IsChar && _optimum[cur].Prev2) + { + posPrev = _optimum[cur].PosPrev2; + pos = _optimum[cur].BackPrev2; + state.UpdateRep(); + } + else + { + pos = _optimum[cur].BackPrev; + if (pos < Base.kNumRepDistances) + state.UpdateRep(); + else + state.UpdateMatch(); + } + Optimal opt = _optimum[posPrev]; + if (pos < Base.kNumRepDistances) + { + if (pos == 0) + { + reps[0] = opt.Backs0; + reps[1] = opt.Backs1; + reps[2] = opt.Backs2; + reps[3] = opt.Backs3; + } + else if (pos == 1) + { + reps[0] = opt.Backs1; + reps[1] = opt.Backs0; + reps[2] = opt.Backs2; + reps[3] = opt.Backs3; + } + else if (pos == 2) + { + reps[0] = opt.Backs2; + reps[1] = opt.Backs0; + reps[2] = opt.Backs1; + reps[3] = opt.Backs3; + } + else + { + reps[0] = opt.Backs3; + reps[1] = opt.Backs0; + reps[2] = opt.Backs1; + reps[3] = opt.Backs2; + } + } + else + { + reps[0] = (pos - Base.kNumRepDistances); + reps[1] = opt.Backs0; + reps[2] = opt.Backs1; + reps[3] = opt.Backs2; + } + } + _optimum[cur].State = state; + _optimum[cur].Backs0 = reps[0]; + _optimum[cur].Backs1 = reps[1]; + _optimum[cur].Backs2 = reps[2]; + _optimum[cur].Backs3 = reps[3]; + UInt32 curPrice = _optimum[cur].Price; + + currentByte = _matchFinder.GetIndexByte(0 - 1); + matchByte = _matchFinder.GetIndexByte((Int32)(0 - reps[0] - 1 - 1)); + + posState = (position & _posStateMask); + + UInt32 curAnd1Price = curPrice + + _isMatch[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice0() + + _literalEncoder.GetSubCoder(position, _matchFinder.GetIndexByte(0 - 2)). + GetPrice(!state.IsCharState(), matchByte, currentByte); + + Optimal nextOptimum = _optimum[cur + 1]; + + bool nextIsChar = false; + if (curAnd1Price < nextOptimum.Price) + { + nextOptimum.Price = curAnd1Price; + nextOptimum.PosPrev = cur; + nextOptimum.MakeAsChar(); + nextIsChar = true; + } + + matchPrice = curPrice + _isMatch[(state.Index << Base.kNumPosStatesBitsMax) + posState].GetPrice1(); + repMatchPrice = matchPrice + _isRep[state.Index].GetPrice1(); + + if (matchByte == currentByte && + !(nextOptimum.PosPrev < cur && nextOptimum.BackPrev == 0)) + { + UInt32 shortRepPrice = repMatchPrice + GetRepLen1Price(state, posState); + if (shortRepPrice <= nextOptimum.Price) + { + nextOptimum.Price = shortRepPrice; + nextOptimum.PosPrev = cur; + nextOptimum.MakeAsShortRep(); + nextIsChar = true; + } + } + + UInt32 numAvailableBytesFull = _matchFinder.GetNumAvailableBytes() + 1; + numAvailableBytesFull = Math.Min(kNumOpts - 1 - cur, numAvailableBytesFull); + numAvailableBytes = numAvailableBytesFull; + + if (numAvailableBytes < 2) + continue; + if (numAvailableBytes > _numFastBytes) + numAvailableBytes = _numFastBytes; + if (!nextIsChar && matchByte != currentByte) + { + // try Literal + rep0 + UInt32 t = Math.Min(numAvailableBytesFull - 1, _numFastBytes); + UInt32 lenTest2 = _matchFinder.GetMatchLen(0, reps[0], t); + if (lenTest2 >= 2) + { + Base.State state2 = state; + state2.UpdateChar(); + UInt32 posStateNext = (position + 1) & _posStateMask; + UInt32 nextRepMatchPrice = curAnd1Price + + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1() + + _isRep[state2.Index].GetPrice1(); + { + UInt32 offset = cur + 1 + lenTest2; + while (lenEnd < offset) + _optimum[++lenEnd].Price = kIfinityPrice; + UInt32 curAndLenPrice = nextRepMatchPrice + GetRepPrice( + 0, lenTest2, state2, posStateNext); + Optimal optimum = _optimum[offset]; + if (curAndLenPrice < optimum.Price) + { + optimum.Price = curAndLenPrice; + optimum.PosPrev = cur + 1; + optimum.BackPrev = 0; + optimum.Prev1IsChar = true; + optimum.Prev2 = false; + } + } + } + } + + UInt32 startLen = 2; // speed optimization + + for (UInt32 repIndex = 0; repIndex < Base.kNumRepDistances; repIndex++) + { + UInt32 lenTest = _matchFinder.GetMatchLen(0 - 1, reps[repIndex], numAvailableBytes); + if (lenTest < 2) + continue; + UInt32 lenTestTemp = lenTest; + do + { + while (lenEnd < cur + lenTest) + _optimum[++lenEnd].Price = kIfinityPrice; + UInt32 curAndLenPrice = repMatchPrice + GetRepPrice(repIndex, lenTest, state, posState); + Optimal optimum = _optimum[cur + lenTest]; + if (curAndLenPrice < optimum.Price) + { + optimum.Price = curAndLenPrice; + optimum.PosPrev = cur; + optimum.BackPrev = repIndex; + optimum.Prev1IsChar = false; + } + } + while(--lenTest >= 2); + lenTest = lenTestTemp; + + if (repIndex == 0) + startLen = lenTest + 1; + + // if (_maxMode) + if (lenTest < numAvailableBytesFull) + { + UInt32 t = Math.Min(numAvailableBytesFull - 1 - lenTest, _numFastBytes); + UInt32 lenTest2 = _matchFinder.GetMatchLen((Int32)lenTest, reps[repIndex], t); + if (lenTest2 >= 2) + { + Base.State state2 = state; + state2.UpdateRep(); + UInt32 posStateNext = (position + lenTest) & _posStateMask; + UInt32 curAndLenCharPrice = + repMatchPrice + GetRepPrice(repIndex, lenTest, state, posState) + + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice0() + + _literalEncoder.GetSubCoder(position + lenTest, + _matchFinder.GetIndexByte((Int32)lenTest - 1 - 1)).GetPrice(true, + _matchFinder.GetIndexByte((Int32)((Int32)lenTest - 1 - (Int32)(reps[repIndex] + 1))), + _matchFinder.GetIndexByte((Int32)lenTest - 1)); + state2.UpdateChar(); + posStateNext = (position + lenTest + 1) & _posStateMask; + UInt32 nextMatchPrice = curAndLenCharPrice + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1(); + UInt32 nextRepMatchPrice = nextMatchPrice + _isRep[state2.Index].GetPrice1(); + + // for(; lenTest2 >= 2; lenTest2--) + { + UInt32 offset = lenTest + 1 + lenTest2; + while(lenEnd < cur + offset) + _optimum[++lenEnd].Price = kIfinityPrice; + UInt32 curAndLenPrice = nextRepMatchPrice + GetRepPrice(0, lenTest2, state2, posStateNext); + Optimal optimum = _optimum[cur + offset]; + if (curAndLenPrice < optimum.Price) + { + optimum.Price = curAndLenPrice; + optimum.PosPrev = cur + lenTest + 1; + optimum.BackPrev = 0; + optimum.Prev1IsChar = true; + optimum.Prev2 = true; + optimum.PosPrev2 = cur; + optimum.BackPrev2 = repIndex; + } + } + } + } + } + + if (newLen > numAvailableBytes) + { + newLen = numAvailableBytes; + for (numDistancePairs = 0; newLen > _matchDistances[numDistancePairs]; numDistancePairs += 2) ; + _matchDistances[numDistancePairs] = newLen; + numDistancePairs += 2; + } + if (newLen >= startLen) + { + normalMatchPrice = matchPrice + _isRep[state.Index].GetPrice0(); + while (lenEnd < cur + newLen) + _optimum[++lenEnd].Price = kIfinityPrice; + + UInt32 offs = 0; + while (startLen > _matchDistances[offs]) + offs += 2; + + for (UInt32 lenTest = startLen; ; lenTest++) + { + UInt32 curBack = _matchDistances[offs + 1]; + UInt32 curAndLenPrice = normalMatchPrice + GetPosLenPrice(curBack, lenTest, posState); + Optimal optimum = _optimum[cur + lenTest]; + if (curAndLenPrice < optimum.Price) + { + optimum.Price = curAndLenPrice; + optimum.PosPrev = cur; + optimum.BackPrev = curBack + Base.kNumRepDistances; + optimum.Prev1IsChar = false; + } + + if (lenTest == _matchDistances[offs]) + { + if (lenTest < numAvailableBytesFull) + { + UInt32 t = Math.Min(numAvailableBytesFull - 1 - lenTest, _numFastBytes); + UInt32 lenTest2 = _matchFinder.GetMatchLen((Int32)lenTest, curBack, t); + if (lenTest2 >= 2) + { + Base.State state2 = state; + state2.UpdateMatch(); + UInt32 posStateNext = (position + lenTest) & _posStateMask; + UInt32 curAndLenCharPrice = curAndLenPrice + + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice0() + + _literalEncoder.GetSubCoder(position + lenTest, + _matchFinder.GetIndexByte((Int32)lenTest - 1 - 1)). + GetPrice(true, + _matchFinder.GetIndexByte((Int32)lenTest - (Int32)(curBack + 1) - 1), + _matchFinder.GetIndexByte((Int32)lenTest - 1)); + state2.UpdateChar(); + posStateNext = (position + lenTest + 1) & _posStateMask; + UInt32 nextMatchPrice = curAndLenCharPrice + _isMatch[(state2.Index << Base.kNumPosStatesBitsMax) + posStateNext].GetPrice1(); + UInt32 nextRepMatchPrice = nextMatchPrice + _isRep[state2.Index].GetPrice1(); + + UInt32 offset = lenTest + 1 + lenTest2; + while (lenEnd < cur + offset) + _optimum[++lenEnd].Price = kIfinityPrice; + curAndLenPrice = nextRepMatchPrice + GetRepPrice(0, lenTest2, state2, posStateNext); + optimum = _optimum[cur + offset]; + if (curAndLenPrice < optimum.Price) + { + optimum.Price = curAndLenPrice; + optimum.PosPrev = cur + lenTest + 1; + optimum.BackPrev = 0; + optimum.Prev1IsChar = true; + optimum.Prev2 = true; + optimum.PosPrev2 = cur; + optimum.BackPrev2 = curBack + Base.kNumRepDistances; + } + } + } + offs += 2; + if (offs == numDistancePairs) + break; + } + } + } + } + } + + bool ChangePair(UInt32 smallDist, UInt32 bigDist) + { + const int kDif = 7; + return (smallDist < ((UInt32)(1) << (32 - kDif)) && bigDist >= (smallDist << kDif)); + } + + void WriteEndMarker(UInt32 posState) + { + if (!_writeEndMark) + return; + + _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].Encode(_rangeEncoder, 1); + _isRep[_state.Index].Encode(_rangeEncoder, 0); + _state.UpdateMatch(); + UInt32 len = Base.kMatchMinLen; + _lenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); + UInt32 posSlot = (1 << Base.kNumPosSlotBits) - 1; + UInt32 lenToPosState = Base.GetLenToPosState(len); + _posSlotEncoder[lenToPosState].Encode(_rangeEncoder, posSlot); + int footerBits = 30; + UInt32 posReduced = (((UInt32)1) << footerBits) - 1; + _rangeEncoder.EncodeDirectBits(posReduced >> Base.kNumAlignBits, footerBits - Base.kNumAlignBits); + _posAlignEncoder.ReverseEncode(_rangeEncoder, posReduced & Base.kAlignMask); + } + + void Flush(UInt32 nowPos) + { + ReleaseMFStream(); + WriteEndMarker(nowPos & _posStateMask); + _rangeEncoder.FlushData(); + _rangeEncoder.FlushStream(); + } + + public void CodeOneBlock(out Int64 inSize, out Int64 outSize, out bool finished) + { + inSize = 0; + outSize = 0; + finished = true; + + if (_inStream != null) + { + _matchFinder.SetStream(_inStream); + _matchFinder.Init(); + _needReleaseMFStream = true; + _inStream = null; + if (_trainSize > 0) + _matchFinder.Skip(_trainSize); + } + + if (_finished) + return; + _finished = true; + + + Int64 progressPosValuePrev = nowPos64; + if (nowPos64 == 0) + { + if (_matchFinder.GetNumAvailableBytes() == 0) + { + Flush((UInt32)nowPos64); + return; + } + UInt32 len, numDistancePairs; // it's not used + ReadMatchDistances(out len, out numDistancePairs); + UInt32 posState = (UInt32)(nowPos64) & _posStateMask; + _isMatch[(_state.Index << Base.kNumPosStatesBitsMax) + posState].Encode(_rangeEncoder, 0); + _state.UpdateChar(); + Byte curByte = _matchFinder.GetIndexByte((Int32)(0 - _additionalOffset)); + _literalEncoder.GetSubCoder((UInt32)(nowPos64), _previousByte).Encode(_rangeEncoder, curByte); + _previousByte = curByte; + _additionalOffset--; + nowPos64++; + } + if (_matchFinder.GetNumAvailableBytes() == 0) + { + Flush((UInt32)nowPos64); + return; + } + while (true) + { + UInt32 pos; + UInt32 len = GetOptimum((UInt32)nowPos64, out pos); + + UInt32 posState = ((UInt32)nowPos64) & _posStateMask; + UInt32 complexState = (_state.Index << Base.kNumPosStatesBitsMax) + posState; + if (len == 1 && pos == 0xFFFFFFFF) + { + _isMatch[complexState].Encode(_rangeEncoder, 0); + Byte curByte = _matchFinder.GetIndexByte((Int32)(0 - _additionalOffset)); + LiteralEncoder.Encoder2 subCoder = _literalEncoder.GetSubCoder((UInt32)nowPos64, _previousByte); + if (!_state.IsCharState()) + { + Byte matchByte = _matchFinder.GetIndexByte((Int32)(0 - _repDistances[0] - 1 - _additionalOffset)); + subCoder.EncodeMatched(_rangeEncoder, matchByte, curByte); + } + else + subCoder.Encode(_rangeEncoder, curByte); + _previousByte = curByte; + _state.UpdateChar(); + } + else + { + _isMatch[complexState].Encode(_rangeEncoder, 1); + if (pos < Base.kNumRepDistances) + { + _isRep[_state.Index].Encode(_rangeEncoder, 1); + if (pos == 0) + { + _isRepG0[_state.Index].Encode(_rangeEncoder, 0); + if (len == 1) + _isRep0Long[complexState].Encode(_rangeEncoder, 0); + else + _isRep0Long[complexState].Encode(_rangeEncoder, 1); + } + else + { + _isRepG0[_state.Index].Encode(_rangeEncoder, 1); + if (pos == 1) + _isRepG1[_state.Index].Encode(_rangeEncoder, 0); + else + { + _isRepG1[_state.Index].Encode(_rangeEncoder, 1); + _isRepG2[_state.Index].Encode(_rangeEncoder, pos - 2); + } + } + if (len == 1) + _state.UpdateShortRep(); + else + { + _repMatchLenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); + _state.UpdateRep(); + } + UInt32 distance = _repDistances[pos]; + if (pos != 0) + { + for (UInt32 i = pos; i >= 1; i--) + _repDistances[i] = _repDistances[i - 1]; + _repDistances[0] = distance; + } + } + else + { + _isRep[_state.Index].Encode(_rangeEncoder, 0); + _state.UpdateMatch(); + _lenEncoder.Encode(_rangeEncoder, len - Base.kMatchMinLen, posState); + pos -= Base.kNumRepDistances; + UInt32 posSlot = GetPosSlot(pos); + UInt32 lenToPosState = Base.GetLenToPosState(len); + _posSlotEncoder[lenToPosState].Encode(_rangeEncoder, posSlot); + + if (posSlot >= Base.kStartPosModelIndex) + { + int footerBits = (int)((posSlot >> 1) - 1); + UInt32 baseVal = ((2 | (posSlot & 1)) << footerBits); + UInt32 posReduced = pos - baseVal; + + if (posSlot < Base.kEndPosModelIndex) + RangeCoder.BitTreeEncoder.ReverseEncode(_posEncoders, + baseVal - posSlot - 1, _rangeEncoder, footerBits, posReduced); + else + { + _rangeEncoder.EncodeDirectBits(posReduced >> Base.kNumAlignBits, footerBits - Base.kNumAlignBits); + _posAlignEncoder.ReverseEncode(_rangeEncoder, posReduced & Base.kAlignMask); + _alignPriceCount++; + } + } + UInt32 distance = pos; + for (UInt32 i = Base.kNumRepDistances - 1; i >= 1; i--) + _repDistances[i] = _repDistances[i - 1]; + _repDistances[0] = distance; + _matchPriceCount++; + } + _previousByte = _matchFinder.GetIndexByte((Int32)(len - 1 - _additionalOffset)); + } + _additionalOffset -= len; + nowPos64 += len; + if (_additionalOffset == 0) + { + // if (!_fastMode) + if (_matchPriceCount >= (1 << 7)) + FillDistancesPrices(); + if (_alignPriceCount >= Base.kAlignTableSize) + FillAlignPrices(); + inSize = nowPos64; + outSize = _rangeEncoder.GetProcessedSizeAdd(); + if (_matchFinder.GetNumAvailableBytes() == 0) + { + Flush((UInt32)nowPos64); + return; + } + + if (nowPos64 - progressPosValuePrev >= (1 << 12)) + { + _finished = false; + finished = false; + return; + } + } + } + } + + void ReleaseMFStream() + { + if (_matchFinder != null && _needReleaseMFStream) + { + _matchFinder.ReleaseStream(); + _needReleaseMFStream = false; + } + } + + void SetOutStream(System.IO.Stream outStream) { _rangeEncoder.SetStream(outStream); } + void ReleaseOutStream() { _rangeEncoder.ReleaseStream(); } + + void ReleaseStreams() + { + ReleaseMFStream(); + ReleaseOutStream(); + } + + void SetStreams(System.IO.Stream inStream, System.IO.Stream outStream, + Int64 inSize, Int64 outSize) + { + _inStream = inStream; + _finished = false; + Create(); + SetOutStream(outStream); + Init(); + + // if (!_fastMode) + { + FillDistancesPrices(); + FillAlignPrices(); + } + + _lenEncoder.SetTableSize(_numFastBytes + 1 - Base.kMatchMinLen); + _lenEncoder.UpdateTables((UInt32)1 << _posStateBits); + _repMatchLenEncoder.SetTableSize(_numFastBytes + 1 - Base.kMatchMinLen); + _repMatchLenEncoder.UpdateTables((UInt32)1 << _posStateBits); + + nowPos64 = 0; + } + + + public void Code(System.IO.Stream inStream, System.IO.Stream outStream, + Int64 inSize, Int64 outSize, ICodeProgress progress) + { + _needReleaseMFStream = false; + try + { + SetStreams(inStream, outStream, inSize, outSize); + while (true) + { + Int64 processedInSize; + Int64 processedOutSize; + bool finished; + CodeOneBlock(out processedInSize, out processedOutSize, out finished); + if (finished) + return; + if (progress != null) + { + progress.SetProgress(processedInSize, processedOutSize); + } + } + } + finally + { + ReleaseStreams(); + } + } + + const int kPropSize = 5; + Byte[] properties = new Byte[kPropSize]; + + public void WriteCoderProperties(System.IO.Stream outStream) + { + properties[0] = (Byte)((_posStateBits * 5 + _numLiteralPosStateBits) * 9 + _numLiteralContextBits); + for (int i = 0; i < 4; i++) + properties[1 + i] = (Byte)((_dictionarySize >> (8 * i)) & 0xFF); + outStream.Write(properties, 0, kPropSize); + } + + UInt32[] tempPrices = new UInt32[Base.kNumFullDistances]; + UInt32 _matchPriceCount; + + void FillDistancesPrices() + { + for (UInt32 i = Base.kStartPosModelIndex; i < Base.kNumFullDistances; i++) + { + UInt32 posSlot = GetPosSlot(i); + int footerBits = (int)((posSlot >> 1) - 1); + UInt32 baseVal = ((2 | (posSlot & 1)) << footerBits); + tempPrices[i] = BitTreeEncoder.ReverseGetPrice(_posEncoders, + baseVal - posSlot - 1, footerBits, i - baseVal); + } + + for (UInt32 lenToPosState = 0; lenToPosState < Base.kNumLenToPosStates; lenToPosState++) + { + UInt32 posSlot; + RangeCoder.BitTreeEncoder encoder = _posSlotEncoder[lenToPosState]; + + UInt32 st = (lenToPosState << Base.kNumPosSlotBits); + for (posSlot = 0; posSlot < _distTableSize; posSlot++) + _posSlotPrices[st + posSlot] = encoder.GetPrice(posSlot); + for (posSlot = Base.kEndPosModelIndex; posSlot < _distTableSize; posSlot++) + _posSlotPrices[st + posSlot] += ((((posSlot >> 1) - 1) - Base.kNumAlignBits) << RangeCoder.BitEncoder.kNumBitPriceShiftBits); + + UInt32 st2 = lenToPosState * Base.kNumFullDistances; + UInt32 i; + for (i = 0; i < Base.kStartPosModelIndex; i++) + _distancesPrices[st2 + i] = _posSlotPrices[st + i]; + for (; i < Base.kNumFullDistances; i++) + _distancesPrices[st2 + i] = _posSlotPrices[st + GetPosSlot(i)] + tempPrices[i]; + } + _matchPriceCount = 0; + } + + void FillAlignPrices() + { + for (UInt32 i = 0; i < Base.kAlignTableSize; i++) + _alignPrices[i] = _posAlignEncoder.ReverseGetPrice(i); + _alignPriceCount = 0; + } + + + static string[] kMatchFinderIDs = + { + "BT2", + "BT4", + }; + + static int FindMatchFinder(string s) + { + for (int m = 0; m < kMatchFinderIDs.Length; m++) + if (s == kMatchFinderIDs[m]) + return m; + return -1; + } + + public void SetCoderProperties(CoderPropID[] propIDs, object[] properties) + { + for (UInt32 i = 0; i < properties.Length; i++) + { + object prop = properties[i]; + switch (propIDs[i]) + { + case CoderPropID.NumFastBytes: + { + if (!(prop is Int32)) + throw new InvalidParamException(); + Int32 numFastBytes = (Int32)prop; + if (numFastBytes < 5 || numFastBytes > Base.kMatchMaxLen) + throw new InvalidParamException(); + _numFastBytes = (UInt32)numFastBytes; + break; + } + case CoderPropID.Algorithm: + { + /* + if (!(prop is Int32)) + throw new InvalidParamException(); + Int32 maximize = (Int32)prop; + _fastMode = (maximize == 0); + _maxMode = (maximize >= 2); + */ + break; + } + case CoderPropID.MatchFinder: + { + if (!(prop is String)) + throw new InvalidParamException(); + EMatchFinderType matchFinderIndexPrev = _matchFinderType; + int m = FindMatchFinder(((string)prop).ToUpper()); + if (m < 0) + throw new InvalidParamException(); + _matchFinderType = (EMatchFinderType)m; + if (_matchFinder != null && matchFinderIndexPrev != _matchFinderType) + { + _dictionarySizePrev = 0xFFFFFFFF; + _matchFinder = null; + } + break; + } + case CoderPropID.DictionarySize: + { + const int kDicLogSizeMaxCompress = 30; + if (!(prop is Int32)) + throw new InvalidParamException(); ; + Int32 dictionarySize = (Int32)prop; + if (dictionarySize < (UInt32)(1 << Base.kDicLogSizeMin) || + dictionarySize > (UInt32)(1 << kDicLogSizeMaxCompress)) + throw new InvalidParamException(); + _dictionarySize = (UInt32)dictionarySize; + int dicLogSize; + for (dicLogSize = 0; dicLogSize < (UInt32)kDicLogSizeMaxCompress; dicLogSize++) + if (dictionarySize <= ((UInt32)(1) << dicLogSize)) + break; + _distTableSize = (UInt32)dicLogSize * 2; + break; + } + case CoderPropID.PosStateBits: + { + if (!(prop is Int32)) + throw new InvalidParamException(); + Int32 v = (Int32)prop; + if (v < 0 || v > (UInt32)Base.kNumPosStatesBitsEncodingMax) + throw new InvalidParamException(); + _posStateBits = (int)v; + _posStateMask = (((UInt32)1) << (int)_posStateBits) - 1; + break; + } + case CoderPropID.LitPosBits: + { + if (!(prop is Int32)) + throw new InvalidParamException(); + Int32 v = (Int32)prop; + if (v < 0 || v > (UInt32)Base.kNumLitPosStatesBitsEncodingMax) + throw new InvalidParamException(); + _numLiteralPosStateBits = (int)v; + break; + } + case CoderPropID.LitContextBits: + { + if (!(prop is Int32)) + throw new InvalidParamException(); + Int32 v = (Int32)prop; + if (v < 0 || v > (UInt32)Base.kNumLitContextBitsMax) + throw new InvalidParamException(); ; + _numLiteralContextBits = (int)v; + break; + } + case CoderPropID.EndMarker: + { + if (!(prop is Boolean)) + throw new InvalidParamException(); + SetWriteEndMarkerMode((Boolean)prop); + break; + } + default: + throw new InvalidParamException(); + } + } + } + + uint _trainSize = 0; + public void SetTrainSize(uint trainSize) + { + _trainSize = trainSize; + } + + } +} diff --git a/7zip/Compress/RangeCoder/RangeCoder.cs b/7zip/Compress/RangeCoder/RangeCoder.cs new file mode 100644 index 0000000..4ced247 --- /dev/null +++ b/7zip/Compress/RangeCoder/RangeCoder.cs @@ -0,0 +1,234 @@ +using System; + +namespace SevenZip.Compression.RangeCoder +{ + class Encoder + { + public const uint kTopValue = (1 << 24); + + System.IO.Stream Stream; + + public UInt64 Low; + public uint Range; + uint _cacheSize; + byte _cache; + + long StartPosition; + + public void SetStream(System.IO.Stream stream) + { + Stream = stream; + } + + public void ReleaseStream() + { + Stream = null; + } + + public void Init() + { + StartPosition = Stream.Position; + + Low = 0; + Range = 0xFFFFFFFF; + _cacheSize = 1; + _cache = 0; + } + + public void FlushData() + { + for (int i = 0; i < 5; i++) + ShiftLow(); + } + + public void FlushStream() + { + Stream.Flush(); + } + + public void CloseStream() + { + Stream.Close(); + } + + public void Encode(uint start, uint size, uint total) + { + Low += start * (Range /= total); + Range *= size; + while (Range < kTopValue) + { + Range <<= 8; + ShiftLow(); + } + } + + public void ShiftLow() + { + if ((uint)Low < (uint)0xFF000000 || (uint)(Low >> 32) == 1) + { + byte temp = _cache; + do + { + Stream.WriteByte((byte)(temp + (Low >> 32))); + temp = 0xFF; + } + while (--_cacheSize != 0); + _cache = (byte)(((uint)Low) >> 24); + } + _cacheSize++; + Low = ((uint)Low) << 8; + } + + public void EncodeDirectBits(uint v, int numTotalBits) + { + for (int i = numTotalBits - 1; i >= 0; i--) + { + Range >>= 1; + if (((v >> i) & 1) == 1) + Low += Range; + if (Range < kTopValue) + { + Range <<= 8; + ShiftLow(); + } + } + } + + public void EncodeBit(uint size0, int numTotalBits, uint symbol) + { + uint newBound = (Range >> numTotalBits) * size0; + if (symbol == 0) + Range = newBound; + else + { + Low += newBound; + Range -= newBound; + } + while (Range < kTopValue) + { + Range <<= 8; + ShiftLow(); + } + } + + public long GetProcessedSizeAdd() + { + return _cacheSize + + Stream.Position - StartPosition + 4; + // (long)Stream.GetProcessedSize(); + } + } + + class Decoder + { + public const uint kTopValue = (1 << 24); + public uint Range; + public uint Code; + // public Buffer.InBuffer Stream = new Buffer.InBuffer(1 << 16); + public System.IO.Stream Stream; + + public void Init(System.IO.Stream stream) + { + // Stream.Init(stream); + Stream = stream; + + Code = 0; + Range = 0xFFFFFFFF; + for (int i = 0; i < 5; i++) + Code = (Code << 8) | (byte)Stream.ReadByte(); + } + + public void ReleaseStream() + { + // Stream.ReleaseStream(); + Stream = null; + } + + public void CloseStream() + { + Stream.Close(); + } + + public void Normalize() + { + while (Range < kTopValue) + { + Code = (Code << 8) | (byte)Stream.ReadByte(); + Range <<= 8; + } + } + + public void Normalize2() + { + if (Range < kTopValue) + { + Code = (Code << 8) | (byte)Stream.ReadByte(); + Range <<= 8; + } + } + + public uint GetThreshold(uint total) + { + return Code / (Range /= total); + } + + public void Decode(uint start, uint size, uint total) + { + Code -= start * Range; + Range *= size; + Normalize(); + } + + public uint DecodeDirectBits(int numTotalBits) + { + uint range = Range; + uint code = Code; + uint result = 0; + for (int i = numTotalBits; i > 0; i--) + { + range >>= 1; + /* + result <<= 1; + if (code >= range) + { + code -= range; + result |= 1; + } + */ + uint t = (code - range) >> 31; + code -= range & (t - 1); + result = (result << 1) | (1 - t); + + if (range < kTopValue) + { + code = (code << 8) | (byte)Stream.ReadByte(); + range <<= 8; + } + } + Range = range; + Code = code; + return result; + } + + public uint DecodeBit(uint size0, int numTotalBits) + { + uint newBound = (Range >> numTotalBits) * size0; + uint symbol; + if (Code < newBound) + { + symbol = 0; + Range = newBound; + } + else + { + symbol = 1; + Code -= newBound; + Range -= newBound; + } + Normalize(); + return symbol; + } + + // ulong GetProcessedSize() {return Stream.GetProcessedSize(); } + } +} diff --git a/7zip/Compress/RangeCoder/RangeCoderBit.cs b/7zip/Compress/RangeCoder/RangeCoderBit.cs new file mode 100644 index 0000000..000a5a0 --- /dev/null +++ b/7zip/Compress/RangeCoder/RangeCoderBit.cs @@ -0,0 +1,117 @@ +using System; + +namespace SevenZip.Compression.RangeCoder +{ + struct BitEncoder + { + public const int kNumBitModelTotalBits = 11; + public const uint kBitModelTotal = (1 << kNumBitModelTotalBits); + const int kNumMoveBits = 5; + const int kNumMoveReducingBits = 2; + public const int kNumBitPriceShiftBits = 6; + + uint Prob; + + public void Init() { Prob = kBitModelTotal >> 1; } + + public void UpdateModel(uint symbol) + { + if (symbol == 0) + Prob += (kBitModelTotal - Prob) >> kNumMoveBits; + else + Prob -= (Prob) >> kNumMoveBits; + } + + public void Encode(Encoder encoder, uint symbol) + { + // encoder.EncodeBit(Prob, kNumBitModelTotalBits, symbol); + // UpdateModel(symbol); + uint newBound = (encoder.Range >> kNumBitModelTotalBits) * Prob; + if (symbol == 0) + { + encoder.Range = newBound; + Prob += (kBitModelTotal - Prob) >> kNumMoveBits; + } + else + { + encoder.Low += newBound; + encoder.Range -= newBound; + Prob -= (Prob) >> kNumMoveBits; + } + if (encoder.Range < Encoder.kTopValue) + { + encoder.Range <<= 8; + encoder.ShiftLow(); + } + } + + private static UInt32[] ProbPrices = new UInt32[kBitModelTotal >> kNumMoveReducingBits]; + + static BitEncoder() + { + const int kNumBits = (kNumBitModelTotalBits - kNumMoveReducingBits); + for (int i = kNumBits - 1; i >= 0; i--) + { + UInt32 start = (UInt32)1 << (kNumBits - i - 1); + UInt32 end = (UInt32)1 << (kNumBits - i); + for (UInt32 j = start; j < end; j++) + ProbPrices[j] = ((UInt32)i << kNumBitPriceShiftBits) + + (((end - j) << kNumBitPriceShiftBits) >> (kNumBits - i - 1)); + } + } + + public uint GetPrice(uint symbol) + { + return ProbPrices[(((Prob - symbol) ^ ((-(int)symbol))) & (kBitModelTotal - 1)) >> kNumMoveReducingBits]; + } + public uint GetPrice0() { return ProbPrices[Prob >> kNumMoveReducingBits]; } + public uint GetPrice1() { return ProbPrices[(kBitModelTotal - Prob) >> kNumMoveReducingBits]; } + } + + struct BitDecoder + { + public const int kNumBitModelTotalBits = 11; + public const uint kBitModelTotal = (1 << kNumBitModelTotalBits); + const int kNumMoveBits = 5; + + uint Prob; + + public void UpdateModel(int numMoveBits, uint symbol) + { + if (symbol == 0) + Prob += (kBitModelTotal - Prob) >> numMoveBits; + else + Prob -= (Prob) >> numMoveBits; + } + + public void Init() { Prob = kBitModelTotal >> 1; } + + public uint Decode(RangeCoder.Decoder rangeDecoder) + { + uint newBound = (uint)(rangeDecoder.Range >> kNumBitModelTotalBits) * (uint)Prob; + if (rangeDecoder.Code < newBound) + { + rangeDecoder.Range = newBound; + Prob += (kBitModelTotal - Prob) >> kNumMoveBits; + if (rangeDecoder.Range < Decoder.kTopValue) + { + rangeDecoder.Code = (rangeDecoder.Code << 8) | (byte)rangeDecoder.Stream.ReadByte(); + rangeDecoder.Range <<= 8; + } + return 0; + } + else + { + rangeDecoder.Range -= newBound; + rangeDecoder.Code -= newBound; + Prob -= (Prob) >> kNumMoveBits; + if (rangeDecoder.Range < Decoder.kTopValue) + { + rangeDecoder.Code = (rangeDecoder.Code << 8) | (byte)rangeDecoder.Stream.ReadByte(); + rangeDecoder.Range <<= 8; + } + return 1; + } + } + } +} diff --git a/7zip/Compress/RangeCoder/RangeCoderBitTree.cs b/7zip/Compress/RangeCoder/RangeCoderBitTree.cs new file mode 100644 index 0000000..3309c14 --- /dev/null +++ b/7zip/Compress/RangeCoder/RangeCoderBitTree.cs @@ -0,0 +1,157 @@ +using System; + +namespace SevenZip.Compression.RangeCoder +{ + struct BitTreeEncoder + { + BitEncoder[] Models; + int NumBitLevels; + + public BitTreeEncoder(int numBitLevels) + { + NumBitLevels = numBitLevels; + Models = new BitEncoder[1 << numBitLevels]; + } + + public void Init() + { + for (uint i = 1; i < (1 << NumBitLevels); i++) + Models[i].Init(); + } + + public void Encode(Encoder rangeEncoder, UInt32 symbol) + { + UInt32 m = 1; + for (int bitIndex = NumBitLevels; bitIndex > 0; ) + { + bitIndex--; + UInt32 bit = (symbol >> bitIndex) & 1; + Models[m].Encode(rangeEncoder, bit); + m = (m << 1) | bit; + } + } + + public void ReverseEncode(Encoder rangeEncoder, UInt32 symbol) + { + UInt32 m = 1; + for (UInt32 i = 0; i < NumBitLevels; i++) + { + UInt32 bit = symbol & 1; + Models[m].Encode(rangeEncoder, bit); + m = (m << 1) | bit; + symbol >>= 1; + } + } + + public UInt32 GetPrice(UInt32 symbol) + { + UInt32 price = 0; + UInt32 m = 1; + for (int bitIndex = NumBitLevels; bitIndex > 0; ) + { + bitIndex--; + UInt32 bit = (symbol >> bitIndex) & 1; + price += Models[m].GetPrice(bit); + m = (m << 1) + bit; + } + return price; + } + + public UInt32 ReverseGetPrice(UInt32 symbol) + { + UInt32 price = 0; + UInt32 m = 1; + for (int i = NumBitLevels; i > 0; i--) + { + UInt32 bit = symbol & 1; + symbol >>= 1; + price += Models[m].GetPrice(bit); + m = (m << 1) | bit; + } + return price; + } + + public static UInt32 ReverseGetPrice(BitEncoder[] Models, UInt32 startIndex, + int NumBitLevels, UInt32 symbol) + { + UInt32 price = 0; + UInt32 m = 1; + for (int i = NumBitLevels; i > 0; i--) + { + UInt32 bit = symbol & 1; + symbol >>= 1; + price += Models[startIndex + m].GetPrice(bit); + m = (m << 1) | bit; + } + return price; + } + + public static void ReverseEncode(BitEncoder[] Models, UInt32 startIndex, + Encoder rangeEncoder, int NumBitLevels, UInt32 symbol) + { + UInt32 m = 1; + for (int i = 0; i < NumBitLevels; i++) + { + UInt32 bit = symbol & 1; + Models[startIndex + m].Encode(rangeEncoder, bit); + m = (m << 1) | bit; + symbol >>= 1; + } + } + } + + struct BitTreeDecoder + { + BitDecoder[] Models; + int NumBitLevels; + + public BitTreeDecoder(int numBitLevels) + { + NumBitLevels = numBitLevels; + Models = new BitDecoder[1 << numBitLevels]; + } + + public void Init() + { + for (uint i = 1; i < (1 << NumBitLevels); i++) + Models[i].Init(); + } + + public uint Decode(RangeCoder.Decoder rangeDecoder) + { + uint m = 1; + for (int bitIndex = NumBitLevels; bitIndex > 0; bitIndex--) + m = (m << 1) + Models[m].Decode(rangeDecoder); + return m - ((uint)1 << NumBitLevels); + } + + public uint ReverseDecode(RangeCoder.Decoder rangeDecoder) + { + uint m = 1; + uint symbol = 0; + for (int bitIndex = 0; bitIndex < NumBitLevels; bitIndex++) + { + uint bit = Models[m].Decode(rangeDecoder); + m <<= 1; + m += bit; + symbol |= (bit << bitIndex); + } + return symbol; + } + + public static uint ReverseDecode(BitDecoder[] Models, UInt32 startIndex, + RangeCoder.Decoder rangeDecoder, int NumBitLevels) + { + uint m = 1; + uint symbol = 0; + for (int bitIndex = 0; bitIndex < NumBitLevels; bitIndex++) + { + uint bit = Models[startIndex + m].Decode(rangeDecoder); + m <<= 1; + m += bit; + symbol |= (bit << bitIndex); + } + return symbol; + } + } +} diff --git a/7zip/ICoder.cs b/7zip/ICoder.cs new file mode 100644 index 0000000..875cb27 --- /dev/null +++ b/7zip/ICoder.cs @@ -0,0 +1,157 @@ +// ICoder.h + +using System; + +namespace SevenZip +{ + /// + /// The exception that is thrown when an error in input stream occurs during decoding. + /// + class DataErrorException : ApplicationException + { + public DataErrorException(): base("Data Error") { } + } + + /// + /// The exception that is thrown when the value of an argument is outside the allowable range. + /// + class InvalidParamException : ApplicationException + { + public InvalidParamException(): base("Invalid Parameter") { } + } + + public interface ICodeProgress + { + /// + /// Callback progress. + /// + /// + /// input size. -1 if unknown. + /// + /// + /// output size. -1 if unknown. + /// + void SetProgress(Int64 inSize, Int64 outSize); + }; + + public interface ICoder + { + /// + /// Codes streams. + /// + /// + /// input Stream. + /// + /// + /// output Stream. + /// + /// + /// input Size. -1 if unknown. + /// + /// + /// output Size. -1 if unknown. + /// + /// + /// callback progress reference. + /// + /// + /// if input stream is not valid + /// + void Code(System.IO.Stream inStream, System.IO.Stream outStream, + Int64 inSize, Int64 outSize, ICodeProgress progress); + }; + + /* + public interface ICoder2 + { + void Code(ISequentialInStream []inStreams, + const UInt64 []inSizes, + ISequentialOutStream []outStreams, + UInt64 []outSizes, + ICodeProgress progress); + }; + */ + + /// + /// Provides the fields that represent properties idenitifiers for compressing. + /// + public enum CoderPropID + { + /// + /// Specifies default property. + /// + DefaultProp = 0, + /// + /// Specifies size of dictionary. + /// + DictionarySize, + /// + /// Specifies size of memory for PPM*. + /// + UsedMemorySize, + /// + /// Specifies order for PPM methods. + /// + Order, + /// + /// Specifies Block Size. + /// + BlockSize, + /// + /// Specifies number of postion state bits for LZMA (0 <= x <= 4). + /// + PosStateBits, + /// + /// Specifies number of literal context bits for LZMA (0 <= x <= 8). + /// + LitContextBits, + /// + /// Specifies number of literal position bits for LZMA (0 <= x <= 4). + /// + LitPosBits, + /// + /// Specifies number of fast bytes for LZ*. + /// + NumFastBytes, + /// + /// Specifies match finder. LZMA: "BT2", "BT4" or "BT4B". + /// + MatchFinder, + /// + /// Specifies the number of match finder cyckes. + /// + MatchFinderCycles, + /// + /// Specifies number of passes. + /// + NumPasses, + /// + /// Specifies number of algorithm. + /// + Algorithm, + /// + /// Specifies the number of threads. + /// + NumThreads, + /// + /// Specifies mode with end marker. + /// + EndMarker + }; + + + public interface ISetCoderProperties + { + void SetCoderProperties(CoderPropID[] propIDs, object[] properties); + }; + + public interface IWriteCoderProperties + { + void WriteCoderProperties(System.IO.Stream outStream); + } + + public interface ISetDecoderProperties + { + void SetDecoderProperties(byte[] properties); + } +} diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..f496d81 --- /dev/null +++ b/App.xaml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..b4de8e1 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Windows; +using System.IO; +using System.Threading; + +namespace WPinternals +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + internal static Action NavigateToGettingStarted; + internal static Action NavigateToUnlockBoot; + internal static PatchEngine PatchEngine; + internal static WPinternalsConfig Config; + internal static Mutex mutex = new Mutex(false, "Global\\WPinternalsRunning"); + internal static DownloadsViewModel DownloadManager; + internal static bool InterruptBoot = false; + internal static bool IsPnPEventLogMissing = true; + + public App() + : base() + { + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + + if (Environment.GetCommandLineArgs().Count() > 1) + CommandLine.OpenConsole(); + + if (!mutex.WaitOne(0, false)) + { + if (Environment.GetCommandLineArgs().Count() > 1) + { + Console.WriteLine("Windows Phone Internals is already running"); + CommandLine.CloseConsole(); + } + else + MessageBox.Show("Windows Phone Internals is already running.", "Windows Phone Internals", MessageBoxButton.OK, MessageBoxImage.Exclamation); + Environment.Exit(0); + } + + Registration.CheckExpiration(); + + string PatchDefintionsXml; + string PatchDefintionsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PatchDefintions.xml"); + if (File.Exists(PatchDefintionsPath)) + { + PatchDefintionsXml = File.ReadAllText(PatchDefintionsPath); + } + else + { + using (Stream stream = System.Reflection.Assembly.GetEntryAssembly().GetManifestResourceStream("WPinternals.PatchDefinitions.xml")) + { + using (StreamReader sr = new StreamReader(stream)) + { + PatchDefintionsXml = sr.ReadToEnd(); + } + } + } + PatchEngine = new PatchEngine(PatchDefintionsXml); + + Config = WPinternalsConfig.ReadConfig(); + } + + void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject is Exception) + { + LogFile.LogException(e.ExceptionObject as Exception); + } + } + } +} diff --git a/CommandLine.cs b/CommandLine.cs new file mode 100644 index 0000000..7dd5bb8 --- /dev/null +++ b/CommandLine.cs @@ -0,0 +1,1744 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace WPinternals +{ + internal static class CommandLine + { + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool AllocConsole(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool AttachConsole(int dwProcessId); + + [DllImport("kernel32.dll")] + static extern IntPtr GetConsoleWindow(); + + [DllImport("user32.dll")] + static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + private const UInt32 StdOutputHandle = 0xFFFFFFF5; + + [DllImport("kernel32.dll")] + private static extern IntPtr GetStdHandle(UInt32 nStdHandle); + + [DllImport("kernel32.dll")] + private static extern void SetStdHandle(UInt32 nStdHandle, IntPtr handle); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess, uint dwShareMode, uint lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, uint hTemplateFile); + + [DllImport("user32.dll")] + private static extern IntPtr GetForegroundWindow(); + + const int SW_HIDE = 0; + const int SW_SHOW = 5; + + private const int MY_CODE_PAGE = 437; + private const uint GENERIC_WRITE = 0x40000000; + private const uint FILE_SHARE_WRITE = 0x2; + private const uint OPEN_EXISTING = 0x3; + + internal static bool IsConsoleVisible = false; + internal static bool IsNewConsoleCreated = false; + + private static IntPtr hConsoleWnd; + + [DllImport("User32.Dll", EntryPoint = "PostMessageA")] + private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam); + + private const int VK_RETURN = 0x0D; + private const int WM_KEYDOWN = 0x100; + + /// + /// When the main window should not be shown, this function should call ExitProcess and it should not return. + /// + internal static async Task ParseCommandLine(System.Threading.SynchronizationContext UIContext) + { + FFU FFU = null; + PhoneNotifierViewModel Notifier; + NokiaFlashModel FlashModel; + NokiaPhoneModel NormalModel; + PhoneInfo Info; + string ProductType; + string ProductCode; + string OperatorCode; + string DownloadFolder; + string FFUFilePath; + string URL; + string[] URLs; + Uri URI; + string FFUFileName; + string EmergencyFileName; + string EmergencyFilePath; + string ProgrammerPath = ""; + string PayloadPath = ""; + string EfiEspImagePath = null; + string MainOsImagePath = null; + DiscUtils.Fat.FatFileSystem UnlockedEFIESPFileSystem; + DiscUtils.Ntfs.NtfsFileSystem UnlockedMainOsFileSystem; + bool PatchResult; + + try + { + string[] args = Environment.GetCommandLineArgs(); + + if (args.Length == 1) + return; + + switch (args[1].ToLower().TrimStart(new char[] { '-', '/' })) + { +#if DEBUG + case "test": + LogFile.BeginAction("Test"); + await TestCode.Test(UIContext); + LogFile.EndAction("Test"); + break; +#endif + case "flashpartition": + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -FlashPartition "); + if (args.Length >= 5) + await LumiaV2UnlockBootViewModel.LumiaV2FlashPartition(UIContext, args[4], args[2], args[3]); + else + await LumiaV2UnlockBootViewModel.LumiaV2FlashPartition(UIContext, null, args[2], args[3]); + break; + case "flashraw": + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -FlashRaw "); + UInt64 StartSector = 0; + try + { + if (args[2].StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + StartSector = Convert.ToUInt64(args[2].Substring(2), 16); + else + StartSector = Convert.ToUInt64(args[2], 10); + } + catch + { + LogFile.Log("Bad start sector", LogType.ConsoleOnly); + break; + } + if (args.Length >= 5) + await LumiaV2UnlockBootViewModel.LumiaV2FlashRaw(UIContext, StartSector, args[3], args[4]); + else + await LumiaV2UnlockBootViewModel.LumiaV2FlashRaw(UIContext, StartSector, args[3], null); + break; + case "flashpartitionimmediately": + if (args.Length < 5) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -FlashPartition "); + await LumiaV2UnlockBootViewModel.LumiaV2FlashPartition(UIContext, args[4], args[2], args[3], false); + break; + case "readgpt": + LogFile.BeginAction("ReadGPT"); + try + { + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + + FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Bootloader)); // This also works for Bootloader Spec A + + GPT GPT = FlashModel.ReadGPT(); // May throw NotSupportedException + foreach (Partition Partition in GPT.Partitions) + LogFile.Log(Partition.Name.PadRight(20) + "0x" + Partition.FirstSector.ToString("X8") + " - 0x" + Partition.LastSector.ToString("X8") + " " + Partition.Volume, LogType.ConsoleOnly); + + if (FlashModel.ReadPhoneInfo(false).FlashAppProtocolVersionMajor >= 2) + FlashModel.SwitchToFlashAppContext(); + else + await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash); + + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("ReadGPT"); + } + break; + case "backupgpt": + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -BackupGPT "); + LogFile.BeginAction("BackupGPT"); + try + { + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + GPT GPT = FlashModel.ReadGPT(); // May throw NotSupportedException + System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(args[2])); + GPT.WritePartitions(args[2]); + FlashModel.SwitchToFlashAppContext(); + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("BackupGPT"); + } + break; + case "restoregpt": + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -RestoreGPT "); + LogFile.BeginAction("RestoreGPT"); + try + { + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + byte[] GptChunk = LumiaV2UnlockBootViewModel.GetGptChunk(FlashModel, 0x20000); + GPT GPT = new GPT(GptChunk); + string Xml = File.ReadAllText(args[2]); + GPT.MergePartitions(Xml, false); + GPT.Rebuild(); + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(Notifier, null, false, false, 0, GptChunk, true, true); + FlashModel.SwitchToFlashAppContext(); + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("RestoreGPT"); + } + break; + case "mergegpt": + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -MergeGPT Or use: WPinternals.exe -MergeGPT "); + LogFile.BeginAction("MergeGPT"); + try + { + GPT GPT = GPT.ReadPartitions(args[2]); + + ZipArchive Archive = null; + FileStream s = null; + try + { + s = new FileStream(args[3], FileMode.Open, FileAccess.Read); + Archive = new ZipArchive(s); + } + catch { } + + if (Archive == null) + { + if (s != null) + s.Close(); + + // Assume Xml-file + GPT.MergePartitionsFromFile(args[3], true); + } + else + { + ZipArchiveEntry PartitionEntry = Archive.GetEntry("Partitions.xml"); + if (PartitionEntry == null) + GPT.MergePartitions(null, true, Archive); + else + { + using (Stream ZipStream = PartitionEntry.Open()) + { + using (StreamReader ZipReader = new StreamReader(ZipStream)) + { + string PartitionXml = ZipReader.ReadToEnd(); + GPT.MergePartitions(PartitionXml, true, Archive); + } + } + } + } + + if (Archive != null) + Archive.Dispose(); + + if (args.Count() >= 5) + GPT.WritePartitions(args[4]); + + foreach (Partition Partition in GPT.Partitions) + LogFile.Log(Partition.Name.PadRight(20) + "0x" + Partition.FirstSector.ToString("X8") + " - 0x" + Partition.LastSector.ToString("X8") + " " + Partition.Volume, LogType.ConsoleOnly); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("MergeGPT"); + } + break; + case "dumpffu": + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -DumpFFU "); + FFU = new FFU(args[2]); + if (args.Length < 5) + { + foreach (Partition Partition in FFU.GPT.Partitions) + { + if (FFU.IsPartitionPresentInFFU(Partition.Name)) + { + FFU.WritePartition(Partition.Name, System.IO.Path.Combine(args[3], Partition.Name + ".bin")); + } + } + } + else + { + Partition Target = FFU.GPT.GetPartition(args[4]); + if ((Target == null) || (!FFU.IsPartitionPresentInFFU(Target.Name))) + throw new InvalidOperationException("Partition not found in FFU!"); + FFU.WritePartition(Target.Name, System.IO.Path.Combine(args[3], Target.Name + ".bin")); + } + break; + case "dumpuefi": + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -DumpUEFI "); + + byte[] UefiBinary; + if (FFU.IsFFU(args[2])) + { + FFU = new FFU(args[2]); + UefiBinary = FFU.GetPartition("UEFI"); + } + else + { + UefiBinary = File.ReadAllBytes(args[2]); + } + + UEFI UEFI = new UEFI(UefiBinary); + + foreach (EFI Efi in UEFI.EFIs) + { + byte[] EfiBinary = UEFI.GetFile(Efi.Guid); + string Name = Efi.Name; + if (Name == null) + Name = Efi.Guid.ToString(); + if (!Name.Contains('.')) + { + switch (Efi.Type) + { + case 5: + case 7: + Name += ".dll"; + break; + case 9: + Name += ".exe"; + break; + default: + Name += ".bin"; + break; + } + } + string EfiPath = Path.Combine(args[3], Name); + Directory.CreateDirectory(Path.GetDirectoryName(EfiPath)); + File.WriteAllBytes(EfiPath, EfiBinary); + } + break; + case "testprogrammer": + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -TestProgrammer "); + await TestCode.TestProgrammer(UIContext, args[2]); + break; + case "findflashingprofile": + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + if (args.Length > 2) + await LumiaV2UnlockBootViewModel.LumiaV2FindFlashingProfile(Notifier, args[2]); + else + await LumiaV2UnlockBootViewModel.LumiaV2FindFlashingProfile(Notifier, null); + Notifier.Stop(); + break; + case "findflashingprofileexperimental": + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + if (args.Length > 2) + await LumiaV2UnlockBootViewModel.LumiaV2FindFlashingProfile(Notifier, args[2], Experimental: true); + else + await LumiaV2UnlockBootViewModel.LumiaV2FindFlashingProfile(Notifier, null, Experimental: true); + Notifier.Stop(); + break; + case "findflashingprofilenorestart": + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + if (args.Length > 2) + await LumiaV2UnlockBootViewModel.LumiaV2FindFlashingProfile(Notifier, args[2], false); + else + await LumiaV2UnlockBootViewModel.LumiaV2FindFlashingProfile(Notifier, null, false); + Notifier.Stop(); + break; + case "enabletestsigning": + if (args.Length > 2) + await LumiaV2UnlockBootViewModel.LumiaV2EnableTestSigning(UIContext, args[2]); + else + await LumiaV2UnlockBootViewModel.LumiaV2EnableTestSigning(UIContext, null); + break; + case "enabletestsigningnorestart": + if (args.Length > 2) + await LumiaV2UnlockBootViewModel.LumiaV2EnableTestSigning(UIContext, args[2], false); + else + await LumiaV2UnlockBootViewModel.LumiaV2EnableTestSigning(UIContext, null, false); + break; + case "clearnv": + if (args.Length > 2) + await LumiaV2UnlockBootViewModel.LumiaV2ClearNV(UIContext, args[2]); + else + await LumiaV2UnlockBootViewModel.LumiaV2ClearNV(UIContext, null); + break; + case "switchtomassstoragemode": + LogFile.BeginAction("SwitchToMassStorageMode"); + try + { + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + LogFile.Log("Command: Switch to Mass Storage Mode", LogType.FileAndConsole); + if (args.Length > 2) + await LumiaV2UnlockBootViewModel.LumiaV2SwitchToMassStorageMode(Notifier, args[2]); + else + await LumiaV2UnlockBootViewModel.LumiaV2SwitchToMassStorageMode(Notifier, null); + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("SwitchToMassStorageMode"); + } + break; + case "relockphone": + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + + if (args.Length > 2) + await LumiaV2UnlockBootViewModel.LumiaV2RelockPhone(Notifier, args[2]); + else + await LumiaV2UnlockBootViewModel.LumiaV2RelockPhone(Notifier, null); + + Notifier.Stop(); + break; + case "addffu": + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -AddFFU "); + App.Config.AddFfuToRepository(args[2]); + break; + case "removeffu": + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -RemoveFFU "); + App.Config.RemoveFfuFromRepository(args[2]); + break; + case "addemergency": + if (args.Length < 5) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -AddEmergnency "); + App.Config.AddEmergencyToRepository(args[2], args[3], args[4]); + break; + case "removeemergency": + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -RemoveEmergency "); + App.Config.RemoveEmergencyFromRepository(args[2]); + break; + case "listrepository": + int Count = 0; + LogFile.Log("", LogType.ConsoleOnly); + LogFile.Log("FFU Repository:", LogType.ConsoleOnly); + foreach (FFUEntry Entry in App.Config.FFURepository) + { + LogFile.Log("", LogType.ConsoleOnly); + LogFile.Log("FFU " + Count.ToString() + ":", LogType.ConsoleOnly); + LogFile.Log("File: " + Entry.Path + (Entry.Exists() ? "" : " (file is missing)"), LogType.ConsoleOnly); + LogFile.Log("Platform ID: " + Entry.PlatformID, LogType.ConsoleOnly); + if (Entry.FirmwareVersion != null) + LogFile.Log("Firmware version: " + Entry.FirmwareVersion, LogType.ConsoleOnly); + if (Entry.OSVersion != null) + LogFile.Log("OS version: " + Entry.OSVersion, LogType.ConsoleOnly); + Count++; + } + LogFile.Log("", LogType.ConsoleOnly); + LogFile.Log("Emergency Repository:", LogType.ConsoleOnly); + Count = 0; + foreach (EmergencyFileEntry Entry in App.Config.EmergencyRepository) + { + LogFile.Log("", LogType.ConsoleOnly); + LogFile.Log("Emergency " + Count.ToString() + ":", LogType.ConsoleOnly); + LogFile.Log("Type: " + Entry.Type, LogType.ConsoleOnly); + LogFile.Log("Programmer file: " + Entry.ProgrammerPath + (Entry.ProgrammerExists() ? "" : " (file is missing)"), LogType.ConsoleOnly); + if (Entry.PayloadPath != null) + LogFile.Log("Payload file: " + Entry.PayloadPath + (Entry.PayloadExists() ? "" : " (file is missing)"), LogType.ConsoleOnly); + Count++; + } + break; + case "showffu": + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -ShowFFU "); + string FFUPath = args[2]; + LogFile.Log("FFU: " + FFUPath, LogType.ConsoleOnly); + FFU = new FFU(FFUPath); + + // Show basics + LogFile.Log("Platform ID: " + FFU.PlatformID, LogType.ConsoleOnly); + string Firmware = FFU.GetFirmwareVersion(); + if (Firmware != null) + LogFile.Log("Firmware version: " + Firmware, LogType.ConsoleOnly); + string OSVersion = FFU.GetOSVersion(); + if (OSVersion != null) + LogFile.Log("OS version: " + OSVersion, LogType.ConsoleOnly); + + // Show partitions from GPT (also show which partitions are in the FFU payload) + LogFile.Log("", LogType.ConsoleOnly); + LogFile.Log("Partition table:", LogType.ConsoleOnly); + LogFile.Log("Name".PadRight(20) + "Start-sector".PadRight(20) + "End-sector".PadRight(20) + "Present in FFU", LogType.ConsoleOnly); + foreach (Partition p in FFU.GPT.Partitions) + { + LogFile.Log(p.Name.PadRight(20) + ("0x" + p.FirstSector.ToString("X16")).PadRight(20) + ("0x" + p.LastSector.ToString("X16")).PadRight(20) + (FFU.IsPartitionPresentInFFU(p.Name) ? "Yes" : "No"), LogType.ConsoleOnly); + } + break; + case "showphoneinfo": + LogFile.Log("Command: Show phone info", LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + Info = FlashModel.ReadPhoneInfo(); + Info.Log(LogType.ConsoleOnly); + Notifier.Stop(); + break; + case "unlockbootloader": + LogFile.BeginAction("UnlockBootloader"); + try + { + LogFile.Log("Command: Unlock Bootloader", LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + Info = FlashModel.ReadPhoneInfo(); + Info.Log(LogType.ConsoleOnly); + + FFU ProfileFFU = null; + FFU SupportedFFU = null; + FFU CurrentFFU; + for (int i = 2; i <= 3; i++) + { + if (args.Length > i) + { + CurrentFFU = new FFU(args[i]); + string CurrentVersion = CurrentFFU.GetOSVersion(); + string PlatformID = CurrentFFU.PlatformID; + + // Check if the current FFU matches the connected phone, so that the FFU can be used for profiling. + if (Info.PlatformID.StartsWith(PlatformID, StringComparison.OrdinalIgnoreCase)) + ProfileFFU = CurrentFFU; + + // Check if the current FFU is supported for unlocking. + if (App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == CurrentVersion)) + SupportedFFU = CurrentFFU; + } + } + + if (ProfileFFU == null) + { + List FFUs = App.Config.FFURepository.Where(e => (Info.PlatformID.StartsWith(e.PlatformID, StringComparison.OrdinalIgnoreCase) && e.Exists())).ToList(); + if (FFUs.Count() > 0) + ProfileFFU = new FFU(FFUs[0].Path); + else + throw new WPinternalsException("Profile FFU missing"); + } + LogFile.Log("Profile FFU: " + ProfileFFU.Path); + + if (SupportedFFU == null) + { + List FFUs = App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).ToList(); + if (FFUs.Count() > 0) + SupportedFFU = new FFU(FFUs[0].Path); + else + throw new WPinternalsException("No donor-FFU found with supported OS version"); + } + + await LumiaV2UnlockBootViewModel.LumiaV2UnlockBootloader(Notifier, ProfileFFU.Path, null, SupportedFFU.Path); + + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("UnlockBootloader"); + } + break; + case "flashcustomrom": + LogFile.BeginAction("FlashCustomROM"); + try + { + LogFile.Log("Command: Flash Custom ROM", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -FlashCustomROM "); + string CustomRomPath = args[2]; + LogFile.Log("Custom ROM: " + CustomRomPath, LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + Info = FlashModel.ReadPhoneInfo(); + Info.Log(LogType.ConsoleOnly); + LogFile.Log("Preparing to flash Custom ROM", LogType.FileAndConsole); + await LumiaV2UnlockBootViewModel.LumiaV2FlashArchive(Notifier, CustomRomPath); + LogFile.Log("Custom ROM flashed successfully", LogType.FileAndConsole); + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("FlashCustomROM"); + } + break; + case "flashffu": + LogFile.BeginAction("FlashFFU"); + try + { + LogFile.Log("Command: Flash FFU", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -FlashFFU "); + FFUPath = args[2]; + LogFile.Log("FFU file: " + FFUPath, LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + Info = FlashModel.ReadPhoneInfo(); + Info.Log(LogType.ConsoleOnly); + LogFile.Log("Flashing FFU...", LogType.FileAndConsole); + await Task.Run(() => FlashModel.FlashFFU(new FFU(FFUPath), true, (byte)((Info.RdcPresent || !Info.SecureFfuEnabled || Info.Authenticated) ? FlashOptions.SkipSignatureCheck : 0))); + LogFile.Log("FFU flashed successfully", LogType.FileAndConsole); + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("FlashFFU"); + } + break; + case "fixbootafterunlockingbootloader": + LogFile.BeginAction("FixBoot"); + try + { + LogFile.Log("Command: Fix boot after unlocking bootloader", LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + string Drive = await LumiaV2UnlockBootViewModel.LumiaV2SwitchToMassStorageMode(Notifier, null); + Notifier.Stop(); + App.PatchEngine.TargetPath = Drive + "\\"; + PatchResult = App.PatchEngine.Patch("SecureBootHack-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Patch failed"); + LogFile.Log("Fixed bootloader", LogType.FileAndConsole); + LogFile.Log("The phone is left in Mass Storage mode", LogType.FileAndConsole); + LogFile.Log("Press and hold the power-button of the phone for at least 10 seconds to reset the phone", LogType.FileAndConsole); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("FixBoot"); + } + break; + case "enablerootaccess": + LogFile.BeginAction("EnableRootAccess"); + try + { + LogFile.Log("Command: Enable root access on the phone", LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + string Drive = await LumiaV2UnlockBootViewModel.LumiaV2SwitchToMassStorageMode(Notifier, null); + Notifier.Stop(); + App.PatchEngine.TargetPath = Drive + "\\EFIESP\\"; + PatchResult = App.PatchEngine.Patch("SecureBootHack-V2-EFIESP"); + if (!PatchResult) + throw new WPinternalsException("Patch failed"); + App.PatchEngine.TargetPath = Drive + "\\"; + PatchResult = App.PatchEngine.Patch("SecureBootHack-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Patch failed"); + PatchResult = App.PatchEngine.Patch("RootAccess-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Patch failed"); + LogFile.Log("Root Access enabled!", LogType.FileAndConsole); + LogFile.Log("The phone is left in Mass Storage mode", LogType.FileAndConsole); + LogFile.Log("Press and hold the power-button of the phone for at least 10 seconds to reset the phone", LogType.FileAndConsole); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("EnableRootAccess"); + } + break; + case "unlockbootloaderonimage": + LogFile.Log("Command: Unlock bootloader on image", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -UnlockBootLoaderOnImage "); + FFUFilePath = null; + EfiEspImagePath = args[2]; + if (args.Length > 3) + { + if (FFU.IsFFU(args[3])) + FFUFilePath = args[3]; + else + MainOsImagePath = args[3]; + } + if (args.Length > 4) + FFUFilePath = args[4]; + + using (FileStream FileSystemStream = new FileStream(EfiEspImagePath, FileMode.Open, FileAccess.ReadWrite)) + { + UnlockedEFIESPFileSystem = new DiscUtils.Fat.FatFileSystem(FileSystemStream); + + if (FFUFilePath != null) + { + FFU SupportedFFU = new FFU(FFUFilePath); + LogFile.Log("Donor FFU: " + SupportedFFU.Path); + byte[] SupportedEFIESP = SupportedFFU.GetPartition("EFIESP"); + DiscUtils.Fat.FatFileSystem SupportedEFIESPFileSystem = new DiscUtils.Fat.FatFileSystem(new MemoryStream(SupportedEFIESP)); + DiscUtils.SparseStream SupportedMobileStartupStream = SupportedEFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Open); + MemoryStream SupportedMobileStartupMemStream = new MemoryStream(); + SupportedMobileStartupStream.CopyTo(SupportedMobileStartupMemStream); + byte[] SupportedMobileStartup = SupportedMobileStartupMemStream.ToArray(); + SupportedMobileStartupMemStream.Close(); + SupportedMobileStartupStream.Close(); + + // Save supported mobilestartup.efi + LogFile.Log("Taking mobilestartup.efi from donor-FFU"); + Stream MobileStartupStream = UnlockedEFIESPFileSystem.OpenFile(@"Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write); + MobileStartupStream.Write(SupportedMobileStartup, 0, SupportedMobileStartup.Length); + MobileStartupStream.Close(); + } + + App.PatchEngine.TargetImage = UnlockedEFIESPFileSystem; + PatchResult = App.PatchEngine.Patch("SecureBootHack-V2-EFIESP"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch bootloader"); + + // Edit BCD + LogFile.Log("Edit BCD"); + using (Stream BCDFileStream = UnlockedEFIESPFileSystem.OpenFile(@"efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite)) + { + using (DiscUtils.Registry.RegistryHive BCDHive = new DiscUtils.Registry.RegistryHive(BCDFileStream)) + { + DiscUtils.BootConfig.Store BCDStore = new DiscUtils.BootConfig.Store(BCDHive.Root); + DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); + DiscUtils.BootConfig.Element NoCodeIntegrityElement = MobileStartupObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + MobileStartupObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + + DiscUtils.BootConfig.BcdObject WinLoadObject = BCDStore.GetObject(new Guid("{7619dcc9-fafe-11d9-b411-000476eba25f}")); + NoCodeIntegrityElement = WinLoadObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + WinLoadObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + } + } + } + + if (MainOsImagePath != null) + { + using (FileStream FileSystemStream = new FileStream(MainOsImagePath, FileMode.Open, FileAccess.ReadWrite)) + { + UnlockedMainOsFileSystem = new DiscUtils.Ntfs.NtfsFileSystem(FileSystemStream); + + App.PatchEngine.TargetImage = UnlockedMainOsFileSystem; + PatchResult = App.PatchEngine.Patch("SecureBootHack-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch MainOS"); + } + } + LogFile.Log("Bootloader unlocked on image", LogType.FileAndConsole); + break; + case "enablerootaccessonimage": + LogFile.Log("Command: Enable root access on image", LogType.FileAndConsole); + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -EnableRootAccessOnImage "); + + FFUFilePath = null; + EfiEspImagePath = args[2]; + MainOsImagePath = args[3]; + if (args.Length > 4) + FFUFilePath = args[4]; + + using (FileStream FileSystemStream = new FileStream(EfiEspImagePath, FileMode.Open, FileAccess.ReadWrite)) + { + UnlockedEFIESPFileSystem = new DiscUtils.Fat.FatFileSystem(FileSystemStream); + + if (FFUFilePath != null) + { + FFU SupportedFFU = new FFU(FFUFilePath); + LogFile.Log("Supported FFU: " + SupportedFFU.Path); + byte[] SupportedEFIESP = SupportedFFU.GetPartition("EFIESP"); + DiscUtils.Fat.FatFileSystem SupportedEFIESPFileSystem = new DiscUtils.Fat.FatFileSystem(new MemoryStream(SupportedEFIESP)); + DiscUtils.SparseStream SupportedMobileStartupStream = SupportedEFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Open); + MemoryStream SupportedMobileStartupMemStream = new MemoryStream(); + SupportedMobileStartupStream.CopyTo(SupportedMobileStartupMemStream); + byte[] SupportedMobileStartup = SupportedMobileStartupMemStream.ToArray(); + SupportedMobileStartupMemStream.Close(); + SupportedMobileStartupStream.Close(); + + // Save supported mobilestartup.efi + LogFile.Log("Taking mobilestartup.efi from donor-FFU"); + Stream MobileStartupStream = UnlockedEFIESPFileSystem.OpenFile(@"Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write); + MobileStartupStream.Write(SupportedMobileStartup, 0, SupportedMobileStartup.Length); + MobileStartupStream.Close(); + } + + App.PatchEngine.TargetImage = UnlockedEFIESPFileSystem; + PatchResult = App.PatchEngine.Patch("SecureBootHack-V2-EFIESP"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch bootloader"); + + // Edit BCD + LogFile.Log("Edit BCD"); + using (Stream BCDFileStream = UnlockedEFIESPFileSystem.OpenFile(@"efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite)) + { + using (DiscUtils.Registry.RegistryHive BCDHive = new DiscUtils.Registry.RegistryHive(BCDFileStream)) + { + DiscUtils.BootConfig.Store BCDStore = new DiscUtils.BootConfig.Store(BCDHive.Root); + DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); + DiscUtils.BootConfig.Element NoCodeIntegrityElement = MobileStartupObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + MobileStartupObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + + DiscUtils.BootConfig.BcdObject WinLoadObject = BCDStore.GetObject(new Guid("{7619dcc9-fafe-11d9-b411-000476eba25f}")); + NoCodeIntegrityElement = WinLoadObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + WinLoadObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + } + } + } + + using (FileStream FileSystemStream = new FileStream(MainOsImagePath, FileMode.Open, FileAccess.ReadWrite)) + { + UnlockedMainOsFileSystem = new DiscUtils.Ntfs.NtfsFileSystem(FileSystemStream); + + App.PatchEngine.TargetImage = UnlockedMainOsFileSystem; + PatchResult = App.PatchEngine.Patch("SecureBootHack-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch MainOS"); + PatchResult = App.PatchEngine.Patch("RootAccess-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch MainOS"); + } + LogFile.Log("Root access enabled on image", LogType.FileAndConsole); + break; + case "unlockbootloaderonmountedimage": + LogFile.Log("Command: Unlock bootloader on mounted image", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -UnlockBootLoaderOnMountedImage "); + FFUFilePath = null; + EfiEspImagePath = args[2]; + if (args.Length > 3) + { + if (FFU.IsFFU(args[3])) + FFUFilePath = args[3]; + else + MainOsImagePath = args[3]; + } + if (args.Length > 4) + FFUFilePath = args[4]; + + if (FFUFilePath != null) + { + FFU SupportedFFU = new FFU(FFUFilePath); + LogFile.Log("Donor-FFU: " + SupportedFFU.Path); + byte[] SupportedEFIESP = SupportedFFU.GetPartition("EFIESP"); + DiscUtils.Fat.FatFileSystem SupportedEFIESPFileSystem = new DiscUtils.Fat.FatFileSystem(new MemoryStream(SupportedEFIESP)); + DiscUtils.SparseStream SupportedMobileStartupStream = SupportedEFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Open); + MemoryStream SupportedMobileStartupMemStream = new MemoryStream(); + SupportedMobileStartupStream.CopyTo(SupportedMobileStartupMemStream); + byte[] SupportedMobileStartup = SupportedMobileStartupMemStream.ToArray(); + SupportedMobileStartupMemStream.Close(); + SupportedMobileStartupStream.Close(); + + // Save supported mobilestartup.efi + LogFile.Log("Taking mobilestartup.efi from donor-FFU"); + Stream MobileStartupStream = File.Open(Path.Combine(EfiEspImagePath, @"Windows\System32\Boot\mobilestartup.efi"), FileMode.Create, FileAccess.Write); + MobileStartupStream.Write(SupportedMobileStartup, 0, SupportedMobileStartup.Length); + MobileStartupStream.Close(); + } + + App.PatchEngine.TargetPath = EfiEspImagePath; + PatchResult = App.PatchEngine.Patch("SecureBootHack-V2-EFIESP"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch bootloader"); + + // Edit BCD + LogFile.Log("Edit BCD"); + using (Stream BCDFileStream = File.Open(Path.Combine(EfiEspImagePath, @"efi\Microsoft\Boot\BCD"), FileMode.Open, FileAccess.ReadWrite)) + { + using (DiscUtils.Registry.RegistryHive BCDHive = new DiscUtils.Registry.RegistryHive(BCDFileStream)) + { + DiscUtils.BootConfig.Store BCDStore = new DiscUtils.BootConfig.Store(BCDHive.Root); + DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); + DiscUtils.BootConfig.Element NoCodeIntegrityElement = MobileStartupObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + MobileStartupObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + + DiscUtils.BootConfig.BcdObject WinLoadObject = BCDStore.GetObject(new Guid("{7619dcc9-fafe-11d9-b411-000476eba25f}")); + NoCodeIntegrityElement = WinLoadObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + WinLoadObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + } + } + + if (MainOsImagePath != null) + { + App.PatchEngine.TargetPath = MainOsImagePath; + PatchResult = App.PatchEngine.Patch("SecureBootHack-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch MainOS"); + } + LogFile.Log("Bootloader unlocked on image", LogType.FileAndConsole); + break; + case "enablerootaccessonmountedimage": + LogFile.Log("Command: Enable root access on mounted image", LogType.FileAndConsole); + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -EnableRootAccessOnMountedImage "); + + FFUFilePath = null; + EfiEspImagePath = args[2]; + MainOsImagePath = args[3]; + if (args.Length > 4) + FFUFilePath = args[4]; + + if (FFUFilePath != null) + { + FFU SupportedFFU = new FFU(FFUFilePath); + LogFile.Log("Supported FFU: " + SupportedFFU.Path); + byte[] SupportedEFIESP = SupportedFFU.GetPartition("EFIESP"); + DiscUtils.Fat.FatFileSystem SupportedEFIESPFileSystem = new DiscUtils.Fat.FatFileSystem(new MemoryStream(SupportedEFIESP)); + DiscUtils.SparseStream SupportedMobileStartupStream = SupportedEFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Open); + MemoryStream SupportedMobileStartupMemStream = new MemoryStream(); + SupportedMobileStartupStream.CopyTo(SupportedMobileStartupMemStream); + byte[] SupportedMobileStartup = SupportedMobileStartupMemStream.ToArray(); + SupportedMobileStartupMemStream.Close(); + SupportedMobileStartupStream.Close(); + + // Save supported mobilestartup.efi + LogFile.Log("Taking mobilestartup.efi from donor-FFU"); + Stream MobileStartupStream = File.Open(Path.Combine(EfiEspImagePath, @"Windows\System32\Boot\mobilestartup.efi"), FileMode.Create, FileAccess.Write); + MobileStartupStream.Write(SupportedMobileStartup, 0, SupportedMobileStartup.Length); + MobileStartupStream.Close(); + } + + App.PatchEngine.TargetPath = EfiEspImagePath; + PatchResult = App.PatchEngine.Patch("SecureBootHack-V2-EFIESP"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch bootloader"); + + // Edit BCD + LogFile.Log("Edit BCD"); + using (Stream BCDFileStream = File.Open(Path.Combine(EfiEspImagePath, @"efi\Microsoft\Boot\BCD"), FileMode.Open, FileAccess.ReadWrite)) + { + using (DiscUtils.Registry.RegistryHive BCDHive = new DiscUtils.Registry.RegistryHive(BCDFileStream)) + { + DiscUtils.BootConfig.Store BCDStore = new DiscUtils.BootConfig.Store(BCDHive.Root); + DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); + DiscUtils.BootConfig.Element NoCodeIntegrityElement = MobileStartupObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + MobileStartupObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + + DiscUtils.BootConfig.BcdObject WinLoadObject = BCDStore.GetObject(new Guid("{7619dcc9-fafe-11d9-b411-000476eba25f}")); + NoCodeIntegrityElement = WinLoadObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + WinLoadObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + } + } + + App.PatchEngine.TargetPath = MainOsImagePath; + PatchResult = App.PatchEngine.Patch("SecureBootHack-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch MainOS"); + PatchResult = App.PatchEngine.Patch("RootAccess-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch MainOS"); + LogFile.Log("Root access enabled on image", LogType.FileAndConsole); + break; + case "downloadffu": + LogFile.Log("Command: Download FFU", LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Normal) + { + NormalModel = (NokiaPhoneModel)Notifier.CurrentModel; + ProductCode = NormalModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); + } + else if ((Notifier.CurrentInterface == PhoneInterfaces.Lumia_Bootloader) || (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Flash)) + { + FlashModel = (NokiaFlashModel)Notifier.CurrentModel; + Info = FlashModel.ReadPhoneInfo(); + ProductCode = Info.ProductCode; + } + else + { + NormalModel = (NokiaPhoneModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Normal)); + ProductCode = NormalModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); + } + URL = LumiaDownloadModel.SearchFFU(null, ProductCode, null, out ProductType); + if (args.Length >= 3) + DownloadFolder = args[4]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + Notifier.Stop(); + break; + case "downloadffubyoperatorcode": + LogFile.Log("Command: Download FFU by Operator Code", LogType.FileAndConsole); + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -DownloadFFUbyOperatorCode "); + ProductType = args[2]; + LogFile.Log("Product type: " + ProductType, LogType.FileAndConsole); + OperatorCode = args[3]; + LogFile.Log("Operator code: " + OperatorCode, LogType.FileAndConsole); + if (args.Length >= 5) + DownloadFolder = args[4]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + URL = LumiaDownloadModel.SearchFFU(ProductType, null, OperatorCode); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + break; + case "downloadffubyproductcode": + LogFile.Log("Command: Download FFU by Product Code", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -DownloadFFUbyProductCode "); + ProductCode = args[2]; + LogFile.Log("Product code: " + ProductCode, LogType.FileAndConsole); + URL = LumiaDownloadModel.SearchFFU(null, ProductCode, null, out ProductType); + if (args.Length >= 4) + DownloadFolder = args[3]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + break; + case "downloadffubyproducttype": + LogFile.Log("Command: Download FFU by Product Type", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -DownloadFFUbyProductType "); + ProductType = args[2]; + LogFile.Log("Product type: " + ProductType, LogType.FileAndConsole); + if (args.Length >= 4) + DownloadFolder = args[3]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + URL = LumiaDownloadModel.SearchFFU(ProductType, null, null); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + break; + case "searchffubyproducttype": + LogFile.Log("Command: Search FFU by Product Type", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -SearchFFUbyProductType "); + ProductType = args[2]; + LogFile.Log("Lumia model: " + ProductType, LogType.FileAndConsole); + URL = LumiaDownloadModel.SearchFFU(ProductType, null, null); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + break; + case "downloademergency": + LogFile.Log("Command: Download Emergency files", LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Normal) + { + NormalModel = (NokiaPhoneModel)Notifier.CurrentModel; + ProductType = NormalModel.ExecuteJsonMethodAsString("ReadManufacturerModelName", "ManufacturerModelName"); + if (ProductType.IndexOf('_') >= 0) ProductType = ProductType.Substring(0, ProductType.IndexOf('_')); + } + else if ((Notifier.CurrentInterface == PhoneInterfaces.Lumia_Bootloader) || (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Flash)) + { + FlashModel = (NokiaFlashModel)Notifier.CurrentModel; + Info = FlashModel.ReadPhoneInfo(); + ProductType = Info.Type; + } + else + { + NormalModel = (NokiaPhoneModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Normal)); + ProductType = NormalModel.ExecuteJsonMethodAsString("ReadManufacturerModelName", "ManufacturerModelName"); + if (ProductType.IndexOf('_') >= 0) ProductType = ProductType.Substring(0, ProductType.IndexOf('_')); + } + URLs = LumiaDownloadModel.SearchEmergencyFiles(ProductType); + if (URLs != null) + { + if (args.Length >= 3) + DownloadFolder = args[2]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + for (int i = 0; i < URLs.Length; i++) + { + LogFile.Log("URL: " + URLs[i], LogType.FileAndConsole); + URI = new Uri(URLs[i]); + EmergencyFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + EmergencyFileName, LogType.FileAndConsole); + EmergencyFilePath = Path.Combine(DownloadFolder, EmergencyFileName); + if (i == 0) + ProgrammerPath = EmergencyFilePath; + else + PayloadPath = EmergencyFilePath; + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URLs[i], EmergencyFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + } + App.Config.AddEmergencyToRepository(ProductType, ProgrammerPath, PayloadPath); + } + Notifier.Stop(); + break; + case "downloademergencybyproducttype": + LogFile.Log("Command: Download Emergency files", LogType.FileAndConsole); + if (args.Length < 3) + throw new WPinternalsException("Wrong number of arguments. Usage: WPinternals.exe -DownloadEmergencyByProductType "); + ProductType = args[2]; + URLs = LumiaDownloadModel.SearchEmergencyFiles(ProductType); + if (URLs != null) + { + if (args.Length >= 4) + DownloadFolder = args[3]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + for (int i = 0; i < URLs.Length; i++) + { + LogFile.Log("URL: " + URLs[i], LogType.FileAndConsole); + URI = new Uri(URLs[i]); + EmergencyFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + EmergencyFileName, LogType.FileAndConsole); + EmergencyFilePath = Path.Combine(DownloadFolder, EmergencyFileName); + if (i == 0) + ProgrammerPath = EmergencyFilePath; + else + PayloadPath = EmergencyFilePath; + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URLs[i], EmergencyFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + } + App.Config.AddEmergencyToRepository(ProductType, ProgrammerPath, PayloadPath); + } + break; + case "downloadall": + LogFile.Log("Command: Download all", LogType.FileAndConsole); + Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Normal) + { + NormalModel = (NokiaPhoneModel)Notifier.CurrentModel; + ProductCode = NormalModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); + } + else if ((Notifier.CurrentInterface == PhoneInterfaces.Lumia_Bootloader) || (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Flash)) + { + FlashModel = (NokiaFlashModel)Notifier.CurrentModel; + Info = FlashModel.ReadPhoneInfo(); + ProductCode = Info.ProductCode; + } + else + { + NormalModel = (NokiaPhoneModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Normal)); + ProductCode = NormalModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); + } + URL = LumiaDownloadModel.SearchFFU(null, ProductCode, null, out ProductType); + if (args.Length >= 3) + DownloadFolder = args[2]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + + URLs = LumiaDownloadModel.SearchEmergencyFiles(ProductType); + if (URLs != null) + { + for (int i = 0; i < URLs.Length; i++) + { + LogFile.Log("URL: " + URLs[i], LogType.FileAndConsole); + URI = new Uri(URLs[i]); + EmergencyFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + EmergencyFileName, LogType.FileAndConsole); + EmergencyFilePath = Path.Combine(DownloadFolder, EmergencyFileName); + if (i == 0) + ProgrammerPath = EmergencyFilePath; + else + PayloadPath = EmergencyFilePath; + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URLs[i], EmergencyFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + } + App.Config.AddEmergencyToRepository(ProductType, ProgrammerPath, PayloadPath); + } + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + { + ProductType = "RM-1085"; + + URL = LumiaDownloadModel.SearchFFU(ProductType, null, null); + if (args.Length >= 3) + DownloadFolder = args[2]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + throw new WPinternalsException("Unable to find compatible FFU"); + } + Notifier.Stop(); + break; + case "downloadallbyproducttype": + LogFile.Log("Command: Download all by Product Type", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -DownloadAllByProductType "); + ProductType = args[2]; + LogFile.Log("Product type: " + ProductType, LogType.FileAndConsole); + if (args.Length >= 4) + DownloadFolder = args[3]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + URL = LumiaDownloadModel.SearchFFU(ProductType, null, null); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + + URLs = LumiaDownloadModel.SearchEmergencyFiles(ProductType); + if (URLs != null) + { + for (int i = 0; i < URLs.Length; i++) + { + LogFile.Log("URL: " + URLs[i], LogType.FileAndConsole); + URI = new Uri(URLs[i]); + EmergencyFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + EmergencyFileName, LogType.FileAndConsole); + EmergencyFilePath = Path.Combine(DownloadFolder, EmergencyFileName); + if (i == 0) + ProgrammerPath = EmergencyFilePath; + else + PayloadPath = EmergencyFilePath; + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URLs[i], EmergencyFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + } + App.Config.AddEmergencyToRepository(ProductType, ProgrammerPath, PayloadPath); + } + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + { + ProductType = "RM-1085"; + + URL = LumiaDownloadModel.SearchFFU(ProductType, null, null); + if (args.Length >= 4) + DownloadFolder = args[3]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + throw new WPinternalsException("Unable to find compatible FFU"); + } + break; + case "downloadallbyproductcode": + LogFile.Log("Command: Download all by Product Code", LogType.FileAndConsole); + if (args.Length < 3) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -DownloadAllByProductCode "); + ProductCode = args[2]; + LogFile.Log("Product code: " + ProductCode, LogType.FileAndConsole); + URL = LumiaDownloadModel.SearchFFU(null, ProductCode, null, out ProductType); + if (args.Length >= 4) + DownloadFolder = args[3]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + + URLs = LumiaDownloadModel.SearchEmergencyFiles(ProductType); + if (URLs != null) + { + for (int i = 0; i < URLs.Length; i++) + { + LogFile.Log("URL: " + URLs[i], LogType.FileAndConsole); + URI = new Uri(URLs[i]); + EmergencyFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + EmergencyFileName, LogType.FileAndConsole); + EmergencyFilePath = Path.Combine(DownloadFolder, EmergencyFileName); + if (i == 0) + ProgrammerPath = EmergencyFilePath; + else + PayloadPath = EmergencyFilePath; + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URLs[i], EmergencyFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + } + App.Config.AddEmergencyToRepository(ProductType, ProgrammerPath, PayloadPath); + } + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + { + ProductType = "RM-1085"; + + URL = LumiaDownloadModel.SearchFFU(ProductType, null, null); + if (args.Length >= 4) + DownloadFolder = args[3]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + throw new WPinternalsException("Unable to find compatible FFU"); + } + break; + case "downloadallbyoperatorcode": + LogFile.Log("Command: Download FFU by Operator Code", LogType.FileAndConsole); + if (args.Length < 4) + throw new ArgumentException("Wrong number of arguments. Usage: WPinternals.exe -DownloadFFUbyOperatorCode "); + ProductType = args[2]; + LogFile.Log("Product type: " + ProductType, LogType.FileAndConsole); + OperatorCode = args[3]; + LogFile.Log("Operator code: " + OperatorCode, LogType.FileAndConsole); + if (args.Length >= 5) + DownloadFolder = args[4]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + URL = LumiaDownloadModel.SearchFFU(ProductType, null, OperatorCode); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + + URLs = LumiaDownloadModel.SearchEmergencyFiles(ProductType); + if (URLs != null) + { + for (int i = 0; i < URLs.Length; i++) + { + LogFile.Log("URL: " + URLs[i], LogType.FileAndConsole); + URI = new Uri(URLs[i]); + EmergencyFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + EmergencyFileName, LogType.FileAndConsole); + EmergencyFilePath = Path.Combine(DownloadFolder, EmergencyFileName); + if (i == 0) + ProgrammerPath = EmergencyFilePath; + else + PayloadPath = EmergencyFilePath; + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URLs[i], EmergencyFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + } + App.Config.AddEmergencyToRepository(ProductType, ProgrammerPath, PayloadPath); + } + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + { + ProductType = "RM-1085"; + + URL = LumiaDownloadModel.SearchFFU(ProductType, null, null); + if (args.Length >= 5) + DownloadFolder = args[4]; + else + DownloadFolder = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\Repository\\" + ProductType.ToUpper()); + if (!Directory.Exists(DownloadFolder)) + Directory.CreateDirectory(DownloadFolder); + LogFile.Log("Download folder: " + DownloadFolder, LogType.FileAndConsole); + LogFile.Log("URL: " + URL, LogType.FileAndConsole); + URI = new Uri(URL); + FFUFileName = System.IO.Path.GetFileName(URI.LocalPath); + LogFile.Log("File: " + FFUFileName, LogType.FileAndConsole); + FFUFilePath = Path.Combine(DownloadFolder, FFUFileName); + LogFile.Log("Downloading...", LogType.FileAndConsole); + using (System.Net.WebClient myWebClient = new System.Net.WebClient()) + { + await myWebClient.DownloadFileTaskAsync(URL, FFUFilePath); + } + LogFile.Log("Download finished", LogType.FileAndConsole); + App.Config.AddFfuToRepository(FFUFilePath); + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + throw new WPinternalsException("Unable to find compatible FFU"); + } + break; + default: + LogFile.Log("", LogType.ConsoleOnly); + LogFile.Log("WPinternals commandline usage:", LogType.ConsoleOnly); + LogFile.Log("", LogType.ConsoleOnly); + LogFile.Log("WPinternals -ShowPhoneInfo", LogType.ConsoleOnly); + LogFile.Log("WPinternals -AddFFU ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -AddEmergency ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -RemoveFFU ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -RemoveEmergency ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -ListRepository", LogType.ConsoleOnly); + LogFile.Log("WPinternals -FindFlashingProfile ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -UnlockBootloader", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -FixBootAfterUnlockingBootloader", LogType.ConsoleOnly); + LogFile.Log("WPinternals -EnableRootAccess", LogType.ConsoleOnly); + LogFile.Log("WPinternals -UnlockBootLoaderOnImage ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -EnableRootAccessOnImage ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -UnlockBootLoaderOnMountedImage ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -EnableRootAccessOnMountedImage ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -EnableTestSigning", LogType.ConsoleOnly); + LogFile.Log("WPinternals -RelockPhone", LogType.ConsoleOnly); + LogFile.Log("WPinternals -SwitchToMassStorageMode", LogType.ConsoleOnly); + LogFile.Log("WPinternals -FlashFFU ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -FlashCustomROM ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -FlashPartition ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -FlashRaw ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -ClearNV", LogType.ConsoleOnly); + LogFile.Log("WPinternals -ReadGPT", LogType.ConsoleOnly); + LogFile.Log("WPinternals -BackupGPT ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -RestoreGPT ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -MergeGPT ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -MergeGPT ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -ShowFFU ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DumpFFU ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadFFU ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadFFUbyProductType ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadFFUbyProductCode ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadFFUbyOperatorCode ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadEmergency ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadEmergencyByProductType ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadAll ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadAllByProductType ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadAllByProductCode ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DownloadAllByOperatorCode ", LogType.ConsoleOnly); + LogFile.Log(" ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -DumpUEFI ", LogType.ConsoleOnly); + LogFile.Log("WPinternals -TestProgrammer ", LogType.ConsoleOnly); + break; + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + + if (Environment.GetCommandLineArgs().Count() > 1) + CloseConsole(); + } + + // http://stackoverflow.com/questions/472282/show-console-in-windows-application + // https://stackoverflow.com/questions/807998/how-do-i-create-a-c-sharp-app-that-decides-itself-whether-to-show-as-a-console-o + // http://www.csharp411.com/console-output-from-winforms-application/#comment-76 + // https://stackoverflow.com/questions/1305257/using-attachconsole-user-must-hit-enter-to-get-regular-command-line + internal static void OpenConsole() + { + if (IsConsoleVisible) + return; + + if (AttachConsole(-1)) + { + Console.Write("\r" + new string(' ', Console.WindowWidth) + "\r"); // Prompt was already printed. Clear that line. + + /* + Other possibility to clear line: + + System.Console.CursorLeft = 0; + char[] bl = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Repeat(' ', System.Console.WindowWidth - 1)); + System.Console.Write(bl); + System.Console.CursorLeft = 0; + */ + + hConsoleWnd = GetForegroundWindow(); + } + else + { + AllocConsole(); + IsNewConsoleCreated = true; + + hConsoleWnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle; + + // Support VS 2017 debugger without VS host process: + try + { + // Console.OpenStandardOutput eventually calls into GetStdHandle. As per MSDN documentation of GetStdHandle: http://msdn.microsoft.com/en-us/library/windows/desktop/ms683231(v=vs.85).aspx will return the redirected handle and not the allocated console: + // "The standard handles of a process may be redirected by a call to SetStdHandle, in which case GetStdHandle returns the redirected handle. If the standard handles have been redirected, you can specify the CONIN$ value in a call to the CreateFile function to get a handle to a console's input buffer. Similarly, you can specify the CONOUT$ value to get a handle to a console's active screen buffer." + // Get the handle to CONOUT$. + IntPtr stdHandle = CreateFile("CONOUT$", GENERIC_WRITE, FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0); + Microsoft.Win32.SafeHandles.SafeFileHandle safeFileHandle = new Microsoft.Win32.SafeHandles.SafeFileHandle(stdHandle, true); + FileStream fileStream = new FileStream(safeFileHandle, FileAccess.Write); + Encoding encoding = System.Text.Encoding.GetEncoding(MY_CODE_PAGE); + StreamWriter standardOutput = new StreamWriter(fileStream, encoding); + standardOutput.AutoFlush = true; + Console.SetOut(standardOutput); + } + catch (Exception) + { + } + } + + IsConsoleVisible = true; + + Console.CancelKeyPress += Console_CancelKeyPress; + + LogFile.LogApplicationVersion(); + } + + private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + LogFile.Log("Operation canceled!", LogType.FileOnly); + LogFile.EndAction(); + +#if PREVIEW + Uploader.WaitForUploads(); +#endif + } + + internal static void CloseConsole(bool Exit = true) + { + if (IsConsoleVisible) + { + if (IsNewConsoleCreated) + { + Console.WriteLine("Press any key to close..."); + Console.ReadKey(); + } + + PostMessage(hConsoleWnd, WM_KEYDOWN, VK_RETURN, 0); + + IsConsoleVisible = false; + + if (Exit) + { +#if PREVIEW + Uploader.WaitForUploads(); +#endif + + Environment.Exit(0); + } + } + } + } +} diff --git a/DiscUtils/Block.cs b/DiscUtils/Block.cs new file mode 100644 index 0000000..22db695 --- /dev/null +++ b/DiscUtils/Block.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + internal class Block + { + public Block() + { + } + + public long Position { get; set; } + + public byte[] Data { get; set; } + + public int Available { get; set; } + + public bool Equals(Block other) + { + return Position == other.Position; + } + } +} diff --git a/DiscUtils/BlockCache.cs b/DiscUtils/BlockCache.cs new file mode 100644 index 0000000..9398d80 --- /dev/null +++ b/DiscUtils/BlockCache.cs @@ -0,0 +1,138 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Text; + + internal class BlockCache + where T : Block, new() + { + private int _blockSize; + private Dictionary _blocks; + private LinkedList _lru; + private List _freeBlocks; + private int _blocksCreated; + private int _totalBlocks; + + private int _freeBlockCount; + + public BlockCache(int blockSize, int blockCount) + { + _blockSize = blockSize; + _totalBlocks = blockCount; + + _blocks = new Dictionary(); + _lru = new LinkedList(); + _freeBlocks = new List(_totalBlocks); + + _freeBlockCount = _totalBlocks; + } + + public int FreeBlockCount + { + get { return _freeBlockCount; } + } + + public bool ContainsBlock(long position) + { + return _blocks.ContainsKey(position); + } + + public bool TryGetBlock(long position, out T block) + { + if (_blocks.TryGetValue(position, out block)) + { + _lru.Remove(block); + _lru.AddFirst(block); + return true; + } + + return false; + } + + public T GetBlock(long position) + { + T result; + + if (TryGetBlock(position, out result)) + { + return result; + } + + result = GetFreeBlock(); + result.Position = position; + result.Available = -1; + StoreBlock(result); + + return result; + } + + public void ReleaseBlock(long position) + { + T block; + if (_blocks.TryGetValue(position, out block)) + { + _blocks.Remove(position); + _lru.Remove(block); + _freeBlocks.Add(block); + _freeBlockCount++; + } + } + + private void StoreBlock(T block) + { + _blocks[block.Position] = block; + _lru.AddFirst(block); + } + + private T GetFreeBlock() + { + T block; + + if (_freeBlocks.Count > 0) + { + int idx = _freeBlocks.Count - 1; + block = _freeBlocks[idx]; + _freeBlocks.RemoveAt(idx); + _freeBlockCount--; + } + else if (_blocksCreated < _totalBlocks) + { + block = new T(); + block.Data = new byte[_blockSize]; + _blocksCreated++; + _freeBlockCount--; + } + else + { + block = _lru.Last.Value; + _lru.RemoveLast(); + _blocks.Remove(block.Position); + } + + return block; + } + } +} diff --git a/DiscUtils/BlockCacheSettings.cs b/DiscUtils/BlockCacheSettings.cs new file mode 100644 index 0000000..ccc3ff6 --- /dev/null +++ b/DiscUtils/BlockCacheSettings.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Settings controlling BlockCache instances. + /// + public sealed class BlockCacheSettings + { + /// + /// Initializes a new instance of the BlockCacheSettings class. + /// + public BlockCacheSettings() + { + this.BlockSize = (int)(4 * Sizes.OneKiB); + this.ReadCacheSize = 4 * Sizes.OneMiB; + this.LargeReadSize = 64 * Sizes.OneKiB; + this.OptimumReadSize = (int)(64 * Sizes.OneKiB); + } + + /// + /// Initializes a new instance of the BlockCacheSettings class. + /// + /// The cache settings. + internal BlockCacheSettings(BlockCacheSettings settings) + { + this.BlockSize = settings.BlockSize; + this.ReadCacheSize = settings.ReadCacheSize; + this.LargeReadSize = settings.LargeReadSize; + this.OptimumReadSize = settings.OptimumReadSize; + } + + /// + /// Gets or sets the size (in bytes) of each cached block. + /// + public int BlockSize { get; set; } + + /// + /// Gets or sets the size (in bytes) of the read cache. + /// + public long ReadCacheSize { get; set; } + + /// + /// Gets or sets the maximum read size that will be cached. + /// + /// Large reads are not cached, on the assumption they will not + /// be repeated. This setting controls what is considered 'large'. + /// Any read that is more than this many bytes will not be cached. + public long LargeReadSize { get; set; } + + /// + /// Gets or sets the optimum size of a read to the wrapped stream. + /// + /// This value must be a multiple of BlockSize. + public int OptimumReadSize { get; set; } + } +} diff --git a/DiscUtils/BlockCacheStatistics.cs b/DiscUtils/BlockCacheStatistics.cs new file mode 100644 index 0000000..97a7d58 --- /dev/null +++ b/DiscUtils/BlockCacheStatistics.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Statistical information about the effectiveness of a BlockCache instance. + /// + public sealed class BlockCacheStatistics + { + /// + /// Gets the number of requested 'large' reads, as defined by the LargeReadSize setting. + /// + public long LargeReadsIn { get; internal set; } + + /// + /// Gets the number of requested unaligned reads. + /// + /// Unaligned reads are reads where the read doesn't start on a multiple of + /// the block size. + public long UnalignedReadsIn { get; internal set; } + + /// + /// Gets the total number of requested reads. + /// + public long TotalReadsIn { get; internal set; } + + /// + /// Gets the total number of reads passed on by the cache. + /// + public long TotalReadsOut { get; internal set; } + + /// + /// Gets the number of times a read request was serviced (in part or whole) from the cache. + /// + public long ReadCacheHits { get; internal set; } + + /// + /// Gets the number of time a read request was serviced (in part or whole) from the wrapped stream. + /// + public long ReadCacheMisses { get; internal set; } + + /// + /// Gets the number of requested unaligned writes. + /// + /// Unaligned writes are writes where the write doesn't start on a multiple of + /// the block size. + public long UnalignedWritesIn { get; internal set; } + + /// + /// Gets the total number of requested writes. + /// + public long TotalWritesIn { get; internal set; } + + /// + /// Gets the number of free blocks in the read cache. + /// + public int FreeReadBlocks { get; internal set; } + } +} diff --git a/DiscUtils/BlockCacheStream.cs b/DiscUtils/BlockCacheStream.cs new file mode 100644 index 0000000..722ce20 --- /dev/null +++ b/DiscUtils/BlockCacheStream.cs @@ -0,0 +1,472 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// A stream implementing a block-oriented read cache. + /// + public sealed class BlockCacheStream : SparseStream + { + private SparseStream _wrappedStream; + private Ownership _ownWrapped; + private BlockCacheSettings _settings; + private BlockCacheStatistics _stats; + + private long _position; + private bool _atEof; + + private BlockCache _cache; + private byte[] _readBuffer; + private int _blocksInReadBuffer; + + /// + /// Initializes a new instance of the BlockCacheStream class. + /// + /// The stream to wrap. + /// Whether to assume ownership of toWrap. + public BlockCacheStream(SparseStream toWrap, Ownership ownership) + : this(toWrap, ownership, new BlockCacheSettings()) + { + } + + /// + /// Initializes a new instance of the BlockCacheStream class. + /// + /// The stream to wrap. + /// Whether to assume ownership of toWrap. + /// The cache settings. + public BlockCacheStream(SparseStream toWrap, Ownership ownership, BlockCacheSettings settings) + { + if (!toWrap.CanRead) + { + throw new ArgumentException("The wrapped stream does not support reading", "toWrap"); + } + + if (!toWrap.CanSeek) + { + throw new ArgumentException("The wrapped stream does not support seeking", "toWrap"); + } + + _wrappedStream = toWrap; + _ownWrapped = ownership; + _settings = new BlockCacheSettings(settings); + + if (_settings.OptimumReadSize % _settings.BlockSize != 0) + { + throw new ArgumentException("Invalid settings, OptimumReadSize must be a multiple of BlockSize", "settings"); + } + + _readBuffer = new byte[_settings.OptimumReadSize]; + _blocksInReadBuffer = _settings.OptimumReadSize / _settings.BlockSize; + + int totalBlocks = (int)(_settings.ReadCacheSize / _settings.BlockSize); + + _cache = new BlockCache(_settings.BlockSize, totalBlocks); + _stats = new BlockCacheStatistics(); + _stats.FreeReadBlocks = totalBlocks; + } + + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public override IEnumerable Extents + { + get + { + CheckDisposed(); + return _wrappedStream.Extents; + } + } + + /// + /// Gets an indication as to whether the stream can be read. + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Gets an indication as to whether the stream position can be changed. + /// + public override bool CanSeek + { + get { return true; } + } + + /// + /// Gets an indication as to whether the stream can be written to. + /// + public override bool CanWrite + { + get { return _wrappedStream.CanWrite; } + } + + /// + /// Gets the length of the stream. + /// + public override long Length + { + get + { + CheckDisposed(); + return _wrappedStream.Length; + } + } + + /// + /// Gets and sets the current stream position. + /// + public override long Position + { + get + { + CheckDisposed(); + return _position; + } + + set + { + CheckDisposed(); + _position = value; + } + } + + /// + /// Gets the performance statistics for this instance. + /// + public BlockCacheStatistics Statistics + { + get + { + _stats.FreeReadBlocks = _cache.FreeBlockCount; + return _stats; + } + } + + /// + /// Gets the parts of a stream that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + CheckDisposed(); + return _wrappedStream.GetExtentsInRange(start, count); + } + + /// + /// Reads data from the stream. + /// + /// The buffer to fill. + /// The buffer offset to start from. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + CheckDisposed(); + + if (_position >= Length) + { + if (_atEof) + { + throw new IOException("Attempt to read beyond end of stream"); + } + else + { + _atEof = true; + return 0; + } + } + + _stats.TotalReadsIn++; + + if (count > _settings.LargeReadSize) + { + _stats.LargeReadsIn++; + _stats.TotalReadsOut++; + _wrappedStream.Position = _position; + int numRead = _wrappedStream.Read(buffer, offset, count); + _position = _wrappedStream.Position; + + if (_position >= Length) + { + _atEof = true; + } + + return numRead; + } + + int totalBytesRead = 0; + bool servicedFromCache = false; + bool servicedOutsideCache = false; + int blockSize = _settings.BlockSize; + + long firstBlock = _position / blockSize; + int offsetInNextBlock = (int)(_position % blockSize); + long endBlock = Utilities.Ceil(Math.Min(_position + count, Length), blockSize); + int numBlocks = (int)(endBlock - firstBlock); + + if (offsetInNextBlock != 0) + { + _stats.UnalignedReadsIn++; + } + + int blocksRead = 0; + while (blocksRead < numBlocks) + { + Block block; + + // Read from the cache as much as possible + while (blocksRead < numBlocks && _cache.TryGetBlock(firstBlock + blocksRead, out block)) + { + int bytesToRead = Math.Min(count - totalBytesRead, block.Available - offsetInNextBlock); + + Array.Copy(block.Data, offsetInNextBlock, buffer, offset + totalBytesRead, bytesToRead); + offsetInNextBlock = 0; + totalBytesRead += bytesToRead; + _position += bytesToRead; + blocksRead++; + + servicedFromCache = true; + } + + // Now handle a sequence of (one or more) blocks that are not cached + if (blocksRead < numBlocks && !_cache.ContainsBlock(firstBlock + blocksRead)) + { + servicedOutsideCache = true; + + // Figure out how many blocks to read from the wrapped stream + int blocksToRead = 0; + while (blocksRead + blocksToRead < numBlocks + && blocksToRead < _blocksInReadBuffer + && !_cache.ContainsBlock(firstBlock + blocksRead + blocksToRead)) + { + ++blocksToRead; + } + + // Allow for the end of the stream not being block-aligned + long readPosition = (firstBlock + blocksRead) * (long)blockSize; + int bytesToRead = (int)Math.Min(blocksToRead * (long)blockSize, Length - readPosition); + + // Do the read + _stats.TotalReadsOut++; + _wrappedStream.Position = readPosition; + int bytesRead = Utilities.ReadFully(_wrappedStream, _readBuffer, 0, bytesToRead); + if (bytesRead != bytesToRead) + { + throw new IOException("Short read before end of stream"); + } + + // Cache the read blocks + for (int i = 0; i < blocksToRead; ++i) + { + int copyBytes = Math.Min(blockSize, bytesToRead - (i * blockSize)); + block = _cache.GetBlock(firstBlock + blocksRead + i); + Array.Copy(_readBuffer, i * blockSize, block.Data, 0, copyBytes); + block.Available = copyBytes; + + if (copyBytes < blockSize) + { + Array.Clear(_readBuffer, (i * blockSize) + copyBytes, blockSize - copyBytes); + } + } + + blocksRead += blocksToRead; + + // Propogate the data onto the caller + int bytesToCopy = Math.Min(count - totalBytesRead, bytesRead - offsetInNextBlock); + Array.Copy(_readBuffer, offsetInNextBlock, buffer, offset + totalBytesRead, bytesToCopy); + totalBytesRead += bytesToCopy; + _position += bytesToCopy; + offsetInNextBlock = 0; + } + } + + if (_position >= Length && totalBytesRead == 0) + { + _atEof = true; + } + + if (servicedFromCache) + { + _stats.ReadCacheHits++; + } + + if (servicedOutsideCache) + { + _stats.ReadCacheMisses++; + } + + return totalBytesRead; + } + + /// + /// Flushes the stream. + /// + public override void Flush() + { + CheckDisposed(); + _wrappedStream.Flush(); + } + + /// + /// Moves the stream position. + /// + /// The origin-relative location. + /// The base location. + /// The new absolute stream position. + public override long Seek(long offset, SeekOrigin origin) + { + CheckDisposed(); + + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += Length; + } + + _atEof = false; + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + else + { + _position = effectiveOffset; + return _position; + } + } + + /// + /// Sets the length of the stream. + /// + /// The new length. + public override void SetLength(long value) + { + CheckDisposed(); + _wrappedStream.SetLength(value); + } + + /// + /// Writes data to the stream at the current location. + /// + /// The data to write. + /// The first byte to write from buffer. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + CheckDisposed(); + + _stats.TotalWritesIn++; + + int blockSize = _settings.BlockSize; + long firstBlock = _position / blockSize; + long endBlock = Utilities.Ceil(Math.Min(_position + count, Length), blockSize); + int numBlocks = (int)(endBlock - firstBlock); + + try + { + _wrappedStream.Position = _position; + _wrappedStream.Write(buffer, offset, count); + } + catch + { + InvalidateBlocks(firstBlock, numBlocks); + throw; + } + + int offsetInNextBlock = (int)(_position % blockSize); + if (offsetInNextBlock != 0) + { + _stats.UnalignedWritesIn++; + } + + // For each block touched, if it's cached, update it + int bytesProcessed = 0; + for (int i = 0; i < numBlocks; ++i) + { + int bufferPos = offset + bytesProcessed; + int bytesThisBlock = Math.Min(count - bytesProcessed, blockSize - offsetInNextBlock); + + Block block; + if (_cache.TryGetBlock(firstBlock + i, out block)) + { + Array.Copy(buffer, bufferPos, block.Data, offsetInNextBlock, bytesThisBlock); + block.Available = Math.Max(block.Available, offsetInNextBlock + bytesThisBlock); + } + + offsetInNextBlock = 0; + bytesProcessed += bytesThisBlock; + } + + _position += count; + } + + /// + /// Disposes of this instance, freeing up associated resources. + /// + /// true if invoked from Dispose, else false. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_wrappedStream != null && _ownWrapped == Ownership.Dispose) + { + _wrappedStream.Dispose(); + } + + _wrappedStream = null; + } + + base.Dispose(disposing); + } + + private void CheckDisposed() + { + if (_wrappedStream == null) + { + throw new ObjectDisposedException("BlockCacheStream"); + } + } + + private void InvalidateBlocks(long firstBlock, int numBlocks) + { + for (long i = firstBlock; i < (firstBlock + numBlocks); ++i) + { + _cache.ReleaseBlock(i); + } + } + } +} diff --git a/DiscUtils/BootConfig/ApplicationImageType.cs b/DiscUtils/BootConfig/ApplicationImageType.cs new file mode 100644 index 0000000..9725607 --- /dev/null +++ b/DiscUtils/BootConfig/ApplicationImageType.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + /// + /// Enumeration of known application image types. + /// + public enum ApplicationImageType + { + /// + /// Unknown type. + /// + None = 0x0, + + /// + /// Firmware application. + /// + Firmware = 0x1, + + /// + /// Windows boot loader. + /// + WindowsBoot = 0x2, + + /// + /// Legacy boot loader. + /// + LegacyLoader = 0x3, + + /// + /// Real mode boot loader. + /// + RealMode = 0x4 + } +} diff --git a/DiscUtils/BootConfig/ApplicationType.cs b/DiscUtils/BootConfig/ApplicationType.cs new file mode 100644 index 0000000..c6d32a9 --- /dev/null +++ b/DiscUtils/BootConfig/ApplicationType.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + /// + /// Enumeration of known application types. + /// + public enum ApplicationType + { + /// + /// Unknown type. + /// + None = 0, + + /// + /// Firmware boot manager (e.g. UEFI). + /// + FirmwareBootManager = 1, + + /// + /// Windows boot manager. + /// + BootManager = 2, + + /// + /// Operating System Loader. + /// + OsLoader = 3, + + /// + /// Resume loader. + /// + Resume = 4, + + /// + /// Memory diagnostic application. + /// + MemoryDiagnostics = 5, + + /// + /// Legacy NT loader application. + /// + NtLoader = 6, + + /// + /// Windows setup application. + /// + SetupLoader = 7, + + /// + /// Boot sector application. + /// + BootSector = 8, + + /// + /// Startup application. + /// + Startup = 9 + } +} diff --git a/DiscUtils/BootConfig/BaseStorage.cs b/DiscUtils/BootConfig/BaseStorage.cs new file mode 100644 index 0000000..3322099 --- /dev/null +++ b/DiscUtils/BootConfig/BaseStorage.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Collections.Generic; + + /// + /// Base class for BCD storage repositories. + /// + internal abstract class BaseStorage + { + /// + /// Tests if an element is present (i.e. has a value) on an object. + /// + /// The object to inspect. + /// The element to inspect. + /// true if present, else false. + public abstract bool HasValue(Guid obj, int element); + + /// + /// Gets the value of a string element. + /// + /// The object to inspect. + /// The element to retrieve. + /// The value as a string. + public abstract string GetString(Guid obj, int element); + + public abstract byte[] GetBinary(Guid obj, int element); + + public abstract void SetString(Guid obj, int element, string value); + + public abstract void SetBinary(Guid obj, int element, byte[] value); + + public abstract string[] GetMultiString(Guid obj, int element); + + public abstract void SetMultiString(Guid obj, int element, string[] values); + + public abstract IEnumerable EnumerateObjects(); + + public abstract IEnumerable EnumerateElements(Guid obj); + + public abstract int GetObjectType(Guid obj); + + public abstract bool ObjectExists(Guid obj); + + public abstract Guid CreateObject(Guid obj, int type); + + public abstract void CreateElement(Guid obj, int element); + + public abstract void DeleteObject(Guid obj); + + public abstract void DeleteElement(Guid obj, int element); + } +} diff --git a/DiscUtils/BootConfig/BcdObject.cs b/DiscUtils/BootConfig/BcdObject.cs new file mode 100644 index 0000000..934ea3e --- /dev/null +++ b/DiscUtils/BootConfig/BcdObject.cs @@ -0,0 +1,365 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Symbolic names of BCD Objects taken from Geoff Chappell's website: +// http://www.geoffchappell.com/viewer.htm?doc=notes/windows/boot/bcd/objects.htm +// +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Collections.Generic; + + /// + /// Represents a Boot Configuration Database object (application, device or inherited settings). + /// + public class BcdObject + { + /// + /// Well-known object for Emergency Management Services settings. + /// + public const string EmsSettingsGroupId = "{0CE4991B-E6B3-4B16-B23C-5E0D9250E5D9}"; + + /// + /// Well-known object for the Resume boot loader. + /// + public const string ResumeLoaderSettingsGroupId = "{1AFA9C49-16AB-4A5C-4A90-212802DA9460}"; + + /// + /// Alias for the Default boot entry. + /// + public const string DefaultBootEntryId = "{1CAE1EB7-A0DF-4D4D-9851-4860E34EF535}"; + + /// + /// Well-known object for Emergency Management Services settings. + /// + public const string DebuggerSettingsGroupId = "{4636856E-540F-4170-A130-A84776F4C654}"; + + /// + /// Well-known object for NTLDR application. + /// + public const string WindowsLegacyNtldrId = "{466F5A88-0AF2-4F76-9038-095B170DC21C}"; + + /// + /// Well-known object for bad memory settings. + /// + public const string BadMemoryGroupId = "{5189B25C-5558-4BF2-BCA4-289B11BD29E2}"; + + /// + /// Well-known object for Boot Loader settings. + /// + public const string BootLoaderSettingsGroupId = "{6EFB52BF-1766-41DB-A6B3-0EE5EFF72BD7}"; + + /// + /// Well-known object for EFI setup. + /// + public const string WindowsSetupEfiId = "{7254A080-1510-4E85-AC0F-E7FB3D444736}"; + + /// + /// Well-known object for Global settings. + /// + public const string GlobalSettingsGroupId = "{7EA2E1AC-2E61-4728-AAA3-896D9D0A9F0E}"; + + /// + /// Well-known object for Windows Boot Manager. + /// + public const string WindowsBootManagerId = "{9DEA862C-5CDD-4E70-ACC1-F32B344D4795}"; + + /// + /// Well-known object for PCAT Template. + /// + public const string WindowsOsTargetTemplatePcatId = "{A1943BBC-EA85-487C-97C7-C9EDE908A38A}"; + + /// + /// Well-known object for Firmware Boot Manager. + /// + public const string FirmwareBootManagerId = "{A5A30FA2-3D06-4E9F-B5F4-A01DF9D1FCBA}"; + + /// + /// Well-known object for Windows Setup RAMDISK options. + /// + public const string WindowsSetupRamdiskOptionsId = "{AE5534E0-A924-466C-B836-758539A3EE3A}"; + + /// + /// Well-known object for EFI template. + /// + public const string WindowsOsTargetTemplateEfiId = "{B012B84D-C47C-4ED5-B722-C0C42163E569}"; + + /// + /// Well-known object for Windows memory tester application. + /// + public const string WindowsMemoryTesterId = "{B2721D73-1DB4-4C62-BF78-C548A880142D}"; + + /// + /// Well-known object for Windows PCAT setup. + /// + public const string WindowsSetupPcatId = "{CBD971BF-B7B8-4885-951A-FA03044F5D71}"; + + /// + /// Alias for the current boot entry. + /// + public const string CurrentBootEntryId = "{FA926493-6F1C-4193-A414-58F0B2456D1E}"; + + private static Dictionary s_nameToGuid; + private static Dictionary s_guidToName; + + private BaseStorage _storage; + private Guid _id; + private int _type; + + static BcdObject() + { + s_nameToGuid = new Dictionary(); + s_guidToName = new Dictionary(); + + AddMapping("{emssettings}", EmsSettingsGroupId); + AddMapping("{resumeloadersettings}", ResumeLoaderSettingsGroupId); + AddMapping("{default}", DefaultBootEntryId); + AddMapping("{dbgsettings}", DebuggerSettingsGroupId); + AddMapping("{legacy}", WindowsLegacyNtldrId); + AddMapping("{ntldr}", WindowsLegacyNtldrId); + AddMapping("{badmemory}", BadMemoryGroupId); + AddMapping("{bootloadersettings}", BootLoaderSettingsGroupId); + AddMapping("{globalsettings}", GlobalSettingsGroupId); + AddMapping("{bootmgr}", WindowsBootManagerId); + AddMapping("{fwbootmgr}", FirmwareBootManagerId); + AddMapping("{ramdiskoptions}", WindowsSetupRamdiskOptionsId); + AddMapping("{memdiag}", WindowsMemoryTesterId); + AddMapping("{current}", CurrentBootEntryId); + } + + internal BcdObject(BaseStorage store, Guid id) + { + _storage = store; + _id = id; + _type = _storage.GetObjectType(id); + } + + /// + /// Gets the identity of this object. + /// + public Guid Identity + { + get { return _id; } + } + + /// + /// Gets the friendly name for this object, if known. + /// + public string FriendlyName + { + get + { + string name; + if (s_guidToName.TryGetValue(_id, out name)) + { + return name; + } + + return _id.ToString("B"); + } + } + + /// + /// Gets the object type for this object. + /// + public ObjectType ObjectType + { + get { return (ObjectType)((_type >> 28) & 0xF); } + } + + /// + /// Gets the image type for this application. + /// + public ApplicationImageType ApplicationImageType + { + get { return IsApplication ? (ApplicationImageType)((_type & 0x00F00000) >> 20) : 0; } + } + + /// + /// Gets the application type for this application. + /// + public ApplicationType ApplicationType + { + get { return IsApplication ? (ApplicationType)(_type & 0xFFFFF) : 0; } + } + + /// + /// Gets the elements in this object. + /// + public IEnumerable Elements + { + get + { + foreach (var el in _storage.EnumerateElements(_id)) + { + yield return new Element(_storage, _id, ApplicationType, el); + } + } + } + + private bool IsApplication + { + get { return ObjectType == ObjectType.Application; } + } + + /// + /// Indicates if the settings in this object are inheritable by another object. + /// + /// The type of the object to test for inheritability. + /// true if the settings can be inherited, else false. + public bool IsInheritableBy(ObjectType type) + { + if (type == ObjectType.Inherit) + { + throw new ArgumentException("Can not test inheritability by inherit objects", "type"); + } + + if (ObjectType != ObjectType.Inherit) + { + return false; + } + + InheritType setting = (InheritType)((_type & 0x00F00000) >> 20); + + return setting == InheritType.AnyObject + || (setting == InheritType.ApplicationObjects && type == ObjectType.Application) + || (setting == InheritType.DeviceObjects && type == ObjectType.Device); + } + + /// + /// Indicates if this object has a specific element. + /// + /// The identity of the element to look for. + /// true if present, else false. + public bool HasElement(int id) + { + return _storage.HasValue(_id, id); + } + + /// + /// Indicates if this object has a specific element. + /// + /// The identity of the element to look for. + /// true if present, else false. + public bool HasElement(WellKnownElement id) + { + return HasElement((int)id); + } + + /// + /// Gets a specific element in this object. + /// + /// The identity of the element to look for. + /// The element object. + public Element GetElement(int id) + { + if (HasElement(id)) + { + return new Element(_storage, _id, ApplicationType, id); + } + + return null; + } + + /// + /// Gets a specific element in this object. + /// + /// The identity of the element to look for. + /// The element object. + public Element GetElement(WellKnownElement id) + { + return GetElement((int)id); + } + + /// + /// Adds an element in this object. + /// + /// The identity of the element to add. + /// The initial value of the element. + /// The element object. + public Element AddElement(int id, ElementValue initialValue) + { + _storage.CreateElement(_id, id); + Element el = new Element(_storage, _id, ApplicationType, id); + el.Value = initialValue; + return el; + } + + /// + /// Adds an element in this object. + /// + /// The identity of the element to add. + /// The initial value of the element. + /// The element object. + public Element AddElement(WellKnownElement id, ElementValue initialValue) + { + return AddElement((int)id, initialValue); + } + + /// + /// Removes a specific element. + /// + /// The element to remove. + public void RemoveElement(int id) + { + _storage.DeleteElement(_id, id); + } + + /// + /// Removes a specific element. + /// + /// The element to remove. + public void RemoveElement(WellKnownElement id) + { + RemoveElement((int)id); + } + + /// + /// Returns the object identity as a GUID string. + /// + /// A string representation, with surrounding curly braces. + public override string ToString() + { + return _id.ToString("B"); + } + + internal static int MakeApplicationType(ApplicationImageType imageType, ApplicationType appType) + { + return 0x10000000 | (((int)imageType << 20) & 0x00F00000) | ((int)appType & 0x0000FFFF); + } + + internal static int MakeInheritType(InheritType inheritType) + { + return 0x20000000 | (((int)inheritType << 20) & 0x00F00000); + } + + private static void AddMapping(string name, string id) + { + Guid guid = new Guid(id); + s_nameToGuid[name] = guid; + s_guidToName[guid] = name; + } + } +} diff --git a/DiscUtils/BootConfig/BooleanElementValue.cs b/DiscUtils/BootConfig/BooleanElementValue.cs new file mode 100644 index 0000000..39c2fe8 --- /dev/null +++ b/DiscUtils/BootConfig/BooleanElementValue.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + internal class BooleanElementValue : ElementValue + { + private bool _value; + + public BooleanElementValue(byte[] value) + { + _value = value[0] != 0; + } + + public BooleanElementValue(bool value) + { + _value = value; + } + + public override ElementFormat Format + { + get { return ElementFormat.Boolean; } + } + + public override string ToString() + { + return _value ? "True" : "False"; + } + + internal byte[] GetBytes() + { + return new byte[] { (_value ? (byte)1 : (byte)0) }; + } + } +} diff --git a/DiscUtils/BootConfig/DeviceAndPathRecord.cs b/DiscUtils/BootConfig/DeviceAndPathRecord.cs new file mode 100644 index 0000000..c86fe1b --- /dev/null +++ b/DiscUtils/BootConfig/DeviceAndPathRecord.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Text; + + internal class DeviceAndPathRecord : DeviceRecord + { + private DeviceRecord _container; + private string _path; + + public override int Size + { + get { throw new NotImplementedException(); } + } + + public override void GetBytes(byte[] data, int offset) + { + throw new NotImplementedException(); + } + + public override string ToString() + { + return _container.ToString() + ":" + _path; + } + + protected override void DoParse(byte[] data, int offset) + { + base.DoParse(data, offset); + + _container = DeviceRecord.Parse(data, offset + 0x34); + + int pathStart = 0x34 + _container.Size; + _path = Encoding.Unicode.GetString(data, offset + pathStart, Length - pathStart); + } + } +} diff --git a/DiscUtils/BootConfig/DeviceElementValue.cs b/DiscUtils/BootConfig/DeviceElementValue.cs new file mode 100644 index 0000000..65aff9a --- /dev/null +++ b/DiscUtils/BootConfig/DeviceElementValue.cs @@ -0,0 +1,114 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Globalization; + + internal class DeviceElementValue : ElementValue + { + private Guid _parentObject; + private DeviceRecord _record; + + public DeviceElementValue() + { + _parentObject = Guid.Empty; + + PartitionRecord record = new PartitionRecord(); + record.Type = 5; + _record = record; + } + + public DeviceElementValue(Guid parentObject, PhysicalVolumeInfo pvi) + { + _parentObject = parentObject; + + PartitionRecord record = new PartitionRecord(); + record.Type = 6; + if (pvi.VolumeType == PhysicalVolumeType.BiosPartition) + { + record.PartitionType = 1; + record.DiskIdentity = new byte[4]; + Utilities.WriteBytesLittleEndian(pvi.DiskSignature, record.DiskIdentity, 0); + record.PartitionIdentity = new byte[8]; + Utilities.WriteBytesLittleEndian(pvi.PhysicalStartSector * 512, record.PartitionIdentity, 0); + } + else if (pvi.VolumeType == PhysicalVolumeType.GptPartition) + { + record.PartitionType = 0; + record.DiskIdentity = new byte[16]; + Utilities.WriteBytesLittleEndian(pvi.DiskIdentity, record.DiskIdentity, 0); + record.PartitionIdentity = new byte[16]; + Utilities.WriteBytesLittleEndian(pvi.PartitionIdentity, record.PartitionIdentity, 0); + } + else + { + throw new NotImplementedException(string.Format(CultureInfo.InvariantCulture, "Unknown how to convert volume type {0} to a Device element", pvi.VolumeType)); + } + + _record = record; + } + + public DeviceElementValue(byte[] value) + { + _parentObject = Utilities.ToGuidLittleEndian(value, 0x00); + _record = DeviceRecord.Parse(value, 0x10); + } + + public override Guid ParentObject + { + get { return _parentObject; } + } + + public override ElementFormat Format + { + get { return ElementFormat.Device; } + } + + public override string ToString() + { + if (_parentObject != Guid.Empty) + { + return _parentObject.ToString() + ":" + _record.ToString(); + } + else if (_record != null) + { + return _record.ToString(); + } + else + { + return ""; + } + } + + internal byte[] GetBytes() + { + byte[] buffer = new byte[_record.Size + 0x10]; + + Utilities.WriteBytesLittleEndian(_parentObject, buffer, 0); + _record.GetBytes(buffer, 0x10); + + return buffer; + } + } +} diff --git a/DiscUtils/BootConfig/DeviceRecord.cs b/DiscUtils/BootConfig/DeviceRecord.cs new file mode 100644 index 0000000..2418d39 --- /dev/null +++ b/DiscUtils/BootConfig/DeviceRecord.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.IO; + + internal abstract class DeviceRecord + { + public int Type { get; set; } + + public int Length { get; set; } + + public abstract int Size { get; } + + public static DeviceRecord Parse(byte[] data, int offset) + { + int type = Utilities.ToInt32LittleEndian(data, offset); + int length = Utilities.ToInt32LittleEndian(data, offset + 0x8); + if (offset + length > data.Length) + { + throw new InvalidDataException("Device record is truncated"); + } + + DeviceRecord newRecord = null; + switch (type) + { + case 0: + newRecord = new DeviceAndPathRecord(); + break; + case 5: // Logical 'boot' device + case 6: // Disk partition + newRecord = new PartitionRecord(); + break; + case 8: // custom:nnnnnn + break; + default: + throw new NotImplementedException("Unknown device type: " + type); + } + + if (newRecord != null) + { + newRecord.DoParse(data, offset); + } + + return newRecord; + } + + public abstract void GetBytes(byte[] data, int offset); + + protected virtual void DoParse(byte[] data, int offset) + { + Type = Utilities.ToInt32LittleEndian(data, offset); + Length = Utilities.ToInt32LittleEndian(data, offset + 0x8); + } + + protected void WriteHeader(byte[] data, int offset) + { + Length = Size; + Utilities.WriteBytesLittleEndian(Type, data, offset); + Utilities.WriteBytesLittleEndian(Size, data, offset + 0x8); + } + } +} diff --git a/DiscUtils/BootConfig/DiscUtilsRegistryStorage.cs b/DiscUtils/BootConfig/DiscUtilsRegistryStorage.cs new file mode 100644 index 0000000..0bdb919 --- /dev/null +++ b/DiscUtils/BootConfig/DiscUtilsRegistryStorage.cs @@ -0,0 +1,162 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using DiscUtils.Registry; + + internal class DiscUtilsRegistryStorage : BaseStorage + { + private const string ElementsPathTemplate = @"Objects\{0}\Elements"; + private const string ElementPathTemplate = @"Objects\{0}\Elements\{1:X8}"; + private const string ObjectTypePathTemplate = @"Objects\{0}\Description"; + private const string ObjectsPath = @"Objects"; + + private RegistryKey _rootKey; + + public DiscUtilsRegistryStorage(RegistryKey key) + { + _rootKey = key; + } + + public override string GetString(Guid obj, int element) + { + return GetValue(obj, element) as string; + } + + public override void SetString(Guid obj, int element, string value) + { + SetValue(obj, element, value); + } + + public override byte[] GetBinary(Guid obj, int element) + { + return GetValue(obj, element) as byte[]; + } + + public override void SetBinary(Guid obj, int element, byte[] value) + { + SetValue(obj, element, value); + } + + public override string[] GetMultiString(Guid obj, int element) + { + return GetValue(obj, element) as string[]; + } + + public override void SetMultiString(Guid obj, int element, string[] values) + { + SetValue(obj, element, values); + } + + public override IEnumerable EnumerateObjects() + { + RegistryKey parentKey = _rootKey.OpenSubKey("Objects"); + foreach (var key in parentKey.GetSubKeyNames()) + { + yield return new Guid(key); + } + } + + public override IEnumerable EnumerateElements(Guid obj) + { + string path = string.Format(CultureInfo.InvariantCulture, ElementsPathTemplate, obj.ToString("B")); + RegistryKey parentKey = _rootKey.OpenSubKey(path); + foreach (var key in parentKey.GetSubKeyNames()) + { + yield return int.Parse(key, NumberStyles.HexNumber); + } + } + + public override int GetObjectType(Guid obj) + { + string path = string.Format(CultureInfo.InvariantCulture, ObjectTypePathTemplate, obj.ToString("B")); + + RegistryKey descKey = _rootKey.OpenSubKey(path); + + object val = descKey.GetValue("Type"); + return (int)val; + } + + public override bool HasValue(Guid obj, int element) + { + string path = string.Format(CultureInfo.InvariantCulture, ElementPathTemplate, obj.ToString("B"), element); + return _rootKey.OpenSubKey(path) != null; + } + + public override bool ObjectExists(Guid obj) + { + string path = string.Format(CultureInfo.InvariantCulture, ObjectTypePathTemplate, obj.ToString("B")); + + return _rootKey.OpenSubKey(path) != null; + } + + public override Guid CreateObject(Guid obj, int type) + { + Guid realObj = (obj == Guid.Empty) ? Guid.NewGuid() : obj; + string path = string.Format(CultureInfo.InvariantCulture, ObjectTypePathTemplate, realObj.ToString("B")); + + RegistryKey key = _rootKey.CreateSubKey(path); + key.SetValue("Type", type, RegistryValueType.Dword); + + return realObj; + } + + public override void CreateElement(Guid obj, int element) + { + string path = string.Format(CultureInfo.InvariantCulture, ElementPathTemplate, obj.ToString("B"), element); + + _rootKey.CreateSubKey(path); + } + + public override void DeleteObject(Guid obj) + { + string path = string.Format(CultureInfo.InvariantCulture, ObjectTypePathTemplate, obj.ToString("B")); + + _rootKey.DeleteSubKeyTree(path); + } + + public override void DeleteElement(Guid obj, int element) + { + string path = string.Format(CultureInfo.InvariantCulture, ElementPathTemplate, obj.ToString("B"), element); + + _rootKey.DeleteSubKeyTree(path); + } + + private object GetValue(Guid obj, int element) + { + string path = string.Format(CultureInfo.InvariantCulture, ElementPathTemplate, obj.ToString("B"), element); + RegistryKey key = _rootKey.OpenSubKey(path); + return key.GetValue("Element"); + } + + private void SetValue(Guid obj, int element, object value) + { + string path = string.Format(CultureInfo.InvariantCulture, ElementPathTemplate, obj.ToString("B"), element); + RegistryKey key = _rootKey.OpenSubKey(path); + key.SetValue("Element", value); + } + } +} diff --git a/DiscUtils/BootConfig/Element.cs b/DiscUtils/BootConfig/Element.cs new file mode 100644 index 0000000..a75c4f2 --- /dev/null +++ b/DiscUtils/BootConfig/Element.cs @@ -0,0 +1,390 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Symbolic names of BCD Elements taken from Geoff Chappell's website: +// http://www.geoffchappell.com/viewer.htm?doc=notes/windows/boot/bcd/elements.htm +// +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Globalization; + + /// + /// Represents an element in a Boot Configuration Database object. + /// + public class Element + { + private BaseStorage _storage; + private Guid _obj; + private ApplicationType _appType; + private int _identifier; + private ElementValue _value; + + internal Element(BaseStorage storage, Guid obj, ApplicationType appType, int identifier) + { + _storage = storage; + _obj = obj; + _appType = appType; + _identifier = identifier; + } + + /// + /// Gets the friendly name of the element, if any. + /// + public string FriendlyName + { + get + { + return "{" + IdentifierToName(_appType, _identifier) + "}"; + } + } + + /// + /// Gets the class of the element. + /// + public ElementClass Class + { + get { return (ElementClass)((_identifier >> 28) & 0xF); } + } + + /// + /// Gets the element's format. + /// + public ElementFormat Format + { + get { return (ElementFormat)((_identifier >> 24) & 0xF); } + } + + /// + /// Gets or sets the element's value. + /// + public ElementValue Value + { + get + { + if (_value == null) + { + _value = LoadValue(); + } + + return _value; + } + + set + { + if (Format != value.Format) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Attempt to assign {1} value to {0} format element", Format, value.Format)); + } + + _value = value; + WriteValue(); + } + } + + /// + /// Gets the element's id as a hex string. + /// + /// A hex string. + public override string ToString() + { + return _identifier.ToString("X8", CultureInfo.InvariantCulture); + } + + private static string IdentifierToName(ApplicationType appType, int identifier) + { + ElementClass idClass = GetClass(identifier); + if (idClass == ElementClass.Library) + { + switch (identifier) + { + case 0x11000001: return "device"; + case 0x12000002: return "path"; + case 0x12000004: return "description"; + case 0x12000005: return "locale"; + case 0x14000006: return "inherit"; + case 0x15000007: return "truncatememory"; + case 0x14000008: return "recoverysequence"; + case 0x16000009: return "recoveryenabled"; + case 0x1700000A: return "badmemorylist"; + case 0x1600000B: return "badmemoryaccess"; + case 0x1500000C: return "firstmegabytepolicy"; + + case 0x16000010: return "bootdebug"; + case 0x15000011: return "debugtype"; + case 0x15000012: return "debugaddress"; + case 0x15000013: return "debugport"; + case 0x15000014: return "baudrate"; + case 0x15000015: return "channel"; + case 0x12000016: return "targetname"; + case 0x16000017: return "noumex"; + case 0x15000018: return "debugstart"; + + case 0x16000020: return "bootems"; + case 0x15000022: return "emsport"; + case 0x15000023: return "emsbaudrate"; + + case 0x12000030: return "loadoptions"; + + case 0x16000040: return "advancedoptions"; + case 0x16000041: return "optionsedit"; + case 0x15000042: return "keyringaddress"; + case 0x16000046: return "graphicsmodedisabled"; + case 0x15000047: return "configaccesspolicy"; + case 0x16000048: return "nointegritychecks"; + case 0x16000049: return "testsigning"; + case 0x16000050: return "extendedinput"; + case 0x15000051: return "initialconsoleinput"; + } + } + else if (idClass == ElementClass.Application) + { + switch (appType) + { + case ApplicationType.FirmwareBootManager: + case ApplicationType.BootManager: + switch (identifier) + { + case 0x24000001: return "displayorder"; + case 0x24000002: return "bootsequence"; + case 0x23000003: return "default"; + case 0x25000004: return "timeout"; + case 0x26000005: return "resume"; + case 0x23000006: return "resumeobject"; + + case 0x24000010: return "toolsdisplayorder"; + + case 0x26000020: return "displaybootmenu"; + case 0x26000021: return "noerrordisplay"; + case 0x21000022: return "bcddevice"; + case 0x22000023: return "bcdfilepath"; + + case 0x27000030: return "customactions"; + } + + break; + + case ApplicationType.OsLoader: + switch (identifier) + { + case 0x21000001: return "osdevice"; + case 0x22000002: return "systemroot"; + case 0x23000003: return "resumeobject"; + + case 0x26000010: return "detecthal"; + case 0x22000011: return "kernel"; + case 0x22000012: return "hal"; + case 0x22000013: return "dbgtransport"; + + case 0x25000020: return "nx"; + case 0x25000021: return "pae"; + case 0x26000022: return "winpe"; + case 0x26000024: return "nocrashautoreboot"; + case 0x26000025: return "lastknowngood"; + case 0x26000026: return "oslnointegritychecks"; + case 0x26000027: return "osltestsigning"; + + case 0x26000030: return "nolowmem"; + case 0x25000031: return "removememory"; + case 0x25000032: return "increaseuserva"; + case 0x25000033: return "perfmem"; + + case 0x26000040: return "vga"; + case 0x26000041: return "quietboot"; + case 0x26000042: return "novesa"; + + case 0x25000050: return "clustermodeaddressing"; + case 0x26000051: return "usephysicaldestination"; + case 0x25000052: return "restrictapiccluster"; + + case 0x26000060: return "onecpu"; + case 0x25000061: return "numproc"; + case 0x26000062: return "maxproc"; + case 0x25000063: return "configflags"; + + case 0x26000070: return "usefirmwarepcisettings"; + case 0x25000071: return "msi"; + case 0x25000072: return "pciexpress"; + + case 0x25000080: return "safeboot"; + case 0x26000081: return "safebootalternateshell"; + + case 0x26000090: return "bootlog"; + case 0x26000091: return "sos"; + + case 0x260000A0: return "debug"; + case 0x260000A1: return "halbreakpoint"; + + case 0x260000B0: return "ems"; + + case 0x250000C0: return "forcefailure"; + case 0x250000C1: return "driverloadfailurepolicy"; + + case 0x250000E0: return "bootstatuspolicy"; + } + + break; + + case ApplicationType.Resume: + switch (identifier) + { + case 0x21000001: return "filedevice"; + case 0x22000002: return "filepath"; + case 0x26000003: return "customsettings"; + case 0x26000004: return "pae"; + case 0x21000005: return "associatedosdevice"; + case 0x26000006: return "debugoptionenabled"; + } + + break; + + case ApplicationType.MemoryDiagnostics: + switch (identifier) + { + case 0x25000001: return "passcount"; + case 0x25000002: return "testmix"; + case 0x25000003: return "failurecount"; + case 0x25000004: return "testtofail"; + } + + break; + + case ApplicationType.NtLoader: + case ApplicationType.SetupLoader: + switch (identifier) + { + case 0x22000001: return "bpbstring"; + } + + break; + + case ApplicationType.Startup: + switch (identifier) + { + case 0x26000001: return "pxesoftreboot"; + case 0x22000002: return "applicationname"; + } + + break; + } + } + else if (idClass == ElementClass.Device) + { + switch (identifier) + { + case 0x35000001: return "ramdiskimageoffset"; + case 0x35000002: return "ramdisktftpclientport"; + case 0x31000003: return "ramdisksdidevice"; + case 0x32000004: return "ramdisksdipath"; + case 0x35000005: return "ramdiskimagelength"; + case 0x36000006: return "exportascd"; + case 0x35000007: return "ramdisktftpblocksize"; + } + } + else if (idClass == ElementClass.Hidden) + { + switch (identifier) + { + case 0x45000001: return "devicetype"; + case 0x42000002: return "apprelativepath"; + case 0x42000003: return "ramdiskdevicerelativepath"; + case 0x46000004: return "omitosloaderelements"; + case 0x46000010: return "recoveryos"; + } + } + + return identifier.ToString("X8", CultureInfo.InvariantCulture); + } + + private static ElementClass GetClass(int identifier) + { + return (ElementClass)((identifier >> 28) & 0xF); + } + + private ElementValue LoadValue() + { + switch (Format) + { + case ElementFormat.Boolean: + return new BooleanElementValue(_storage.GetBinary(_obj, _identifier)); + + case ElementFormat.Device: + return new DeviceElementValue(_storage.GetBinary(_obj, _identifier)); + + case ElementFormat.Guid: + return new GuidElementValue(_storage.GetString(_obj, _identifier)); + + case ElementFormat.GuidList: + return new GuidListElementValue(_storage.GetMultiString(_obj, _identifier)); + + case ElementFormat.Integer: + return new IntegerElementValue(_storage.GetBinary(_obj, _identifier)); + + case ElementFormat.IntegerList: + return new IntegerListElementValue(_storage.GetBinary(_obj, _identifier)); + + case ElementFormat.String: + return new StringElementValue(_storage.GetString(_obj, _identifier)); + + default: + throw new NotImplementedException("Unknown element format: " + Format); + } + } + + private void WriteValue() + { + switch (_value.Format) + { + case ElementFormat.Boolean: + _storage.SetBinary(_obj, _identifier, ((BooleanElementValue)_value).GetBytes()); + break; + + case ElementFormat.Device: + _storage.SetBinary(_obj, _identifier, ((DeviceElementValue)_value).GetBytes()); + break; + + case ElementFormat.GuidList: + _storage.SetMultiString(_obj, _identifier, ((GuidListElementValue)_value).GetGuidStrings()); + break; + + case ElementFormat.Integer: + _storage.SetBinary(_obj, _identifier, ((IntegerElementValue)_value).GetBytes()); + break; + + case ElementFormat.IntegerList: + _storage.SetBinary(_obj, _identifier, ((IntegerListElementValue)_value).GetBytes()); + break; + + case ElementFormat.Guid: + case ElementFormat.String: + _storage.SetString(_obj, _identifier, _value.ToString()); + break; + + default: + throw new NotImplementedException("Unknown element format: " + Format); + } + } + } +} diff --git a/DiscUtils/BootConfig/ElementClass.cs b/DiscUtils/BootConfig/ElementClass.cs new file mode 100644 index 0000000..cf8aecd --- /dev/null +++ b/DiscUtils/BootConfig/ElementClass.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + /// + /// The known classes of element. + /// + public enum ElementClass + { + /// + /// Unknown class. + /// + None = 0, + + /// + /// Common setting. + /// + Library = 1, + + /// + /// An application setting. + /// + Application = 2, + + /// + /// A device (or partition) setting. + /// + Device = 3, + + /// + /// A hidden setting. + /// + Hidden = 4 + } +} diff --git a/DiscUtils/BootConfig/ElementFormat.cs b/DiscUtils/BootConfig/ElementFormat.cs new file mode 100644 index 0000000..49101e3 --- /dev/null +++ b/DiscUtils/BootConfig/ElementFormat.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + /// + /// The known formats used to store BCD values. + /// + public enum ElementFormat + { + /// + /// Unknown format. + /// + None = 0, + + /// + /// A block device, or partition. + /// + Device = 1, + + /// + /// A unicode string. + /// + String = 2, + + /// + /// A Globally Unique Identifier (GUID). + /// + Guid = 3, + + /// + /// A GUID list. + /// + GuidList = 4, + + /// + /// An integer. + /// + Integer = 5, + + /// + /// A boolean. + /// + Boolean = 6, + + /// + /// An integer list. + /// + IntegerList = 7 + } +} diff --git a/DiscUtils/BootConfig/ElementValue.cs b/DiscUtils/BootConfig/ElementValue.cs new file mode 100644 index 0000000..9d40ad9 --- /dev/null +++ b/DiscUtils/BootConfig/ElementValue.cs @@ -0,0 +1,138 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Globalization; + + /// + /// The value of an element. + /// + public abstract class ElementValue + { + /// + /// Gets the format of the value. + /// + public abstract ElementFormat Format { get; } + + /// + /// Gets the parent object (only for Device values). + /// + public virtual Guid ParentObject + { + get { return Guid.Empty; } + } + + /// + /// Gets a value representing a device (aka partition). + /// + /// Object containing detailed information about the device. + /// The volume to represent. + /// The value as an object. + public static ElementValue ForDevice(Guid parentObject, PhysicalVolumeInfo physicalVolume) + { + return new DeviceElementValue(parentObject, physicalVolume); + } + + /// + /// Gets a value representing the logical boot device. + /// + /// The boot pseudo-device as an object. + public static ElementValue ForBootDevice() + { + return new DeviceElementValue(); + } + + /// + /// Gets a value representing a string value. + /// + /// The value to convert. + /// The value as an object. + public static ElementValue ForString(string value) + { + return new StringElementValue(value); + } + + /// + /// Gets a value representing an integer value. + /// + /// The value to convert. + /// The value as an object. + public static ElementValue ForInteger(long value) + { + return new IntegerElementValue((ulong)value); + } + + /// + /// Gets a value representing an integer list value. + /// + /// The value to convert. + /// The value as an object. + public static ElementValue ForIntegerList(long[] values) + { + ulong[] ulValues = new ulong[values.Length]; + for (int i = 0; i < values.Length; ++i) + { + ulValues[i] = (ulong)values[i]; + } + + return new IntegerListElementValue(ulValues); + } + + /// + /// Gets a value representing a boolean value. + /// + /// The value to convert. + /// The value as an object. + public static ElementValue ForBoolean(bool value) + { + return new BooleanElementValue(value); + } + + /// + /// Gets a value representing a GUID value. + /// + /// The value to convert. + /// The value as an object. + public static ElementValue ForGuid(Guid value) + { + return new GuidElementValue(value.ToString("B")); + } + + /// + /// Gets a value representing a GUID list value. + /// + /// The value to convert. + /// The value as an object. + public static ElementValue ForGuidList(Guid[] values) + { + string[] strValues = new string[values.Length]; + for (int i = 0; i < values.Length; ++i) + { + strValues[i] = values[i].ToString("B"); + } + + return new GuidListElementValue(strValues); + } + } +} diff --git a/DiscUtils/BootConfig/GuidElementValue.cs b/DiscUtils/BootConfig/GuidElementValue.cs new file mode 100644 index 0000000..d6d89f7 --- /dev/null +++ b/DiscUtils/BootConfig/GuidElementValue.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + internal class GuidElementValue : ElementValue + { + private string _value; + + public GuidElementValue(string value) + { + _value = value; + } + + public override ElementFormat Format + { + get { return ElementFormat.Guid; } + } + + public override string ToString() + { + if (string.IsNullOrEmpty(_value)) + { + return ""; + } + + return _value; + } + } +} diff --git a/DiscUtils/BootConfig/GuidListElementValue.cs b/DiscUtils/BootConfig/GuidListElementValue.cs new file mode 100644 index 0000000..edad6cd --- /dev/null +++ b/DiscUtils/BootConfig/GuidListElementValue.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + internal class GuidListElementValue : ElementValue + { + private string[] _values; + + public GuidListElementValue(string[] values) + { + _values = values; + } + + public override ElementFormat Format + { + get { return ElementFormat.GuidList; } + } + + public override string ToString() + { + if (_values == null || _values.Length == 0) + { + return ""; + } + + string result = _values[0]; + for (int i = 1; i < _values.Length; ++i) + { + result += "," + _values[i]; + } + + return result; + } + + internal string[] GetGuidStrings() + { + return _values; + } + } +} diff --git a/DiscUtils/BootConfig/InheritType.cs b/DiscUtils/BootConfig/InheritType.cs new file mode 100644 index 0000000..4a4c86b --- /dev/null +++ b/DiscUtils/BootConfig/InheritType.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// Indicates the type of objects that can inherit from an object. + /// + public enum InheritType + { + /// + /// Undefined value. + /// + None = 0x0, + + /// + /// Any type of object may inherit from this object. + /// + AnyObject = 0x1, + + /// + /// Only Application objects may inherit from this object. + /// + ApplicationObjects = 0x2, + + /// + /// Only Device objects may inherit from this object. + /// + DeviceObjects = 0x3 + } +} diff --git a/DiscUtils/BootConfig/IntegerElementValue.cs b/DiscUtils/BootConfig/IntegerElementValue.cs new file mode 100644 index 0000000..8deea77 --- /dev/null +++ b/DiscUtils/BootConfig/IntegerElementValue.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Globalization; + + internal class IntegerElementValue : ElementValue + { + private ulong _value; + + public IntegerElementValue(byte[] value) + { + // Actual bytes stored may be less than 8 + byte[] buffer = new byte[8]; + Array.Copy(value, buffer, value.Length); + + _value = Utilities.ToUInt64LittleEndian(buffer, 0); + } + + public IntegerElementValue(ulong value) + { + _value = value; + } + + public override ElementFormat Format + { + get { return ElementFormat.Integer; } + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0}", _value); + } + + internal byte[] GetBytes() + { + byte[] bytes = new byte[8]; + Utilities.WriteBytesLittleEndian(_value, bytes, 0); + return bytes; + } + } +} diff --git a/DiscUtils/BootConfig/IntegerListElementValue.cs b/DiscUtils/BootConfig/IntegerListElementValue.cs new file mode 100644 index 0000000..ab2fb11 --- /dev/null +++ b/DiscUtils/BootConfig/IntegerListElementValue.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System.Globalization; + + internal class IntegerListElementValue : ElementValue + { + private ulong[] _values; + + public IntegerListElementValue(byte[] value) + { + _values = new ulong[value.Length / 8]; + for (int i = 0; i < _values.Length; ++i) + { + _values[i] = Utilities.ToUInt64LittleEndian(value, i * 8); + } + } + + public IntegerListElementValue(ulong[] values) + { + _values = values; + } + + public override ElementFormat Format + { + get { return ElementFormat.IntegerList; } + } + + public override string ToString() + { + if (_values == null || _values.Length == 0) + { + return ""; + } + + string result = string.Empty; + for (int i = 0; i < _values.Length; ++i) + { + if (i != 0) + { + result += " "; + } + + result += _values[i].ToString("X16", CultureInfo.InvariantCulture); + } + + return result; + } + + internal byte[] GetBytes() + { + byte[] bytes = new byte[_values.Length * 8]; + for (int i = 0; i < _values.Length; ++i) + { + Utilities.WriteBytesLittleEndian(_values[i], bytes, i * 8); + } + + return bytes; + } + } +} diff --git a/DiscUtils/BootConfig/ObjectType.cs b/DiscUtils/BootConfig/ObjectType.cs new file mode 100644 index 0000000..89db8b8 --- /dev/null +++ b/DiscUtils/BootConfig/ObjectType.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + /// + /// Enumeration of known object types. + /// + public enum ObjectType + { + /// + /// An unknown type. + /// + None = 0x0, + + /// + /// An application object. + /// + Application = 0x1, + + /// + /// Inheritable common settings. + /// + Inherit = 0x2, + + /// + /// Device (or partition) object. + /// + Device = 0x3 + } +} diff --git a/DiscUtils/BootConfig/PartitionRecord.cs b/DiscUtils/BootConfig/PartitionRecord.cs new file mode 100644 index 0000000..537084b --- /dev/null +++ b/DiscUtils/BootConfig/PartitionRecord.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Globalization; + + internal class PartitionRecord : DeviceRecord + { + public int PartitionType { get; set; } + + public byte[] DiskIdentity { get; set; } + + public byte[] PartitionIdentity { get; set; } + + public override int Size + { + get { return 0x48; } + } + + public override void GetBytes(byte[] data, int offset) + { + WriteHeader(data, offset); + + if (Type == 5) + { + Array.Clear(data, offset + 0x10, 0x38); + } + else if (Type == 6) + { + Utilities.WriteBytesLittleEndian(PartitionType, data, offset + 0x24); + + if (PartitionType == 1) + { + Array.Copy(DiskIdentity, 0, data, offset + 0x28, 4); + Array.Copy(PartitionIdentity, 0, data, offset + 0x10, 8); + } + else if (PartitionType == 0) + { + Array.Copy(DiskIdentity, 0, data, offset + 0x28, 16); + Array.Copy(PartitionIdentity, 0, data, offset + 0x10, 16); + } + else + { + throw new NotImplementedException("Unknown partition type: " + PartitionType); + } + } + else + { + throw new NotImplementedException("Unknown device type: " + Type); + } + } + + public override string ToString() + { + if (Type == 5) + { + return ""; + } + else if (Type == 6) + { + if (PartitionType == 1) + { + return string.Format( + CultureInfo.InvariantCulture, + "(disk:{0:X2}{1:X2}{2:X2}{3:X2} part-offset:{4})", + DiskIdentity[0], + DiskIdentity[1], + DiskIdentity[2], + DiskIdentity[3], + Utilities.ToUInt64LittleEndian(PartitionIdentity, 0)); + } + else + { + Guid diskGuid = Utilities.ToGuidLittleEndian(DiskIdentity, 0); + Guid partitionGuid = Utilities.ToGuidLittleEndian(PartitionIdentity, 0); + return string.Format(CultureInfo.InvariantCulture, "(disk:{0} partition:{1})", diskGuid, partitionGuid); + } + } + else if (Type == 8) + { + return "custom:"; + } + else + { + return ""; + } + } + + protected override void DoParse(byte[] data, int offset) + { + base.DoParse(data, offset); + + if (Type == 5) + { + // Nothing to do - just empty... + } + else if (Type == 6) + { + PartitionType = Utilities.ToInt32LittleEndian(data, offset + 0x24); + + if (PartitionType == 1) + { + // BIOS disk + DiskIdentity = new byte[4]; + Array.Copy(data, offset + 0x28, DiskIdentity, 0, 4); + PartitionIdentity = new byte[8]; + Array.Copy(data, offset + 0x10, PartitionIdentity, 0, 8); + } + else if (PartitionType == 0) + { + // GPT disk + DiskIdentity = new byte[16]; + Array.Copy(data, offset + 0x28, DiskIdentity, 0, 16); + PartitionIdentity = new byte[16]; + Array.Copy(data, offset + 0x10, PartitionIdentity, 0, 16); + } + else + { + throw new NotImplementedException("Unknown partition type: " + PartitionType); + } + } + else + { + throw new NotImplementedException("Unknown device type: " + Type); + } + } + } +} diff --git a/DiscUtils/BootConfig/Store.cs b/DiscUtils/BootConfig/Store.cs new file mode 100644 index 0000000..56450d5 --- /dev/null +++ b/DiscUtils/BootConfig/Store.cs @@ -0,0 +1,168 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + using System; + using System.Collections.Generic; + using DiscUtils.Registry; + + /// + /// Represents a Boot Configuration Database store (i.e. a BCD file). + /// + public class Store + { + private BaseStorage _store; + + /// + /// Initializes a new instance of the Store class. + /// + /// The registry key that is the root of the configuration database. + public Store(RegistryKey key) + { + _store = new DiscUtilsRegistryStorage(key); + } + + /// + /// Gets the objects within the store. + /// + public IEnumerable Objects + { + get + { + foreach (var obj in _store.EnumerateObjects()) + { + yield return new BcdObject(_store, obj); + } + } + } + + /// + /// Initializes a new Boot Configuration Database. + /// + /// The registry key at the root of the database. + /// The BCD store. + public static Store Initialize(RegistryKey root) + { + RegistryKey descKey = root.CreateSubKey("Description"); + descKey.SetValue("KeyName", "BCD00000001"); + root.CreateSubKey("Objects"); + return new Store(root); + } + + /// + /// Gets an object from the store. + /// + /// The identity of the object. + /// The requested object, or null. + public BcdObject GetObject(Guid id) + { + if (_store.ObjectExists(id)) + { + return new BcdObject(_store, id); + } + else + { + return null; + } + } + + /// + /// Creates a specific object. + /// + /// The identity of the object to create. + /// The object's type. + /// The object representing the new application. + public BcdObject CreateObject(Guid id, int type) + { + _store.CreateObject(id, type); + return new BcdObject(_store, id); + } + + /// + /// Creates an application object. + /// + /// The image type of the application. + /// The application's type. + /// The object representing the new application. + public BcdObject CreateApplication(ApplicationImageType imageType, ApplicationType appType) + { + Guid obj = _store.CreateObject(Guid.NewGuid(), BcdObject.MakeApplicationType(imageType, appType)); + return new BcdObject(_store, obj); + } + + /// + /// Creates an application object. + /// + /// The identity of the object to create. + /// The image type of the application. + /// The application's type. + /// The object representing the new application. + public BcdObject CreateApplication(Guid id, ApplicationImageType imageType, ApplicationType appType) + { + Guid obj = _store.CreateObject(id, BcdObject.MakeApplicationType(imageType, appType)); + return new BcdObject(_store, obj); + } + + /// + /// Creates an 'inherit' object that contains common settings. + /// + /// The type of object the settings apply to. + /// The object representing the new settings. + public BcdObject CreateInherit(InheritType inheritType) + { + Guid obj = _store.CreateObject(Guid.NewGuid(), BcdObject.MakeInheritType(inheritType)); + return new BcdObject(_store, obj); + } + + /// + /// Creates an 'inherit' object that contains common settings. + /// + /// The identity of the object to create. + /// The type of object the settings apply to. + /// The object representing the new settings. + public BcdObject CreateInherit(Guid id, InheritType inheritType) + { + Guid obj = _store.CreateObject(id, BcdObject.MakeInheritType(inheritType)); + return new BcdObject(_store, obj); + } + + /// + /// Creates a new device object, representing a partition. + /// + /// The object representing the new device. + public BcdObject CreateDevice() + { + Guid obj = _store.CreateObject(Guid.NewGuid(), 0x30000000); + return new BcdObject(_store, obj); + } + + /// + /// Removes an object. + /// + /// The identity of the object to remove. + public void RemoveObject(Guid id) + { + _store.DeleteObject(id); + } + } +} diff --git a/DiscUtils/BootConfig/StringElementValue.cs b/DiscUtils/BootConfig/StringElementValue.cs new file mode 100644 index 0000000..39ff24e --- /dev/null +++ b/DiscUtils/BootConfig/StringElementValue.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.BootConfig +{ + internal class StringElementValue : ElementValue + { + private string _value; + + public StringElementValue(string value) + { + _value = value; + } + + public override ElementFormat Format + { + get { return ElementFormat.String; } + } + + public override string ToString() + { + return _value; + } + } +} diff --git a/DiscUtils/BootConfig/WellKnownElement.cs b/DiscUtils/BootConfig/WellKnownElement.cs new file mode 100644 index 0000000..84ae8b5 --- /dev/null +++ b/DiscUtils/BootConfig/WellKnownElement.cs @@ -0,0 +1,618 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Symbolic names of BCD Elements taken from Geoff Chappell's website: +// http://www.geoffchappell.com/viewer.htm?doc=notes/windows/boot/bcd/elements.htm +// +// + +namespace DiscUtils.BootConfig +{ + /// + /// Enumeration of known BCD elements. + /// + public enum WellKnownElement : int + { + /// + /// Not specified. + /// + None = 0, + + /// + /// Device containing the application. + /// + LibraryApplicationDevice = 0x11000001, + + /// + /// Path to the application. + /// + LibraryApplicationPath = 0x12000002, + + /// + /// Description of the object. + /// + LibraryDescription = 0x12000004, + + /// + /// Preferred locale of the object. + /// + LibraryPreferredLocale = 0x12000005, + + /// + /// Objects containing elements inherited by the object. + /// + LibraryInheritedObjects = 0x14000006, + + /// + /// Upper bound on physical addresses used by Windows. + /// + LibraryTruncatePhysicalMemory = 0x15000007, + + /// + /// List of objects, indicating recovery sequence. + /// + LibraryRecoverySequence = 0x14000008, + + /// + /// Enables auto recovery. + /// + LibraryAutoRecoveryEnabled = 0x16000009, + + /// + /// List of bad memory regions. + /// + LibraryBadMemoryList = 0x1700000A, + + /// + /// Allow use of bad memory regions. + /// + LibraryAllowBadMemoryAccess = 0x1600000B, + + /// + /// Policy on use of first mega-byte of physical RAM. + /// + /// 0 = UseNone, 1 = UseAll, 2 = UsePrivate. + LibraryFirstMegaBytePolicy = 0x1500000C, + + /// + /// Debugger enabled. + /// + LibraryDebuggerEnabled = 0x16000010, + + /// + /// Debugger type. + /// + /// 0 = Serial, 1 = 1394, 2 = USB. + LibraryDebuggerType = 0x15000011, + + /// + /// Debugger serial port address. + /// + LibraryDebuggerSerialAddress = 0x15000012, + + /// + /// Debugger serial port. + /// + LibraryDebuggerSerialPort = 0x15000013, + + /// + /// Debugger serial port baud rate. + /// + LibraryDebuggerSerialBaudRate = 0x15000014, + + /// + /// Debugger 1394 channel. + /// + LibraryDebugger1394Channel = 0x15000015, + + /// + /// Debugger USB target name. + /// + LibraryDebuggerUsbTargetName = 0x12000016, + + /// + /// Debugger ignores user mode exceptions. + /// + LibraryDebuggerIgnoreUserModeExceptions = 0x16000017, + + /// + /// Debugger start policy. + /// + /// 0 = Active, 1 = AutoEnable, 2 = Disable. + LibraryDebuggerStartPolicy = 0x15000018, + + /// + /// Emergency Management System enabled. + /// + LibraryEmergencyManagementSystemEnabled = 0x16000020, + + /// + /// Emergency Management System serial port. + /// + LibraryEmergencyManagementSystemPort = 0x15000022, + + /// + /// Emergency Management System baud rate. + /// + LibraryEmergencyManagementSystemBaudRate = 0x15000023, + + /// + /// Load options. + /// + LibraryLoadOptions = 0x12000030, + + /// + /// Displays advanced options. + /// + LibraryDisplayAdvancedOptions = 0x16000040, + + /// + /// Displays UI to edit advanced options. + /// + LibraryDisplayOptionsEdit = 0x16000041, + + /// + /// FVE (Full Volume Encryption - aka BitLocker?) KeyRing address. + /// + LibraryFveKeyRingAddress = 0x16000042, + + /// + /// Device to contain Boot Status Log. + /// + LibraryBootStatusLogDevice = 0x11000043, + + /// + /// Path to Boot Status Log. + /// + LibraryBootStatusLogFile = 0x12000044, + + /// + /// Whether to append to the existing Boot Status Log. + /// + LibraryBootStatusLogAppend = 0x12000045, + + /// + /// Disables graphics mode. + /// + LibraryGraphicsModeDisabled = 0x16000046, + + /// + /// Configure access policy. + /// + /// 0 = default, 1 = DisallowMmConfig. + LibraryConfigAccessPolicy = 0x15000047, + + /// + /// Disables integrity checks. + /// + LibraryDisableIntegrityChecks = 0x16000048, + + /// + /// Allows pre-release signatures (test signing). + /// + LibraryAllowPrereleaseSignatures = 0x16000049, + + /// + /// Console extended input. + /// + LibraryConsoleExtendedInput = 0x16000050, + + /// + /// Initial console input. + /// + LibraryInitialConsoleInput = 0x15000051, + + /// + /// Application display order. + /// + BootMgrDisplayOrder = 0x24000001, + + /// + /// Application boot sequence. + /// + BootMgrBootSequence = 0x24000002, + + /// + /// Default application. + /// + BootMgrDefaultObject = 0x23000003, + + /// + /// User input timeout. + /// + BootMgrTimeout = 0x25000004, + + /// + /// Attempt to resume from hibernated state. + /// + BootMgrAttemptResume = 0x26000005, + + /// + /// The resume application. + /// + BootMgrResumeObject = 0x23000006, + + /// + /// The tools display order. + /// + BootMgrToolsDisplayOrder = 0x24000010, + + /// + /// Displays the boot menu. + /// + BootMgrDisplayBootMenu = 0x26000020, + + /// + /// No error display. + /// + BootMgrNoErrorDisplay = 0x26000021, + + /// + /// The BCD device. + /// + BootMgrBcdDevice = 0x21000022, + + /// + /// The BCD file path. + /// + BootMgrBcdFilePath = 0x22000023, + + /// + /// The custom actions list. + /// + BootMgrCustomActionsList = 0x27000030, + + /// + /// Device containing the Operating System. + /// + OsLoaderOsDevice = 0x21000001, + + /// + /// System root on the OS device. + /// + OsLoaderSystemRoot = 0x22000002, + + /// + /// The resume application associated with this OS. + /// + OsLoaderAssociatedResumeObject = 0x23000003, + + /// + /// Auto-detect the correct kernel & HAL. + /// + OsLoaderDetectKernelAndHal = 0x26000010, + + /// + /// The filename of the kernel. + /// + OsLoaderKernelPath = 0x22000011, + + /// + /// The filename of the HAL. + /// + OsLoaderHalPath = 0x22000012, + + /// + /// The debug transport path. + /// + OsLoaderDebugTransportPath = 0x22000013, + + /// + /// NX (No-Execute) policy. + /// + /// 0 = OptIn, 1 = OptOut, 2 = AlwaysOff, 3 = AlwaysOn. + OsLoaderNxPolicy = 0x25000020, + + /// + /// PAE policy. + /// + /// 0 = default, 1 = ForceEnable, 2 = ForceDisable. + OsLoaderPaePolicy = 0x25000021, + + /// + /// WinPE mode. + /// + OsLoaderWinPeMode = 0x26000022, + + /// + /// Disable automatic reboot on OS crash. + /// + OsLoaderDisableCrashAutoReboot = 0x26000024, + + /// + /// Use the last known good settings. + /// + OsLoaderUseLastGoodSettings = 0x26000025, + + /// + /// Disable integrity checks. + /// + OsLoaderDisableIntegrityChecks = 0x26000026, + + /// + /// Allows pre-release signatures (test signing). + /// + OsLoaderAllowPrereleaseSignatures = 0x26000027, + + /// + /// Loads all executables above 4GB boundary. + /// + OsLoaderNoLowMemory = 0x26000030, + + /// + /// Excludes a given amount of memory from use by Windows. + /// + OsLoaderRemoveMemory = 0x25000031, + + /// + /// Increases the User Mode virtual address space. + /// + OsLoaderIncreaseUserVa = 0x25000032, + + /// + /// Size of buffer (in MB) for perfomance data logging. + /// + OsLoaderPerformanceDataMemory = 0x25000033, + + /// + /// Uses the VGA display driver. + /// + OsLoaderUseVgaDriver = 0x26000040, + + /// + /// Quiet boot. + /// + OsLoaderDisableBootDisplay = 0x26000041, + + /// + /// Disables use of the VESA BIOS. + /// + OsLoaderDisableVesaBios = 0x26000042, + + /// + /// Maximum processors in a single APIC cluster. + /// + OsLoaderClusterModeAddressing = 0x25000050, + + /// + /// Forces the physical APIC to be used. + /// + OsLoaderUsePhysicalDestination = 0x26000051, + + /// + /// The largest APIC cluster number the system can use. + /// + OsLoaderRestrictApicCluster = 0x25000052, + + /// + /// Forces only the boot processor to be used. + /// + OsLoaderUseBootProcessorOnly = 0x26000060, + + /// + /// The number of processors to be used. + /// + OsLoaderNumberOfProcessors = 0x25000061, + + /// + /// Use maximum number of processors. + /// + OsLoaderForceMaxProcessors = 0x26000062, + + /// + /// Processor specific configuration flags. + /// + OsLoaderProcessorConfigurationFlags = 0x25000063, + + /// + /// Uses BIOS-configured PCI resources. + /// + OsLoaderUseFirmwarePciSettings = 0x26000070, + + /// + /// Message Signalled Interrupt setting. + /// + OsLoaderMsiPolicy = 0x25000071, + + /// + /// PCE Express Policy. + /// + OsLoaderPciExpressPolicy = 0x25000072, + + /// + /// The safe boot option. + /// + /// 0 = Minimal, 1 = Network, 2 = DsRepair. + OsLoaderSafeBoot = 0x25000080, + + /// + /// Loads the configured alternate shell during a safe boot. + /// + OsLoaderSafeBootAlternateShell = 0x26000081, + + /// + /// Enables boot log. + /// + OsLoaderBootLogInitialization = 0x26000090, + + /// + /// Displays diagnostic information during boot. + /// + OsLoaderVerboseObjectLoadMode = 0x26000091, + + /// + /// Enables the kernel debugger. + /// + OsLoaderKernelDebuggerEnabled = 0x260000A0, + + /// + /// Causes the kernal to halt early during boot. + /// + OsLoaderDebuggerHalBreakpoint = 0x260000A1, + + /// + /// Enables Windows Emergency Management System. + /// + OsLoaderEmsEnabled = 0x260000B0, + + /// + /// Forces a failure on boot. + /// + OsLoaderForceFailure = 0x250000C0, + + /// + /// The OS failure policy. + /// + OsLoaderDriverLoadFailurePolicy = 0x250000C1, + + /// + /// The OS boot status policy. + /// + OsLoaderBootStatusPolicy = 0x250000E0, + + /// + /// The device containing the hibernation file. + /// + ResumeHiberFileDevice = 0x21000001, + + /// + /// The path to the hibernation file. + /// + ResumeHiberFilePath = 0x22000002, + + /// + /// Allows resume loader to use custom settings. + /// + ResumeUseCustomSettings = 0x26000003, + + /// + /// PAE settings for resume application. + /// + ResumePaeMode = 0x26000004, + + /// + /// An MS-DOS device with containing resume application. + /// + ResumeAssociatedDosDevice = 0x21000005, + + /// + /// Enables debug option. + /// + ResumeDebugOptionEnabled = 0x26000006, + + /// + /// The number of iterations to run. + /// + MemDiagPassCount = 0x25000001, + + /// + /// The test mix. + /// + MemDiagTestMix = 0x25000002, + + /// + /// The failure count. + /// + MemDiagFailureCount = 0x25000003, + + /// + /// The tests to fail. + /// + MemDiagTestToFail = 0x25000004, + + /// + /// BPB string. + /// + LoaderBpbString = 0x22000001, + + /// + /// Causes a soft PXE reboot. + /// + StartupPxeSoftReboot = 0x26000001, + + /// + /// PXE application name. + /// + StartupPxeApplicationName = 0x22000002, + + /// + /// Offset of the RAM disk image. + /// + DeviceRamDiskImageOffset = 0x35000001, + + /// + /// Client port for TFTP. + /// + DeviceRamDiskTftpClientPort = 0x35000002, + + /// + /// Device containing the SDI file. + /// + DeviceRamDiskSdiDevice = 0x31000003, + + /// + /// Path to the SDI file. + /// + DeviceRamDiskSdiPath = 0x32000004, + + /// + /// Length of the RAM disk image. + /// + DeviceRamDiskRamDiskImageLength = 0x35000005, + + /// + /// Exports the image as a CD. + /// + DeviceRamDiskExportAsCd = 0x36000006, + + /// + /// The TFTP transfer block size. + /// + DeviceRamDiskTftpBlockSize = 0x35000007, + + /// + /// The device type. + /// + SetupDeviceType = 0x45000001, + + /// + /// The application relative path. + /// + SetupAppRelativePath = 0x42000002, + + /// + /// The device relative path. + /// + SetupRamDiskDeviceRelativePath = 0x42000003, + + /// + /// Omit OS loader elements. + /// + SetupOmitOsLoaderElements = 0x46000004, + + /// + /// Recovery OS flag. + /// + SetupRecoveryOs = 0x46000010, + } +} diff --git a/DiscUtils/Buffer.cs b/DiscUtils/Buffer.cs new file mode 100644 index 0000000..26e2cec --- /dev/null +++ b/DiscUtils/Buffer.cs @@ -0,0 +1,120 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + + /// + /// Abstract base class for implementations of IBuffer. + /// + public abstract class Buffer : MarshalByRefObject, IBuffer + { + /// + /// Gets a value indicating whether this buffer can be read. + /// + public abstract bool CanRead { get; } + + /// + /// Gets a value indicating whether this buffer can be modified. + /// + public abstract bool CanWrite { get; } + + /// + /// Gets the current capacity of the buffer, in bytes. + /// + public abstract long Capacity { get; } + + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public virtual IEnumerable Extents + { + get + { + return GetExtentsInRange(0, Capacity); + } + } + + /// + /// Reads from the buffer into a byte array. + /// + /// The offset within the buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + public abstract int Read(long pos, byte[] buffer, int offset, int count); + + /// + /// Writes a byte array into the buffer. + /// + /// The start offset within the buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + public abstract void Write(long pos, byte[] buffer, int offset, int count); + + /// + /// Clears bytes from the buffer. + /// + /// The start offset within the buffer. + /// The number of bytes to clear. + /// + /// Logically equivalent to writing count null/zero bytes to the buffer, some + /// implementations determine that some (or all) of the range indicated is not actually + /// stored. There is no direct, automatic, correspondence to clearing bytes and them + /// not being represented as an 'extent' - for example, the implementation of the underlying + /// stream may not permit fine-grained extent storage. + /// It is always safe to call this method to 'zero-out' a section of a buffer, regardless of + /// the underlying buffer implementation. + /// + public virtual void Clear(long pos, int count) + { + Write(pos, new byte[count], 0, count); + } + + /// + /// Flushes all data to the underlying storage. + /// + /// The default behaviour, implemented by this class, is to take no action. + public virtual void Flush() + { + } + + /// + /// Sets the capacity of the buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + public abstract void SetCapacity(long value); + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public abstract IEnumerable GetExtentsInRange(long start, long count); + } +} diff --git a/DiscUtils/BufferStream.cs b/DiscUtils/BufferStream.cs new file mode 100644 index 0000000..260e033 --- /dev/null +++ b/DiscUtils/BufferStream.cs @@ -0,0 +1,218 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Collections.Generic; + using System.IO; + + /// + /// Converts a Buffer into a Stream. + /// + public class BufferStream : SparseStream + { + private IBuffer _buffer; + private FileAccess _access; + + private long _position; + + /// + /// Initializes a new instance of the BufferStream class. + /// + /// The buffer to use. + /// The access permitted to clients. + public BufferStream(IBuffer buffer, FileAccess access) + { + _buffer = buffer; + _access = access; + } + + /// + /// Gets an indication of whether read access is permitted. + /// + public override bool CanRead + { + get { return _access != FileAccess.Write; } + } + + /// + /// Gets an indication of whether seeking is permitted. + /// + public override bool CanSeek + { + get { return true; } + } + + /// + /// Gets an indication of whether write access is permitted. + /// + public override bool CanWrite + { + get { return _access != FileAccess.Read; } + } + + /// + /// Gets the length of the stream (the capacity of the underlying buffer). + /// + public override long Length + { + get { return _buffer.Capacity; } + } + + /// + /// Gets and sets the current position within the stream. + /// + public override long Position + { + get { return _position; } + set { _position = value; } + } + + /// + /// Gets the stored extents within the sparse stream. + /// + public override IEnumerable Extents + { + get { return _buffer.Extents; } + } + + /// + /// Flushes all data to the underlying storage. + /// + public override void Flush() + { + } + + /// + /// Reads a number of bytes from the stream. + /// + /// The destination buffer. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + if (!CanRead) + { + throw new IOException("Attempt to read from write-only stream"); + } + + Utilities.AssertBufferParameters(buffer, offset, count); + + int numRead = _buffer.Read(_position, buffer, offset, count); + _position += numRead; + return numRead; + } + + /// + /// Changes the current stream position. + /// + /// The origin-relative stream position. + /// The origin for the stream position. + /// The new stream position. + public override long Seek(long offset, SeekOrigin origin) + { + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += _buffer.Capacity; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + else + { + _position = effectiveOffset; + return _position; + } + } + + /// + /// Sets the length of the stream (the underlying buffer's capacity). + /// + /// The new length of the stream. + public override void SetLength(long value) + { + _buffer.SetCapacity(value); + } + + /// + /// Writes a buffer to the stream. + /// + /// The buffer to write. + /// The starting offset within buffer. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + if (!CanWrite) + { + throw new IOException("Attempt to write to read-only stream"); + } + + Utilities.AssertBufferParameters(buffer, offset, count); + + _buffer.Write(_position, buffer, offset, count); + _position += count; + } + + /// + /// Clears bytes from the stream. + /// + /// The number of bytes (from the current position) to clear. + /// + /// Logically equivalent to writing count null/zero bytes to the stream, some + /// implementations determine that some (or all) of the range indicated is not actually + /// stored. There is no direct, automatic, correspondence to clearing bytes and them + /// not being represented as an 'extent' - for example, the implementation of the underlying + /// stream may not permit fine-grained extent storage. + /// It is always safe to call this method to 'zero-out' a section of a stream, regardless of + /// the underlying stream implementation. + /// + public override void Clear(int count) + { + if (!CanWrite) + { + throw new IOException("Attempt to erase bytes in a read-only stream"); + } + + _buffer.Clear(_position, count); + _position += count; + } + + /// + /// Gets the parts of a stream that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + return _buffer.GetExtentsInRange(start, count); + } + } +} diff --git a/DiscUtils/BuilderBufferExtent.cs b/DiscUtils/BuilderBufferExtent.cs new file mode 100644 index 0000000..0d43f62 --- /dev/null +++ b/DiscUtils/BuilderBufferExtent.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + internal class BuilderBufferExtent : BuilderExtent + { + private bool _fixedBuffer; + private byte[] _buffer; + + public BuilderBufferExtent(long start, long length) + : base(start, length) + { + } + + public BuilderBufferExtent(long start, byte[] buffer) + : base(start, buffer.Length) + { + _fixedBuffer = true; + _buffer = buffer; + } + + public override void Dispose() + { + } + + internal override void PrepareForRead() + { + if (!_fixedBuffer) + { + _buffer = GetBuffer(); + } + } + + internal override int Read(long diskOffset, byte[] block, int offset, int count) + { + int startOffset = (int)(diskOffset - Start); + int numBytes = (int)Math.Min(Length - startOffset, count); + Array.Copy(_buffer, startOffset, block, offset, numBytes); + return numBytes; + } + + internal override void DisposeReadState() + { + if (!_fixedBuffer) + { + _buffer = null; + } + } + + protected virtual byte[] GetBuffer() + { + throw new NotSupportedException("Derived class should implement"); + } + } +} diff --git a/DiscUtils/BuilderExtent.cs b/DiscUtils/BuilderExtent.cs new file mode 100644 index 0000000..a6e6a44 --- /dev/null +++ b/DiscUtils/BuilderExtent.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + + internal abstract class BuilderExtent : IDisposable + { + private long _start; + private long _length; + + public BuilderExtent(long start, long length) + { + _start = start; + _length = length; + } + + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public virtual IEnumerable StreamExtents + { + get + { + return new StreamExtent[] { new StreamExtent(Start, Length) }; + } + } + + internal long Start + { + get { return _start; } + } + + internal long Length + { + get { return _length; } + } + + public abstract void Dispose(); + + internal abstract void PrepareForRead(); + + internal abstract int Read(long diskOffset, byte[] block, int offset, int count); + + internal abstract void DisposeReadState(); + } +} diff --git a/DiscUtils/BuilderSparseStreamExtent.cs b/DiscUtils/BuilderSparseStreamExtent.cs new file mode 100644 index 0000000..c221f01 --- /dev/null +++ b/DiscUtils/BuilderSparseStreamExtent.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Text; + + internal class BuilderSparseStreamExtent : BuilderExtent + { + private SparseStream _stream; + private Ownership _ownership; + + public BuilderSparseStreamExtent(long start, SparseStream stream) + : this(start, stream, Ownership.None) + { + } + + public BuilderSparseStreamExtent(long start, SparseStream stream, Ownership ownership) + : base(start, stream.Length) + { + _stream = stream; + _ownership = ownership; + } + + public override IEnumerable StreamExtents + { + get { return StreamExtent.Offset(_stream.Extents, Start); } + } + + public override void Dispose() + { + if (_stream != null && _ownership == Ownership.Dispose) + { + _stream.Dispose(); + _stream = null; + } + } + + internal override void PrepareForRead() + { + } + + internal override int Read(long diskOffset, byte[] block, int offset, int count) + { + _stream.Position = diskOffset - Start; + return _stream.Read(block, offset, count); + } + + internal override void DisposeReadState() + { + } + } +} diff --git a/DiscUtils/BuiltStream.cs b/DiscUtils/BuiltStream.cs new file mode 100644 index 0000000..33eef09 --- /dev/null +++ b/DiscUtils/BuiltStream.cs @@ -0,0 +1,327 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class BuiltStream : SparseStream + { + private Stream _baseStream; + private long _length; + private List _extents; + + private BuilderExtent _currentExtent; + private long _position; + + public BuiltStream(long length, List extents) + { + _baseStream = new ZeroStream(length); + _length = length; + _extents = extents; + + // Make sure the extents are sorted, so binary searches will work. + _extents.Sort(new ExtentStartComparer()); + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + set { _position = value; } + } + + public override IEnumerable Extents + { + get + { + foreach (var extent in _extents) + { + foreach (var streamExtent in extent.StreamExtents) + { + yield return streamExtent; + } + } + } + } + + public override void Flush() + { + return; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_position >= _length) + { + return 0; + } + + int totalRead = 0; + while (totalRead < count && _position < _length) + { + // If current region is outside the area of interest, clean it up + if (_currentExtent != null && (_position < _currentExtent.Start || _position >= _currentExtent.Start + _currentExtent.Length)) + { + _currentExtent.DisposeReadState(); + _currentExtent = null; + } + + // If we need to find a new region, look for it + if (_currentExtent == null) + { + using (SearchExtent searchExtent = new SearchExtent(_position)) + { + int idx = _extents.BinarySearch(searchExtent, new ExtentRangeComparer()); + if (idx >= 0) + { + BuilderExtent extent = _extents[idx]; + extent.PrepareForRead(); + _currentExtent = extent; + } + } + } + + int numRead = 0; + + // If the block is outside any known extent, defer to base stream. + if (_currentExtent == null) + { + _baseStream.Position = _position; + BuilderExtent nextExtent = FindNext(_position); + if (nextExtent != null) + { + numRead = _baseStream.Read(buffer, offset + totalRead, (int)Math.Min(count - totalRead, nextExtent.Start - _position)); + } + else + { + numRead = _baseStream.Read(buffer, offset + totalRead, count - totalRead); + } + } + else + { + numRead = _currentExtent.Read(_position, buffer, offset + totalRead, count - totalRead); + } + + _position += numRead; + totalRead += numRead; + } + + return totalRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = offset; + if (origin == SeekOrigin.Current) + { + newPos += _position; + } + else if (origin == SeekOrigin.End) + { + newPos += _length; + } + + _position = newPos; + return newPos; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_currentExtent != null) + { + _currentExtent.DisposeReadState(); + _currentExtent = null; + } + + if (_baseStream != null) + { + _baseStream.Dispose(); + _baseStream = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + private BuilderExtent FindNext(long pos) + { + int min = 0; + int max = _extents.Count - 1; + + if (_extents.Count == 0 || (_extents[_extents.Count - 1].Start + _extents[_extents.Count - 1].Length) <= pos) + { + return null; + } + + while (true) + { + if (min >= max) + { + return _extents[min]; + } + + int mid = (max + min) / 2; + if (_extents[mid].Start < pos) + { + min = mid + 1; + } + else if (_extents[mid].Start > pos) + { + max = mid; + } + else + { + return _extents[mid]; + } + } + } + + private class SearchExtent : BuilderExtent + { + public SearchExtent(long pos) + : base(pos, 1) + { + } + + public override void Dispose() + { + } + + internal override void PrepareForRead() + { + // Not valid to use this 'dummy' extent for actual construction + throw new NotSupportedException(); + } + + internal override int Read(long diskOffset, byte[] block, int offset, int count) + { + // Not valid to use this 'dummy' extent for actual construction + throw new NotSupportedException(); + } + + internal override void DisposeReadState() + { + // Not valid to use this 'dummy' extent for actual construction + throw new NotSupportedException(); + } + } + + private class ExtentRangeComparer : IComparer + { + public int Compare(BuilderExtent x, BuilderExtent y) + { + if (x == null) + { + throw new ArgumentNullException("x"); + } + + if (y == null) + { + throw new ArgumentNullException("y"); + } + + if (x.Start + x.Length <= y.Start) + { + // x < y, with no intersection + return -1; + } + else if (x.Start >= y.Start + y.Length) + { + // x > y, with no intersection + return 1; + } + + // x intersects y + return 0; + } + } + + private class ExtentStartComparer : IComparer + { + public int Compare(BuilderExtent x, BuilderExtent y) + { + if (x == null) + { + throw new ArgumentNullException("x"); + } + + if (y == null) + { + throw new ArgumentNullException("y"); + } + + long val = x.Start - y.Start; + if (val < 0) + { + return -1; + } + else if (val > 0) + { + return 1; + } + else + { + return 0; + } + } + } + } +} diff --git a/DiscUtils/ChsAddress.cs b/DiscUtils/ChsAddress.cs new file mode 100644 index 0000000..126fa4f --- /dev/null +++ b/DiscUtils/ChsAddress.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Class whose instances represent a CHS (Cylinder, Head, Sector) address on a disk. + /// + /// Instances of this class are immutable. + public sealed class ChsAddress + { + /// + /// The address of the first sector on any disk. + /// + public static readonly ChsAddress First = new ChsAddress(0, 0, 1); + + private int _cylinder; + private int _head; + private int _sector; + + /// + /// Initializes a new instance of the ChsAddress class. + /// + /// The number of cylinders of the disk. + /// The number of heads (aka platters) of the disk. + /// The number of sectors per track/cylinder of the disk. + public ChsAddress(int cylinder, int head, int sector) + { + _cylinder = cylinder; + _head = head; + _sector = sector; + } + + /// + /// Gets the cylinder number (zero-based). + /// + public int Cylinder + { + get { return _cylinder; } + } + + /// + /// Gets the head (zero-based). + /// + public int Head + { + get { return _head; } + } + + /// + /// Gets the sector number (one-based). + /// + public int Sector + { + get { return _sector; } + } + + /// + /// Determines if this object is equivalent to another. + /// + /// The object to test against. + /// true if the is equivalent, else false. + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != GetType()) + { + return false; + } + + ChsAddress other = (ChsAddress)obj; + + return _cylinder == other._cylinder && _head == other._head && _sector == other._sector; + } + + /// + /// Calculates the hash code for this object. + /// + /// The hash code. + public override int GetHashCode() + { + return _cylinder.GetHashCode() ^ _head.GetHashCode() ^ _sector.GetHashCode(); + } + + /// + /// Gets a string representation of this object, in the form (C/H/S). + /// + /// The string representation. + public override string ToString() + { + return "(" + _cylinder + "/" + _head + "/" + _sector + ")"; + } + } +} diff --git a/DiscUtils/ClusterMap.cs b/DiscUtils/ClusterMap.cs new file mode 100644 index 0000000..e3e4b19 --- /dev/null +++ b/DiscUtils/ClusterMap.cs @@ -0,0 +1,128 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// Enumeration of possible cluster roles. + /// + /// A cluster may be in more than one role. + [Flags] + public enum ClusterRoles + { + /// + /// Unknown, or unspecified role. + /// + None = 0x00, + + /// + /// Cluster is free. + /// + Free = 0x01, + + /// + /// Cluster is in use by a normal file. + /// + DataFile = 0x02, + + /// + /// Cluster is in use by a system file. + /// + /// This isn't a file marked with the 'system' attribute, + /// rather files that form part of the file system namespace but also + /// form part of the file system meta-data. + SystemFile = 0x04, + + /// + /// Cluster is in use for meta-data. + /// + Metadata = 0x08, + + /// + /// Cluster contains the boot region. + /// + BootArea = 0x10, + + /// + /// Cluster is marked bad. + /// + Bad = 0x20, + } + + /// + /// Class that identifies the role of each cluster in a file system. + /// + public sealed class ClusterMap + { + private ClusterRoles[] _clusterToRole; + private object[] _clusterToFileId; + private Dictionary _fileIdToPaths; + + internal ClusterMap(ClusterRoles[] clusterToRole, object[] clusterToFileId, Dictionary fileIdToPaths) + { + _clusterToRole = clusterToRole; + _clusterToFileId = clusterToFileId; + _fileIdToPaths = fileIdToPaths; + } + + /// + /// Gets the role of a cluster within the file system. + /// + /// The cluster to inspect. + /// The clusters role (or roles). + public ClusterRoles GetRole(long cluster) + { + if (_clusterToRole == null || _clusterToRole.Length < cluster) + { + return ClusterRoles.None; + } + else + { + return _clusterToRole[cluster]; + } + } + + /// + /// Converts a cluster to a list of file names. + /// + /// The cluster to inspect. + /// A list of paths that map to the cluster. + /// A list is returned because on file systems with the notion of + /// hard links, a cluster may correspond to multiple directory entries. + public string[] ClusterToPaths(long cluster) + { + if ((GetRole(cluster) & (ClusterRoles.DataFile | ClusterRoles.SystemFile)) != 0) + { + object fileId = _clusterToFileId[cluster]; + return _fileIdToPaths[fileId]; + } + else + { + return new string[0]; + } + } + } +} diff --git a/DiscUtils/Compression/Adler32.cs b/DiscUtils/Compression/Adler32.cs new file mode 100644 index 0000000..8a22297 --- /dev/null +++ b/DiscUtils/Compression/Adler32.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + using System; + + /// + /// Implementation of the Adler-32 checksum algorithm. + /// + public class Adler32 + { + private uint _a; + private uint _b; + + /// + /// Initializes a new instance of the Adler32 class. + /// + public Adler32() + { + _a = 1; + } + + /// + /// Gets the checksum of all data processed so far. + /// + public int Value + { + get { return (int)(_b << 16 | _a); } + } + + /// + /// Provides data that should be checksummed. + /// + /// Buffer containing the data to checksum. + /// Offset of the first byte to checksum. + /// The number of bytes to checksum. + /// + /// Call this method repeatedly until all checksummed + /// data has been processed. + /// + public void Process(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentException("Offset outside of array bounds", "offset"); + } + + if (count < 0 || offset + count > buffer.Length) + { + throw new ArgumentException("Array index out of bounds", "count"); + } + + int processed = 0; + while (processed < count) + { + int innerEnd = Math.Min(count, processed + 2000); + while (processed < innerEnd) + { + _a += buffer[processed++]; + _b += _a; + } + + _a %= 65521; + _b %= 65521; + } + } + } +} diff --git a/DiscUtils/Compression/BZip2BlockDecoder.cs b/DiscUtils/Compression/BZip2BlockDecoder.cs new file mode 100644 index 0000000..6b53405 --- /dev/null +++ b/DiscUtils/Compression/BZip2BlockDecoder.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +namespace DiscUtils.Compression +{ + using System.IO; + + internal class BZip2BlockDecoder + { + private InverseBurrowsWheeler _inverseBurrowsWheeler; + private uint _crc; + + public BZip2BlockDecoder(int blockSize) + { + _inverseBurrowsWheeler = new InverseBurrowsWheeler(blockSize); + } + + public uint Crc + { + get { return _crc; } + } + + public int Process(BitStream bitstream, byte[] outputBuffer, int outputBufferOffset) + { + _crc = 0; + for (int i = 0; i < 4; ++i) + { + _crc = (_crc << 8) | bitstream.Read(8); + } + + bool rand = bitstream.Read(1) != 0; + int origPtr = (int)bitstream.Read(24); + + int thisBlockSize = ReadBuffer(bitstream, outputBuffer, outputBufferOffset); + + _inverseBurrowsWheeler.OriginalIndex = origPtr; + _inverseBurrowsWheeler.Process(outputBuffer, outputBufferOffset, thisBlockSize, outputBuffer, outputBufferOffset); + + if (rand) + { + BZip2Randomizer randomizer = new BZip2Randomizer(); + randomizer.Process(outputBuffer, outputBufferOffset, thisBlockSize, outputBuffer, outputBufferOffset); + } + + return thisBlockSize; + } + + private static int ReadBuffer(BitStream bitstream, byte[] buffer, int offset) + { + // The MTF state + int numInUse = 0; + MoveToFront moveFrontTransform = new MoveToFront(); + bool[] inUseGroups = new bool[16]; + for (int i = 0; i < 16; ++i) + { + inUseGroups[i] = bitstream.Read(1) != 0; + } + + for (int i = 0; i < 256; ++i) + { + if (inUseGroups[i / 16]) + { + if (bitstream.Read(1) != 0) + { + moveFrontTransform.Set(numInUse, (byte)i); + numInUse++; + } + } + } + + // Initialize 'virtual' Huffman tree from bitstream + BZip2CombinedHuffmanTrees huffmanTree = new BZip2CombinedHuffmanTrees(bitstream, numInUse + 2); + + // Main loop reading data + int readBytes = 0; + while (true) + { + uint symbol = huffmanTree.NextSymbol(); + + if (symbol < 2) + { + // RLE, with length stored in a binary-style format + uint runLength = 0; + int bitShift = 0; + while (symbol < 2) + { + runLength += (symbol + 1) << bitShift; + bitShift++; + + symbol = huffmanTree.NextSymbol(); + } + + byte b = moveFrontTransform.Head; + while (runLength > 0) + { + buffer[offset + readBytes] = b; + ++readBytes; + --runLength; + } + } + + if (symbol <= numInUse) + { + // Single byte + byte b = moveFrontTransform.GetAndMove((int)symbol - 1); + buffer[offset + readBytes] = b; + ++readBytes; + } + else if (symbol == numInUse + 1) + { + // End of block marker + return readBytes; + } + else + { + throw new InvalidDataException("Invalid symbol from Huffman table"); + } + } + } + } +} diff --git a/DiscUtils/Compression/BZip2CombinedHuffmanTrees.cs b/DiscUtils/Compression/BZip2CombinedHuffmanTrees.cs new file mode 100644 index 0000000..78d4915 --- /dev/null +++ b/DiscUtils/Compression/BZip2CombinedHuffmanTrees.cs @@ -0,0 +1,135 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +namespace DiscUtils.Compression +{ + using System.IO; + + /// + /// Represents scheme used by BZip2 where multiple Huffman trees are used as a + /// virtual Huffman tree, with a logical selector every 50 bits in the bit stream. + /// + internal class BZip2CombinedHuffmanTrees + { + private BitStream _bitstream; + private byte[] _selectors; + private HuffmanTree[] _trees; + + private HuffmanTree _activeTree; + private int _symbolsToNextSelector; + private int _nextSelector; + + public BZip2CombinedHuffmanTrees(BitStream bitstream, int maxSymbols) + { + _bitstream = bitstream; + + Initialize(maxSymbols); + } + + public uint NextSymbol() + { + if (_symbolsToNextSelector == 0) + { + _symbolsToNextSelector = 50; + _activeTree = _trees[_selectors[_nextSelector]]; + _nextSelector++; + } + + _symbolsToNextSelector--; + + return _activeTree.NextSymbol(_bitstream); + } + + private void Initialize(int maxSymbols) + { + int numTrees = (int)_bitstream.Read(3); + if (numTrees < 2 || numTrees > 6) + { + throw new InvalidDataException("Invalid number of tables"); + } + + int numSelectors = (int)_bitstream.Read(15); + if (numSelectors < 1) + { + throw new InvalidDataException("Invalid number of selectors"); + } + + _selectors = new byte[numSelectors]; + MoveToFront mtf = new MoveToFront(numTrees, true); + for (int i = 0; i < numSelectors; ++i) + { + _selectors[i] = mtf.GetAndMove(CountSetBits(numTrees)); + } + + _trees = new HuffmanTree[numTrees]; + for (int t = 0; t < numTrees; ++t) + { + uint[] lengths = new uint[maxSymbols]; + + uint len = _bitstream.Read(5); + for (int i = 0; i < maxSymbols; ++i) + { + if (len < 1 || len > 20) + { + throw new InvalidDataException("Invalid length constructing Huffman tree"); + } + + while (_bitstream.Read(1) != 0) + { + len = (_bitstream.Read(1) == 0) ? len + 1 : len - 1; + + if (len < 1 || len > 20) + { + throw new InvalidDataException("Invalid length constructing Huffman tree"); + } + } + + lengths[i] = len; + } + + _trees[t] = new HuffmanTree(lengths); + } + + _symbolsToNextSelector = 0; + _nextSelector = 0; + } + + private byte CountSetBits(int max) + { + byte val = 0; + while (_bitstream.Read(1) != 0) + { + val++; + if (val >= max) + { + throw new InvalidDataException("Exceeded max number of consecutive bits"); + } + } + + return val; + } + } +} diff --git a/DiscUtils/Compression/BZip2DecoderStream.cs b/DiscUtils/Compression/BZip2DecoderStream.cs new file mode 100644 index 0000000..2a48975 --- /dev/null +++ b/DiscUtils/Compression/BZip2DecoderStream.cs @@ -0,0 +1,349 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +namespace DiscUtils.Compression +{ + using System; + using System.IO; + + /// + /// Implementation of a BZip2 decoder. + /// + public sealed class BZip2DecoderStream : Stream + { + private long _position; + private Stream _compressedStream; + private Ownership _ownsCompressed; + private BitStream _bitstream; + private BZip2RleStream _rleStream; + private BZip2BlockDecoder _blockDecoder; + private Crc32 _calcBlockCrc; + + private byte[] _blockBuffer; + private uint _blockCrc; + private uint _compoundCrc; + private uint _calcCompoundCrc; + + private bool _eof; + + /// + /// Initializes a new instance of the BZip2DecoderStream class. + /// + /// The compressed input stream. + /// Whether ownership of stream passes to the new instance. + public BZip2DecoderStream(Stream stream, Ownership ownsStream) + { + _compressedStream = stream; + _ownsCompressed = ownsStream; + + _bitstream = new BigEndianBitStream(new BufferedStream(stream)); + + // The Magic BZh + byte[] magic = new byte[3]; + magic[0] = (byte)_bitstream.Read(8); + magic[1] = (byte)_bitstream.Read(8); + magic[2] = (byte)_bitstream.Read(8); + if (magic[0] != 0x42 || magic[1] != 0x5A || magic[2] != 0x68) + { + throw new InvalidDataException("Bad magic at start of stream"); + } + + // The size of the decompression blocks in multiples of 100,000 + int blockSize = (int)_bitstream.Read(8) - 0x30; + if (blockSize < 1 || blockSize > 9) + { + throw new InvalidDataException("Unexpected block size in header: " + blockSize); + } + + blockSize *= 100000; + + _rleStream = new BZip2RleStream(); + _blockDecoder = new BZip2BlockDecoder(blockSize); + _blockBuffer = new byte[blockSize]; + + if (ReadBlock() == 0) + { + _eof = true; + } + } + + /// + /// Gets an indication of whether read access is permitted. + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Gets an indication of whether seeking is permitted. + /// + public override bool CanSeek + { + get { return false; } + } + + /// + /// Gets an indication of whether write access is permitted. + /// + public override bool CanWrite + { + get { return false; } + } + + /// + /// Gets the length of the stream (the capacity of the underlying buffer). + /// + public override long Length + { + get { throw new NotSupportedException(); } + } + + /// + /// Gets and sets the current position within the stream. + /// + public override long Position + { + get { return _position; } + set { throw new NotSupportedException(); } + } + + /// + /// Flushes all data to the underlying storage. + /// + public override void Flush() + { + throw new NotSupportedException(); + } + + /// + /// Reads a number of bytes from the stream. + /// + /// The destination buffer. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + if (buffer.Length < offset + count) + { + throw new ArgumentException("Buffer smaller than declared"); + } + + if (offset < 0) + { + throw new ArgumentException("Offset less than zero", "offset"); + } + + if (count < 0) + { + throw new ArgumentException("Count less than zero", "count"); + } + + if (_eof) + { + throw new IOException("Attempt to read beyond end of stream"); + } + + if (count == 0) + { + return 0; + } + + int numRead = _rleStream.Read(buffer, offset, count); + if (numRead == 0) + { + // If there was an existing block, check it's crc. + if (_calcBlockCrc != null) + { + if (_blockCrc != _calcBlockCrc.Value) + { + throw new InvalidDataException("Decompression failed - block CRC mismatch"); + } + + _calcCompoundCrc = ((_calcCompoundCrc << 1) | (_calcCompoundCrc >> 31)) ^ _blockCrc; + } + + // Read a new block (if any), if none - check the overall CRC before returning + if (ReadBlock() == 0) + { + _eof = true; + if (_calcCompoundCrc != _compoundCrc) + { + throw new InvalidDataException("Decompression failed - compound CRC"); + } + + return 0; + } + + numRead = _rleStream.Read(buffer, offset, count); + } + + _calcBlockCrc.Process(buffer, offset, numRead); + + // Pre-read next block, so a client that knows the decompressed length will still + // have the overall CRC calculated. + if (_rleStream.AtEof) + { + // If there was an existing block, check it's crc. + if (_calcBlockCrc != null) + { + if (_blockCrc != _calcBlockCrc.Value) + { + throw new InvalidDataException("Decompression failed - block CRC mismatch"); + } + } + + _calcCompoundCrc = ((_calcCompoundCrc << 1) | (_calcCompoundCrc >> 31)) ^ _blockCrc; + if (ReadBlock() == 0) + { + _eof = true; + if (_calcCompoundCrc != _compoundCrc) + { + throw new InvalidDataException("Decompression failed - compound CRC mismatch"); + } + + return numRead; + } + } + + _position += numRead; + return numRead; + } + + /// + /// Changes the current stream position. + /// + /// The origin-relative stream position. + /// The origin for the stream position. + /// The new stream position. + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + /// Sets the length of the stream (the underlying buffer's capacity). + /// + /// The new length of the stream. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Writes a buffer to the stream. + /// + /// The buffer to write. + /// The starting offset within buffer. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + /// Releases underlying resources. + /// + /// Whether this method is called from Dispose. + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_compressedStream != null && _ownsCompressed == Ownership.Dispose) + { + _compressedStream.Dispose(); + } + + _compressedStream = null; + + if (_rleStream != null) + { + _rleStream.Dispose(); + _rleStream = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + private int ReadBlock() + { + ulong marker = ReadMarker(); + if (marker == 0x314159265359) + { + int blockSize = _blockDecoder.Process(_bitstream, _blockBuffer, 0); + _rleStream.Reset(_blockBuffer, 0, blockSize); + _blockCrc = _blockDecoder.Crc; + _calcBlockCrc = new Crc32BigEndian(Crc32Algorithm.Common); + return blockSize; + } + else if (marker == 0x177245385090) + { + _compoundCrc = ReadUint(); + return 0; + } + else + { + throw new InvalidDataException("Found invalid marker in stream"); + } + } + + private uint ReadUint() + { + uint val = 0; + + for (int i = 0; i < 4; ++i) + { + val = (val << 8) | _bitstream.Read(8); + } + + return val; + } + + private ulong ReadMarker() + { + ulong marker = 0; + + for (int i = 0; i < 6; ++i) + { + marker = (marker << 8) | _bitstream.Read(8); + } + + return marker; + } + } +} diff --git a/DiscUtils/Compression/BZip2Randomizer.cs b/DiscUtils/Compression/BZip2Randomizer.cs new file mode 100644 index 0000000..65a6808 --- /dev/null +++ b/DiscUtils/Compression/BZip2Randomizer.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +namespace DiscUtils.Compression +{ + using System; + + internal class BZip2Randomizer : DataBlockTransform + { + private static readonly int[] RandomVals = + { + 619, 720, 127, 481, 931, 816, 813, 233, 566, 247, + 985, 724, 205, 454, 863, 491, 741, 242, 949, 214, + 733, 859, 335, 708, 621, 574, 73, 654, 730, 472, + 419, 436, 278, 496, 867, 210, 399, 680, 480, 51, + 878, 465, 811, 169, 869, 675, 611, 697, 867, 561, + 862, 687, 507, 283, 482, 129, 807, 591, 733, 623, + 150, 238, 59, 379, 684, 877, 625, 169, 643, 105, + 170, 607, 520, 932, 727, 476, 693, 425, 174, 647, + 73, 122, 335, 530, 442, 853, 695, 249, 445, 515, + 909, 545, 703, 919, 874, 474, 882, 500, 594, 612, + 641, 801, 220, 162, 819, 984, 589, 513, 495, 799, + 161, 604, 958, 533, 221, 400, 386, 867, 600, 782, + 382, 596, 414, 171, 516, 375, 682, 485, 911, 276, + 98, 553, 163, 354, 666, 933, 424, 341, 533, 870, + 227, 730, 475, 186, 263, 647, 537, 686, 600, 224, + 469, 68, 770, 919, 190, 373, 294, 822, 808, 206, + 184, 943, 795, 384, 383, 461, 404, 758, 839, 887, + 715, 67, 618, 276, 204, 918, 873, 777, 604, 560, + 951, 160, 578, 722, 79, 804, 96, 409, 713, 940, + 652, 934, 970, 447, 318, 353, 859, 672, 112, 785, + 645, 863, 803, 350, 139, 93, 354, 99, 820, 908, + 609, 772, 154, 274, 580, 184, 79, 626, 630, 742, + 653, 282, 762, 623, 680, 81, 927, 626, 789, 125, + 411, 521, 938, 300, 821, 78, 343, 175, 128, 250, + 170, 774, 972, 275, 999, 639, 495, 78, 352, 126, + 857, 956, 358, 619, 580, 124, 737, 594, 701, 612, + 669, 112, 134, 694, 363, 992, 809, 743, 168, 974, + 944, 375, 748, 52, 600, 747, 642, 182, 862, 81, + 344, 805, 988, 739, 511, 655, 814, 334, 249, 515, + 897, 955, 664, 981, 649, 113, 974, 459, 893, 228, + 433, 837, 553, 268, 926, 240, 102, 654, 459, 51, + 686, 754, 806, 760, 493, 403, 415, 394, 687, 700, + 946, 670, 656, 610, 738, 392, 760, 799, 887, 653, + 978, 321, 576, 617, 626, 502, 894, 679, 243, 440, + 680, 879, 194, 572, 640, 724, 926, 56, 204, 700, + 707, 151, 457, 449, 797, 195, 791, 558, 945, 679, + 297, 59, 87, 824, 713, 663, 412, 693, 342, 606, + 134, 108, 571, 364, 631, 212, 174, 643, 304, 329, + 343, 97, 430, 751, 497, 314, 983, 374, 822, 928, + 140, 206, 73, 263, 980, 736, 876, 478, 430, 305, + 170, 514, 364, 692, 829, 82, 855, 953, 676, 246, + 369, 970, 294, 750, 807, 827, 150, 790, 288, 923, + 804, 378, 215, 828, 592, 281, 565, 555, 710, 82, + 896, 831, 547, 261, 524, 462, 293, 465, 502, 56, + 661, 821, 976, 991, 658, 869, 905, 758, 745, 193, + 768, 550, 608, 933, 378, 286, 215, 979, 792, 961, + 61, 688, 793, 644, 986, 403, 106, 366, 905, 644, + 372, 567, 466, 434, 645, 210, 389, 550, 919, 135, + 780, 773, 635, 389, 707, 100, 626, 958, 165, 504, + 920, 176, 193, 713, 857, 265, 203, 50, 668, 108, + 645, 990, 626, 197, 510, 357, 358, 850, 858, 364, + 936, 638 + }; + + protected override bool BuffersMustNotOverlap + { + get { return false; } + } + + protected override int DoProcess(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + { + if (input != output || inputOffset != outputOffset) + { + Array.Copy(input, inputOffset, output, outputOffset, inputCount); + } + + int randIndex = 1; + int nextByte = RandomVals[0] - 2; + + while (nextByte < inputCount) + { + output[nextByte] ^= 1; + nextByte += RandomVals[randIndex++]; + randIndex &= 0x1FF; + } + + return inputCount; + } + + protected override int MaxOutputCount(int inputCount) + { + return inputCount; + } + + protected override int MinOutputCount(int inputCount) + { + return inputCount; + } + } +} diff --git a/DiscUtils/Compression/BZip2RleStream.cs b/DiscUtils/Compression/BZip2RleStream.cs new file mode 100644 index 0000000..004d265 --- /dev/null +++ b/DiscUtils/Compression/BZip2RleStream.cs @@ -0,0 +1,161 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +namespace DiscUtils.Compression +{ + using System; + using System.IO; + + internal class BZip2RleStream : Stream + { + private long _position; + private byte[] _blockBuffer; + private int _blockOffset; + private int _blockRemaining; + + private int _numSame; + private byte _lastByte; + private int _runBytesOutstanding; + + public BZip2RleStream() + { + } + + public bool AtEof + { + get { return _runBytesOutstanding == 0 && _blockRemaining == 0; } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { return _position; } + set { throw new NotSupportedException(); } + } + + public void Reset(byte[] buffer, int offset, int count) + { + _position = 0; + _blockBuffer = buffer; + _blockOffset = offset; + _blockRemaining = count; + _numSame = -1; + _lastByte = 0; + _runBytesOutstanding = 0; + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int numRead = 0; + + while (numRead < count && _runBytesOutstanding > 0) + { + int runCount = Math.Min(_runBytesOutstanding, count); + for (int i = 0; i < runCount; ++i) + { + buffer[offset + numRead] = _lastByte; + } + + _runBytesOutstanding -= runCount; + numRead += runCount; + } + + while (numRead < count && _blockRemaining > 0) + { + byte b = _blockBuffer[_blockOffset]; + ++_blockOffset; + --_blockRemaining; + + if (_numSame == 4) + { + int runCount = Math.Min(b, count - numRead); + for (int i = 0; i < runCount; ++i) + { + buffer[offset + numRead] = _lastByte; + numRead++; + } + + _runBytesOutstanding = b - runCount; + _numSame = 0; + } + else + { + if (b != _lastByte || _numSame <= 0) + { + _lastByte = b; + _numSame = 0; + } + + buffer[offset + numRead] = b; + numRead++; + _numSame++; + } + } + + _position += numRead; + return numRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/DiscUtils/Compression/BigEndianBitStream.cs b/DiscUtils/Compression/BigEndianBitStream.cs new file mode 100644 index 0000000..389d376 --- /dev/null +++ b/DiscUtils/Compression/BigEndianBitStream.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + using System.IO; + + /// + /// Converts a byte stream into a bit stream. + /// + internal class BigEndianBitStream : BitStream + { + private Stream _byteStream; + + private uint _buffer; + private int _bufferAvailable; + + private byte[] _readBuffer = new byte[2]; + + public BigEndianBitStream(Stream byteStream) + { + _byteStream = byteStream; + } + + public override int MaxReadAhead + { + get { return 16; } + } + + public override uint Read(int count) + { + if (count > 16) + { + uint result = Read(16) << (count - 16); + return result | Read(count - 16); + } + + EnsureBufferFilled(); + + _bufferAvailable -= count; + + uint mask = (uint)((1 << count) - 1); + + return (uint)((_buffer >> _bufferAvailable) & mask); + } + + public override uint Peek(int count) + { + EnsureBufferFilled(); + + uint mask = (uint)((1 << count) - 1); + + return (uint)((_buffer >> (_bufferAvailable - count)) & mask); + } + + public override void Consume(int count) + { + EnsureBufferFilled(); + + _bufferAvailable -= count; + } + + private void EnsureBufferFilled() + { + if (_bufferAvailable < 16) + { + _readBuffer[0] = 0; + _readBuffer[1] = 0; + _byteStream.Read(_readBuffer, 0, 2); + + _buffer = (uint)((uint)(_buffer << 16) | (uint)(_readBuffer[0] << 8) | (uint)_readBuffer[1]); + _bufferAvailable += 16; + } + } + } +} diff --git a/DiscUtils/Compression/BitStream.cs b/DiscUtils/Compression/BitStream.cs new file mode 100644 index 0000000..62c0306 --- /dev/null +++ b/DiscUtils/Compression/BitStream.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + /// + /// Base class for bit streams. + /// + /// + /// The rules for conversion of a byte stream to a bit stream vary + /// between implementations. + /// + internal abstract class BitStream + { + /// + /// Gets the maximum number of bits that can be peeked on the stream. + /// + public abstract int MaxReadAhead { get; } + + /// + /// Reads bits from the stream. + /// + /// The number of bits to read. + /// The bits as a UInt32. + public abstract uint Read(int count); + + /// + /// Queries data from the stream. + /// + /// The number of bits to query. + /// The bits as a UInt32. + /// This method does not consume the bits (i.e. move the file pointer). + public abstract uint Peek(int count); + + /// + /// Consumes bits from the stream without returning them. + /// + /// The number of bits to consume. + public abstract void Consume(int count); + } +} diff --git a/DiscUtils/Compression/BlockCompressor.cs b/DiscUtils/Compression/BlockCompressor.cs new file mode 100644 index 0000000..34fba7c --- /dev/null +++ b/DiscUtils/Compression/BlockCompressor.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + /// + /// Base class for block compression algorithms. + /// + public abstract class BlockCompressor + { + /// + /// Gets or sets the block size parameter to the algorithm. + /// + /// + /// Some algorithms may use this to control both compression and decompression, others may + /// only use it to control compression. Some may ignore it entirely. + /// + public int BlockSize { get; set; } + + /// + /// Compresses some data. + /// + /// The uncompressed input. + /// Offset of the input data in source. + /// The amount of uncompressed data. + /// The destination for the output compressed data. + /// Offset for the output data in compressed. + /// The maximum size of the compressed data on input, and the actual size on output. + /// Indication of success, or indication the data could not compress into the requested space. + public abstract CompressionResult Compress(byte[] source, int sourceOffset, int sourceLength, byte[] compressed, int compressedOffset, ref int compressedLength); + + /// + /// Decompresses some data. + /// + /// The compressed input. + /// Offset of the input data in source. + /// The amount of compressed data. + /// The destination for the output decompressed data. + /// Offset for the output data in decompressed. + /// The amount of decompressed data. + public abstract int Decompress(byte[] source, int sourceOffset, int sourceLength, byte[] decompressed, int decompressedOffset); + } +} diff --git a/DiscUtils/Compression/CompressionResult.cs b/DiscUtils/Compression/CompressionResult.cs new file mode 100644 index 0000000..4df13b7 --- /dev/null +++ b/DiscUtils/Compression/CompressionResult.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + /// + /// Possible results of attempting to compress data. + /// + /// + /// A compression routine may return Compressed, even if the data + /// was 'all zeros' or increased in size. The AllZeros and Incompressible + /// values are for algorithms that include special detection for these cases. + /// + public enum CompressionResult + { + /// + /// The data compressed succesfully. + /// + Compressed, + + /// + /// The data was all-zero's. + /// + AllZeros, + + /// + /// The data was incompressible (could not fit into destination buffer). + /// + Incompressible + } +} diff --git a/DiscUtils/Compression/DataBlockTransform.cs b/DiscUtils/Compression/DataBlockTransform.cs new file mode 100644 index 0000000..8cb2f2a --- /dev/null +++ b/DiscUtils/Compression/DataBlockTransform.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + using System; + using System.Globalization; + + internal abstract class DataBlockTransform + { + protected abstract bool BuffersMustNotOverlap { get; } + + public int Process(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + { + if (output.Length < outputOffset + (long)MinOutputCount(inputCount)) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "Output buffer to small, must be at least {0} bytes may need to be {1} bytes", + MinOutputCount(inputCount), + MaxOutputCount(inputCount))); + } + + if (BuffersMustNotOverlap) + { + int maxOut = MaxOutputCount(inputCount); + + if (input == output + && (inputOffset + (long)inputCount > outputOffset) + && (inputOffset <= outputOffset + (long)maxOut)) + { + byte[] tempBuffer = new byte[maxOut]; + + int outCount = DoProcess(input, inputOffset, inputCount, tempBuffer, 0); + Array.Copy(tempBuffer, 0, output, outputOffset, outCount); + + return outCount; + } + } + + return DoProcess(input, inputOffset, inputCount, output, outputOffset); + } + + protected abstract int DoProcess(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset); + + protected abstract int MaxOutputCount(int inputCount); + + protected abstract int MinOutputCount(int inputCount); + } +} diff --git a/DiscUtils/Compression/HuffmanTree.cs b/DiscUtils/Compression/HuffmanTree.cs new file mode 100644 index 0000000..2459cf1 --- /dev/null +++ b/DiscUtils/Compression/HuffmanTree.cs @@ -0,0 +1,109 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + /// + /// A canonical Huffman tree implementation. + /// + /// + /// A lookup table is created that will take any bit sequence (max tree depth in length), + /// indicating the output symbol. In WIM files, in practice, no chunk exceeds 32768 bytes + /// in length, so we often end up generating a bigger lookup table than the data it's + /// encoding. This makes for exceptionally fast symbol lookups O(1), but is inefficient + /// overall. + /// + internal sealed class HuffmanTree + { + private int _numBits; // Max bits per symbol + private int _numSymbols; // Max symbols + private uint[] _lengths; + private uint[] _buffer; + + public HuffmanTree(uint[] lengths) + { + _lengths = lengths; + _numSymbols = lengths.Length; + + uint maxLength = 0; + for (int i = 0; i < _lengths.Length; ++i) + { + if (_lengths[i] > maxLength) + { + maxLength = _lengths[i]; + } + } + + _numBits = (int)maxLength; + _buffer = new uint[1 << _numBits]; + + Build(); + } + + public uint[] Lengths + { + get + { + return _lengths; + } + } + + public uint NextSymbol(BitStream bitStream) + { + uint symbol = _buffer[bitStream.Peek(_numBits)]; + + // We may have over-read, reset bitstream position + bitStream.Consume((int)_lengths[symbol]); + + return symbol; + } + + private void Build() + { + int position = 0; + + // For each bit-length... + for (int i = 1; i <= _numBits; ++i) + { + // Check each symbol + for (uint symbol = 0; symbol < _numSymbols; ++symbol) + { + if (_lengths[symbol] == i) + { + int numToFill = 1 << (_numBits - i); + for (int n = 0; n < numToFill; ++n) + { + _buffer[position + n] = symbol; + } + + position += numToFill; + } + } + } + + for (int i = position; i < _buffer.Length; ++i) + { + _buffer[i] = uint.MaxValue; + } + } + } +} diff --git a/DiscUtils/Compression/InverseBurrowsWheeler.cs b/DiscUtils/Compression/InverseBurrowsWheeler.cs new file mode 100644 index 0000000..c3d6a25 --- /dev/null +++ b/DiscUtils/Compression/InverseBurrowsWheeler.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + using System; + + internal sealed class InverseBurrowsWheeler : DataBlockTransform + { + private int[] _pointers; + private int[] _nextPos; + + public InverseBurrowsWheeler(int bufferSize) + { + _pointers = new int[bufferSize]; + _nextPos = new int[256]; + } + + public int OriginalIndex { get; set; } + + protected override bool BuffersMustNotOverlap + { + get { return true; } + } + + protected override int DoProcess(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + { + int outputCount = inputCount; + + // First find the frequency of each value + Array.Clear(_nextPos, 0, _nextPos.Length); + for (int i = inputOffset; i < inputOffset + inputCount; ++i) + { + _nextPos[input[i]]++; + } + + // We know they're 'sorted' in the first column, so now can figure + // out the position of the first instance of each. + int sum = 0; + for (int i = 0; i < 256; ++i) + { + int tempSum = sum; + sum += _nextPos[i]; + _nextPos[i] = tempSum; + } + + // For each value in the final column, put a pointer to to the + // 'next' character in the first (sorted) column. + for (int i = 0; i < inputCount; ++i) + { + _pointers[_nextPos[input[inputOffset + i]]++] = i; + } + + // The 'next' character after the end of the original string is the + // first character of the original string. + int focus = _pointers[OriginalIndex]; + + // We can now just walk the pointers to reconstruct the original string + for (int i = 0; i < outputCount; ++i) + { + output[outputOffset + i] = input[inputOffset + focus]; + focus = _pointers[focus]; + } + + return outputCount; + } + + protected override int MaxOutputCount(int inputCount) + { + return inputCount; + } + + protected override int MinOutputCount(int inputCount) + { + return inputCount; + } + } +} diff --git a/DiscUtils/Compression/MoveToFront.cs b/DiscUtils/Compression/MoveToFront.cs new file mode 100644 index 0000000..32a43de --- /dev/null +++ b/DiscUtils/Compression/MoveToFront.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + internal class MoveToFront + { + private byte[] _buffer; + + public MoveToFront() + : this(256, false) + { + } + + public MoveToFront(int size, bool autoInit) + { + _buffer = new byte[size]; + + if (autoInit) + { + for (byte i = 0; i < size; ++i) + { + _buffer[i] = i; + } + } + } + + public byte Head + { + get { return _buffer[0]; } + } + + public void Set(int pos, byte val) + { + _buffer[pos] = val; + } + + public byte GetAndMove(int pos) + { + byte val = _buffer[pos]; + + for (int i = pos; i > 0; --i) + { + _buffer[i] = _buffer[i - 1]; + } + + _buffer[0] = val; + return val; + } + } +} diff --git a/DiscUtils/Compression/ZlibStream.cs b/DiscUtils/Compression/ZlibStream.cs new file mode 100644 index 0000000..1a0b87b --- /dev/null +++ b/DiscUtils/Compression/ZlibStream.cs @@ -0,0 +1,237 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Compression +{ + using System; + using System.IO; + using System.IO.Compression; + + /// + /// Implementation of the Zlib compression algorithm. + /// + /// Only decompression is currently implemented. + public class ZlibStream : Stream + { + private Stream _stream; + private CompressionMode _mode; + private DeflateStream _deflateStream; + private Adler32 _adler32; + + /// + /// Initializes a new instance of the ZlibStream class. + /// + /// The stream to compress of decompress. + /// Whether to compress or decompress. + /// Whether closing this stream should leave stream open. + public ZlibStream(Stream stream, CompressionMode mode, bool leaveOpen) + { + _stream = stream; + _mode = mode; + + if (mode == CompressionMode.Decompress) + { + // We just sanity check against expected header values... + byte[] headerBuffer = Utilities.ReadFully(stream, 2); + ushort header = Utilities.ToUInt16BigEndian(headerBuffer, 0); + + if ((header % 31) != 0) + { + throw new IOException("Invalid Zlib header found"); + } + + if ((header & 0x0F00) != (8 << 8)) + { + throw new NotSupportedException("Zlib compression not using DEFLATE algorithm"); + } + + if ((header & 0x0020) != 0) + { + throw new NotSupportedException("Zlib compression using preset dictionary"); + } + } + else + { + ushort header = + (8 << 8) // DEFLATE + | (7 << 12) // 32K window size + | 0x80; // Default algorithm + header |= (ushort)(31 - (header % 31)); + + byte[] headerBuffer = new byte[2]; + Utilities.WriteBytesBigEndian(header, headerBuffer, 0); + stream.Write(headerBuffer, 0, 2); + } + + _deflateStream = new DeflateStream(stream, mode, leaveOpen); + _adler32 = new Adler32(); + } + + /// + /// Gets whether the stream can be read. + /// + public override bool CanRead + { + get { return _deflateStream.CanRead; } + } + + /// + /// Gets whether the stream pointer can be changed. + /// + public override bool CanSeek + { + get { return false; } + } + + /// + /// Gets whether the stream can be written to. + /// + public override bool CanWrite + { + get { return _deflateStream.CanWrite; } + } + + /// + /// Gets the length of the stream. + /// + public override long Length + { + get { throw new NotSupportedException(); } + } + + /// + /// Gets and sets the stream position. + /// + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// Closes the stream. + /// + public override void Close() + { + if (_mode == CompressionMode.Decompress) + { + // Can only check Adler checksum on seekable streams. Since DeflateStream + // aggresively caches input, it normally has already consumed the footer. + if (_stream.CanSeek) + { + _stream.Seek(-4, SeekOrigin.End); + byte[] footerBuffer = Utilities.ReadFully(_stream, 4); + if (Utilities.ToInt32BigEndian(footerBuffer, 0) != _adler32.Value) + { + throw new InvalidDataException("Corrupt decompressed data detected"); + } + } + + _deflateStream.Close(); + } + else + { + _deflateStream.Close(); + + byte[] footerBuffer = new byte[4]; + Utilities.WriteBytesBigEndian(_adler32.Value, footerBuffer, 0); + _stream.Write(footerBuffer, 0, 4); + } + } + + /// + /// Flushes the stream. + /// + public override void Flush() + { + _deflateStream.Flush(); + } + + /// + /// Reads data from the stream. + /// + /// The buffer to populate. + /// The first byte to write. + /// The number of bytes requested. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + CheckParams(buffer, offset, count); + + int numRead = _deflateStream.Read(buffer, offset, count); + _adler32.Process(buffer, offset, numRead); + return numRead; + } + + /// + /// Seeks to a new position. + /// + /// Relative position to seek to. + /// The origin of the seek. + /// The new position. + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + /// Changes the length of the stream. + /// + /// The new desired length of the stream. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Writes data to the stream. + /// + /// Buffer containing the data to write. + /// Offset of the first byte to write. + /// Number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + CheckParams(buffer, offset, count); + + _adler32.Process(buffer, offset, count); + _deflateStream.Write(buffer, offset, count); + } + + private static void CheckParams(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentException("Offset outside of array bounds", "offset"); + } + + if (count < 0 || offset + count > buffer.Length) + { + throw new ArgumentException("Array index out of bounds", "count"); + } + } + } +} diff --git a/DiscUtils/ConcatStream.cs b/DiscUtils/ConcatStream.cs new file mode 100644 index 0000000..96fb057 --- /dev/null +++ b/DiscUtils/ConcatStream.cs @@ -0,0 +1,289 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + + /// + /// The concatenation of multiple streams (read-only, for now). + /// + internal class ConcatStream : SparseStream + { + private SparseStream[] _streams; + private Ownership _ownsStreams; + private bool _canWrite; + + private long _position; + + public ConcatStream(Ownership ownsStreams, params SparseStream[] streams) + { + _ownsStreams = ownsStreams; + _streams = streams; + + // Only allow writes if all streams can be written + _canWrite = true; + foreach (var stream in streams) + { + if (!stream.CanWrite) + { + _canWrite = false; + } + } + } + + public override bool CanRead + { + get + { + CheckDisposed(); + return true; + } + } + + public override bool CanSeek + { + get + { + CheckDisposed(); + return true; + } + } + + public override bool CanWrite + { + get + { + CheckDisposed(); + return _canWrite; + } + } + + public override long Length + { + get + { + CheckDisposed(); + long length = 0; + for (int i = 0; i < _streams.Length; ++i) + { + length += _streams[i].Length; + } + + return length; + } + } + + public override long Position + { + get + { + CheckDisposed(); + return _position; + } + + set + { + CheckDisposed(); + _position = value; + } + } + + public override IEnumerable Extents + { + get + { + CheckDisposed(); + List extents = new List(); + + long pos = 0; + for (int i = 0; i < _streams.Length; ++i) + { + foreach (StreamExtent extent in _streams[i].Extents) + { + extents.Add(new StreamExtent(extent.Start + pos, extent.Length)); + } + + pos += _streams[i].Length; + } + + return extents; + } + } + + public override void Flush() + { + CheckDisposed(); + for (int i = 0; i < _streams.Length; ++i) + { + _streams[i].Flush(); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + CheckDisposed(); + + int totalRead = 0; + int numRead = 0; + + do + { + long activeStreamStartPos; + int activeStream = GetActiveStream(out activeStreamStartPos); + + _streams[activeStream].Position = _position - activeStreamStartPos; + + numRead = _streams[activeStream].Read(buffer, offset + totalRead, count - totalRead); + + totalRead += numRead; + _position += numRead; + } + while (numRead != 0); + + return totalRead; + } + + public override long Seek(long offset, System.IO.SeekOrigin origin) + { + CheckDisposed(); + + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += Length; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + else + { + Position = effectiveOffset; + return Position; + } + } + + public override void SetLength(long value) + { + CheckDisposed(); + + long lastStreamOffset; + int lastStream = GetStream(Length, out lastStreamOffset); + if (value < lastStreamOffset) + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, "Unable to reduce stream length to less than {0}", lastStreamOffset)); + } + + _streams[lastStream].SetLength(value - lastStreamOffset); + } + + public override void Write(byte[] buffer, int offset, int count) + { + CheckDisposed(); + + int totalWritten = 0; + while (totalWritten != count) + { + // Offset of the stream = streamOffset + long streamOffset; + int streamIdx = GetActiveStream(out streamOffset); + + // Offset within the stream = streamPos + long streamPos = _position - streamOffset; + _streams[streamIdx].Position = streamPos; + + // Write (limited to the stream's length), except for final stream - that may be + // extendable + int numToWrite; + if (streamIdx == _streams.Length - 1) + { + numToWrite = count - totalWritten; + } + else + { + numToWrite = (int)Math.Min(count - totalWritten, _streams[streamIdx].Length - streamPos); + } + + _streams[streamIdx].Write(buffer, offset + totalWritten, numToWrite); + + totalWritten += numToWrite; + _position += numToWrite; + } + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsStreams == Ownership.Dispose && _streams != null) + { + foreach (var stream in _streams) + { + stream.Dispose(); + } + + _streams = null; + } + } + finally + { + base.Dispose(disposing); + } + } + + private int GetActiveStream(out long startPos) + { + return GetStream(_position, out startPos); + } + + private int GetStream(long targetPos, out long streamStartPos) + { + // Find the stream that _position is within + streamStartPos = 0; + int focusStream = 0; + while (focusStream < _streams.Length - 1 && streamStartPos + _streams[focusStream].Length <= targetPos) + { + streamStartPos += _streams[focusStream].Length; + focusStream++; + } + + return focusStream; + } + + private void CheckDisposed() + { + if (_streams == null) + { + throw new ObjectDisposedException("ConcatStream"); + } + } + } +} diff --git a/DiscUtils/Crc32.cs b/DiscUtils/Crc32.cs new file mode 100644 index 0000000..c6a2ec7 --- /dev/null +++ b/DiscUtils/Crc32.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + internal abstract class Crc32 + { + protected readonly uint[] Table; + protected uint _value; + + protected Crc32(uint[] table) + { + Table = table; + _value = 0xFFFFFFFF; + } + + public uint Value + { + get { return _value ^ 0xFFFFFFFF; } + } + + public abstract void Process(byte[] buffer, int offset, int count); + } +} diff --git a/DiscUtils/Crc32Algorithm.cs b/DiscUtils/Crc32Algorithm.cs new file mode 100644 index 0000000..bda8573 --- /dev/null +++ b/DiscUtils/Crc32Algorithm.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + internal enum Crc32Algorithm + { + /// + /// Used in Ethernet, PKZIP, BZIP2, Gzip, PNG, etc. (aka CRC32). + /// + Common = 0, + + /// + /// Used in iSCSI, SCTP, Btrfs, Vhdx. (aka CRC32C). + /// + Castagnoli = 1, + + /// + /// Unknown usage. (aka CRC32K). + /// + Koopman = 2, + + /// + /// Used in AIXM. (aka CRC32Q). + /// + Aeronautical = 3 + } +} diff --git a/DiscUtils/Crc32BigEndian.cs b/DiscUtils/Crc32BigEndian.cs new file mode 100644 index 0000000..6811aea --- /dev/null +++ b/DiscUtils/Crc32BigEndian.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Calculates CRC32 of buffers. + /// + internal sealed class Crc32BigEndian : Crc32 + { + private static readonly uint[][] Tables; + + static Crc32BigEndian() + { + Tables = new uint[4][]; + + Tables[(int)Crc32Algorithm.Common] = CalcTable(0x04C11DB7); + Tables[(int)Crc32Algorithm.Castagnoli] = CalcTable(0x1EDC6F41); + Tables[(int)Crc32Algorithm.Koopman] = CalcTable(0x741B8CD7); + Tables[(int)Crc32Algorithm.Aeronautical] = CalcTable(0x814141AB); + } + + public Crc32BigEndian(Crc32Algorithm algorithm) + : base(Tables[(int)algorithm]) + { + } + + public static uint Compute(Crc32Algorithm algorithm, byte[] buffer, int offset, int count) + { + return Process(Tables[(int)algorithm], 0xFFFFFFFF, buffer, offset, count) ^ 0xFFFFFFFF; + } + + public override void Process(byte[] buffer, int offset, int count) + { + _value = Process(Table, _value, buffer, offset, count); + } + + private static uint[] CalcTable(uint polynomial) + { + uint[] table = new uint[256]; + + for (uint i = 0; i < 256; ++i) + { + uint crc = i << 24; + + for (int j = 8; j > 0; --j) + { + if ((crc & 0x80000000) != 0) + { + crc = (crc << 1) ^ polynomial; + } + else + { + crc <<= 1; + } + } + + table[i] = crc; + } + + return table; + } + + private static uint Process(uint[] table, uint accumulator, byte[] buffer, int offset, int count) + { + uint value = accumulator; + + for (int i = 0; i < count; ++i) + { + byte b = buffer[offset + i]; + value = table[(value >> 24) ^ b] ^ (value << 8); + } + + return value; + } + } +} diff --git a/DiscUtils/Crc32LittleEndian.cs b/DiscUtils/Crc32LittleEndian.cs new file mode 100644 index 0000000..ee23270 --- /dev/null +++ b/DiscUtils/Crc32LittleEndian.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Calculates CRC32 of buffers. + /// + internal sealed class Crc32LittleEndian : Crc32 + { + private static readonly uint[][] Tables; + + static Crc32LittleEndian() + { + Tables = new uint[4][]; + + Tables[(int)Crc32Algorithm.Common] = CalcTable(0xEDB88320); + Tables[(int)Crc32Algorithm.Castagnoli] = CalcTable(0x82F63B78); + Tables[(int)Crc32Algorithm.Koopman] = CalcTable(0xEB31D82E); + Tables[(int)Crc32Algorithm.Aeronautical] = CalcTable(0xD5828281); + } + + public Crc32LittleEndian(Crc32Algorithm algorithm) + : base(Tables[(int)algorithm]) + { + } + + public static uint Compute(Crc32Algorithm algorithm, byte[] buffer, int offset, int count) + { + return Process(Tables[(int)algorithm], 0xFFFFFFFF, buffer, offset, count) ^ 0xFFFFFFFF; + } + + public override void Process(byte[] buffer, int offset, int count) + { + _value = Process(Table, _value, buffer, offset, count); + } + + private static uint[] CalcTable(uint polynomial) + { + uint[] table = new uint[256]; + + table[0] = 0; + for (uint i = 0; i <= 255; ++i) + { + uint crc = i; + + for (int j = 8; j > 0; --j) + { + if ((crc & 1) != 0) + { + crc = (crc >> 1) ^ polynomial; + } + else + { + crc >>= 1; + } + } + + table[i] = crc; + } + + return table; + } + + private static uint Process(uint[] table, uint accumulator, byte[] buffer, int offset, int count) + { + uint value = accumulator; + + for (int i = 0; i < count; ++i) + { + byte b = buffer[offset + i]; + + uint temp1 = (value >> 8) & 0x00FFFFFF; + uint temp2 = table[(value ^ b) & 0xFF]; + value = temp1 ^ temp2; + } + + return value; + } + } +} diff --git a/DiscUtils/DiscDirectoryInfo.cs b/DiscUtils/DiscDirectoryInfo.cs new file mode 100644 index 0000000..c84a7fd --- /dev/null +++ b/DiscUtils/DiscDirectoryInfo.cs @@ -0,0 +1,186 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.IO; + + /// + /// Provides information about a directory on a disc. + /// + /// + /// This class allows navigation of the disc directory/file hierarchy. + /// + public sealed class DiscDirectoryInfo : DiscFileSystemInfo + { + /// + /// Initializes a new instance of the DiscDirectoryInfo class. + /// + /// The file system the directory info relates to. + /// The path within the file system of the directory. + internal DiscDirectoryInfo(DiscFileSystem fileSystem, string path) + : base(fileSystem, path) + { + } + + /// + /// Gets a value indicating whether the directory exists. + /// + public override bool Exists + { + get { return FileSystem.DirectoryExists(Path); } + } + + /// + /// Gets the full path of the directory. + /// + public override string FullName + { + get { return base.FullName + @"\"; } + } + + /// + /// Creates a directory. + /// + public void Create() + { + FileSystem.CreateDirectory(Path); + } + + /// + /// Deletes a directory, even if it's not empty. + /// + public override void Delete() + { + FileSystem.DeleteDirectory(Path, false); + } + + /// + /// Deletes a directory, with the caller choosing whether to recurse. + /// + /// true to delete all child node, false to fail if the directory is not empty. + public void Delete(bool recursive) + { + FileSystem.DeleteDirectory(Path, recursive); + } + + /// + /// Moves a directory and it's contents to a new path. + /// + /// The destination directory name. + public void MoveTo(string destinationDirName) + { + FileSystem.MoveDirectory(Path, destinationDirName); + } + + /// + /// Gets all child directories. + /// + /// An array of child directories. + public DiscDirectoryInfo[] GetDirectories() + { + return Utilities.Map(FileSystem.GetDirectories(Path), (p) => new DiscDirectoryInfo(FileSystem, p)); + } + + /// + /// Gets all child directories matching a search pattern. + /// + /// The search pattern. + /// An array of child directories, or empty if none match. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). + public DiscDirectoryInfo[] GetDirectories(string pattern) + { + return GetDirectories(pattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets all descendant directories matching a search pattern. + /// + /// The search pattern. + /// Whether to search just this directory, or all children. + /// An array of descendant directories, or empty if none match. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). The option parameter determines whether only immediate + /// children, or all children are returned. + public DiscDirectoryInfo[] GetDirectories(string pattern, SearchOption searchOption) + { + return Utilities.Map(FileSystem.GetDirectories(Path, pattern, searchOption), (p) => new DiscDirectoryInfo(FileSystem, p)); + } + + /// + /// Gets all files. + /// + /// An array of files. + public DiscFileInfo[] GetFiles() + { + return Utilities.Map(FileSystem.GetFiles(Path), (p) => new DiscFileInfo(FileSystem, p)); + } + + /// + /// Gets all files matching a search pattern. + /// + /// The search pattern. + /// An array of files, or empty if none match. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). + public DiscFileInfo[] GetFiles(string pattern) + { + return GetFiles(pattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets all descendant files matching a search pattern. + /// + /// The search pattern. + /// Whether to search just this directory, or all children. + /// An array of descendant files, or empty if none match. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). The option parameter determines whether only immediate + /// children, or all children are returned. + public DiscFileInfo[] GetFiles(string pattern, SearchOption searchOption) + { + return Utilities.Map(FileSystem.GetFiles(Path, pattern, searchOption), (p) => new DiscFileInfo(FileSystem, p)); + } + + /// + /// Gets all files and directories in this directory. + /// + /// An array of files and directories. + public DiscFileSystemInfo[] GetFileSystemInfos() + { + return Utilities.Map(FileSystem.GetFileSystemEntries(Path), (p) => new DiscFileSystemInfo(FileSystem, p)); + } + + /// + /// Gets all files and directories in this directory. + /// + /// The search pattern. + /// An array of files and directories. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). + public DiscFileSystemInfo[] GetFileSystemInfos(string pattern) + { + return Utilities.Map(FileSystem.GetFileSystemEntries(Path, pattern), (p) => new DiscFileSystemInfo(FileSystem, p)); + } + } +} diff --git a/DiscUtils/DiscFileInfo.cs b/DiscUtils/DiscFileInfo.cs new file mode 100644 index 0000000..afe5697 --- /dev/null +++ b/DiscUtils/DiscFileInfo.cs @@ -0,0 +1,205 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.IO; + + /// + /// Provides information about a file on a disc. + /// + public sealed class DiscFileInfo : DiscFileSystemInfo + { + internal DiscFileInfo(DiscFileSystem fileSystem, string path) + : base(fileSystem, path) + { + } + + /// + /// Gets a value indicating whether the file exists. + /// + public override bool Exists + { + get { return FileSystem.FileExists(Path); } + } + + /// + /// Gets an instance of the parent directory. + /// + public DiscDirectoryInfo Directory + { + get { return Parent; } + } + + /// + /// Gets a string representing the directory's full path. + /// + public string DirectoryName + { + get { return Directory.FullName; } + } + + /// + /// Gets or sets a value indicating whether the file is read-only. + /// + public bool IsReadOnly + { + get + { + return (Attributes & FileAttributes.ReadOnly) != 0; + } + + set + { + if (value) + { + Attributes = Attributes | FileAttributes.ReadOnly; + } + else + { + Attributes = Attributes & ~FileAttributes.ReadOnly; + } + } + } + + /// + /// Gets the length of the current file in bytes. + /// + public long Length + { + get { return FileSystem.GetFileLength(Path); } + } + + /// + /// Deletes a file. + /// + public override void Delete() + { + FileSystem.DeleteFile(Path); + } + + /// + /// Creates a that appends text to the file represented by this . + /// + /// The newly created writer. + public StreamWriter AppendText() + { + return new StreamWriter(Open(FileMode.Append)); + } + + /// + /// Copies an existing file to a new file. + /// + /// The destination file. + public void CopyTo(string destinationFileName) + { + CopyTo(destinationFileName, false); + } + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The destination file. + /// Whether to permit over-writing of an existing file. + public void CopyTo(string destinationFileName, bool overwrite) + { + FileSystem.CopyFile(Path, destinationFileName, overwrite); + } + + /// + /// Creates a new file for reading and writing. + /// + /// The newly created stream. + public Stream Create() + { + return Open(FileMode.Create); + } + + /// + /// Creates a new that writes a new text file. + /// + /// A new stream writer that can write to the file contents. + public StreamWriter CreateText() + { + return new StreamWriter(Open(FileMode.Create)); + } + + /// + /// Moves a file to a new location. + /// + /// The new name of the file. + public void MoveTo(string destinationFileName) + { + FileSystem.MoveFile(Path, destinationFileName); + } + + /// + /// Opens the current file. + /// + /// The file mode for the created stream. + /// The newly created stream. + /// Read-only file systems only support FileMode.Open. + public Stream Open(FileMode mode) + { + return FileSystem.OpenFile(Path, mode); + } + + /// + /// Opens the current file. + /// + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The newly created stream. + /// Read-only file systems only support FileMode.Open and FileAccess.Read. + public Stream Open(FileMode mode, FileAccess access) + { + return FileSystem.OpenFile(Path, mode, access); + } + + /// + /// Opens an existing file for read-only access. + /// + /// The newly created stream. + public Stream OpenRead() + { + return Open(FileMode.Open, FileAccess.Read); + } + + /// + /// Opens an existing file for reading as UTF-8 text. + /// + /// The newly created reader. + public StreamReader OpenText() + { + return new StreamReader(OpenRead()); + } + + /// + /// Opens a file for writing. + /// + /// The newly created stream. + public Stream OpenWrite() + { + return Open(FileMode.Open, FileAccess.Write); + } + } +} diff --git a/DiscUtils/DiscFileLocator.cs b/DiscUtils/DiscFileLocator.cs new file mode 100644 index 0000000..f0a8161 --- /dev/null +++ b/DiscUtils/DiscFileLocator.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + internal sealed class DiscFileLocator : FileLocator + { + private DiscFileSystem _fileSystem; + private string _basePath; + + public DiscFileLocator(DiscFileSystem fileSystem, string basePath) + { + _fileSystem = fileSystem; + _basePath = basePath; + } + + public override bool Exists(string fileName) + { + return _fileSystem.FileExists(Utilities.CombinePaths(_basePath, fileName)); + } + + public override Stream Open(string fileName, FileMode mode, FileAccess access, FileShare share) + { + return _fileSystem.OpenFile(Utilities.CombinePaths(_basePath, fileName), mode, access); + } + + public override FileLocator GetRelativeLocator(string path) + { + return new DiscFileLocator(_fileSystem, Utilities.CombinePaths(_basePath, path)); + } + + public override string GetFullPath(string path) + { + return Utilities.CombinePaths(_basePath, path); + } + + public override string GetDirectoryFromPath(string path) + { + return Utilities.GetDirectoryFromPath(path); + } + + public override string GetFileFromPath(string path) + { + return Utilities.GetFileFromPath(path); + } + + public override DateTime GetLastWriteTimeUtc(string path) + { + return _fileSystem.GetLastWriteTimeUtc(Utilities.CombinePaths(_basePath, path)); + } + + public override bool HasCommonRoot(FileLocator other) + { + DiscFileLocator otherDiscLocator = other as DiscFileLocator; + + if (otherDiscLocator == null) + { + return false; + } + + // Common root if the same file system instance. + return Object.ReferenceEquals(otherDiscLocator._fileSystem, _fileSystem); + } + + public override string ResolveRelativePath(string path) + { + return Utilities.ResolveRelativePath(_basePath, path); + } + } +} diff --git a/DiscUtils/DiscFileSystem.cs b/DiscUtils/DiscFileSystem.cs new file mode 100644 index 0000000..1b1eaf9 --- /dev/null +++ b/DiscUtils/DiscFileSystem.cs @@ -0,0 +1,499 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + /// + /// Provides the base class for all file systems. + /// + public abstract class DiscFileSystem : MarshalByRefObject, IFileSystem, IDisposable + { + private DiscFileSystemOptions _options; + + /// + /// Initializes a new instance of the DiscFileSystem class. + /// + protected DiscFileSystem() + { + _options = new DiscFileSystemOptions(); + } + + /// + /// Initializes a new instance of the DiscFileSystem class. + /// + /// The options instance to use for this file system instance. + protected DiscFileSystem(DiscFileSystemOptions defaultOptions) + { + _options = defaultOptions; + } + + /// + /// Finalizes an instance of the DiscFileSystem class. + /// + ~DiscFileSystem() + { + Dispose(false); + } + + /// + /// Gets the file system options, which can be modified. + /// + public virtual DiscFileSystemOptions Options + { + get { return _options; } + } + + /// + /// Gets a friendly description of the file system type. + /// + public abstract string FriendlyName + { + get; + } + + /// + /// Gets a value indicating whether the file system is read-only or read-write. + /// + /// true if the file system is read-write. + public abstract bool CanWrite { get; } + + /// + /// Gets the root directory of the file system. + /// + public virtual DiscDirectoryInfo Root + { + get { return new DiscDirectoryInfo(this, string.Empty); } + } + + /// + /// Gets the volume label. + /// + public virtual string VolumeLabel + { + get { return string.Empty; } + } + + /// + /// Gets a value indicating whether the file system is thread-safe. + /// + public virtual bool IsThreadSafe + { + get { return false; } + } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + public virtual void CopyFile(string sourceFile, string destinationFile) + { + CopyFile(sourceFile, destinationFile, false); + } + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public abstract void CopyFile(string sourceFile, string destinationFile, bool overwrite); + + /// + /// Creates a directory. + /// + /// The path of the new directory. + public abstract void CreateDirectory(string path); + + /// + /// Deletes a directory. + /// + /// The path of the directory to delete. + public abstract void DeleteDirectory(string path); + + /// + /// Deletes a directory, optionally with all descendants. + /// + /// The path of the directory to delete. + /// Determines if the all descendants should be deleted. + public virtual void DeleteDirectory(string path, bool recursive) + { + if (recursive) + { + foreach (string dir in GetDirectories(path)) + { + DeleteDirectory(dir, true); + } + + foreach (string file in GetFiles(path)) + { + DeleteFile(file); + } + } + + DeleteDirectory(path); + } + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + public abstract void DeleteFile(string path); + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public abstract bool DirectoryExists(string path); + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public abstract bool FileExists(string path); + + /// + /// Indicates if a file or directory exists. + /// + /// The path to test. + /// true if the file or directory exists. + public virtual bool Exists(string path) + { + return FileExists(path) || DirectoryExists(path); + } + + /// + /// Gets the names of subdirectories in a specified directory. + /// + /// The path to search. + /// Array of directories. + public virtual string[] GetDirectories(string path) + { + return GetDirectories(path, "*.*", SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of directories matching the search pattern. + public virtual string[] GetDirectories(string path, string searchPattern) + { + return GetDirectories(path, searchPattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public abstract string[] GetDirectories(string path, string searchPattern, SearchOption searchOption); + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// Array of files. + public virtual string[] GetFiles(string path) + { + return GetFiles(path, "*.*", SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// The search string to match against. + /// Array of files matching the search pattern. + public virtual string[] GetFiles(string path, string searchPattern) + { + return GetFiles(path, searchPattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public abstract string[] GetFiles(string path, string searchPattern, SearchOption searchOption); + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + public abstract string[] GetFileSystemEntries(string path); + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public abstract string[] GetFileSystemEntries(string path, string searchPattern); + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public abstract void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName); + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + public virtual void MoveFile(string sourceName, string destinationName) + { + MoveFile(sourceName, destinationName, false); + } + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + public abstract void MoveFile(string sourceName, string destinationName, bool overwrite); + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + public virtual SparseStream OpenFile(string path, FileMode mode) + { + return OpenFile(path, mode, FileAccess.ReadWrite); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + public abstract SparseStream OpenFile(string path, FileMode mode, FileAccess access); + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public abstract FileAttributes GetAttributes(string path); + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public abstract void SetAttributes(string path, FileAttributes newValue); + + /// + /// Gets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public virtual DateTime GetCreationTime(string path) + { + return GetCreationTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public virtual void SetCreationTime(string path, DateTime newTime) + { + SetCreationTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public abstract DateTime GetCreationTimeUtc(string path); + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public abstract void SetCreationTimeUtc(string path, DateTime newTime); + + /// + /// Gets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public virtual DateTime GetLastAccessTime(string path) + { + return GetLastAccessTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public virtual void SetLastAccessTime(string path, DateTime newTime) + { + SetLastAccessTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public abstract DateTime GetLastAccessTimeUtc(string path); + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public abstract void SetLastAccessTimeUtc(string path, DateTime newTime); + + /// + /// Gets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public virtual DateTime GetLastWriteTime(string path) + { + return GetLastWriteTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public virtual void SetLastWriteTime(string path, DateTime newTime) + { + SetLastWriteTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public abstract DateTime GetLastWriteTimeUtc(string path); + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public abstract void SetLastWriteTimeUtc(string path, DateTime newTime); + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public abstract long GetFileLength(string path); + + /// + /// Gets an object representing a possible file. + /// + /// The file path. + /// The representing object. + /// The file does not need to exist. + public virtual DiscFileInfo GetFileInfo(string path) + { + return new DiscFileInfo(this, path); + } + + /// + /// Gets an object representing a possible directory. + /// + /// The directory path. + /// The representing object. + /// The directory does not need to exist. + public virtual DiscDirectoryInfo GetDirectoryInfo(string path) + { + return new DiscDirectoryInfo(this, path); + } + + /// + /// Gets an object representing a possible file system object (file or directory). + /// + /// The file system path. + /// The representing object. + /// The file system object does not need to exist. + public virtual DiscFileSystemInfo GetFileSystemInfo(string path) + { + return new DiscFileSystemInfo(this, path); + } + + /// + /// Reads the boot code of the file system into a byte array. + /// + /// The boot code, or null if not available. + public virtual byte[] ReadBootCode() + { + return null; + } + + #region IDisposable Members + + /// + /// Disposes of this instance, releasing all resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of this instance. + /// + /// The value true if Disposing. + protected virtual void Dispose(bool disposing) + { + } + + #endregion + } +} diff --git a/DiscUtils/DiscFileSystemChecker.cs b/DiscUtils/DiscFileSystemChecker.cs new file mode 100644 index 0000000..919b1fb --- /dev/null +++ b/DiscUtils/DiscFileSystemChecker.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + /// + /// Flags for the amount of detail to include in a report. + /// + [Flags] + public enum ReportLevels + { + /// + /// Report no information. + /// + None = 0x00, + + /// + /// Report informational level items. + /// + Information = 0x01, + + /// + /// Report warning level items. + /// + Warnings = 0x02, + + /// + /// Report error level items. + /// + Errors = 0x04, + + /// + /// Report all items. + /// + All = 0x07 + } + + /// + /// Base class for objects that validate file system integrity. + /// + /// Instances of this class do not offer the ability to fix/correct + /// file system issues, just to perform a limited number of checks on + /// integrity of the file system. + public abstract class DiscFileSystemChecker + { + /// + /// Checks the integrity of a file system held in a stream. + /// + /// A report on issues found. + /// The amount of detail to report. + /// true if the file system appears valid, else false. + public abstract bool Check(TextWriter reportOutput, ReportLevels levels); + } +} diff --git a/DiscUtils/DiscFileSystemInfo.cs b/DiscUtils/DiscFileSystemInfo.cs new file mode 100644 index 0000000..8812577 --- /dev/null +++ b/DiscUtils/DiscFileSystemInfo.cs @@ -0,0 +1,226 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + /// + /// Provides the base class for both and objects. + /// + public class DiscFileSystemInfo + { + private DiscFileSystem _fileSystem; + private string _path; + + internal DiscFileSystemInfo(DiscFileSystem fileSystem, string path) + { + if (path == null) + { + throw new ArgumentNullException("path"); + } + + _fileSystem = fileSystem; + _path = path.Trim('\\'); + } + + /// + /// Gets the file system the referenced file or directory exists on. + /// + public DiscFileSystem FileSystem + { + get { return _fileSystem; } + } + + /// + /// Gets the name of the file or directory. + /// + public virtual string Name + { + get { return Utilities.GetFileFromPath(_path); } + } + + /// + /// Gets the full path of the file or directory. + /// + public virtual string FullName + { + get { return _path; } + } + + /// + /// Gets the extension part of the file or directory name. + /// + public virtual string Extension + { + get + { + string name = Name; + int sepIdx = name.LastIndexOf('.'); + if (sepIdx >= 0) + { + return name.Substring(sepIdx + 1); + } + + return string.Empty; + } + } + + /// + /// Gets or sets the of the current object. + /// + public virtual FileAttributes Attributes + { + get { return FileSystem.GetAttributes(_path); } + set { FileSystem.SetAttributes(_path, value); } + } + + /// + /// Gets the of the directory containing the current object. + /// + public virtual DiscDirectoryInfo Parent + { + get + { + if (string.IsNullOrEmpty(_path)) + { + return null; + } + + return new DiscDirectoryInfo(FileSystem, Utilities.GetDirectoryFromPath(_path)); + } + } + + /// + /// Gets a value indicating whether the file system object exists. + /// + public virtual bool Exists + { + get { return FileSystem.Exists(_path); } + } + + /// + /// Gets or sets the creation time (in local time) of the current object. + /// + public virtual DateTime CreationTime + { + get { return CreationTimeUtc.ToLocalTime(); } + set { CreationTimeUtc = value.ToUniversalTime(); } + } + + /// + /// Gets or sets the creation time (in UTC) of the current object. + /// + public virtual DateTime CreationTimeUtc + { + get { return FileSystem.GetCreationTimeUtc(_path); } + set { FileSystem.SetCreationTimeUtc(_path, value); } + } + + /// + /// Gets or sets the last time (in local time) the file or directory was accessed. + /// + /// Read-only file systems will never update this value, it will remain at a fixed value. + public virtual DateTime LastAccessTime + { + get { return LastAccessTimeUtc.ToLocalTime(); } + set { LastAccessTimeUtc = value.ToUniversalTime(); } + } + + /// + /// Gets or sets the last time (in UTC) the file or directory was accessed. + /// + /// Read-only file systems will never update this value, it will remain at a fixed value. + public virtual DateTime LastAccessTimeUtc + { + get { return FileSystem.GetLastAccessTimeUtc(_path); } + set { FileSystem.SetLastAccessTimeUtc(_path, value); } + } + + /// + /// Gets or sets the last time (in local time) the file or directory was written to. + /// + public virtual DateTime LastWriteTime + { + get { return LastWriteTimeUtc.ToLocalTime(); } + set { LastWriteTimeUtc = value.ToUniversalTime(); } + } + + /// + /// Gets or sets the last time (in UTC) the file or directory was written to. + /// + public virtual DateTime LastWriteTimeUtc + { + get { return FileSystem.GetLastWriteTimeUtc(_path); } + set { FileSystem.SetLastWriteTimeUtc(_path, value); } + } + + /// + /// Gets the path to the referenced file. + /// + protected string Path + { + get { return _path; } + } + + /// + /// Deletes a file or directory. + /// + public virtual void Delete() + { + if ((Attributes & FileAttributes.Directory) != 0) + { + FileSystem.DeleteDirectory(_path); + } + else + { + FileSystem.DeleteFile(_path); + } + } + + /// + /// Indicates if is equivalent to this object. + /// + /// The object to compare. + /// true if is equivalent, else false. + public override bool Equals(object obj) + { + DiscFileSystemInfo asInfo = obj as DiscFileSystemInfo; + if (obj == null) + { + return false; + } + + return string.Compare(Path, asInfo.Path, StringComparison.Ordinal) == 0 && DiscFileSystem.Equals(FileSystem, asInfo.FileSystem); + } + + /// + /// Gets the hash code for this object. + /// + /// The hash code. + public override int GetHashCode() + { + return _path.GetHashCode() ^ _fileSystem.GetHashCode(); + } + } +} diff --git a/DiscUtils/DiscFileSystemOptions.cs b/DiscUtils/DiscFileSystemOptions.cs new file mode 100644 index 0000000..8bd5fb4 --- /dev/null +++ b/DiscUtils/DiscFileSystemOptions.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + /// + /// Common file system options. + /// + /// Not all options are honoured by all file systems. + public class DiscFileSystemOptions + { + private Random _rng; + + /// + /// Initializes a new instance of the DiscFileSystemOptions class. + /// + /// You shouldn't normally create a new instance. File systems will provide + /// an instance of the correct derived type. + public DiscFileSystemOptions() + { + } + + /// + /// Gets or sets the random number generator the file system should use. + /// + /// This option is normally null, which is fine for most purposes. + /// Use this option when you need to finely control the filesystem for + /// reproducibility of behaviour (for example in a test harness). + public Random RandomNumberGenerator + { + get { return _rng; } + set { _rng = value; } + } + } +} diff --git a/DiscUtils/DiskImageBuilder.cs b/DiscUtils/DiskImageBuilder.cs new file mode 100644 index 0000000..b51a964 --- /dev/null +++ b/DiscUtils/DiskImageBuilder.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Globalization; + + /// + /// Base class for all disk image builders. + /// + public abstract class DiskImageBuilder + { + private static Dictionary s_typeMap; + + private SparseStream _content; + private Geometry _geometry; + private Geometry _biosGeometry; + private GenericDiskAdapterType _adaptorType; + + /// + /// Gets or sets the content for this disk, implying the size of the disk. + /// + public SparseStream Content + { + get { return _content; } + set { _content = value; } + } + + /// + /// Gets or sets the geometry of this disk, will be implied from the content stream if not set. + /// + public Geometry Geometry + { + get { return _geometry; } + set { _geometry = value; } + } + + /// + /// Gets or sets the geometry of this disk, as reported by the BIOS, will be implied from the content stream if not set. + /// + public Geometry BiosGeometry + { + get { return _biosGeometry; } + set { _biosGeometry = value; } + } + + /// + /// Gets or sets the adapter type for created virtual disk, for file formats that encode this information. + /// + public virtual GenericDiskAdapterType GenericAdapterType + { + get { return _adaptorType; } + set { _adaptorType = value; } + } + + /// + /// Gets a value indicating whether this file format preserves BIOS geometry information. + /// + public virtual bool PreservesBiosGeometry + { + get { return false; } + } + + private static Dictionary TypeMap + { + get + { + if (s_typeMap == null) + { + InitializeMaps(); + } + + return s_typeMap; + } + } + + /// + /// Gets an instance that constructs the specified type (and variant) of virtual disk image. + /// + /// The type of image to build (VHD, VMDK, etc). + /// The variant type (differencing/dynamic, fixed/static, etc). + /// The builder instance. + public static DiskImageBuilder GetBuilder(string type, string variant) + { + VirtualDiskFactory factory; + if (!TypeMap.TryGetValue(type, out factory)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unknown disk type '{0}'", type), "type"); + } + + return factory.GetImageBuilder(variant); + } + + /// + /// Initiates the construction of the disk image. + /// + /// The base name for the disk images. + /// A set of one or more logical files that constitute the + /// disk image. The first file is the 'primary' file that is normally attached to VMs. + /// The supplied baseName is the start of the file name, with no file + /// extension. The set of file specifications will indicate the actual name corresponding + /// to each logical file that comprises the disk image. For example, given a base name + /// 'foo', the files 'foo.vmdk' and 'foo-flat.vmdk' could be returned. + public abstract DiskImageFileSpecification[] Build(string baseName); + + private static void InitializeMaps() + { + Dictionary typeMap = new Dictionary(); + + foreach (var type in typeof(VirtualDisk).Assembly.GetTypes()) + { + VirtualDiskFactoryAttribute attr = (VirtualDiskFactoryAttribute)Attribute.GetCustomAttribute(type, typeof(VirtualDiskFactoryAttribute), false); + if (attr != null) + { + VirtualDiskFactory factory = (VirtualDiskFactory)Activator.CreateInstance(type); + typeMap.Add(attr.Type, factory); + } + } + + s_typeMap = typeMap; + } + } +} diff --git a/DiscUtils/DiskImageFileSpecification.cs b/DiscUtils/DiskImageFileSpecification.cs new file mode 100644 index 0000000..2cabc48 --- /dev/null +++ b/DiscUtils/DiskImageFileSpecification.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Describes a particular file that is a constituent part of a virtual disk. + /// + public sealed class DiskImageFileSpecification + { + private string _name; + private StreamBuilder _builder; + + internal DiskImageFileSpecification(string name, StreamBuilder builder) + { + _name = name; + _builder = builder; + } + + /// + /// Gets name of the file. + /// + public string Name + { + get { return _name; } + } + + /// + /// Gets the object that provides access to the file's content. + /// + /// A stream object that contains the file's content. + public SparseStream OpenStream() + { + return _builder.Build(); + } + } +} diff --git a/DiscUtils/Fat/ClusterReader.cs b/DiscUtils/Fat/ClusterReader.cs new file mode 100644 index 0000000..e999af9 --- /dev/null +++ b/DiscUtils/Fat/ClusterReader.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.IO; + + internal sealed class ClusterReader + { + private Stream _stream; + private int _firstDataSector; + private int _sectorsPerCluster; + private int _bytesPerSector; + + /// + /// Pre-calculated value because of number of uses of this externally. + /// + private int _clusterSize; + + public ClusterReader(Stream stream, int firstDataSector, int sectorsPerCluster, int bytesPerSector) + { + _stream = stream; + _firstDataSector = firstDataSector; + _sectorsPerCluster = sectorsPerCluster; + _bytesPerSector = bytesPerSector; + + _clusterSize = _sectorsPerCluster * _bytesPerSector; + } + + public int ClusterSize + { + get { return _clusterSize; } + } + + public void ReadCluster(uint cluster, byte[] buffer, int offset) + { + if (offset + ClusterSize > buffer.Length) + { + throw new ArgumentOutOfRangeException("offset", "buffer is too small - cluster would overflow buffer"); + } + + uint firstSector = (uint)(((cluster - 2) * _sectorsPerCluster) + _firstDataSector); + + _stream.Position = firstSector * _bytesPerSector; + if (Utilities.ReadFully(_stream, buffer, offset, _clusterSize) != _clusterSize) + { + throw new IOException("Failed to read cluster " + cluster); + } + } + + internal void WriteCluster(uint cluster, byte[] buffer, int offset) + { + if (offset + ClusterSize > buffer.Length) + { + throw new ArgumentOutOfRangeException("offset", "buffer is too small - cluster would overflow buffer"); + } + + uint firstSector = (uint)(((cluster - 2) * _sectorsPerCluster) + _firstDataSector); + + _stream.Position = firstSector * _bytesPerSector; + + _stream.Write(buffer, offset, _clusterSize); + } + + internal void WipeCluster(uint cluster) + { + uint firstSector = (uint)(((cluster - 2) * _sectorsPerCluster) + _firstDataSector); + + _stream.Position = firstSector * _bytesPerSector; + + _stream.Write(new byte[_clusterSize], 0, _clusterSize); + } + } +} diff --git a/DiscUtils/Fat/ClusterStream.cs b/DiscUtils/Fat/ClusterStream.cs new file mode 100644 index 0000000..9a3b071 --- /dev/null +++ b/DiscUtils/Fat/ClusterStream.cs @@ -0,0 +1,475 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal delegate void FirstClusterChangedDelegate(uint cluster); + + internal class ClusterStream : Stream + { + private FileAccess _access; + private ClusterReader _reader; + private FileAllocationTable _fat; + private uint _length; + + private List _knownClusters; + private long _position; + + private uint _currentCluster = 0; + private byte[] _clusterBuffer; + + private bool _atEOF; + + internal ClusterStream(FatFileSystem fileSystem, FileAccess access, uint firstCluster, uint length) + { + _access = access; + _reader = fileSystem.ClusterReader; + _fat = fileSystem.Fat; + _length = length; + + _knownClusters = new List(); + if (firstCluster != 0) + { + _knownClusters.Add(firstCluster); + } + else + { + _knownClusters.Add(FatBuffer.EndOfChain); + } + + if (_length == uint.MaxValue) + { + _length = DetectLength(); + } + + _currentCluster = uint.MaxValue; + _clusterBuffer = new byte[_reader.ClusterSize]; + } + + public event FirstClusterChangedDelegate FirstClusterChanged; + + public override bool CanRead + { + get { return _access == FileAccess.Read || _access == FileAccess.ReadWrite; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return _access == FileAccess.ReadWrite || _access == FileAccess.Write; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get + { + return _position; + } + + set + { + if (value >= 0) + { + _position = value; + _atEOF = false; + } + else + { + throw new ArgumentOutOfRangeException("value", "Attempt to move before beginning of stream"); + } + } + } + + public override void Flush() + { + return; + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!CanRead) + { + throw new IOException("Attempt to read from file not opened for read"); + } + + if (_position > _length) + { + throw new IOException("Attempt to read beyond end of file"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException("count", "Attempt to read negative number of bytes"); + } + + int target = count; + if (_length - _position < count) + { + target = (int)(_length - _position); + } + + if (!TryLoadCurrentCluster()) + { + if ((_position == _length || _position == DetectLength()) && !_atEOF) + { + _atEOF = true; + return 0; + } + else + { + throw new IOException("Attempt to read beyond known clusters"); + } + } + + int numRead = 0; + while (numRead < target) + { + int clusterOffset = (int)(_position % _reader.ClusterSize); + int toCopy = Math.Min(_reader.ClusterSize - clusterOffset, target - numRead); + Array.Copy(_clusterBuffer, clusterOffset, buffer, offset + numRead, toCopy); + + // Remember how many we've read in total + numRead += toCopy; + + // Increment the position + _position += toCopy; + + // Abort if we've hit the end of the file + if (!TryLoadCurrentCluster()) + { + break; + } + } + + if (numRead == 0) + { + _atEOF = true; + } + + return numRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = offset; + if (origin == SeekOrigin.Current) + { + newPos += _position; + } + else if (origin == SeekOrigin.End) + { + newPos += Length; + } + + _position = newPos; + _atEOF = false; + return newPos; + } + + public override void SetLength(long value) + { + long desiredNumClusters = (value + _reader.ClusterSize - 1) / _reader.ClusterSize; + long actualNumClusters = (_length + _reader.ClusterSize - 1) / _reader.ClusterSize; + + if (desiredNumClusters < actualNumClusters) + { + uint cluster; + if (!TryGetClusterByPosition(value, out cluster)) + { + throw new IOException("Internal state corrupt - unable to find cluster"); + } + + uint firstToFree = _fat.GetNext(cluster); + _fat.SetEndOfChain(cluster); + _fat.FreeChain(firstToFree); + + while (_knownClusters.Count > desiredNumClusters) + { + _knownClusters.RemoveAt(_knownClusters.Count - 1); + } + + _knownClusters.Add(FatBuffer.EndOfChain); + + if (desiredNumClusters == 0) + { + FireFirstClusterAllocated(0); + } + } + else if (desiredNumClusters > actualNumClusters) + { + uint cluster; + while (!TryGetClusterByPosition(value, out cluster)) + { + cluster = ExtendChain(); + _reader.WipeCluster(cluster); + } + } + + if (_length != value) + { + _length = (uint)value; + if (_position > _length) + { + _position = _length; + } + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + int bytesRemaining = count; + + if (!CanWrite) + { + throw new IOException("Attempting to write to file not opened for writing"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException("count", count, "Attempting to write negative number of bytes"); + } + + if (offset > buffer.Length || (offset + count) > buffer.Length) + { + throw new ArgumentException("Attempt to write bytes outside of the buffer"); + } + + // TODO: Free space check... + try + { + while (bytesRemaining > 0) + { + // Extend the stream until it encompasses _position + uint cluster; + while (!TryGetClusterByPosition(_position, out cluster)) + { + cluster = ExtendChain(); + _reader.WipeCluster(cluster); + } + + // Fill this cluster with as much data as we can (WriteToCluster preserves existing cluster + // data, if necessary) + int numWritten = WriteToCluster(cluster, (int)(_position % _reader.ClusterSize), buffer, offset, bytesRemaining); + offset += numWritten; + bytesRemaining -= numWritten; + _position += numWritten; + } + + _length = (uint)Math.Max(_length, _position); + } + finally + { + _fat.Flush(); + } + + _atEOF = false; + } + + /// + /// Writes up to the next cluster boundary, making sure to preserve existing data in the cluster + /// that falls outside of the updated range. + /// + /// The cluster to write to. + /// The file position of the write (within the cluster). + /// The buffer with the new data. + /// Offset into buffer of the first byte to write. + /// The maximum number of bytes to write. + /// The number of bytes written - either count, or the number that fit up to + /// the cluster boundary. + private int WriteToCluster(uint cluster, int pos, byte[] buffer, int offset, int count) + { + if (pos == 0 && count >= _reader.ClusterSize) + { + _currentCluster = cluster; + Array.Copy(buffer, offset, _clusterBuffer, 0, _reader.ClusterSize); + + WriteCurrentCluster(); + + return _reader.ClusterSize; + } + else + { + // Partial cluster, so need to read existing cluster data first + LoadCluster(cluster); + + int copyLength = Math.Min(count, (int)(_reader.ClusterSize - (pos % _reader.ClusterSize))); + Array.Copy(buffer, offset, _clusterBuffer, pos, copyLength); + + WriteCurrentCluster(); + + return copyLength; + } + } + + /// + /// Adds a new cluster to the end of the existing chain, by allocating a free cluster. + /// + /// The cluster allocated. + /// This method does not initialize the data in the cluster, the caller should + /// perform a write to ensure the cluster data is in known state. + private uint ExtendChain() + { + // Sanity check - make sure the final known cluster is the EOC marker + if (!_fat.IsEndOfChain(_knownClusters[_knownClusters.Count - 1])) + { + throw new IOException("Corrupt file system: final cluster isn't End-of-Chain"); + } + + uint cluster; + if (!_fat.TryGetFreeCluster(out cluster)) + { + throw new IOException("Out of disk space"); + } + + _fat.SetEndOfChain(cluster); + if (_knownClusters.Count == 1) + { + FireFirstClusterAllocated(cluster); + } + else + { + _fat.SetNext(_knownClusters[_knownClusters.Count - 2], cluster); + } + + _knownClusters[_knownClusters.Count - 1] = cluster; + _knownClusters.Add(_fat.GetNext(cluster)); + + return cluster; + } + + private void FireFirstClusterAllocated(uint cluster) + { + if (FirstClusterChanged != null) + { + FirstClusterChanged(cluster); + } + } + + private bool TryLoadCurrentCluster() + { + return TryLoadClusterByPosition(_position); + } + + private bool TryLoadClusterByPosition(long pos) + { + uint cluster; + if (!TryGetClusterByPosition(pos, out cluster)) + { + return false; + } + + // Read the cluster, it's different to the one currently loaded + if (cluster != _currentCluster) + { + _reader.ReadCluster(cluster, _clusterBuffer, 0); + _currentCluster = cluster; + } + + return true; + } + + private void LoadCluster(uint cluster) + { + // Read the cluster, it's different to the one currently loaded + if (cluster != _currentCluster) + { + _reader.ReadCluster(cluster, _clusterBuffer, 0); + _currentCluster = cluster; + } + } + + private void WriteCurrentCluster() + { + _reader.WriteCluster(_currentCluster, _clusterBuffer, 0); + } + + private bool TryGetClusterByPosition(long pos, out uint cluster) + { + int index = (int)(pos / _reader.ClusterSize); + + if (_knownClusters.Count <= index) + { + if (!TryPopulateKnownClusters(index)) + { + cluster = uint.MaxValue; + return false; + } + } + + // Chain is shorter than the current stream position + if (_knownClusters.Count <= index) + { + cluster = uint.MaxValue; + return false; + } + + cluster = _knownClusters[(int)index]; + + // This is the 'special' End-of-chain cluster identifer, so the stream position + // is greater than the actual file length. + if (_fat.IsEndOfChain(cluster)) + { + return false; + } + + return true; + } + + private bool TryPopulateKnownClusters(int index) + { + uint lastKnown = _knownClusters[_knownClusters.Count - 1]; + while (!_fat.IsEndOfChain(lastKnown) && _knownClusters.Count <= index) + { + lastKnown = _fat.GetNext(lastKnown); + _knownClusters.Add(lastKnown); + } + + return _knownClusters.Count > index; + } + + private uint DetectLength() + { + while (!_fat.IsEndOfChain(_knownClusters[_knownClusters.Count - 1])) + { + if (!TryPopulateKnownClusters(_knownClusters.Count)) + { + throw new IOException("Corrupt file stream - unable to discover end of cluster chain"); + } + } + + return (uint)((long)(_knownClusters.Count - 1) * (long)_reader.ClusterSize); + } + } +} diff --git a/DiscUtils/Fat/Directory.cs b/DiscUtils/Fat/Directory.cs new file mode 100644 index 0000000..4460c09 --- /dev/null +++ b/DiscUtils/Fat/Directory.cs @@ -0,0 +1,568 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class Directory : IDisposable + { + private FatFileSystem _fileSystem; + private Directory _parent; + private long _parentId; + private Stream _dirStream; + + private Dictionary _entries; + private List _freeEntries; + private long _endOfEntries; + + private DirectoryEntry _selfEntry; + private long _selfEntryLocation; + private DirectoryEntry _parentEntry; + private long _parentEntryLocation; + + internal Dictionary LongFileNames_ShortKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + internal Dictionary LongFileNames_LongKey = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of the Directory class. Use this constructor to represent non-root directories. + /// + /// The parent directory. + /// The identity of the entry representing this directory in the parent. + internal Directory(Directory parent, long parentId) + { + _fileSystem = parent._fileSystem; + _parent = parent; + _parentId = parentId; + + DirectoryEntry dirEntry = ParentsChildEntry; + _dirStream = new ClusterStream(_fileSystem, FileAccess.ReadWrite, dirEntry.FirstCluster, uint.MaxValue); + + LoadEntries(); + } + + /// + /// Initializes a new instance of the Directory class. Use this constructor to represent the root directory. + /// + /// The file system. + /// The stream containing the directory info. + internal Directory(FatFileSystem fileSystem, Stream dirStream) + { + _fileSystem = fileSystem; + _dirStream = dirStream; + + LoadEntries(); + } + + private static string GetLfnChunk(byte[] buffer) + { + // see http://home.teleport.com/~brainy/lfn.htm + // NOTE: we assume ordinals are ok here. + char[] chars = new char[13]; + chars[0] = (char)(256 * buffer[2] + buffer[1]); + chars[1] = (char)(256 * buffer[4] + buffer[3]); + chars[2] = (char)(256 * buffer[6] + buffer[5]); + chars[3] = (char)(256 * buffer[8] + buffer[7]); + chars[4] = (char)(256 * buffer[10] + buffer[9]); + + chars[5] = (char)(256 * buffer[15] + buffer[14]); + chars[6] = (char)(256 * buffer[17] + buffer[16]); + chars[7] = (char)(256 * buffer[19] + buffer[18]); + chars[8] = (char)(256 * buffer[21] + buffer[20]); + chars[9] = (char)(256 * buffer[23] + buffer[22]); + chars[10] = (char)(256 * buffer[25] + buffer[24]); + + chars[11] = (char)(256 * buffer[29] + buffer[28]); + chars[12] = (char)(256 * buffer[31] + buffer[30]); + string chunk = new string(chars); + int zero = chunk.IndexOf('\0'); + return zero >= 0 ? chunk.Substring(0, zero) : chunk; + } + + public FatFileSystem FileSystem + { + get { return _fileSystem; } + } + + public bool IsEmpty + { + get { return _entries.Count == 0; } + } + + public DirectoryEntry[] Entries + { + get { return new List(_entries.Values).ToArray(); } + } + + #region Convenient accessors for special entries + internal DirectoryEntry ParentsChildEntry + { + get + { + if (_parent == null) + { + return new DirectoryEntry(_fileSystem.FatOptions, FileName.ParentEntryName, FatAttributes.Directory, _fileSystem.FatVariant); + } + else + { + return _parent.GetEntry(_parentId); + } + } + + set + { + if (_parent != null) + { + _parent.UpdateEntry(_parentId, value); + } + } + } + + internal DirectoryEntry SelfEntry + { + get + { + if (_parent == null) + { + // If we're the root directory, simulate the parent entry with a dummy record + return new DirectoryEntry(_fileSystem.FatOptions, FileName.Null, FatAttributes.Directory, _fileSystem.FatVariant); + } + else + { + return _selfEntry; + } + } + + set + { + if (_selfEntryLocation >= 0) + { + _dirStream.Position = _selfEntryLocation; + value.WriteTo(_dirStream); + _selfEntry = value; + } + } + } + + internal DirectoryEntry ParentEntry + { + get + { + return _parentEntry; + } + + set + { + if (_parentEntryLocation < 0) + { + throw new IOException("No parent entry on disk to update"); + } + + _dirStream.Position = _parentEntryLocation; + value.WriteTo(_dirStream); + _parentEntry = value; + } + } + #endregion + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public DirectoryEntry[] GetDirectories() + { + List dirs = new List(_entries.Count); + foreach (DirectoryEntry dirEntry in _entries.Values) + { + if ((dirEntry.Attributes & FatAttributes.Directory) != 0) + { + dirs.Add(dirEntry); + } + } + + return dirs.ToArray(); + } + + public DirectoryEntry[] GetFiles() + { + List files = new List(_entries.Count); + foreach (DirectoryEntry dirEntry in _entries.Values) + { + if ((dirEntry.Attributes & (FatAttributes.Directory | FatAttributes.VolumeId)) == 0) + { + files.Add(dirEntry); + } + } + + return files.ToArray(); + } + + public DirectoryEntry GetEntry(long id) + { + return (id < 0) ? null : _entries[id]; + } + + public Directory GetChildDirectory(FileName name) + { + long id = FindEntry(name); + if (id < 0) + { + return null; + } + else if ((_entries[id].Attributes & FatAttributes.Directory) == 0) + { + return null; + } + else + { + return _fileSystem.GetDirectory(this, id); + } + } + + internal Directory CreateChildDirectory(FileName name) + { + long id = FindEntry(name); + if (id >= 0) + { + if ((_entries[id].Attributes & FatAttributes.Directory) == 0) + { + throw new IOException("A file exists with the same name"); + } + else + { + return _fileSystem.GetDirectory(this, id); + } + } + else + { + try + { + uint firstCluster; + if (!_fileSystem.Fat.TryGetFreeCluster(out firstCluster)) + { + throw new IOException("Failed to allocate first cluster for new directory"); + } + + _fileSystem.Fat.SetEndOfChain(firstCluster); + + DirectoryEntry newEntry = new DirectoryEntry(_fileSystem.FatOptions, name, FatAttributes.Directory, _fileSystem.FatVariant); + newEntry.FirstCluster = firstCluster; + newEntry.CreationTime = _fileSystem.ConvertFromUtc(DateTime.UtcNow); + newEntry.LastWriteTime = newEntry.CreationTime; + + id = AddEntry(newEntry); + + PopulateNewChildDirectory(newEntry); + + // Rather than just creating a new instance, pull it through the fileSystem cache + // to ensure the cache model is preserved. + return _fileSystem.GetDirectory(this, id); + } + finally + { + _fileSystem.Fat.Flush(); + } + } + } + + internal void AttachChildDirectory(FileName name, Directory newChild) + { + long id = FindEntry(name); + if (id >= 0) + { + throw new IOException("Directory entry already exists"); + } + + DirectoryEntry newEntry = new DirectoryEntry(newChild.ParentsChildEntry); + newEntry.Name = name; + AddEntry(newEntry); + + DirectoryEntry newParentEntry = new DirectoryEntry(SelfEntry); + newParentEntry.Name = FileName.ParentEntryName; + newChild.ParentEntry = newParentEntry; + } + + internal long FindVolumeId() + { + foreach (long id in _entries.Keys) + { + DirectoryEntry focus = _entries[id]; + if ((focus.Attributes & FatAttributes.VolumeId) != 0) + { + return id; + } + } + + return -1; + } + + internal long FindEntry(FileName name) + { + foreach (long id in _entries.Keys) + { + DirectoryEntry focus = _entries[id]; + if (focus.Name == name && (focus.Attributes & FatAttributes.VolumeId) == 0) + { + return id; + } + } + + return -1; + } + + internal SparseStream OpenFile(FileName name, FileMode mode, FileAccess fileAccess) + { + if (mode == FileMode.Append || mode == FileMode.Truncate) + { + throw new NotImplementedException(); + } + + long fileId = FindEntry(name); + bool exists = fileId != -1; + + if (mode == FileMode.CreateNew && exists) + { + throw new IOException("File already exists"); + } + else if (mode == FileMode.Open && !exists) + { + throw new FileNotFoundException("File not found", name.GetDisplayName(_fileSystem.FatOptions.FileNameEncoding)); + } + else if ((mode == FileMode.Open || mode == FileMode.OpenOrCreate || mode == FileMode.Create) && exists) + { + SparseStream stream = new FatFileStream(_fileSystem, this, fileId, fileAccess); + if (mode == FileMode.Create) + { + stream.SetLength(0); + } + + HandleAccessed(false); + + return stream; + } + else if ((mode == FileMode.OpenOrCreate || mode == FileMode.CreateNew || mode == FileMode.Create) && !exists) + { + // Create new file + DirectoryEntry newEntry = new DirectoryEntry(_fileSystem.FatOptions, name, FatAttributes.Archive, _fileSystem.FatVariant); + newEntry.FirstCluster = 0; // i.e. Zero-length + newEntry.CreationTime = _fileSystem.ConvertFromUtc(DateTime.UtcNow); + newEntry.LastWriteTime = newEntry.CreationTime; + + fileId = AddEntry(newEntry); + + return new FatFileStream(_fileSystem, this, fileId, fileAccess); + } + else + { + // Should never get here... + throw new NotImplementedException(); + } + } + + internal long AddEntry(DirectoryEntry newEntry) + { + // Unlink an entry from the free list (or add to the end of the existing directory) + long pos; + if (_freeEntries.Count > 0) + { + pos = _freeEntries[0]; + _freeEntries.RemoveAt(0); + } + else + { + pos = _endOfEntries; + _endOfEntries += 32; + } + + // Put the new entry into it's slot + _dirStream.Position = pos; + newEntry.WriteTo(_dirStream); + + // Update internal structures to reflect new entry (as if read from disk) + _entries.Add(pos, newEntry); + + HandleAccessed(true); + + return pos; + } + + internal void DeleteEntry(long id, bool releaseContents) + { + if (id < 0) + { + throw new IOException("Attempt to delete unknown directory entry"); + } + + try + { + DirectoryEntry entry = _entries[id]; + + DirectoryEntry copy = new DirectoryEntry(entry); + copy.Name = entry.Name.Deleted(); + _dirStream.Position = id; + copy.WriteTo(_dirStream); + + if (releaseContents) + { + _fileSystem.Fat.FreeChain(entry.FirstCluster); + } + + _entries.Remove(id); + _freeEntries.Add(id); + + HandleAccessed(true); + } + finally + { + _fileSystem.Fat.Flush(); + } + } + + internal void UpdateEntry(long id, DirectoryEntry entry) + { + if (id < 0) + { + throw new IOException("Attempt to update unknown directory entry"); + } + + _dirStream.Position = id; + entry.WriteTo(_dirStream); + _entries[id] = entry; + } + + private void LoadEntries() + { + _entries = new Dictionary(); + _freeEntries = new List(); + + _selfEntryLocation = -1; + _parentEntryLocation = -1; + + string lfn = null; //+++ + while (_dirStream.Position < _dirStream.Length) + { + long streamPos = _dirStream.Position; + DirectoryEntry entry = new DirectoryEntry(_fileSystem.FatOptions, _dirStream, _fileSystem.FatVariant); + + if (entry.Attributes == (FatAttributes.ReadOnly | FatAttributes.Hidden | FatAttributes.System | FatAttributes.VolumeId)) + { + // Long File Name entry + _dirStream.Position = streamPos; //+++ + lfn = GetLfnChunk(Utilities.ReadFully(_dirStream, 32)) + lfn; //+++ + } + else if (entry.Name.IsDeleted()) + { + // E5 = Free Entry + _freeEntries.Add(streamPos); + lfn = null; //+++ + } + else if (entry.Name == FileName.SelfEntryName) + { + _selfEntry = entry; + _selfEntryLocation = streamPos; + lfn = null; //+++ + } + else if (entry.Name == FileName.ParentEntryName) + { + _parentEntry = entry; + _parentEntryLocation = streamPos; + lfn = null; //+++ + } + else if (entry.Name.IsEndMarker()) + { + // Free Entry, no more entries available + _endOfEntries = streamPos; + break; + } + else + { + if (lfn != null) //+++ + { //+++ + LongFileNames_ShortKey.Add(entry.Name.GetDisplayName(_fileSystem.FatOptions.FileNameEncoding), lfn); //+++ + LongFileNames_LongKey.Add(lfn, entry.Name.GetDisplayName(_fileSystem.FatOptions.FileNameEncoding)); //+++ + } //+++ + _entries.Add(streamPos, entry); + lfn = null; //+++ + } + } + } + + private void HandleAccessed(bool forWrite) + { + if (_fileSystem.CanWrite && _parent != null) + { + DateTime now = DateTime.Now; + DirectoryEntry entry = SelfEntry; + + DateTime oldAccessTime = entry.LastAccessTime; + DateTime oldWriteTime = entry.LastWriteTime; + + entry.LastAccessTime = now; + if (forWrite) + { + entry.LastWriteTime = now; + } + + if (entry.LastAccessTime != oldAccessTime || entry.LastWriteTime != oldWriteTime) + { + SelfEntry = entry; + + DirectoryEntry parentEntry = ParentsChildEntry; + parentEntry.LastAccessTime = entry.LastAccessTime; + parentEntry.LastWriteTime = entry.LastWriteTime; + ParentsChildEntry = parentEntry; + } + } + } + + private void PopulateNewChildDirectory(DirectoryEntry newEntry) + { + // Populate new directory with initial (special) entries. First one is easy, just change the name! + using (ClusterStream stream = new ClusterStream(_fileSystem, FileAccess.Write, newEntry.FirstCluster, uint.MaxValue)) + { + // First is the self-referencing entry... + DirectoryEntry selfEntry = new DirectoryEntry(newEntry); + selfEntry.Name = FileName.SelfEntryName; + selfEntry.WriteTo(stream); + + // Second is a clone of our self entry (i.e. parent) - though dates are odd... + DirectoryEntry parentEntry = new DirectoryEntry(SelfEntry); + parentEntry.Name = FileName.ParentEntryName; + parentEntry.CreationTime = newEntry.CreationTime; + parentEntry.LastWriteTime = newEntry.LastWriteTime; + parentEntry.WriteTo(stream); + } + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _dirStream.Dispose(); + } + } + } +} diff --git a/DiscUtils/Fat/DirectoryEntry.cs b/DiscUtils/Fat/DirectoryEntry.cs new file mode 100644 index 0000000..c09dc71 --- /dev/null +++ b/DiscUtils/Fat/DirectoryEntry.cs @@ -0,0 +1,223 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.IO; + using System.Text; + + internal class DirectoryEntry + { + private FatFileSystemOptions _options; + private FatType _fatVariant; + private FileName _name; + private byte _attr; + private byte _creationTimeTenth; + private ushort _creationTime; + private ushort _creationDate; + private ushort _lastAccessDate; + private ushort _firstClusterHi; + private ushort _lastWriteTime; + private ushort _lastWriteDate; + private ushort _firstClusterLo; + private uint _fileSize; + + internal DirectoryEntry(FatFileSystemOptions options, Stream stream, FatType fatVariant) + { + _options = options; + _fatVariant = fatVariant; + byte[] buffer = Utilities.ReadFully(stream, 32); + Load(buffer, 0); + } + + internal DirectoryEntry(FatFileSystemOptions options, FileName name, FatAttributes attrs, FatType fatVariant) + { + _options = options; + _fatVariant = fatVariant; + _name = name; + _attr = (byte)attrs; + } + + internal DirectoryEntry(DirectoryEntry toCopy) + { + _options = toCopy._options; + _fatVariant = toCopy._fatVariant; + _name = toCopy._name; + _attr = toCopy._attr; + _creationTimeTenth = toCopy._creationTimeTenth; + _creationTime = toCopy._creationTime; + _creationDate = toCopy._creationDate; + _lastAccessDate = toCopy._lastAccessDate; + _firstClusterHi = toCopy._firstClusterHi; + _lastWriteTime = toCopy._lastWriteTime; + _firstClusterLo = toCopy._firstClusterLo; + _fileSize = toCopy._fileSize; + } + + public FileName Name + { + get + { + return _name; + } + + set + { + _name = value; + } + } + + public FatAttributes Attributes + { + get { return (FatAttributes)_attr; } + set { _attr = (byte)value; } + } + + public DateTime CreationTime + { + get { return FileTimeToDateTime(_creationDate, _creationTime, _creationTimeTenth); } + set { DateTimeToFileTime(value, out _creationDate, out _creationTime, out _creationTimeTenth); } + } + + public DateTime LastAccessTime + { + get { return FileTimeToDateTime(_lastAccessDate, 0, 0); } + set { DateTimeToFileTime(value, out _lastAccessDate); } + } + + public DateTime LastWriteTime + { + get { return FileTimeToDateTime(_lastWriteDate, _lastWriteTime, 0); } + set { DateTimeToFileTime(value, out _lastWriteDate, out _lastWriteTime); } + } + + public int FileSize + { + get { return (int)_fileSize; } + set { _fileSize = (uint)value; } + } + + public uint FirstCluster + { + get + { + if (_fatVariant == FatType.Fat32) + { + return (uint)(_firstClusterHi << 16) | _firstClusterLo; + } + else + { + return _firstClusterLo; + } + } + + set + { + if (_fatVariant == FatType.Fat32) + { + _firstClusterHi = (ushort)((value >> 16) & 0xFFFF); + } + + _firstClusterLo = (ushort)(value & 0xFFFF); + } + } + + internal void WriteTo(Stream stream) + { + byte[] buffer = new byte[32]; + + _name.GetBytes(buffer, 0); + buffer[11] = _attr; + buffer[13] = _creationTimeTenth; + Utilities.WriteBytesLittleEndian((ushort)_creationTime, buffer, 14); + Utilities.WriteBytesLittleEndian((ushort)_creationDate, buffer, 16); + Utilities.WriteBytesLittleEndian((ushort)_lastAccessDate, buffer, 18); + Utilities.WriteBytesLittleEndian((ushort)_firstClusterHi, buffer, 20); + Utilities.WriteBytesLittleEndian((ushort)_lastWriteTime, buffer, 22); + Utilities.WriteBytesLittleEndian((ushort)_lastWriteDate, buffer, 24); + Utilities.WriteBytesLittleEndian((ushort)_firstClusterLo, buffer, 26); + Utilities.WriteBytesLittleEndian((uint)_fileSize, buffer, 28); + + stream.Write(buffer, 0, buffer.Length); + } + + private static DateTime FileTimeToDateTime(ushort date, ushort time, byte tenths) + { + if (date == 0 || date == 0xFFFF) + { + // Return Epoch - this is an invalid date + return FatFileSystem.Epoch; + } + + int year = 1980 + ((date & 0xFE00) >> 9); + int month = (date & 0x01E0) >> 5; + int day = date & 0x001F; + int hour = (time & 0xF800) >> 11; + int minute = (time & 0x07E0) >> 5; + int second = ((time & 0x001F) * 2) + (tenths / 100); + int millis = (tenths % 100) * 10; + + return new DateTime(year, month, day, hour, minute, second, millis); + } + + private static void DateTimeToFileTime(DateTime value, out ushort date) + { + byte tenths; + ushort time; + DateTimeToFileTime(value, out date, out time, out tenths); + } + + private static void DateTimeToFileTime(DateTime value, out ushort date, out ushort time) + { + byte tenths; + DateTimeToFileTime(value, out date, out time, out tenths); + } + + private static void DateTimeToFileTime(DateTime value, out ushort date, out ushort time, out byte tenths) + { + if (value.Year < 1980) + { + value = FatFileSystem.Epoch; + } + + date = (ushort)((((value.Year - 1980) << 9) & 0xFE00) | ((value.Month << 5) & 0x01E0) | (value.Day & 0x001F)); + time = (ushort)(((value.Hour << 11) & 0xF800) | ((value.Minute << 5) & 0x07E0) | ((value.Second / 2) & 0x001F)); + tenths = (byte)(((value.Second % 2) * 100) + (value.Millisecond / 10)); + } + + private void Load(byte[] data, int offset) + { + _name = new FileName(data, offset); + _attr = data[offset + 11]; + _creationTimeTenth = data[offset + 13]; + _creationTime = Utilities.ToUInt16LittleEndian(data, offset + 14); + _creationDate = Utilities.ToUInt16LittleEndian(data, offset + 16); + _lastAccessDate = Utilities.ToUInt16LittleEndian(data, offset + 18); + _firstClusterHi = Utilities.ToUInt16LittleEndian(data, offset + 20); + _lastWriteTime = Utilities.ToUInt16LittleEndian(data, offset + 22); + _lastWriteDate = Utilities.ToUInt16LittleEndian(data, offset + 24); + _firstClusterLo = Utilities.ToUInt16LittleEndian(data, offset + 26); + _fileSize = Utilities.ToUInt32LittleEndian(data, offset + 28); + } + } +} diff --git a/DiscUtils/Fat/FatAttributes.cs b/DiscUtils/Fat/FatAttributes.cs new file mode 100644 index 0000000..b3043c4 --- /dev/null +++ b/DiscUtils/Fat/FatAttributes.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + + [Flags] + internal enum FatAttributes : byte + { + ReadOnly = 0x01, + Hidden = 0x02, + System = 0x04, + VolumeId = 0x08, + Directory = 0x10, + Archive = 0x20, + } +} diff --git a/DiscUtils/Fat/FatBuffer.cs b/DiscUtils/Fat/FatBuffer.cs new file mode 100644 index 0000000..b84a658 --- /dev/null +++ b/DiscUtils/Fat/FatBuffer.cs @@ -0,0 +1,262 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class FatBuffer + { + /// + /// The End-of-chain marker to WRITE (SetNext). Don't use this value to test for end of chain. + /// + /// + /// The actual end-of-chain marker bits on disk vary by FAT type, and can end ...F8 through ...FF. + /// + public const uint EndOfChain = 0xFFFFFFFF; + + /// + /// The Bad-Cluster marker to WRITE (SetNext). Don't use this value to test for bad clusters. + /// + /// + /// The actual bad-cluster marker bits on disk vary by FAT type. + /// + public const uint BadCluster = 0xFFFFFFF7; + + /// + /// The Free-Cluster marker to WRITE (SetNext). Don't use this value to test for free clusters. + /// + /// + /// The actual free-cluster marker bits on disk vary by FAT type. + /// + public const uint FreeCluster = 0; + + private const uint DirtyRegionSize = 512; + + private FatType _type; + private byte[] _buffer; + private uint _nextFreeCandidate; + private Dictionary _dirtySectors; + + public FatBuffer(FatType type, byte[] buffer) + { + _type = type; + _buffer = buffer; + _dirtySectors = new Dictionary(); + } + + internal int NumEntries + { + get + { + switch (_type) + { + case FatType.Fat12: + return (_buffer.Length / 3) * 2; + case FatType.Fat16: + return _buffer.Length / 2; + default: // FAT32 + return _buffer.Length / 4; + } + } + } + + internal int Size + { + get { return _buffer.Length; } + } + + internal bool IsFree(uint val) + { + return val == 0; + } + + internal bool IsEndOfChain(uint val) + { + switch (_type) + { + case FatType.Fat12: return (val & 0x0FFF) >= 0x0FF8; + case FatType.Fat16: return (val & 0xFFFF) >= 0xFFF8; + case FatType.Fat32: return (val & 0x0FFFFFF8) >= 0x0FFFFFF8; + default: throw new ArgumentException("Unknown FAT type"); + } + } + + internal bool IsBadCluster(uint val) + { + switch (_type) + { + case FatType.Fat12: return (val & 0x0FFF) == 0x0FF7; + case FatType.Fat16: return (val & 0xFFFF) == 0xFFF7; + case FatType.Fat32: return (val & 0x0FFFFFF8) == 0x0FFFFFF7; + default: throw new ArgumentException("Unknown FAT type"); + } + } + + internal uint GetNext(uint cluster) + { + if (_type == FatType.Fat16) + { + return Utilities.ToUInt16LittleEndian(_buffer, (int)(cluster * 2)); + } + else if (_type == FatType.Fat32) + { + return Utilities.ToUInt32LittleEndian(_buffer, (int)(cluster * 4)) & 0x0FFFFFFF; + } + else + { + // FAT12 + if ((cluster & 1) != 0) + { + return (uint)((Utilities.ToUInt16LittleEndian(_buffer, (int)(cluster + (cluster / 2))) >> 4) & 0x0FFF); + } + else + { + return (uint)(Utilities.ToUInt16LittleEndian(_buffer, (int)(cluster + (cluster / 2))) & 0x0FFF); + } + } + } + + internal void SetEndOfChain(uint cluster) + { + SetNext(cluster, EndOfChain); + } + + internal void SetBadCluster(uint cluster) + { + SetNext(cluster, BadCluster); + } + + internal void SetFree(uint cluster) + { + if (cluster < _nextFreeCandidate) + { + _nextFreeCandidate = cluster; + } + + SetNext(cluster, FreeCluster); + } + + internal void SetNext(uint cluster, uint next) + { + if (_type == FatType.Fat16) + { + MarkDirty(cluster * 2); + Utilities.WriteBytesLittleEndian((ushort)next, _buffer, (int)(cluster * 2)); + } + else if (_type == FatType.Fat32) + { + MarkDirty(cluster * 4); + uint oldVal = Utilities.ToUInt32LittleEndian(_buffer, (int)(cluster * 4)); + uint newVal = (oldVal & 0xF0000000) | (next & 0x0FFFFFFF); + Utilities.WriteBytesLittleEndian((uint)newVal, _buffer, (int)(cluster * 4)); + } + else + { + uint offset = cluster + (cluster / 2); + MarkDirty(offset); + MarkDirty(offset + 1); // On alternate sector boundaries, cluster info crosses two sectors + + ushort maskedOldVal; + if ((cluster & 1) != 0) + { + next = next << 4; + maskedOldVal = (ushort)(Utilities.ToUInt16LittleEndian(_buffer, (int)offset) & 0x000F); + } + else + { + next = next & 0x0FFF; + maskedOldVal = (ushort)(Utilities.ToUInt16LittleEndian(_buffer, (int)offset) & 0xF000); + } + + ushort newVal = (ushort)(maskedOldVal | next); + + Utilities.WriteBytesLittleEndian(newVal, _buffer, (int)offset); + } + } + + internal bool TryGetFreeCluster(out uint cluster) + { + // Simple scan - don't hold a free list... + uint numEntries = (uint)NumEntries; + for (uint i = 0; i < numEntries; i++) + { + uint candidate = (i + _nextFreeCandidate) % numEntries; + if (IsFree(GetNext(candidate))) + { + cluster = candidate; + _nextFreeCandidate = candidate + 1; + return true; + } + } + + cluster = 0; + return false; + } + + internal void FreeChain(uint head) + { + foreach (uint cluster in GetChain(head)) + { + SetFree(cluster); + } + } + + internal List GetChain(uint head) + { + List result = new List(); + + if (head != 0) + { + uint focus = head; + while (!IsEndOfChain(focus)) + { + result.Add(focus); + focus = GetNext(focus); + } + } + + return result; + } + + internal void MarkDirty(uint offset) + { + _dirtySectors[offset / DirtyRegionSize] = offset / DirtyRegionSize; + } + + internal void WriteDirtyRegions(Stream stream, long position) + { + foreach (uint val in _dirtySectors.Values) + { + stream.Position = position + (val * DirtyRegionSize); + stream.Write(_buffer, (int)(val * DirtyRegionSize), (int)DirtyRegionSize); + } + } + + internal void ClearDirtyRegions() + { + _dirtySectors.Clear(); + } + } +} diff --git a/DiscUtils/Fat/FatFileStream.cs b/DiscUtils/Fat/FatFileStream.cs new file mode 100644 index 0000000..e609881 --- /dev/null +++ b/DiscUtils/Fat/FatFileStream.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class FatFileStream : SparseStream + { + private Directory _dir; + private long _dirId; + private ClusterStream _stream; + + private bool didWrite = false; + + public FatFileStream(FatFileSystem fileSystem, Directory dir, long fileId, FileAccess access) + { + _dir = dir; + _dirId = fileId; + + DirectoryEntry dirEntry = _dir.GetEntry(_dirId); + _stream = new ClusterStream(fileSystem, access, (uint)dirEntry.FirstCluster, (uint)dirEntry.FileSize); + _stream.FirstClusterChanged += FirstClusterAllocatedHandler; + } + + public override long Position + { + get { return _stream.Position; } + set { _stream.Position = value; } + } + + public override bool CanRead + { + get { return _stream.CanRead; } + } + + public override bool CanSeek + { + get { return _stream.CanSeek; } + } + + public override bool CanWrite + { + get { return _stream.CanWrite; } + } + + public override long Length + { + get { return _stream.Length; } + } + + public override IEnumerable Extents + { + get + { + return new StreamExtent[] { new StreamExtent(0, Length) }; + } + } + + public override void Close() + { + if (_dir.FileSystem.CanWrite) + { + try + { + DateTime now = _dir.FileSystem.ConvertFromUtc(DateTime.UtcNow); + + DirectoryEntry dirEntry = _dir.GetEntry(_dirId); + dirEntry.LastAccessTime = now; + if (didWrite) + { + dirEntry.FileSize = (int)_stream.Length; + dirEntry.LastWriteTime = now; + } + + _dir.UpdateEntry(_dirId, dirEntry); + } + finally + { + base.Close(); + } + } + } + + public override void SetLength(long value) + { + didWrite = true; + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + didWrite = true; + _stream.Write(buffer, offset, count); + } + + public override void Flush() + { + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _stream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _stream.Seek(offset, origin); + } + + private void FirstClusterAllocatedHandler(uint cluster) + { + DirectoryEntry dirEntry = _dir.GetEntry(_dirId); + dirEntry.FirstCluster = cluster; + _dir.UpdateEntry(_dirId, dirEntry); + } + } +} diff --git a/DiscUtils/Fat/FatFileSystem.cs b/DiscUtils/Fat/FatFileSystem.cs new file mode 100644 index 0000000..639367e --- /dev/null +++ b/DiscUtils/Fat/FatFileSystem.cs @@ -0,0 +1,2016 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + + /// + /// Class for accessing FAT file systems. + /// + public sealed class FatFileSystem : DiscFileSystem + { + /// + /// The Epoch for FAT file systems (1st Jan, 1980). + /// + public static readonly DateTime Epoch = new DateTime(1980, 1, 1); + + private TimeConverter _timeConverter; + private Stream _data; + private Ownership _ownsData; + private byte[] _bootSector; + private FileAllocationTable _fat; + private ClusterReader _clusterReader; + private Directory _rootDir; + private Dictionary _dirCache; + + private FatType _type; + private string _bpbOEMName; + private ushort _bpbBytesPerSec; + private byte _bpbSecPerClus; + private ushort _bpbRsvdSecCnt; + private byte _bpbNumFATs; + private ushort _bpbRootEntCnt; + private ushort _bpbTotSec16; + private byte _bpbMedia; + private ushort _bpbFATSz16; + private ushort _bpbSecPerTrk; + private ushort _bpbNumHeads; + private uint _bpbHiddSec; + private uint _bpbTotSec32; + + private byte _bsDrvNum; + private byte _bsBootSig; + private uint _bsVolId; + private string _bsVolLab; + private string _bsFilSysType; + + private uint _bpbFATSz32; + private ushort _bpbExtFlags; + private ushort _bpbFSVer; + private uint _bpbRootClus; + private ushort _bpbFSInfo; + private ushort _bpbBkBootSec; + + /// + /// Initializes a new instance of the FatFileSystem class. + /// + /// The stream containing the file system. + /// + /// Local time is the effective timezone of the new instance. + /// + public FatFileSystem(Stream data) + : base(new FatFileSystemOptions()) + { + _dirCache = new Dictionary(); + _timeConverter = DefaultTimeConverter; + Initialize(data); + } + + /// + /// Initializes a new instance of the FatFileSystem class. + /// + /// The stream containing the file system. + /// Indicates if the new instance should take ownership + /// of . + /// + /// Local time is the effective timezone of the new instance. + /// + public FatFileSystem(Stream data, Ownership ownsData) + : base(new FatFileSystemOptions()) + { + _dirCache = new Dictionary(); + _timeConverter = DefaultTimeConverter; + Initialize(data); + _ownsData = ownsData; + } + + /// + /// Initializes a new instance of the FatFileSystem class. + /// + /// The stream containing the file system. + /// A delegate to convert to/from the file system's timezone. + public FatFileSystem(Stream data, TimeConverter timeConverter) + : base(new FatFileSystemOptions()) + { + _dirCache = new Dictionary(); + _timeConverter = timeConverter; + Initialize(data); + } + + /// + /// Initializes a new instance of the FatFileSystem class. + /// + /// The stream containing the file system. + /// Indicates if the new instance should take ownership + /// of . + /// A delegate to convert to/from the file system's timezone. + public FatFileSystem(Stream data, Ownership ownsData, TimeConverter timeConverter) + : base(new FatFileSystemOptions()) + { + _dirCache = new Dictionary(); + _timeConverter = timeConverter; + Initialize(data); + _ownsData = ownsData; + } + + /// + /// Initializes a new instance of the FatFileSystem class. + /// + /// The stream containing the file system. + /// Indicates if the new instance should take ownership + /// of . + /// The parameters for the file system. + public FatFileSystem(Stream data, Ownership ownsData, FileSystemParameters parameters) + : base(new FatFileSystemOptions(parameters)) + { + _dirCache = new Dictionary(); + + if (parameters != null && parameters.TimeConverter != null) + { + _timeConverter = parameters.TimeConverter; + } + else + { + _timeConverter = DefaultTimeConverter; + } + + Initialize(data); + _ownsData = ownsData; + } + + private delegate void EntryUpdateAction(DirectoryEntry entry); + + /// + /// Gets the FAT file system options, which can be modified. + /// + public FatFileSystemOptions FatOptions + { + get { return (FatFileSystemOptions)Options; } + } + + /// + /// Gets the FAT variant of the file system. + /// + public FatType FatVariant + { + get { return _type; } + } + + /// + /// Gets the friendly name for the file system, including FAT variant. + /// + public override string FriendlyName + { + get + { + switch (_type) + { + case FatType.Fat12: return "Microsoft FAT12"; + case FatType.Fat16: return "Microsoft FAT16"; + case FatType.Fat32: return "Microsoft FAT32"; + default: return "Unknown FAT"; + } + } + } + + /// + /// Gets the OEM name from the file system. + /// + public string OemName + { + get { return _bpbOEMName; } + } + + /// + /// Gets the number of bytes per sector (as stored in the file-system meta data). + /// + public int BytesPerSector + { + get { return _bpbBytesPerSec; } + } + + /// + /// Gets the number of contiguous sectors that make up one cluster. + /// + public byte SectorsPerCluster + { + get { return _bpbSecPerClus; } + } + + /// + /// Gets the number of reserved sectors at the start of the disk. + /// + public int ReservedSectorCount + { + get { return _bpbRsvdSecCnt; } + } + + /// + /// Gets the number of FATs present. + /// + public byte FatCount + { + get { return _bpbNumFATs; } + } + + /// + /// Gets the maximum number of root directory entries (on FAT variants that have a limit). + /// + public int MaxRootDirectoryEntries + { + get { return _bpbRootEntCnt; } + } + + /// + /// Gets the total number of sectors on the disk. + /// + public long TotalSectors + { + get { return (_bpbTotSec16 != 0) ? _bpbTotSec16 : _bpbTotSec32; } + } + + /// + /// Gets the Media marker byte, which indicates fixed or removable media. + /// + public byte Media + { + get { return _bpbMedia; } + } + + /// + /// Gets the size of a single FAT, in sectors. + /// + public long FatSize + { + get { return (_bpbFATSz16 != 0) ? _bpbFATSz16 : _bpbFATSz32; } + } + + /// + /// Gets the number of sectors per logical track. + /// + public int SectorsPerTrack + { + get { return _bpbSecPerTrk; } + } + + /// + /// Gets the number of logical heads. + /// + public int Heads + { + get { return _bpbNumHeads; } + } + + /// + /// Gets the number of hidden sectors, hiding partition tables, etc. + /// + public long HiddenSectors + { + get { return _bpbHiddSec; } + } + + /// + /// Gets the BIOS drive number for BIOS Int 13h calls. + /// + public byte BiosDriveNumber + { + get { return _bsDrvNum; } + } + + /// + /// Gets a value indicating whether the VolumeId, VolumeLabel and FileSystemType fields are valid. + /// + public bool ExtendedBootSignaturePresent + { + get { return _bsBootSig == 0x29; } + } + + /// + /// Gets the volume serial number. + /// + public int VolumeId + { + get { return (int)_bsVolId; } + } + + /// + /// Gets the volume label. + /// + public override string VolumeLabel + { + get + { + long volId = _rootDir.FindVolumeId(); + if (volId < 0) + { + return _bsVolLab; + } + else + { + return _rootDir.GetEntry(volId).Name.GetRawName(FatOptions.FileNameEncoding); + } + } + } + + /// + /// Gets the (informational only) file system type recorded in the meta-data. + /// + public string FileSystemType + { + get { return _bsFilSysType; } + } + + /// + /// Gets the active FAT (zero-based index). + /// + public byte ActiveFat + { + get { return (byte)(((_bpbExtFlags & 0x08) != 0) ? _bpbExtFlags & 0x7 : 0); } + } + + /// + /// Gets a value indicating whether FAT changes are mirrored to all copies of the FAT. + /// + public bool MirrorFat + { + get { return (_bpbExtFlags & 0x08) == 0; } + } + + /// + /// Gets the file-system version (usually 0). + /// + public int Version + { + get { return _bpbFSVer; } + } + + /// + /// Gets the cluster number of the first cluster of the root directory (FAT32 only). + /// + public long RootDirectoryCluster + { + get { return _bpbRootClus; } + } + + /// + /// Gets the sector location of the FSINFO structure (FAT32 only). + /// + public int FSInfoSector + { + get { return _bpbFSInfo; } + } + + /// + /// Gets the Sector location of the backup boot sector (FAT32 only). + /// + public int BackupBootSector + { + get { return _bpbBkBootSec; } + } + + /// + /// Indicates if this file system is read-only or read-write. + /// + /// . + public override bool CanWrite + { + get { return _data.CanWrite; } + } + + internal FileAllocationTable Fat + { + get { return _fat; } + } + + internal ClusterReader ClusterReader + { + get { return _clusterReader; } + } + + #region Disk Formatting + /// + /// Creates a formatted floppy disk image in a stream. + /// + /// The stream to write the blank image to. + /// The type of floppy to create. + /// The volume label for the floppy (or null). + /// An object that provides access to the newly created floppy disk image. + public static FatFileSystem FormatFloppy(Stream stream, FloppyDiskType type, string label) + { + long pos = stream.Position; + + long ticks = DateTime.UtcNow.Ticks; + uint volId = (uint)((ticks & 0xFFFF) | (ticks >> 32)); + + // Write the BIOS Parameter Block (BPB) - a single sector + byte[] bpb = new byte[512]; + uint sectors; + if (type == FloppyDiskType.DoubleDensity) + { + sectors = 1440; + WriteBPB(bpb, sectors, FatType.Fat12, 224, 0, 1, 1, new Geometry(80, 2, 9), true, volId, label); + } + else if (type == FloppyDiskType.HighDensity) + { + sectors = 2880; + WriteBPB(bpb, sectors, FatType.Fat12, 224, 0, 1, 1, new Geometry(80, 2, 18), true, volId, label); + } + else if (type == FloppyDiskType.Extended) + { + sectors = 5760; + WriteBPB(bpb, sectors, FatType.Fat12, 224, 0, 1, 1, new Geometry(80, 2, 36), true, volId, label); + } + else + { + throw new ArgumentException("Unrecognised Floppy Disk type", "type"); + } + + stream.Write(bpb, 0, bpb.Length); + + // Write both FAT copies + uint fatSize = CalcFatSize(sectors, FatType.Fat12, 1); + byte[] fat = new byte[fatSize * Utilities.SectorSize]; + FatBuffer fatBuffer = new FatBuffer(FatType.Fat12, fat); + fatBuffer.SetNext(0, 0xFFFFFFF0); + fatBuffer.SetEndOfChain(1); + stream.Write(fat, 0, fat.Length); + stream.Write(fat, 0, fat.Length); + + // Write the (empty) root directory + uint rootDirSectors = ((224 * 32) + Utilities.SectorSize - 1) / Utilities.SectorSize; + byte[] rootDir = new byte[rootDirSectors * Utilities.SectorSize]; + stream.Write(rootDir, 0, rootDir.Length); + + // Write a single byte at the end of the disk to ensure the stream is at least as big + // as needed for this disk image. + stream.Position = pos + (sectors * Utilities.SectorSize) - 1; + stream.WriteByte(0); + + // Give the caller access to the new file system + stream.Position = pos; + return new FatFileSystem(stream); + } + + /// + /// Formats a virtual hard disk partition. + /// + /// The disk containing the partition. + /// The index of the partition on the disk. + /// The volume label for the partition (or null). + /// An object that provides access to the newly created partition file system. + public static FatFileSystem FormatPartition(VirtualDisk disk, int partitionIndex, string label) + { + using (Stream partitionStream = disk.Partitions[partitionIndex].Open()) + { + return FormatPartition( + partitionStream, + label, + disk.Geometry, + (int)disk.Partitions[partitionIndex].FirstSector, + (int)(1 + disk.Partitions[partitionIndex].LastSector - disk.Partitions[partitionIndex].FirstSector), + 0); + } + } + + /// + /// Creates a formatted hard disk partition in a stream. + /// + /// The stream to write the new file system to. + /// The volume label for the partition (or null). + /// The geometry of the disk containing the partition. + /// The starting sector number of this partition (hide's sectors in other partitions). + /// The number of sectors in this partition. + /// The number of reserved sectors at the start of the partition. + /// An object that provides access to the newly created partition file system. + public static FatFileSystem FormatPartition( + Stream stream, + string label, + Geometry diskGeometry, + int firstSector, + int sectorCount, + short reservedSectors) + { + long pos = stream.Position; + + long ticks = DateTime.UtcNow.Ticks; + uint volId = (uint)((ticks & 0xFFFF) | (ticks >> 32)); + + byte sectorsPerCluster; + FatType fatType; + ushort maxRootEntries; + + /* + * Write the BIOS Parameter Block (BPB) - a single sector + */ + + byte[] bpb = new byte[512]; + if (sectorCount <= 8400) + { + throw new ArgumentException("Requested size is too small for a partition"); + } + else if (sectorCount < 1024 * 1024) + { + fatType = FatType.Fat16; + maxRootEntries = 512; + if (sectorCount <= 32680) + { + sectorsPerCluster = 2; + } + else if (sectorCount <= 262144) + { + sectorsPerCluster = 4; + } + else if (sectorCount <= 524288) + { + sectorsPerCluster = 8; + } + else + { + sectorsPerCluster = 16; + } + + if (reservedSectors < 1) + { + reservedSectors = 1; + } + } + else + { + fatType = FatType.Fat32; + maxRootEntries = 0; + if (sectorCount <= 532480) + { + sectorsPerCluster = 1; + } + else if (sectorCount <= 16777216) + { + sectorsPerCluster = 8; + } + else if (sectorCount <= 33554432) + { + sectorsPerCluster = 16; + } + else if (sectorCount <= 67108864) + { + sectorsPerCluster = 32; + } + else + { + sectorsPerCluster = 64; + } + + if (reservedSectors < 32) + { + reservedSectors = 32; + } + } + + WriteBPB(bpb, (uint)sectorCount, fatType, maxRootEntries, (uint)firstSector, (ushort)reservedSectors, sectorsPerCluster, diskGeometry, false, volId, label); + stream.Write(bpb, 0, bpb.Length); + + /* + * Skip the reserved sectors + */ + + stream.Position = pos + (((ushort)reservedSectors) * Utilities.SectorSize); + + /* + * Write both FAT copies + */ + + byte[] fat = new byte[CalcFatSize((uint)sectorCount, fatType, sectorsPerCluster) * Utilities.SectorSize]; + FatBuffer fatBuffer = new FatBuffer(fatType, fat); + fatBuffer.SetNext(0, 0xFFFFFFF8); + fatBuffer.SetEndOfChain(1); + if (fatType >= FatType.Fat32) + { + // Mark cluster 2 as End-of-chain (i.e. root directory + // is a single cluster in length) + fatBuffer.SetEndOfChain(2); + } + + stream.Write(fat, 0, fat.Length); + stream.Write(fat, 0, fat.Length); + + /* + * Write the (empty) root directory + */ + + uint rootDirSectors; + if (fatType < FatType.Fat32) + { + rootDirSectors = (uint)(((maxRootEntries * 32) + Utilities.SectorSize - 1) / Utilities.SectorSize); + } + else + { + rootDirSectors = sectorsPerCluster; + } + + byte[] rootDir = new byte[rootDirSectors * Utilities.SectorSize]; + stream.Write(rootDir, 0, rootDir.Length); + + /* + * Make sure the stream is at least as large as the partition requires. + */ + + if (stream.Length < pos + (sectorCount * Utilities.SectorSize)) + { + stream.SetLength(pos + (sectorCount * Utilities.SectorSize)); + } + + /* + * Give the caller access to the new file system + */ + + stream.Position = pos; + return new FatFileSystem(stream); + } + #endregion + + /// + /// Detects if a stream contains a FAT file system. + /// + /// The stream to inspect. + /// true if the stream appears to be a FAT file system, else false. + public static bool Detect(Stream stream) + { + if (stream.Length < 512) + { + return false; + } + + stream.Position = 0; + byte[] bytes = Utilities.ReadFully(stream, 512); + ushort bpbBytesPerSec = Utilities.ToUInt16LittleEndian(bytes, 11); + if (bpbBytesPerSec != 512) + { + return false; + } + + byte bpbNumFATs = bytes[16]; + if (bpbNumFATs == 0 || bpbNumFATs > 2) + { + return false; + } + + ushort bpbTotSec16 = Utilities.ToUInt16LittleEndian(bytes, 19); + uint bpbTotSec32 = Utilities.ToUInt32LittleEndian(bytes, 32); + + if (!((bpbTotSec16 == 0) ^ (bpbTotSec32 == 0))) + { + return false; + } + + uint totalSectors = bpbTotSec16 + bpbTotSec32; + return totalSectors * (long)bpbBytesPerSec <= stream.Length; + } + + /// + /// Opens a file for reading and/or writing. + /// + /// The full path to the file. + /// The file mode. + /// The desired access. + /// The stream to the opened file. + public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) + { + Directory parent; + long entryId; + try + { + entryId = GetDirectoryEntry(_rootDir, path, out parent); + } + catch (ArgumentException) + { + throw new IOException("Invalid path: " + path); + } + + if (parent == null) + { + throw new FileNotFoundException("Could not locate file", path); + } + + if (entryId < 0) + { + return parent.OpenFile(FileName.FromPath(path, FatOptions.FileNameEncoding), mode, access); + } + + DirectoryEntry dirEntry = parent.GetEntry(entryId); + + if ((dirEntry.Attributes & FatAttributes.Directory) != 0) + { + throw new IOException("Attempt to open directory as a file"); + } + else + { + return parent.OpenFile(dirEntry.Name, mode, access); + } + } + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public override FileAttributes GetAttributes(string path) + { + // Simulate a root directory entry - doesn't really exist though + if (IsRootPath(path)) + { + return FileAttributes.Directory; + } + + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file", path); + } + + // Luckily, FAT and .NET FileAttributes match, bit-for-bit + return (FileAttributes)dirEntry.Attributes; + } + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + if (IsRootPath(path)) + { + if (newValue != FileAttributes.Directory) + { + throw new NotSupportedException("The attributes of the root directory cannot be modified"); + } + + return; + } + + Directory parent; + long id = GetDirectoryEntry(path, out parent); + DirectoryEntry dirEntry = parent.GetEntry(id); + + FatAttributes newFatAttr = (FatAttributes)newValue; + + if ((newFatAttr & FatAttributes.Directory) != (dirEntry.Attributes & FatAttributes.Directory)) + { + throw new ArgumentException("Attempted to change the directory attribute"); + } + + dirEntry.Attributes = newFatAttr; + parent.UpdateEntry(id, dirEntry); + + // For directories, need to update their 'self' entry also + if ((dirEntry.Attributes & FatAttributes.Directory) != 0) + { + Directory dir = GetDirectory(path); + dirEntry = dir.SelfEntry; + dirEntry.Attributes = newFatAttr; + dir.SelfEntry = dirEntry; + } + } + + /// + /// Gets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTime(string path) + { + if (IsRootPath(path)) + { + return Epoch; + } + + return GetDirectoryEntry(path).CreationTime; + } + + /// + /// Sets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTime(string path, DateTime newTime) + { + if (IsRootPath(path)) + { + if (newTime != Epoch) + { + throw new NotSupportedException("The creation time of the root directory cannot be modified"); + } + + return; + } + + UpdateDirEntry(path, (e) => { e.CreationTime = newTime; }); + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTimeUtc(string path) + { + if (IsRootPath(path)) + { + return ConvertToUtc(Epoch); + } + + return ConvertToUtc(GetDirectoryEntry(path).CreationTime); + } + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + if (IsRootPath(path)) + { + if (ConvertFromUtc(newTime) != Epoch) + { + throw new NotSupportedException("The last write time of the root directory cannot be modified"); + } + + return; + } + + UpdateDirEntry(path, (e) => { e.CreationTime = ConvertFromUtc(newTime); }); + } + + /// + /// Gets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The time the file or directory was last accessed. + public override DateTime GetLastAccessTime(string path) + { + if (IsRootPath(path)) + { + return Epoch; + } + + return GetDirectoryEntry(path).LastAccessTime; + } + + /// + /// Sets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTime(string path, DateTime newTime) + { + if (IsRootPath(path)) + { + if (newTime != Epoch) + { + throw new NotSupportedException("The last access time of the root directory cannot be modified"); + } + + return; + } + + UpdateDirEntry(path, (e) => { e.LastAccessTime = newTime; }); + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The time the file or directory was last accessed. + public override DateTime GetLastAccessTimeUtc(string path) + { + if (IsRootPath(path)) + { + return ConvertToUtc(Epoch); + } + + return ConvertToUtc(GetDirectoryEntry(path).LastAccessTime); + } + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + if (IsRootPath(path)) + { + if (ConvertFromUtc(newTime) != Epoch) + { + throw new NotSupportedException("The last write time of the root directory cannot be modified"); + } + + return; + } + + UpdateDirEntry(path, (e) => { e.LastAccessTime = ConvertFromUtc(newTime); }); + } + + /// + /// Gets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The time the file or directory was last modified. + public override DateTime GetLastWriteTime(string path) + { + if (IsRootPath(path)) + { + return Epoch; + } + + return GetDirectoryEntry(path).LastWriteTime; + } + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTime(string path, DateTime newTime) + { + if (IsRootPath(path)) + { + if (newTime != Epoch) + { + throw new NotSupportedException("The last write time of the root directory cannot be modified"); + } + + return; + } + + UpdateDirEntry(path, (e) => { e.LastWriteTime = newTime; }); + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The time the file or directory was last modified. + public override DateTime GetLastWriteTimeUtc(string path) + { + if (IsRootPath(path)) + { + return ConvertToUtc(Epoch); + } + + return ConvertToUtc(GetDirectoryEntry(path).LastWriteTime); + } + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + if (IsRootPath(path)) + { + if (ConvertFromUtc(newTime) != Epoch) + { + throw new NotSupportedException("The last write time of the root directory cannot be modified"); + } + + return; + } + + UpdateDirEntry(path, (e) => { e.LastWriteTime = ConvertFromUtc(newTime); }); + } + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public override long GetFileLength(string path) + { + return GetDirectoryEntry(path).FileSize; + } + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + Directory sourceDir; + long sourceEntryId = GetDirectoryEntry(sourceFile, out sourceDir); + + if (sourceDir == null || sourceEntryId < 0) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "The source file '{0}' was not found", sourceFile)); + } + + DirectoryEntry sourceEntry = sourceDir.GetEntry(sourceEntryId); + + if ((sourceEntry.Attributes & FatAttributes.Directory) != 0) + { + throw new IOException("The source file is a directory"); + } + + DirectoryEntry newEntry = new DirectoryEntry(sourceEntry); + newEntry.Name = FileName.FromPath(destinationFile, FatOptions.FileNameEncoding); + newEntry.FirstCluster = 0; + + Directory destDir; + long destEntryId = GetDirectoryEntry(destinationFile, out destDir); + + if (destDir == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The destination directory for '{0}' was not found", destinationFile)); + } + + // If the destination is a directory, use the old file name to construct a full path. + if (destEntryId >= 0) + { + DirectoryEntry destEntry = destDir.GetEntry(destEntryId); + if ((destEntry.Attributes & FatAttributes.Directory) != 0) + { + newEntry.Name = FileName.FromPath(sourceFile, FatOptions.FileNameEncoding); + destinationFile = Utilities.CombinePaths(destinationFile, Utilities.GetFileFromPath(sourceFile)); + + destEntryId = GetDirectoryEntry(destinationFile, out destDir); + } + } + + // If there's an existing entry... + if (destEntryId >= 0) + { + DirectoryEntry destEntry = destDir.GetEntry(destEntryId); + + if ((destEntry.Attributes & FatAttributes.Directory) != 0) + { + throw new IOException("Destination file is an existing directory"); + } + + if (!overwrite) + { + throw new IOException("Destination file already exists"); + } + + // Remove the old file + destDir.DeleteEntry(destEntryId, true); + } + + // Add the new file's entry + destEntryId = destDir.AddEntry(newEntry); + + // Copy the contents... + using (Stream sourceStream = new FatFileStream(this, sourceDir, sourceEntryId, FileAccess.Read), + destStream = new FatFileStream(this, destDir, destEntryId, FileAccess.Write)) + { + Utilities.PumpStreams(sourceStream, destStream); + } + } + + /// + /// Creates a directory. + /// + /// The directory to create. + public override void CreateDirectory(string path) + { + string[] pathElements = path.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + + Directory focusDir = _rootDir; + + for (int i = 0; i < pathElements.Length; ++i) + { + FileName name; + try + { + name = new FileName(pathElements[i], FatOptions.FileNameEncoding); + } + catch (ArgumentException ae) + { + throw new IOException("Invalid path", ae); + } + + Directory child = focusDir.GetChildDirectory(name); + if (child == null) + { + child = focusDir.CreateChildDirectory(name); + } + + focusDir = child; + } + } + + /// + /// Deletes a directory, optionally with all descendants. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + Directory dir = GetDirectory(path); + if (dir == null) + { + throw new DirectoryNotFoundException(String.Format(CultureInfo.InvariantCulture, "No such directory: {0}", path)); + } + + if (!dir.IsEmpty) + { + throw new IOException("Unable to delete non-empty directory"); + } + + Directory parent; + long id = GetDirectoryEntry(path, out parent); + if (parent == null && id == 0) + { + throw new IOException("Unable to delete root directory"); + } + else if (parent != null && id >= 0) + { + DirectoryEntry deadEntry = parent.GetEntry(id); + parent.DeleteEntry(id, true); + ForgetDirectory(deadEntry); + } + else + { + throw new DirectoryNotFoundException("No such directory: " + path); + } + } + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + Directory parent; + long id = GetDirectoryEntry(path, out parent); + if (parent == null || id < 0) + { + throw new FileNotFoundException("No such file", path); + } + + DirectoryEntry entry = parent.GetEntry(id); + if (entry == null || (entry.Attributes & FatAttributes.Directory) != 0) + { + throw new FileNotFoundException("No such file", path); + } + + parent.DeleteEntry(id, true); + } + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public override bool DirectoryExists(string path) + { + // Special case - root directory + if (String.IsNullOrEmpty(path)) + { + return true; + } + else + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + return dirEntry != null && (dirEntry.Attributes & FatAttributes.Directory) != 0; + } + } + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public override bool FileExists(string path) + { + // Special case - root directory + if (String.IsNullOrEmpty(path)) + { + return true; + } + else + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + return dirEntry != null && (dirEntry.Attributes & FatAttributes.Directory) == 0; + } + } + + /// + /// Indicates if a file or directory exists. + /// + /// The path to test. + /// true if the file or directory exists. + public override bool Exists(string path) + { + // Special case - root directory + if (String.IsNullOrEmpty(path)) + { + return true; + } + else + { + return GetDirectoryEntry(path) != null; + } + } + + /// + /// Gets the names of subdirectories in a specified directory. + /// + /// The path to search. + /// Array of directories. + public override string[] GetDirectories(string path) + { + Directory dir = GetDirectory(path); + if (dir == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' was not found", path)); + } + + DirectoryEntry[] entries = dir.GetDirectories(); + List dirs = new List(entries.Length); + foreach (DirectoryEntry dirEntry in entries) + { + dirs.Add(Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding))); + } + + return dirs.ToArray(); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + List dirs = new List(); + DoSearch(dirs, path, re, searchOption == SearchOption.AllDirectories, true, false); + return dirs.ToArray(); + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// Array of files. + public override string[] GetFiles(string path) + { + Directory dir = GetDirectory(path); + DirectoryEntry[] entries = dir.GetFiles(); + + List files = new List(entries.Length); + foreach (DirectoryEntry dirEntry in entries) + { + files.Add(Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding))); + } + + return files.ToArray(); + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern, SearchOption searchOption) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + List results = new List(); + DoSearch(results, path, re, searchOption == SearchOption.AllDirectories, false, true); + return results.ToArray(); + } + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path) + { + Directory dir = GetDirectory(path); + DirectoryEntry[] entries = dir.Entries; + + List result = new List(entries.Length); + foreach (DirectoryEntry dirEntry in entries) + { + result.Add(Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding))); + } + + return result.ToArray(); + } + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path, string searchPattern) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + Directory dir = GetDirectory(path); + DirectoryEntry[] entries = dir.Entries; + + List result = new List(entries.Length); + foreach (DirectoryEntry dirEntry in entries) + { + if (re.IsMatch(dirEntry.Name.GetSearchName(FatOptions.FileNameEncoding))) + { + result.Add(Utilities.CombinePaths(path, dirEntry.Name.GetDisplayName(FatOptions.FileNameEncoding))); + } + } + + return result.ToArray(); + } + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + if (string.IsNullOrEmpty(destinationDirectoryName)) + { + if (destinationDirectoryName == null) + { + throw new ArgumentNullException("destinationDirectoryName"); + } + else + { + throw new ArgumentException("Invalid destination name (empty string)"); + } + } + + Directory destParent; + long destId = GetDirectoryEntry(destinationDirectoryName, out destParent); + if (destParent == null) + { + throw new DirectoryNotFoundException("Target directory doesn't exist"); + } + else if (destId >= 0) + { + throw new IOException("Target directory already exists"); + } + + Directory sourceParent; + long sourceId = GetDirectoryEntry(sourceDirectoryName, out sourceParent); + if (sourceParent == null || sourceId < 0) + { + throw new IOException("Source directory doesn't exist"); + } + + destParent.AttachChildDirectory(FileName.FromPath(destinationDirectoryName, FatOptions.FileNameEncoding), GetDirectory(sourceDirectoryName)); + sourceParent.DeleteEntry(sourceId, false); + } + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + Directory sourceDir; + long sourceEntryId = GetDirectoryEntry(sourceName, out sourceDir); + + if (sourceDir == null || sourceEntryId < 0) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "The source file '{0}' was not found", sourceName)); + } + + DirectoryEntry sourceEntry = sourceDir.GetEntry(sourceEntryId); + + if ((sourceEntry.Attributes & FatAttributes.Directory) != 0) + { + throw new IOException("The source file is a directory"); + } + + DirectoryEntry newEntry = new DirectoryEntry(sourceEntry); + newEntry.Name = FileName.FromPath(destinationName, FatOptions.FileNameEncoding); + + Directory destDir; + long destEntryId = GetDirectoryEntry(destinationName, out destDir); + + if (destDir == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The destination directory for '{0}' was not found", destinationName)); + } + + // If the destination is a directory, use the old file name to construct a full path. + if (destEntryId >= 0) + { + DirectoryEntry destEntry = destDir.GetEntry(destEntryId); + if ((destEntry.Attributes & FatAttributes.Directory) != 0) + { + newEntry.Name = FileName.FromPath(sourceName, FatOptions.FileNameEncoding); + destinationName = Utilities.CombinePaths(destinationName, Utilities.GetFileFromPath(sourceName)); + + destEntryId = GetDirectoryEntry(destinationName, out destDir); + } + } + + // If there's an existing entry... + if (destEntryId >= 0) + { + DirectoryEntry destEntry = destDir.GetEntry(destEntryId); + + if ((destEntry.Attributes & FatAttributes.Directory) != 0) + { + throw new IOException("Destination file is an existing directory"); + } + + if (!overwrite) + { + throw new IOException("Destination file already exists"); + } + + // Remove the old file + destDir.DeleteEntry(destEntryId, true); + } + + // Add the new file's entry and remove the old link to the file's contents + destDir.AddEntry(newEntry); + sourceDir.DeleteEntry(sourceEntryId, false); + } + + internal DateTime ConvertToUtc(DateTime dateTime) + { + return _timeConverter(dateTime, true); + } + + internal DateTime ConvertFromUtc(DateTime dateTime) + { + return _timeConverter(dateTime, false); + } + + internal Directory GetDirectory(string path) + { + Directory parent; + + if (string.IsNullOrEmpty(path) || path == "\\") + { + return _rootDir; + } + + long id = GetDirectoryEntry(_rootDir, path, out parent); + if (id >= 0) + { + return GetDirectory(parent, id); + } + else + { + return null; + } + } + + internal Directory GetDirectory(Directory parent, long parentId) + { + if (parent == null) + { + return _rootDir; + } + + DirectoryEntry dirEntry = parent.GetEntry(parentId); + if ((dirEntry.Attributes & FatAttributes.Directory) == 0) + { + throw new DirectoryNotFoundException(); + } + + // If we have this one cached, return it + Directory result; + if (_dirCache.TryGetValue(dirEntry.FirstCluster, out result)) + { + return result; + } + + // Not cached - create a new one. + result = new Directory(parent, parentId); + _dirCache[dirEntry.FirstCluster] = result; + return result; + } + + internal void ForgetDirectory(DirectoryEntry entry) + { + uint index = entry.FirstCluster; + if (index != 0 && _dirCache.ContainsKey(index)) + { + Directory dir = _dirCache[index]; + _dirCache.Remove(index); + dir.Dispose(); + } + } + + internal DirectoryEntry GetDirectoryEntry(string path) + { + Directory parent; + + long id = GetDirectoryEntry(_rootDir, path, out parent); + if (parent == null || id < 0) + { + return null; + } + + return parent.GetEntry(id); + } + + internal long GetDirectoryEntry(string path, out Directory parent) + { + return GetDirectoryEntry(_rootDir, path, out parent); + } + + /// + /// Disposes of this instance. + /// + /// The value true if Disposing. + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + foreach (Directory dir in _dirCache.Values) + { + dir.Dispose(); + } + + _rootDir.Dispose(); + + if (_ownsData == Ownership.Dispose) + { + _data.Dispose(); + _data = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + /// + /// Writes a FAT12/FAT16 BPB. + /// + /// The buffer to fill. + /// The total capacity of the disk (in sectors). + /// The number of bits in each FAT entry. + /// The maximum number of root directory entries. + /// The number of hidden sectors before this file system (i.e. partition offset). + /// The number of reserved sectors before the FAT. + /// The number of sectors per cluster. + /// The geometry of the disk containing the Fat file system. + /// Indicates if the disk is a removable media (a floppy disk). + /// The disk's volume Id. + /// The disk's label (or null). + private static void WriteBPB( + byte[] bootSector, + uint sectors, + FatType fatType, + ushort maxRootEntries, + uint hiddenSectors, + ushort reservedSectors, + byte sectorsPerCluster, + Geometry diskGeometry, + bool isFloppy, + uint volId, + string label) + { + uint fatSectors = CalcFatSize(sectors, fatType, sectorsPerCluster); + + bootSector[0] = 0xEB; + bootSector[1] = 0x3C; + bootSector[2] = 0x90; + + // OEM Name + Utilities.StringToBytes("DISCUTIL", bootSector, 3, 8); + + // Bytes Per Sector (512) + bootSector[11] = 0; + bootSector[12] = 2; + + // Sectors Per Cluster + bootSector[13] = sectorsPerCluster; + + // Reserved Sector Count + Utilities.WriteBytesLittleEndian(reservedSectors, bootSector, 14); + + // Number of FATs + bootSector[16] = 2; + + // Number of Entries in the root directory + Utilities.WriteBytesLittleEndian((ushort)maxRootEntries, bootSector, 17); + + // Total number of sectors (small) + Utilities.WriteBytesLittleEndian((ushort)(sectors < 0x10000 ? sectors : 0), bootSector, 19); + + // Media + bootSector[21] = (byte)(isFloppy ? 0xF0 : 0xF8); + + // FAT size (FAT12/FAT16) + Utilities.WriteBytesLittleEndian((ushort)(fatType < FatType.Fat32 ? fatSectors : 0), bootSector, 22); + + // Sectors Per Track + Utilities.WriteBytesLittleEndian((ushort)diskGeometry.SectorsPerTrack, bootSector, 24); + + // Heads Per Cylinder + Utilities.WriteBytesLittleEndian((ushort)diskGeometry.HeadsPerCylinder, bootSector, 26); + + // Hidden Sectors + Utilities.WriteBytesLittleEndian((uint)hiddenSectors, bootSector, 28); + + // Total number of sectors (large) + Utilities.WriteBytesLittleEndian((uint)(sectors >= 0x10000 ? sectors : 0), bootSector, 32); + + if (fatType < FatType.Fat32) + { + WriteBS(bootSector, 36, isFloppy, volId, label, fatType); + } + else + { + // FAT size (FAT32) + Utilities.WriteBytesLittleEndian((uint)fatSectors, bootSector, 36); + + // Ext flags: 0x80 = FAT 1 (i.e. Zero) active, mirroring + bootSector[40] = 0x00; + bootSector[41] = 0x00; + + // Filesystem version (0.0) + bootSector[42] = 0; + bootSector[43] = 0; + + // First cluster of the root directory, always 2 since we don't do bad sectors... + Utilities.WriteBytesLittleEndian((uint)2, bootSector, 44); + + // Sector number of FSINFO + Utilities.WriteBytesLittleEndian((uint)1, bootSector, 48); + + // Sector number of the Backup Boot Sector + Utilities.WriteBytesLittleEndian((uint)6, bootSector, 50); + + // Reserved area - must be set to 0 + Array.Clear(bootSector, 52, 12); + + WriteBS(bootSector, 64, isFloppy, volId, label, fatType); + } + + bootSector[510] = 0x55; + bootSector[511] = 0xAA; + } + + private static uint CalcFatSize(uint sectors, FatType fatType, byte sectorsPerCluster) + { + uint numClusters = (uint)(sectors / sectorsPerCluster); + uint fatBytes = (numClusters * (ushort)fatType) / 8; + return (fatBytes + Utilities.SectorSize - 1) / Utilities.SectorSize; + } + + private static void WriteBS(byte[] bootSector, int offset, bool isFloppy, uint volId, string label, FatType fatType) + { + if (string.IsNullOrEmpty(label)) + { + label = "NO NAME "; + } + + string fsType = "FAT32 "; + if (fatType == FatType.Fat12) + { + fsType = "FAT12 "; + } + else if (fatType == FatType.Fat16) + { + fsType = "FAT16 "; + } + + // Drive Number (for BIOS) + bootSector[offset + 0] = (byte)(isFloppy ? 0x00 : 0x80); + + // Reserved + bootSector[offset + 1] = 0; + + // Boot Signature (indicates next 3 fields present) + bootSector[offset + 2] = 0x29; + + // Volume Id + Utilities.WriteBytesLittleEndian(volId, bootSector, offset + 3); + + // Volume Label + Utilities.StringToBytes(label + " ", bootSector, offset + 7, 11); + + // File System Type + Utilities.StringToBytes(fsType, bootSector, offset + 18, 8); + } + + private static FatType DetectFATType(byte[] bpb) + { + uint bpbBytesPerSec = Utilities.ToUInt16LittleEndian(bpb, 11); + uint bpbRootEntCnt = Utilities.ToUInt16LittleEndian(bpb, 17); + uint bpbFATSz16 = Utilities.ToUInt16LittleEndian(bpb, 22); + uint bpbFATSz32 = Utilities.ToUInt32LittleEndian(bpb, 36); + uint bpbTotSec16 = Utilities.ToUInt16LittleEndian(bpb, 19); + uint bpbTotSec32 = Utilities.ToUInt32LittleEndian(bpb, 32); + uint bpbResvdSecCnt = Utilities.ToUInt16LittleEndian(bpb, 14); + uint bpbNumFATs = bpb[16]; + uint bpbSecPerClus = bpb[13]; + + uint rootDirSectors = ((bpbRootEntCnt * 32) + bpbBytesPerSec - 1) / bpbBytesPerSec; + uint fatSz = (bpbFATSz16 != 0) ? (uint)bpbFATSz16 : bpbFATSz32; + uint totalSec = (bpbTotSec16 != 0) ? (uint)bpbTotSec16 : bpbTotSec32; + + uint dataSec = totalSec - (bpbResvdSecCnt + (bpbNumFATs * fatSz) + rootDirSectors); + uint countOfClusters = dataSec / bpbSecPerClus; + + if (countOfClusters < 4085) + { + return FatType.Fat12; + } + else if (countOfClusters < 65525) + { + return FatType.Fat16; + } + else + { + return FatType.Fat32; + } + } + + private static bool IsRootPath(string path) + { + return string.IsNullOrEmpty(path) || path == @"\"; + } + + private static DateTime DefaultTimeConverter(DateTime time, bool toUtc) + { + return toUtc ? time.ToUniversalTime() : time.ToLocalTime(); + } + + private void Initialize(Stream data) + { + _data = data; + _data.Position = 0; + _bootSector = Utilities.ReadSector(_data); + + _type = DetectFATType(_bootSector); + + ReadBPB(); + + LoadFAT(); + + LoadClusterReader(); + + LoadRootDirectory(); + } + + private void LoadClusterReader() + { + int rootDirSectors = ((_bpbRootEntCnt * 32) + (_bpbBytesPerSec - 1)) / _bpbBytesPerSec; + int firstDataSector = (int)(_bpbRsvdSecCnt + (_bpbNumFATs * FatSize) + rootDirSectors); + _clusterReader = new ClusterReader(_data, firstDataSector, _bpbSecPerClus, _bpbBytesPerSec); + } + + private void LoadRootDirectory() + { + Stream fatStream; + if (_type != FatType.Fat32) + { + fatStream = new SubStream(_data, (_bpbRsvdSecCnt + (_bpbNumFATs * _bpbFATSz16)) * _bpbBytesPerSec, _bpbRootEntCnt * 32); + } + else + { + fatStream = new ClusterStream(this, FileAccess.ReadWrite, _bpbRootClus, uint.MaxValue); + } + + _rootDir = new Directory(this, fatStream); + } + + private void LoadFAT() + { + _fat = new FileAllocationTable(_type, _data, _bpbRsvdSecCnt, (uint)FatSize, (byte)FatCount, ActiveFat); + } + + private void ReadBPB() + { + _bpbOEMName = Encoding.ASCII.GetString(_bootSector, 3, 8).TrimEnd('\0'); + _bpbBytesPerSec = Utilities.ToUInt16LittleEndian(_bootSector, 11); + _bpbSecPerClus = _bootSector[13]; + _bpbRsvdSecCnt = Utilities.ToUInt16LittleEndian(_bootSector, 14); + _bpbNumFATs = _bootSector[16]; + _bpbRootEntCnt = Utilities.ToUInt16LittleEndian(_bootSector, 17); + _bpbTotSec16 = Utilities.ToUInt16LittleEndian(_bootSector, 19); + _bpbMedia = _bootSector[21]; + _bpbFATSz16 = Utilities.ToUInt16LittleEndian(_bootSector, 22); + _bpbSecPerTrk = Utilities.ToUInt16LittleEndian(_bootSector, 24); + _bpbNumHeads = Utilities.ToUInt16LittleEndian(_bootSector, 26); + _bpbHiddSec = Utilities.ToUInt32LittleEndian(_bootSector, 28); + _bpbTotSec32 = Utilities.ToUInt32LittleEndian(_bootSector, 32); + + if (_type != FatType.Fat32) + { + ReadBS(36); + } + else + { + _bpbFATSz32 = Utilities.ToUInt32LittleEndian(_bootSector, 36); + _bpbExtFlags = Utilities.ToUInt16LittleEndian(_bootSector, 40); + _bpbFSVer = Utilities.ToUInt16LittleEndian(_bootSector, 42); + _bpbRootClus = Utilities.ToUInt32LittleEndian(_bootSector, 44); + _bpbFSInfo = Utilities.ToUInt16LittleEndian(_bootSector, 48); + _bpbBkBootSec = Utilities.ToUInt16LittleEndian(_bootSector, 50); + ReadBS(64); + } + } + + private void ReadBS(int offset) + { + _bsDrvNum = _bootSector[offset]; + _bsBootSig = _bootSector[offset + 2]; + _bsVolId = Utilities.ToUInt32LittleEndian(_bootSector, offset + 3); + _bsVolLab = Encoding.ASCII.GetString(_bootSector, offset + 7, 11); + _bsFilSysType = Encoding.ASCII.GetString(_bootSector, offset + 18, 8); + } + + private long GetDirectoryEntry(Directory dir, string path, out Directory parent) + { + string[] pathElements = path.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + return GetDirectoryEntry(dir, pathElements, 0, out parent); + } + + private long GetDirectoryEntry(Directory dir, string[] pathEntries, int pathOffset, out Directory parent) + { + long entryId; + + if (pathEntries.Length == 0) + { + // Looking for root directory, simulate the directory entry in its parent... + parent = null; + return 0; + } + else + { + // Long filename support - translate long filename lookup to short filename + string ShortName; + if (dir.LongFileNames_LongKey.TryGetValue(pathEntries[pathOffset], out ShortName)) + pathEntries[pathOffset] = ShortName; + + entryId = dir.FindEntry(new FileName(pathEntries[pathOffset], FatOptions.FileNameEncoding)); + if (entryId >= 0) + { + if (pathOffset == pathEntries.Length - 1) + { + parent = dir; + return entryId; + } + else + { + return GetDirectoryEntry(GetDirectory(dir, entryId), pathEntries, pathOffset + 1, out parent); + } + } + else if (pathOffset == pathEntries.Length - 1) + { + parent = dir; + return -1; + } + else + { + parent = null; + return -1; + } + } + } + + private void DoSearch(List results, string path, Regex regex, bool subFolders, bool dirs, bool files) + { + Directory dir = GetDirectory(path); + if (dir == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' was not found", path)); + } + + DirectoryEntry[] entries = dir.Entries; + + foreach (DirectoryEntry de in entries) + { + bool isDir = (de.Attributes & FatAttributes.Directory) != 0; + + if ((isDir && dirs) || (!isDir && files)) + { + if (regex.IsMatch(de.Name.GetSearchName(FatOptions.FileNameEncoding))) + { + results.Add(Utilities.CombinePaths(path, de.Name.GetDisplayName(FatOptions.FileNameEncoding))); + } + } + + if (subFolders && isDir) + { + DoSearch(results, Utilities.CombinePaths(path, de.Name.GetDisplayName(FatOptions.FileNameEncoding)), regex, subFolders, dirs, files); + } + } + } + + private void UpdateDirEntry(string path, EntryUpdateAction action) + { + Directory parent; + long id = GetDirectoryEntry(path, out parent); + DirectoryEntry entry = parent.GetEntry(id); + action(entry); + parent.UpdateEntry(id, entry); + + if ((entry.Attributes & FatAttributes.Directory) != 0) + { + Directory dir = GetDirectory(path); + DirectoryEntry selfEntry = dir.SelfEntry; + action(selfEntry); + dir.SelfEntry = selfEntry; + } + } + + /// + /// Gets the long name of a given file. + /// + /// The short full path to the file. Input path segments must be short names. + /// The corresponding long file name. + public string GetLongFileName(string shortFullPath) + { + if (shortFullPath == null) + throw new ArgumentNullException("shortFullPath"); + + string dirPath = Path.GetDirectoryName(shortFullPath); + string fileName = Path.GetFileName(shortFullPath); + Directory dir = GetDirectory(dirPath); + if (dir == null) + return fileName; + + string lfn; + if (dir.LongFileNames_ShortKey.TryGetValue(Path.GetFileName(shortFullPath), out lfn)) + return lfn; + + return fileName; + } + + /// + /// Gets the long path to a given file. + /// + /// The short full path to the file. Input path segments must be short names. + /// The corresponding long file path to the file or null if not found. + public string GetLongFilePath(string shortFullPath) + { + if (shortFullPath == null) + throw new ArgumentNullException("shortFullPath"); + + string path = null; + string current = null; + foreach (string segment in shortFullPath.Split(Path.DirectorySeparatorChar)) + { + if (current == null) + { + current = segment; + path = GetLongFileName(current); + } + else + { + current = Path.Combine(current, segment); + path = Path.Combine(path, GetLongFileName(current)); + } + } + return path; + } + } +} diff --git a/DiscUtils/Fat/FatFileSystemOptions.cs b/DiscUtils/Fat/FatFileSystemOptions.cs new file mode 100644 index 0000000..7abf0a8 --- /dev/null +++ b/DiscUtils/Fat/FatFileSystemOptions.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.Text; + + /// + /// FAT file system options. + /// + public sealed class FatFileSystemOptions : DiscFileSystemOptions + { + private Encoding _encoding; + + internal FatFileSystemOptions() + { + FileNameEncoding = Encoding.GetEncoding(437); + } + + internal FatFileSystemOptions(FileSystemParameters parameters) + { + if (parameters != null && parameters.FileNameEncoding != null) + { + FileNameEncoding = parameters.FileNameEncoding; + } + else + { + FileNameEncoding = Encoding.GetEncoding(437); + } + } + + /// + /// Gets or sets the character encoding used for file names. + /// + public Encoding FileNameEncoding + { + get + { + return _encoding; + } + + set + { + if (!value.IsSingleByte) + { + throw new ArgumentException(value.EncodingName + " is not a single byte encoding"); + } + + _encoding = value; + } + } + } +} diff --git a/DiscUtils/Fat/FatType.cs b/DiscUtils/Fat/FatType.cs new file mode 100644 index 0000000..2e7e57e --- /dev/null +++ b/DiscUtils/Fat/FatType.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + /// + /// Enumeration of known FAT types. + /// + public enum FatType + { + /// + /// Represents no known FAT type. + /// + None = 0, + + /// + /// Represents a 12-bit FAT. + /// + Fat12 = 12, + + /// + /// Represents a 16-bit FAT. + /// + Fat16 = 16, + + /// + /// Represents a 32-bit FAT. + /// + Fat32 = 32 + } +} diff --git a/DiscUtils/Fat/FileAllocationTable.cs b/DiscUtils/Fat/FileAllocationTable.cs new file mode 100644 index 0000000..9daed7b --- /dev/null +++ b/DiscUtils/Fat/FileAllocationTable.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.IO; + + internal class FileAllocationTable + { + private Stream _stream; + private ushort _firstFatSector; + private byte _numFats; + + private FatBuffer _buffer; + + public FileAllocationTable(FatType type, Stream stream, ushort firstFatSector, uint fatSize, byte numFats, byte activeFat) + { + _stream = stream; + _firstFatSector = firstFatSector; + _numFats = numFats; + + _stream.Position = (firstFatSector + (fatSize * activeFat)) * Utilities.SectorSize; + _buffer = new FatBuffer(type, Utilities.ReadFully(_stream, (int)(fatSize * Utilities.SectorSize))); + } + + internal bool IsFree(uint val) + { + return _buffer.IsFree(val); + } + + internal bool IsEndOfChain(uint val) + { + return _buffer.IsEndOfChain(val); + } + + internal bool IsBadCluster(uint val) + { + return _buffer.IsBadCluster(val); + } + + internal uint GetNext(uint cluster) + { + return _buffer.GetNext(cluster); + } + + internal void SetEndOfChain(uint cluster) + { + _buffer.SetEndOfChain(cluster); + } + + internal void SetBadCluster(uint cluster) + { + _buffer.SetBadCluster(cluster); + } + + internal void SetNext(uint cluster, uint next) + { + _buffer.SetNext(cluster, next); + } + + internal void Flush() + { + for (int i = 0; i < _numFats; ++i) + { + _buffer.WriteDirtyRegions(_stream, (_firstFatSector * Utilities.SectorSize) + (_buffer.Size * i)); + } + + _buffer.ClearDirtyRegions(); + } + + internal bool TryGetFreeCluster(out uint cluster) + { + return _buffer.TryGetFreeCluster(out cluster); + } + + internal void FreeChain(uint head) + { + _buffer.FreeChain(head); + } + } +} diff --git a/DiscUtils/Fat/FileName.cs b/DiscUtils/Fat/FileName.cs new file mode 100644 index 0000000..8c5df36 --- /dev/null +++ b/DiscUtils/Fat/FileName.cs @@ -0,0 +1,212 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System; + using System.Text; + + internal sealed class FileName : IEquatable + { + public static readonly FileName SelfEntryName = new FileName(new byte[] { 0x2E, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, 0); + public static readonly FileName ParentEntryName = new FileName(new byte[] { 0x2E, 0x2E, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, 0); + public static readonly FileName Null = new FileName(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, 0); + + private const byte SpaceByte = 0x20; + + private static readonly byte[] InvalidBytes = new byte[] { 0x22, 0x2A, 0x2B, 0x2C, 0x2E, 0x2F, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x5B, 0x5C, 0x5D, 0x7C }; + + private byte[] _raw; + + public FileName(byte[] data, int offset) + { + _raw = new byte[11]; + Array.Copy(data, offset, _raw, 0, 11); + } + + public FileName(string name, Encoding encoding) + { + _raw = new byte[11]; + byte[] bytes = encoding.GetBytes(name.ToUpperInvariant()); + + int nameIdx = 0; + int rawIdx = 0; + while (nameIdx < bytes.Length && bytes[nameIdx] != '.' && rawIdx < _raw.Length) + { + byte b = bytes[nameIdx++]; + if (b < 0x20 || Contains(InvalidBytes, b)) + { + throw new ArgumentException("Invalid character in file name '" + (char)b + "'", "name"); + } + + _raw[rawIdx++] = b; + } + + if (rawIdx > 8) + { + throw new ArgumentException("File name too long '" + name + "'", "name"); + } + else if (rawIdx == 0) + { + throw new ArgumentException("File name too short '" + name + "'", "name"); + } + + while (rawIdx < 8) + { + _raw[rawIdx++] = SpaceByte; + } + + if (nameIdx < bytes.Length && bytes[nameIdx] == '.') + { + ++nameIdx; + } + + while (nameIdx < bytes.Length && rawIdx < _raw.Length) + { + byte b = bytes[nameIdx++]; + if (b < 0x20 || Contains(InvalidBytes, b)) + { + throw new ArgumentException("Invalid character in file extension '" + (char)b + "'", "name"); + } + + _raw[rawIdx++] = b; + } + + while (rawIdx < 11) + { + _raw[rawIdx++] = SpaceByte; + } + + if (nameIdx != bytes.Length) + { + throw new ArgumentException("File extension too long '" + name + "'", "name"); + } + } + + public static FileName FromPath(string path, Encoding encoding) + { + return new FileName(Utilities.GetFileFromPath(path), encoding); + } + + public static bool operator ==(FileName a, FileName b) + { + return CompareRawNames(a, b) == 0; + } + + public static bool operator !=(FileName a, FileName b) + { + return CompareRawNames(a, b) != 0; + } + + public string GetDisplayName(Encoding encoding) + { + return GetSearchName(encoding).TrimEnd('.'); + } + + public string GetSearchName(Encoding encoding) + { + return encoding.GetString(_raw, 0, 8).TrimEnd() + "." + encoding.GetString(_raw, 8, 3).TrimEnd(); + } + + public string GetRawName(Encoding encoding) + { + return encoding.GetString(_raw, 0, 11).TrimEnd(); + } + + public FileName Deleted() + { + byte[] data = new byte[11]; + Array.Copy(_raw, data, 11); + data[0] = 0xE5; + + return new FileName(data, 0); + } + + public bool IsDeleted() + { + return _raw[0] == 0xE5; + } + + public bool IsEndMarker() + { + return _raw[0] == 0x00; + } + + public void GetBytes(byte[] data, int offset) + { + Array.Copy(_raw, 0, data, offset, 11); + } + + public override bool Equals(object other) + { + return Equals(other as FileName); + } + + public bool Equals(FileName other) + { + if (other == null) + { + return false; + } + + return CompareRawNames(this, other) == 0; + } + + public override int GetHashCode() + { + int val = 0x1A8D3C4E; + + for (int i = 0; i < 11; ++i) + { + val = (val << 2) ^ _raw[i]; + } + + return val; + } + + private static int CompareRawNames(FileName a, FileName b) + { + for (int i = 0; i < 11; ++i) + { + if (a._raw[i] != b._raw[i]) + { + return (int)a._raw[i] - (int)b._raw[i]; + } + } + + return 0; + } + + private static bool Contains(byte[] array, byte val) + { + foreach (byte b in array) + { + if (b == val) + { + return true; + } + } + + return false; + } + } +} diff --git a/DiscUtils/Fat/FileSystemFactory.cs b/DiscUtils/Fat/FileSystemFactory.cs new file mode 100644 index 0000000..3327827 --- /dev/null +++ b/DiscUtils/Fat/FileSystemFactory.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Fat +{ + using System.IO; + using DiscUtils.Vfs; + + [VfsFileSystemFactory] + internal class FileSystemFactory : VfsFileSystemFactory + { + public override DiscUtils.FileSystemInfo[] Detect(Stream stream, VolumeInfo volume) + { + if (FatFileSystem.Detect(stream)) + { + return new DiscUtils.FileSystemInfo[] { new VfsFileSystemInfo("FAT", "Microsoft FAT", Open) }; + } + + return new DiscUtils.FileSystemInfo[0]; + } + + private DiscFileSystem Open(Stream stream, VolumeInfo volumeInfo, FileSystemParameters parameters) + { + return new FatFileSystem(stream, Ownership.None, parameters); + } + } +} diff --git a/DiscUtils/FileLocator.cs b/DiscUtils/FileLocator.cs new file mode 100644 index 0000000..5fac093 --- /dev/null +++ b/DiscUtils/FileLocator.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + internal abstract class FileLocator + { + public abstract bool Exists(string fileName); + + public abstract Stream Open(string fileName, FileMode mode, FileAccess access, FileShare share); + + public abstract FileLocator GetRelativeLocator(string path); + + public abstract string GetFullPath(string path); + + public abstract string GetDirectoryFromPath(string path); + + public abstract string GetFileFromPath(string path); + + public abstract DateTime GetLastWriteTimeUtc(string path); + + public abstract bool HasCommonRoot(FileLocator other); + + public abstract string ResolveRelativePath(string path); + + internal string MakeRelativePath(FileLocator fileLocator, string path) + { + if (!HasCommonRoot(fileLocator)) + { + return null; + } + + string ourFullPath = GetFullPath(string.Empty) + @"\"; + string otherFullPath = fileLocator.GetFullPath(path); + + return Utilities.MakeRelativePath(otherFullPath, ourFullPath); + } + } +} diff --git a/DiscUtils/FileSystemInfo.cs b/DiscUtils/FileSystemInfo.cs new file mode 100644 index 0000000..de3d5de --- /dev/null +++ b/DiscUtils/FileSystemInfo.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.IO; + + /// + /// Base class holding information about a file system. + /// + /// + /// File system implementations derive from this class, to provide information about the file system. + /// + public abstract class FileSystemInfo + { + /// + /// Gets the name of the file system. + /// + public abstract string Name { get; } + + /// + /// Gets a one-line description of the file system. + /// + public abstract string Description { get; } + + /// + /// Opens a volume using the file system. + /// + /// The volume to access. + /// A file system instance. + public DiscFileSystem Open(VolumeInfo volume) + { + return Open(volume, null); + } + + /// + /// Opens a stream using the file system. + /// + /// The stream to access. + /// A file system instance. + public DiscFileSystem Open(Stream stream) + { + return Open(stream, null); + } + + /// + /// Opens a volume using the file system. + /// + /// The volume to access. + /// Parameters for the file system. + /// A file system instance. + public abstract DiscFileSystem Open(VolumeInfo volume, FileSystemParameters parameters); + + /// + /// Opens a stream using the file system. + /// + /// The stream to access. + /// Parameters for the file system. + /// A file system instance. + public abstract DiscFileSystem Open(Stream stream, FileSystemParameters parameters); + + /// + /// Gets the name of the file system. + /// + /// The file system name. + public override string ToString() + { + return Name; + } + } +} diff --git a/DiscUtils/FileSystemParameters.cs b/DiscUtils/FileSystemParameters.cs new file mode 100644 index 0000000..be1d00a --- /dev/null +++ b/DiscUtils/FileSystemParameters.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Text; + + /// + /// Converts a time to/from UTC. + /// + /// The time to convert. + /// true to convert FAT time to UTC, false to convert UTC to FAT time. + /// The converted time. + public delegate DateTime TimeConverter(DateTime time, bool toUtc); + + /// + /// Class with generic file system parameters. + /// + /// Note - not all parameters apply to all types of file system. + public sealed class FileSystemParameters + { + /// + /// Gets or sets the character encoding for file names, or null for default. + /// + /// Some file systems, such as FAT, don't specify a particular character set for + /// file names. This parameter determines the character set that will be used for such + /// file systems. + public Encoding FileNameEncoding { get; set; } + + /// + /// Gets or sets the algorithm to convert file system time to UTC. + /// + /// Some file system, such as FAT, don't have a defined way to convert from file system + /// time (local time where the file system is authored) to UTC time. This parameter determines + /// the algorithm to use. + public TimeConverter TimeConverter { get; set; } + } +} diff --git a/DiscUtils/FloppyDiskType.cs b/DiscUtils/FloppyDiskType.cs new file mode 100644 index 0000000..1830c7b --- /dev/null +++ b/DiscUtils/FloppyDiskType.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// The supported Floppy Disk logical formats. + /// + public enum FloppyDiskType + { + /// + /// 720KiB capacity disk. + /// + DoubleDensity = 0, + + /// + /// 1440KiB capacity disk. + /// + HighDensity = 1, + + /// + /// 2880KiB capacity disk. + /// + Extended = 2 + } +} diff --git a/DiscUtils/GenericDiskAdapterType.cs b/DiscUtils/GenericDiskAdapterType.cs new file mode 100644 index 0000000..550885b --- /dev/null +++ b/DiscUtils/GenericDiskAdapterType.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Well known hard disk adaptor types. + /// + public enum GenericDiskAdapterType + { + /// + /// IDE adaptor. + /// + Ide = 0, + + /// + /// SCSI adaptor. + /// + Scsi = 1 + } +} diff --git a/DiscUtils/Geometry.cs b/DiscUtils/Geometry.cs new file mode 100644 index 0000000..1d7555a --- /dev/null +++ b/DiscUtils/Geometry.cs @@ -0,0 +1,497 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Globalization; + + /// + /// Class whose instances represent disk geometries. + /// + /// Instances of this class are immutable. + public sealed class Geometry + { + private int _cylinders; + private int _headsPerCylinder; + private int _sectorsPerTrack; + private int _bytesPerSector; + + /// + /// Initializes a new instance of the Geometry class. The default 512 bytes per sector is assumed. + /// + /// The number of cylinders of the disk. + /// The number of heads (aka platters) of the disk. + /// The number of sectors per track/cylinder of the disk. + public Geometry(int cylinders, int headsPerCylinder, int sectorsPerTrack) + { + _cylinders = cylinders; + _headsPerCylinder = headsPerCylinder; + _sectorsPerTrack = sectorsPerTrack; + _bytesPerSector = 512; + } + + /// + /// Initializes a new instance of the Geometry class. + /// + /// The number of cylinders of the disk. + /// The number of heads (aka platters) of the disk. + /// The number of sectors per track/cylinder of the disk. + /// The number of bytes per sector of the disk. + public Geometry(int cylinders, int headsPerCylinder, int sectorsPerTrack, int bytesPerSector) + { + _cylinders = cylinders; + _headsPerCylinder = headsPerCylinder; + _sectorsPerTrack = sectorsPerTrack; + _bytesPerSector = bytesPerSector; + } + + /// + /// Initializes a new instance of the Geometry class. + /// + /// The total capacity of the disk. + /// The number of heads (aka platters) of the disk. + /// The number of sectors per track/cylinder of the disk. + /// The number of bytes per sector of the disk. + public Geometry(long capacity, int headsPerCylinder, int sectorsPerTrack, int bytesPerSector) + { + _cylinders = (int)(capacity / (headsPerCylinder * (long)sectorsPerTrack * bytesPerSector)); + _headsPerCylinder = headsPerCylinder; + _sectorsPerTrack = sectorsPerTrack; + _bytesPerSector = bytesPerSector; + } + + /// + /// Gets a null geometry, which has 512-byte sectors but zero sectors, tracks or cylinders. + /// + public static Geometry Null + { + get { return new Geometry(0, 0, 0, 512); } + } + + /// + /// Gets the number of cylinders. + /// + public int Cylinders + { + get { return _cylinders; } + } + + /// + /// Gets the number of heads (aka platters). + /// + public int HeadsPerCylinder + { + get { return _headsPerCylinder; } + } + + /// + /// Gets the number of sectors per track. + /// + public int SectorsPerTrack + { + get { return _sectorsPerTrack; } + } + + /// + /// Gets the number of bytes in each sector. + /// + public int BytesPerSector + { + get { return _bytesPerSector; } + } + + /// + /// Gets the total size of the disk (in sectors). + /// + [Obsolete("Use TotalSectorsLong instead, to support very large disks.")] + public int TotalSectors + { + get { return Cylinders * HeadsPerCylinder * SectorsPerTrack; } + } + + /// + /// Gets the total size of the disk (in sectors). + /// + public long TotalSectorsLong + { + get { return (long)Cylinders * (long)HeadsPerCylinder * (long)SectorsPerTrack; } + } + + /// + /// Gets the total capacity of the disk (in bytes). + /// + public long Capacity + { + get { return TotalSectorsLong * (long)BytesPerSector; } + } + + /// + /// Gets the address of the last sector on the disk. + /// + public ChsAddress LastSector + { + get { return new ChsAddress(_cylinders - 1, _headsPerCylinder - 1, _sectorsPerTrack); } + } + + /// + /// Gets a value indicating whether the Geometry is consistent with the values a BIOS can support. + /// + public bool IsBiosSafe + { + get { return _cylinders <= 1024 && _headsPerCylinder <= 255 && _sectorsPerTrack <= 63; } + } + + /// + /// Gets a value indicating whether the Geometry is consistent with the values IDE can represent. + /// + public bool IsIdeSafe + { + get { return _cylinders <= 65536 && _headsPerCylinder <= 16 && _sectorsPerTrack <= 255; } + } + + /// + /// Gets a value indicating whether the Geometry is representable both by the BIOS and by IDE. + /// + public bool IsBiosAndIdeSafe + { + get { return _cylinders <= 1024 && _headsPerCylinder <= 16 && _sectorsPerTrack <= 63; } + } + + /// + /// Gets the 'Large' BIOS geometry for a disk, given it's physical geometry. + /// + /// The physical (aka IDE) geometry of the disk. + /// The geometry a BIOS using the 'Large' method for calculating disk geometry will indicate for the disk. + public static Geometry LargeBiosGeometry(Geometry ideGeometry) + { + int cylinders = ideGeometry.Cylinders; + int heads = ideGeometry.HeadsPerCylinder; + int sectors = ideGeometry.SectorsPerTrack; + + while (cylinders > 1024 && heads <= 127) + { + cylinders >>= 1; + heads <<= 1; + } + + return new Geometry(cylinders, heads, sectors); + } + + /// + /// Gets the 'LBA Assisted' BIOS geometry for a disk, given it's capacity. + /// + /// The capacity of the disk. + /// The geometry a BIOS using the 'LBA Assisted' method for calculating disk geometry will indicate for the disk. + public static Geometry LbaAssistedBiosGeometry(long capacity) + { + int heads; + if (capacity <= 504 * Sizes.OneMiB) + { + heads = 16; + } + else if (capacity <= 1008 * Sizes.OneMiB) + { + heads = 32; + } + else if (capacity <= 2016 * Sizes.OneMiB) + { + heads = 64; + } + else if (capacity <= 4032 * Sizes.OneMiB) + { + heads = 128; + } + else + { + heads = 255; + } + + int sectors = 63; + int cylinders = (int)Math.Min(1024, capacity / (sectors * (long)heads * Sizes.Sector)); + return new Geometry(cylinders, heads, sectors, Sizes.Sector); + } + + /// + /// Converts a geometry into one that is BIOS-safe, if not already. + /// + /// The geometry to make BIOS-safe. + /// The capacity of the disk. + /// The new geometry. + /// This method returns the LBA-Assisted geometry if the given geometry isn't BIOS-safe. + public static Geometry MakeBiosSafe(Geometry geometry, long capacity) + { + if (geometry == null) + { + return LbaAssistedBiosGeometry(capacity); + } + else if (geometry.IsBiosSafe) + { + return geometry; + } + else + { + return LbaAssistedBiosGeometry(capacity); + } + } + + /// + /// Calculates a sensible disk geometry for a disk capacity using the VHD algorithm (errs under). + /// + /// The desired capacity of the disk. + /// The appropriate disk geometry. + /// The geometry returned tends to produce a disk with less capacity + /// than requested (an exact capacity is not always possible). The geometry returned is the IDE + /// (aka Physical) geometry of the disk, not necessarily the geometry used by the BIOS. + public static Geometry FromCapacity(long capacity) + { + return FromCapacity(capacity, Utilities.SectorSize); + } + + /// + /// Calculates a sensible disk geometry for a disk capacity using the VHD algorithm (errs under). + /// + /// The desired capacity of the disk. + /// The logical sector size of the disk. + /// The appropriate disk geometry. + /// The geometry returned tends to produce a disk with less capacity + /// than requested (an exact capacity is not always possible). The geometry returned is the IDE + /// (aka Physical) geometry of the disk, not necessarily the geometry used by the BIOS. + public static Geometry FromCapacity(long capacity, int sectorSize) + { + int totalSectors; + int cylinders; + int headsPerCylinder; + int sectorsPerTrack; + + // If more than ~128GB truncate at ~128GB + if (capacity > 65535 * (long)16 * 255 * sectorSize) + { + totalSectors = 65535 * 16 * 255; + } + else + { + totalSectors = (int)(capacity / sectorSize); + } + + // If more than ~32GB, break partition table compatibility. + // Partition table has max 63 sectors per track. Otherwise + // we're looking for a geometry that's valid for both BIOS + // and ATA. + if (totalSectors > 65535 * 16 * 63) + { + sectorsPerTrack = 255; + headsPerCylinder = 16; + } + else + { + sectorsPerTrack = 17; + int cylindersTimesHeads = totalSectors / sectorsPerTrack; + headsPerCylinder = (cylindersTimesHeads + 1023) / 1024; + + if (headsPerCylinder < 4) + { + headsPerCylinder = 4; + } + + // If we need more than 1023 cylinders, or 16 heads, try more sectors per track + if (cylindersTimesHeads >= (headsPerCylinder * 1024U) || headsPerCylinder > 16) + { + sectorsPerTrack = 31; + headsPerCylinder = 16; + cylindersTimesHeads = totalSectors / sectorsPerTrack; + } + + // We need 63 sectors per track to keep the cylinder count down + if (cylindersTimesHeads >= (headsPerCylinder * 1024U)) + { + sectorsPerTrack = 63; + headsPerCylinder = 16; + } + } + + cylinders = (totalSectors / sectorsPerTrack) / headsPerCylinder; + + return new Geometry(cylinders, headsPerCylinder, sectorsPerTrack, sectorSize); + } + + /// + /// Converts a CHS (Cylinder,Head,Sector) address to a LBA (Logical Block Address). + /// + /// The CHS address to convert. + /// The Logical Block Address (in sectors). + public long ToLogicalBlockAddress(ChsAddress chsAddress) + { + return ToLogicalBlockAddress(chsAddress.Cylinder, chsAddress.Head, chsAddress.Sector); + } + + /// + /// Converts a CHS (Cylinder,Head,Sector) address to a LBA (Logical Block Address). + /// + /// The cylinder of the address. + /// The head of the address. + /// The sector of the address. + /// The Logical Block Address (in sectors). + public long ToLogicalBlockAddress(int cylinder, int head, int sector) + { + if (cylinder < 0) + { + throw new ArgumentOutOfRangeException("cylinder", cylinder, "cylinder number is negative"); + } + + if (head >= _headsPerCylinder) + { + throw new ArgumentOutOfRangeException("head", head, "head number is larger than disk geometry"); + } + + if (head < 0) + { + throw new ArgumentOutOfRangeException("head", head, "head number is negative"); + } + + if (sector > _sectorsPerTrack) + { + throw new ArgumentOutOfRangeException("sector", sector, "sector number is larger than disk geometry"); + } + + if (sector < 1) + { + throw new ArgumentOutOfRangeException("sector", sector, "sector number is less than one (sectors are 1-based)"); + } + + return (((cylinder * (long)_headsPerCylinder) + head) * _sectorsPerTrack) + sector - 1; + } + + /// + /// Converts a LBA (Logical Block Address) to a CHS (Cylinder, Head, Sector) address. + /// + /// The logical block address (in sectors). + /// The address in CHS form. + public ChsAddress ToChsAddress(long logicalBlockAddress) + { + if (logicalBlockAddress < 0) + { + throw new ArgumentOutOfRangeException("logicalBlockAddress", logicalBlockAddress, "Logical Block Address is negative"); + } + + int cylinder = (int)(logicalBlockAddress / (_headsPerCylinder * _sectorsPerTrack)); + int temp = (int)(logicalBlockAddress % (_headsPerCylinder * _sectorsPerTrack)); + int head = temp / _sectorsPerTrack; + int sector = (temp % _sectorsPerTrack) + 1; + + return new ChsAddress(cylinder, head, sector); + } + + /// + /// Translates an IDE (aka Physical) geometry to a BIOS (aka Logical) geometry. + /// + /// The translation to perform. + /// The translated disk geometry. + public Geometry TranslateToBios(GeometryTranslation translation) + { + return TranslateToBios(0, translation); + } + + /// + /// Translates an IDE (aka Physical) geometry to a BIOS (aka Logical) geometry. + /// + /// The capacity of the disk, required if the geometry is an approximation on the actual disk size. + /// The translation to perform. + /// The translated disk geometry. + public Geometry TranslateToBios(long capacity, GeometryTranslation translation) + { + if (capacity <= 0) + { + capacity = TotalSectorsLong * 512L; + } + + switch (translation) + { + case GeometryTranslation.None: + return this; + + case GeometryTranslation.Auto: + if (IsBiosSafe) + { + return this; + } + else + { + return Geometry.LbaAssistedBiosGeometry(capacity); + } + + case GeometryTranslation.Lba: + return Geometry.LbaAssistedBiosGeometry(capacity); + + case GeometryTranslation.Large: + return Geometry.LargeBiosGeometry(this); + + default: + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Translation mode '{0}' not yet implemented", translation), "translation"); + } + } + + /// + /// Determines if this object is equivalent to another. + /// + /// The object to test against. + /// true if the is equivalent, else false. + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != GetType()) + { + return false; + } + + Geometry other = (Geometry)obj; + + return _cylinders == other._cylinders && _headsPerCylinder == other._headsPerCylinder + && _sectorsPerTrack == other._sectorsPerTrack && _bytesPerSector == other._bytesPerSector; + } + + /// + /// Calculates the hash code for this object. + /// + /// The hash code. + public override int GetHashCode() + { + return _cylinders.GetHashCode() ^ _headsPerCylinder.GetHashCode() + ^ _sectorsPerTrack.GetHashCode() ^ _bytesPerSector.GetHashCode(); + } + + /// + /// Gets a string representation of this object, in the form (C/H/S). + /// + /// The string representation. + public override string ToString() + { + if (_bytesPerSector == 512) + { + return "(" + _cylinders + "/" + _headsPerCylinder + "/" + _sectorsPerTrack + ")"; + } + else + { + return "(" + _cylinders + "/" + _headsPerCylinder + "/" + _sectorsPerTrack + ":" + _bytesPerSector + ")"; + } + } + } +} diff --git a/DiscUtils/GeometryTranslation.cs b/DiscUtils/GeometryTranslation.cs new file mode 100644 index 0000000..26476a8 --- /dev/null +++ b/DiscUtils/GeometryTranslation.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Enumeration of standard BIOS disk geometry translation methods. + /// + public enum GeometryTranslation + { + /// + /// Apply no translation. + /// + None = 0, + + /// + /// Automatic, based on the physical geometry select the most appropriate translation. + /// + Auto = 1, + + /// + /// LBA assisted translation, based on just the disk capacity. + /// + Lba = 2, + + /// + /// Bit-shifting translation, based on the physical geometry of the disk. + /// + Large = 3, + } +} diff --git a/DiscUtils/IBuffer.cs b/DiscUtils/IBuffer.cs new file mode 100644 index 0000000..8922e0a --- /dev/null +++ b/DiscUtils/IBuffer.cs @@ -0,0 +1,118 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Collections.Generic; + + /// + /// Interface shared by all buffers. + /// + /// + /// Buffers are very similar to streams, except the buffer has no notion of + /// 'current position'. All I/O operations instead specify the position, as + /// needed. Buffers also support sparse behaviour. + /// + public interface IBuffer + { + /// + /// Gets a value indicating whether this buffer can be read. + /// + bool CanRead { get; } + + /// + /// Gets a value indicating whether this buffer can be modified. + /// + bool CanWrite { get; } + + /// + /// Gets the current capacity of the buffer, in bytes. + /// + long Capacity + { + get; + } + + /// + /// Gets the parts of the buffer that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + IEnumerable Extents + { + get; + } + + /// + /// Reads from the buffer into a byte array. + /// + /// The offset within the buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + int Read(long pos, byte[] buffer, int offset, int count); + + /// + /// Writes a byte array into the buffer. + /// + /// The start offset within the buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + void Write(long pos, byte[] buffer, int offset, int count); + + /// + /// Clears bytes from the buffer. + /// + /// The start offset within the buffer. + /// The number of bytes to clear. + /// + /// Logically equivalent to writing count null/zero bytes to the buffer, some + /// implementations determine that some (or all) of the range indicated is not actually + /// stored. There is no direct, automatic, correspondence to clearing bytes and them + /// not being represented as an 'extent' - for example, the implementation of the underlying + /// stream may not permit fine-grained extent storage. + /// It is always safe to call this method to 'zero-out' a section of a buffer, regardless of + /// the underlying buffer implementation. + /// + void Clear(long pos, int count); + + /// + /// Flushes all data to the underlying storage. + /// + void Flush(); + + /// + /// Sets the capacity of the buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + void SetCapacity(long value); + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + IEnumerable GetExtentsInRange(long start, long count); + } +} diff --git a/DiscUtils/IByteArraySerializable.cs b/DiscUtils/IByteArraySerializable.cs new file mode 100644 index 0000000..73904ad --- /dev/null +++ b/DiscUtils/IByteArraySerializable.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Common interface for reading structures to/from byte arrays. + /// + internal interface IByteArraySerializable + { + /// + /// Gets the total number of bytes the structure occupies. + /// + int Size { get; } + + /// + /// Reads the structure from a byte array. + /// + /// The buffer to read from. + /// The buffer offset to start reading from. + /// The number of bytes read. + int ReadFrom(byte[] buffer, int offset); + + /// + /// Writes a structure to a byte array. + /// + /// The buffer to write to. + /// The buffer offset to start writing at. + void WriteTo(byte[] buffer, int offset); + } +} diff --git a/DiscUtils/IClusterBasedFileSystem.cs b/DiscUtils/IClusterBasedFileSystem.cs new file mode 100644 index 0000000..0aff2c1 --- /dev/null +++ b/DiscUtils/IClusterBasedFileSystem.cs @@ -0,0 +1,85 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Base class for all file systems based on a cluster model. + /// + public interface IClusterBasedFileSystem : IFileSystem + { + /// + /// Gets the size (in bytes) of each cluster. + /// + long ClusterSize + { + get; + } + + /// + /// Gets the total number of clusters managed by the file system. + /// + long TotalClusters + { + get; + } + + /// + /// Converts a cluster (index) into an absolute byte position in the underlying stream. + /// + /// The cluster to convert. + /// The corresponding absolute byte position. + long ClusterToOffset(long cluster); + + /// + /// Converts an absolute byte position in the underlying stream to a cluster (index). + /// + /// The byte position to convert. + /// The cluster containing the specified byte. + long OffsetToCluster(long offset); + + /// + /// Converts a file name to the list of clusters occupied by the file's data. + /// + /// The path to inspect. + /// The clusters. + /// Note that in some file systems, small files may not have dedicated + /// clusters. Only dedicated clusters will be returned. + Range[] PathToClusters(string path); + + /// + /// Converts a file name to the extents containing its data. + /// + /// The path to inspect. + /// The file extents, as absolute byte positions in the underlying stream. + /// Use this method with caution - not all file systems will store all bytes + /// directly in extents. Files may be compressed, sparse or encrypted. This method + /// merely indicates where file data is stored, not what's stored. + StreamExtent[] PathToExtents(string path); + + /// + /// Gets an object that can convert between clusters and files. + /// + /// The cluster map. + ClusterMap BuildClusterMap(); + } +} diff --git a/DiscUtils/IDiagnosticTraceable.cs b/DiscUtils/IDiagnosticTraceable.cs new file mode 100644 index 0000000..44a103b --- /dev/null +++ b/DiscUtils/IDiagnosticTraceable.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.IO; + + /// + /// Interface exposed by objects that can provide a structured trace of their content. + /// + public interface IDiagnosticTraceable + { + /// + /// Writes a diagnostic report about the state of the object to a writer. + /// + /// The writer to send the report to. + /// The prefix to place at the start of each line. + void Dump(TextWriter writer, string linePrefix); + } +} diff --git a/DiscUtils/IFileSystem.cs b/DiscUtils/IFileSystem.cs new file mode 100644 index 0000000..d69ecd3 --- /dev/null +++ b/DiscUtils/IFileSystem.cs @@ -0,0 +1,354 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + /// + /// Common interface for all file systems. + /// + public interface IFileSystem + { + /// + /// Gets a value indicating whether the file system is read-only or read-write. + /// + /// true if the file system is read-write. + bool CanWrite { get; } + + /// + /// Gets the root directory of the file system. + /// + DiscDirectoryInfo Root + { + get; + } + + /// + /// Gets a value indicating whether the file system is thread-safe. + /// + bool IsThreadSafe { get; } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + void CopyFile(string sourceFile, string destinationFile); + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + void CopyFile(string sourceFile, string destinationFile, bool overwrite); + + /// + /// Creates a directory. + /// + /// The path of the new directory. + void CreateDirectory(string path); + + /// + /// Deletes a directory. + /// + /// The path of the directory to delete. + void DeleteDirectory(string path); + + /// + /// Deletes a directory, optionally with all descendants. + /// + /// The path of the directory to delete. + /// Determines if the all descendants should be deleted. + void DeleteDirectory(string path, bool recursive); + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + void DeleteFile(string path); + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + bool DirectoryExists(string path); + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + bool FileExists(string path); + + /// + /// Indicates if a file or directory exists. + /// + /// The path to test. + /// true if the file or directory exists. + bool Exists(string path); + + /// + /// Gets the names of subdirectories in a specified directory. + /// + /// The path to search. + /// Array of directories. + string[] GetDirectories(string path); + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of directories matching the search pattern. + string[] GetDirectories(string path, string searchPattern); + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + string[] GetDirectories(string path, string searchPattern, SearchOption searchOption); + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// Array of files. + string[] GetFiles(string path); + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// The search string to match against. + /// Array of files matching the search pattern. + string[] GetFiles(string path, string searchPattern); + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + string[] GetFiles(string path, string searchPattern, SearchOption searchOption); + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + string[] GetFileSystemEntries(string path); + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + string[] GetFileSystemEntries(string path, string searchPattern); + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName); + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + void MoveFile(string sourceName, string destinationName); + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + void MoveFile(string sourceName, string destinationName, bool overwrite); + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + SparseStream OpenFile(string path, FileMode mode); + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + SparseStream OpenFile(string path, FileMode mode, FileAccess access); + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + FileAttributes GetAttributes(string path); + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + void SetAttributes(string path, FileAttributes newValue); + + /// + /// Gets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + DateTime GetCreationTime(string path); + + /// + /// Sets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetCreationTime(string path, DateTime newTime); + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + DateTime GetCreationTimeUtc(string path); + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetCreationTimeUtc(string path, DateTime newTime); + + /// + /// Gets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + DateTime GetLastAccessTime(string path); + + /// + /// Sets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetLastAccessTime(string path, DateTime newTime); + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + DateTime GetLastAccessTimeUtc(string path); + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetLastAccessTimeUtc(string path, DateTime newTime); + + /// + /// Gets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + DateTime GetLastWriteTime(string path); + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetLastWriteTime(string path, DateTime newTime); + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + DateTime GetLastWriteTimeUtc(string path); + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetLastWriteTimeUtc(string path, DateTime newTime); + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + long GetFileLength(string path); + + /// + /// Gets an object representing a possible file. + /// + /// The file path. + /// The representing object. + /// The file does not need to exist. + DiscFileInfo GetFileInfo(string path); + + /// + /// Gets an object representing a possible directory. + /// + /// The directory path. + /// The representing object. + /// The directory does not need to exist. + DiscDirectoryInfo GetDirectoryInfo(string path); + + /// + /// Gets an object representing a possible file system object (file or directory). + /// + /// The file system path. + /// The representing object. + /// The file system object does not need to exist. + DiscFileSystemInfo GetFileSystemInfo(string path); + + /// + /// Reads the boot code of the file system into a byte array. + /// + /// The boot code, or null if not available. + byte[] ReadBootCode(); + } +} diff --git a/DiscUtils/IMappedBuffer.cs b/DiscUtils/IMappedBuffer.cs new file mode 100644 index 0000000..410a098 --- /dev/null +++ b/DiscUtils/IMappedBuffer.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + internal interface IMappedBuffer : IBuffer + { + long MapPosition(long position); + } +} diff --git a/DiscUtils/IWindowsFileSystem.cs b/DiscUtils/IWindowsFileSystem.cs new file mode 100644 index 0000000..01b8ac0 --- /dev/null +++ b/DiscUtils/IWindowsFileSystem.cs @@ -0,0 +1,129 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Security.AccessControl; + + /// + /// Provides the base class for all file systems that support Windows semantics. + /// + public interface IWindowsFileSystem : IFileSystem + { + /// + /// Gets the security descriptor associated with the file or directory. + /// + /// The file or directory to inspect. + /// The security descriptor. + RawSecurityDescriptor GetSecurity(string path); + + /// + /// Sets the security descriptor associated with the file or directory. + /// + /// The file or directory to change. + /// The new security descriptor. + void SetSecurity(string path, RawSecurityDescriptor securityDescriptor); + + /// + /// Gets the reparse point data associated with a file or directory. + /// + /// The file to query. + /// The reparse point information. + ReparsePoint GetReparsePoint(string path); + + /// + /// Sets the reparse point data on a file or directory. + /// + /// The file to set the reparse point on. + /// The new reparse point. + void SetReparsePoint(string path, ReparsePoint reparsePoint); + + /// + /// Removes a reparse point from a file or directory, without deleting the file or directory. + /// + /// The path to the file or directory to remove the reparse point from. + void RemoveReparsePoint(string path); + + /// + /// Gets the short name for a given path. + /// + /// The path to convert. + /// The short name. + /// + /// This method only gets the short name for the final part of the path, to + /// convert a complete path, call this method repeatedly, once for each path + /// segment. If there is no short name for the given path,null is + /// returned. + /// + string GetShortName(string path); + + /// + /// Sets the short name for a given file or directory. + /// + /// The full path to the file or directory to change. + /// The shortName, which should not include a path. + void SetShortName(string path, string shortName); + + /// + /// Gets the standard file information for a file. + /// + /// The full path to the file or directory to query. + /// The standard file information. + WindowsFileInformation GetFileStandardInformation(string path); + + /// + /// Sets the standard file information for a file. + /// + /// The full path to the file or directory to query. + /// The standard file information. + void SetFileStandardInformation(string path, WindowsFileInformation info); + + /// + /// Gets the names of the alternate data streams for a file. + /// + /// The path to the file. + /// + /// The list of alternate data streams (or empty, if none). To access the contents + /// of the alternate streams, use OpenFile(path + ":" + name, ...). + /// + string[] GetAlternateDataStreams(string path); + + /// + /// Gets the file id for a given path. + /// + /// The path to get the id of. + /// The file id, or -1. + /// + /// The returned file id uniquely identifies the file, and is shared by all hard + /// links to the same file. The value -1 indicates no unique identifier is + /// available, and so it can be assumed the file has no hard links. + /// + long GetFileId(string path); + + /// + /// Indicates whether the file is known by other names. + /// + /// The file to inspect. + /// true if the file has other names, else false. + bool HasHardLinks(string path); + } +} diff --git a/DiscUtils/InvalidFileSystemException.cs b/DiscUtils/InvalidFileSystemException.cs new file mode 100644 index 0000000..59b66b1 --- /dev/null +++ b/DiscUtils/InvalidFileSystemException.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + using System.Runtime.Serialization; + + /// + /// Exception thrown when some invalid file system data is found, indicating probably corruption. + /// + [Serializable] + public class InvalidFileSystemException : IOException + { + /// + /// Initializes a new instance of the InvalidFileSystemException class. + /// + public InvalidFileSystemException() + { + } + + /// + /// Initializes a new instance of the InvalidFileSystemException class. + /// + /// The exception message. + public InvalidFileSystemException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the InvalidFileSystemException class. + /// + /// The exception message. + /// The inner exception. + public InvalidFileSystemException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the InvalidFileSystemException class. + /// + /// The serialization info. + /// The streaming context. + protected InvalidFileSystemException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/DiscUtils/LogicalDiskManager/ComponentRecord.cs b/DiscUtils/LogicalDiskManager/ComponentRecord.cs new file mode 100644 index 0000000..102bc9f --- /dev/null +++ b/DiscUtils/LogicalDiskManager/ComponentRecord.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class ComponentRecord : DatabaseRecord + { + public string StatusString; + public ExtentMergeType MergeType; // (02 Spanned, Simple, Mirrored) (01 on striped) + public uint Unknown1; // Zero + public ulong NumExtents; // Could be num disks + public uint Unknown2; // Zero + public uint LinkId; // Identical on mirrors + public ulong Unknown3; // 00 .. 00 + public ulong VolumeId; + public ulong Unknown4; // ?? + public long StripeSizeSectors; + public long StripeStride; // aka num partitions + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + StatusString = ReadVarString(buffer, ref pos); + MergeType = (ExtentMergeType)ReadByte(buffer, ref pos); + Unknown1 = ReadUInt(buffer, ref pos); // Zero + NumExtents = ReadVarULong(buffer, ref pos); + Unknown2 = ReadUInt(buffer, ref pos); + LinkId = ReadUInt(buffer, ref pos); + Unknown3 = ReadULong(buffer, ref pos); // Zero + VolumeId = ReadVarULong(buffer, ref pos); + Unknown4 = ReadVarULong(buffer, ref pos); // Zero + + if ((Flags & 0x1000) != 0) + { + StripeSizeSectors = ReadVarLong(buffer, ref pos); + StripeStride = ReadVarLong(buffer, ref pos); + } + } + } +} diff --git a/DiscUtils/LogicalDiskManager/Database.cs b/DiscUtils/LogicalDiskManager/Database.cs new file mode 100644 index 0000000..b33be0f --- /dev/null +++ b/DiscUtils/LogicalDiskManager/Database.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class Database + { + private DatabaseHeader _vmdb; + private Dictionary _records; + + public Database(Stream stream) + { + long dbStart = stream.Position; + + byte[] buffer = new byte[Sizes.Sector]; + stream.Read(buffer, 0, buffer.Length); + _vmdb = new DatabaseHeader(); + _vmdb.ReadFrom(buffer, 0); + + stream.Position = dbStart + _vmdb.HeaderSize; + + buffer = Utilities.ReadFully(stream, (int)(_vmdb.BlockSize * _vmdb.NumVBlks)); + + _records = new Dictionary(); + for (int i = 0; i < _vmdb.NumVBlks; ++i) + { + DatabaseRecord rec = DatabaseRecord.ReadFrom(buffer, (int)(i * _vmdb.BlockSize)); + if (rec != null) + { + _records.Add(rec.Id, rec); + } + } + } + + internal IEnumerable Disks + { + get + { + foreach (var record in _records.Values) + { + if (record.RecordType == RecordType.Disk) + { + yield return (DiskRecord)record; + } + } + } + } + + internal IEnumerable Volumes + { + get + { + foreach (var record in _records.Values) + { + if (record.RecordType == RecordType.Volume) + { + yield return (VolumeRecord)record; + } + } + } + } + + internal DiskGroupRecord GetDiskGroup(Guid guid) + { + foreach (var record in _records.Values) + { + if (record.RecordType == RecordType.DiskGroup) + { + DiskGroupRecord dgRecord = (DiskGroupRecord)record; + if (new Guid(dgRecord.GroupGuidString) == guid || guid == Guid.Empty) + { + return dgRecord; + } + } + } + + return null; + } + + internal IEnumerable GetVolumeComponents(ulong volumeId) + { + foreach (var record in _records.Values) + { + if (record.RecordType == RecordType.Component) + { + ComponentRecord cmpntRecord = (ComponentRecord)record; + if (cmpntRecord.VolumeId == volumeId) + { + yield return cmpntRecord; + } + } + } + } + + internal IEnumerable GetComponentExtents(ulong componentId) + { + foreach (var record in _records.Values) + { + if (record.RecordType == RecordType.Extent) + { + ExtentRecord extentRecord = (ExtentRecord)record; + if (extentRecord.ComponentId == componentId) + { + yield return extentRecord; + } + } + } + } + + internal DiskRecord GetDisk(ulong diskId) + { + return (DiskRecord)_records[diskId]; + } + + internal VolumeRecord GetVolume(ulong volumeId) + { + return (VolumeRecord)_records[volumeId]; + } + + internal VolumeRecord GetVolume(Guid id) + { + return FindRecord(r => (r.VolumeGuid == id), RecordType.Volume); + } + + internal IEnumerable GetVolumes() + { + foreach (var record in _records.Values) + { + if (record.RecordType == RecordType.Volume) + { + yield return (VolumeRecord)record; + } + } + } + + internal T FindRecord(Predicate pred, RecordType typeId) + where T : DatabaseRecord + { + foreach (var record in _records.Values) + { + if (record.RecordType == typeId) + { + T t = (T)record; + if (pred(t)) + { + return t; + } + } + } + + return null; + } + } +} diff --git a/DiscUtils/LogicalDiskManager/DatabaseHeader.cs b/DiscUtils/LogicalDiskManager/DatabaseHeader.cs new file mode 100644 index 0000000..83728c5 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DatabaseHeader.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + + internal class DatabaseHeader + { + public string Signature; // VMDB + public uint NumVBlks; // 00 00 17 24 + public uint BlockSize; // 00 00 00 80 + public uint HeaderSize; // 00 00 02 00 + public ushort Unknown1; // 00 01 + public ushort VersionNum; // 00 04 + public ushort VersionDenom; // 00 0a + public string GroupName; + public string DiskGroupId; + public long CommittedSequence; // 0xA + public long PendingSequence; // 0xA + public uint Unknown2; // 1 + public uint Unknown3; // 1 + public uint Unknown4; // 3 + public uint Unknown5; // 3 + public long Unknown6; // 0 + public long Unknown7; // 1 + public uint Unknown8; // 1 + public uint Unknown9; // 3 + public uint UnknownA; // 3 + public long UnknownB; // 0 + public uint UnknownC; // 0 + public DateTime Timestamp; + + public void ReadFrom(byte[] buffer, int offset) + { + Signature = Utilities.BytesToString(buffer, offset + 0x00, 4); + NumVBlks = Utilities.ToUInt32BigEndian(buffer, offset + 0x04); + BlockSize = Utilities.ToUInt32BigEndian(buffer, offset + 0x08); + HeaderSize = Utilities.ToUInt32BigEndian(buffer, offset + 0x0C); + Unknown1 = Utilities.ToUInt16BigEndian(buffer, offset + 0x10); + VersionNum = Utilities.ToUInt16BigEndian(buffer, offset + 0x12); + VersionDenom = Utilities.ToUInt16BigEndian(buffer, offset + 0x14); + GroupName = Utilities.BytesToString(buffer, offset + 0x16, 31).Trim('\0'); + DiskGroupId = Utilities.BytesToString(buffer, offset + 0x35, 0x40).Trim('\0'); + + // May be wrong way round... + CommittedSequence = Utilities.ToInt64BigEndian(buffer, offset + 0x75); + PendingSequence = Utilities.ToInt64BigEndian(buffer, offset + 0x7D); + + Unknown2 = Utilities.ToUInt32BigEndian(buffer, offset + 0x85); + Unknown3 = Utilities.ToUInt32BigEndian(buffer, offset + 0x89); + Unknown4 = Utilities.ToUInt32BigEndian(buffer, offset + 0x8D); + Unknown5 = Utilities.ToUInt32BigEndian(buffer, offset + 0x91); + Unknown6 = Utilities.ToInt64BigEndian(buffer, offset + 0x95); + Unknown7 = Utilities.ToInt64BigEndian(buffer, offset + 0x9D); + Unknown8 = Utilities.ToUInt32BigEndian(buffer, offset + 0xA5); + Unknown9 = Utilities.ToUInt32BigEndian(buffer, offset + 0xA9); + UnknownA = Utilities.ToUInt32BigEndian(buffer, offset + 0xAD); + + UnknownB = Utilities.ToInt64BigEndian(buffer, offset + 0xB1); + UnknownC = Utilities.ToUInt32BigEndian(buffer, offset + 0xB9); + + Timestamp = DateTime.FromFileTimeUtc(Utilities.ToInt64BigEndian(buffer, offset + 0xBD)); + } + + ////private static int CalcChecksum() + ////{ + //// // Zero checksum bytes (0x08, 4) + //// // Add all byte values for ?? bytes + //// throw new NotImplementedException(); + ////} + } +} diff --git a/DiscUtils/LogicalDiskManager/DatabaseRecord.cs b/DiscUtils/LogicalDiskManager/DatabaseRecord.cs new file mode 100644 index 0000000..406d5f0 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DatabaseRecord.cs @@ -0,0 +1,157 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + + internal abstract class DatabaseRecord + { + public string Signature; // VBLK + public uint Label; + public uint Counter; + public uint Valid; + public uint Flags; + public RecordType RecordType; + public uint DataLength; + + public ulong Id; + public string Name; + + protected DatabaseRecord() + { + } + + public static DatabaseRecord ReadFrom(byte[] buffer, int offset) + { + DatabaseRecord result = null; + + if (Utilities.ToInt32BigEndian(buffer, offset + 0xC) != 0) + { + switch ((RecordType)(buffer[offset + 0x13] & 0xF)) + { + case RecordType.Volume: + result = new VolumeRecord(); + break; + + case RecordType.Component: + result = new ComponentRecord(); + break; + + case RecordType.Extent: + result = new ExtentRecord(); + break; + + case RecordType.Disk: + result = new DiskRecord(); + break; + + case RecordType.DiskGroup: + result = new DiskGroupRecord(); + break; + + default: + throw new NotImplementedException("Unrecognized record type: " + buffer[offset + 0x13]); + } + + result.DoReadFrom(buffer, offset); + } + + return result; + } + + protected static ulong ReadVarULong(byte[] buffer, ref int offset) + { + int length = buffer[offset]; + + ulong result = 0; + for (int i = 0; i < length; ++i) + { + result = (result << 8) | buffer[offset + i + 1]; + } + + offset += length + 1; + + return result; + } + + protected static long ReadVarLong(byte[] buffer, ref int offset) + { + return (long)ReadVarULong(buffer, ref offset); + } + + protected static string ReadVarString(byte[] buffer, ref int offset) + { + int length = buffer[offset]; + + string result = Utilities.BytesToString(buffer, offset + 1, length); + offset += length + 1; + return result; + } + + protected static byte ReadByte(byte[] buffer, ref int offset) + { + return buffer[offset++]; + } + + protected static uint ReadUInt(byte[] buffer, ref int offset) + { + offset += 4; + return Utilities.ToUInt32BigEndian(buffer, offset - 4); + } + + protected static long ReadLong(byte[] buffer, ref int offset) + { + offset += 8; + return Utilities.ToInt64BigEndian(buffer, offset - 8); + } + + protected static ulong ReadULong(byte[] buffer, ref int offset) + { + offset += 8; + return Utilities.ToUInt64BigEndian(buffer, offset - 8); + } + + protected static string ReadString(byte[] buffer, int len, ref int offset) + { + offset += len; + return Utilities.BytesToString(buffer, offset - len, len); + } + + protected static Guid ReadBinaryGuid(byte[] buffer, ref int offset) + { + offset += 16; + return Utilities.ToGuidBigEndian(buffer, offset - 16); + } + + protected virtual void DoReadFrom(byte[] buffer, int offset) + { + Signature = Utilities.BytesToString(buffer, offset + 0x00, 4); + Label = Utilities.ToUInt32BigEndian(buffer, offset + 0x04); + Counter = Utilities.ToUInt32BigEndian(buffer, offset + 0x08); + Valid = Utilities.ToUInt32BigEndian(buffer, offset + 0x0C); + Flags = Utilities.ToUInt32BigEndian(buffer, offset + 0x10); + RecordType = (RecordType)(Flags & 0xF); + DataLength = Utilities.ToUInt32BigEndian(buffer, 0x14); + } + } +} diff --git a/DiscUtils/LogicalDiskManager/DiskGroupRecord.cs b/DiscUtils/LogicalDiskManager/DiskGroupRecord.cs new file mode 100644 index 0000000..0051bcc --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DiskGroupRecord.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class DiskGroupRecord : DatabaseRecord + { + public string GroupGuidString; + public uint Unknown1; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + if ((Flags & 0xF0) == 0x40) + { + GroupGuidString = ReadBinaryGuid(buffer, ref pos).ToString(); + } + else + { + GroupGuidString = ReadVarString(buffer, ref pos); + } + Unknown1 = ReadUInt(buffer, ref pos); + } + } +} diff --git a/DiscUtils/LogicalDiskManager/DiskRecord.cs b/DiscUtils/LogicalDiskManager/DiskRecord.cs new file mode 100644 index 0000000..5972935 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DiskRecord.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class DiskRecord : DatabaseRecord + { + public string DiskGuidString; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + if ((Flags & 0xF0) == 0x40) + { + DiskGuidString = ReadBinaryGuid(buffer, ref pos).ToString(); + } + else + { + DiskGuidString = ReadVarString(buffer, ref pos); + } + } + } +} diff --git a/DiscUtils/LogicalDiskManager/DynamicDisk.cs b/DiscUtils/LogicalDiskManager/DynamicDisk.cs new file mode 100644 index 0000000..4c825aa --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DynamicDisk.cs @@ -0,0 +1,146 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + using System.IO; + using DiscUtils.Partitions; + + internal class DynamicDisk : IDiagnosticTraceable + { + private VirtualDisk _disk; + private PrivateHeader _header; + private Database _database; + + internal DynamicDisk(VirtualDisk disk) + { + _disk = disk; + _header = GetPrivateHeader(_disk); + + TocBlock toc = GetTableOfContents(); + + long dbStart = (_header.ConfigurationStartLba * 512) + (toc.Item1Start * 512); + _disk.Content.Position = dbStart; + _database = new Database(_disk.Content); + } + + public SparseStream Content + { + get { return _disk.Content; } + } + + public long DataOffset + { + get { return _header.DataStartLba; } + } + + public Guid Id + { + get { return new Guid(_header.DiskId); } + } + + public Guid GroupId + { + get { return string.IsNullOrEmpty(_header.DiskGroupId) ? Guid.Empty : new Guid(_header.DiskGroupId); } + } + + public Database Database + { + get { return _database; } + } + + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + "DISK (" + _header.DiskId + ")"); + writer.WriteLine(linePrefix + " Metadata Version: " + ((_header.Version >> 16) & 0xFFFF) + "." + (_header.Version & 0xFFFF)); + writer.WriteLine(linePrefix + " Timestamp: " + _header.Timestamp); + writer.WriteLine(linePrefix + " Disk Id: " + _header.DiskId); + writer.WriteLine(linePrefix + " Host Id: " + _header.HostId); + writer.WriteLine(linePrefix + " Disk Group Id: " + _header.DiskGroupId); + writer.WriteLine(linePrefix + " Disk Group Name: " + _header.DiskGroupName); + writer.WriteLine(linePrefix + " Data Start: " + _header.DataStartLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Data Size: " + _header.DataSizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Configuration Start: " + _header.ConfigurationStartLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Configuration Size: " + _header.ConfigurationSizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " TOC Size: " + _header.TocSizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Next TOC: " + _header.NextTocLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Number of Configs: " + _header.NumberOfConfigs); + writer.WriteLine(linePrefix + " Config Size: " + _header.ConfigurationSizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Number of Logs: " + _header.NumberOfLogs); + writer.WriteLine(linePrefix + " Log Size: " + _header.LogSizeLba + " (Sectors)"); + } + + internal static PrivateHeader GetPrivateHeader(VirtualDisk disk) + { + if (disk.IsPartitioned) + { + long headerPos = 0; + PartitionTable pt = disk.Partitions; + if (pt is BiosPartitionTable) + { + headerPos = 0xc00; + } + else + { + foreach (var part in pt.Partitions) + { + if (part.GuidType == GuidPartitionTypes.WindowsLdmMetadata) + { + headerPos = part.LastSector * Sizes.Sector; + } + } + } + + if (headerPos != 0) + { + disk.Content.Position = headerPos; + byte[] buffer = new byte[Sizes.Sector]; + disk.Content.Read(buffer, 0, buffer.Length); + + PrivateHeader hdr = new PrivateHeader(); + hdr.ReadFrom(buffer, 0); + return hdr; + } + } + + return null; + } + + private TocBlock GetTableOfContents() + { + byte[] buffer = new byte[_header.TocSizeLba * 512]; + _disk.Content.Position = (_header.ConfigurationStartLba * 512) + (1 * _header.TocSizeLba * 512); + + _disk.Content.Read(buffer, 0, buffer.Length); + TocBlock tocBlock = new TocBlock(); + tocBlock.ReadFrom(buffer, 0); + + if (tocBlock.Signature == "TOCBLOCK") + { + return tocBlock; + } + + return null; + } + } +} diff --git a/DiscUtils/LogicalDiskManager/DynamicDiskGroup.cs b/DiscUtils/LogicalDiskManager/DynamicDiskGroup.cs new file mode 100644 index 0000000..a9b52e7 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DynamicDiskGroup.cs @@ -0,0 +1,319 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using DiscUtils.Partitions; + + internal class DynamicDiskGroup : IDiagnosticTraceable + { + private DiskGroupRecord _record; + private Dictionary _disks; + private Database _database; + + internal DynamicDiskGroup(VirtualDisk disk) + { + _disks = new Dictionary(); + + DynamicDisk dynDisk = new DynamicDisk(disk); + _database = dynDisk.Database; + _disks.Add(dynDisk.Id, dynDisk); + _record = dynDisk.Database.GetDiskGroup(dynDisk.GroupId); + } + + public void Add(VirtualDisk disk) + { + DynamicDisk dynDisk = new DynamicDisk(disk); + _disks.Add(dynDisk.Id, dynDisk); + } + + #region IDiagnosticTraceable Members + + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + "DISK GROUP (" + _record.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + _record.Name); + writer.WriteLine(linePrefix + " Flags: 0x" + (_record.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + _record.Id); + writer.WriteLine(linePrefix + " Guid: " + _record.GroupGuidString); + writer.WriteLine(); + + writer.WriteLine(linePrefix + " DISKS"); + foreach (var disk in _database.Disks) + { + writer.WriteLine(linePrefix + " DISK (" + disk.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + disk.Name); + writer.WriteLine(linePrefix + " Flags: 0x" + (disk.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + disk.Id); + writer.WriteLine(linePrefix + " Guid: " + disk.DiskGuidString); + + DynamicDisk dynDisk; + if (_disks.TryGetValue(new Guid(disk.DiskGuidString), out dynDisk)) + { + writer.WriteLine(linePrefix + " PRIVATE HEADER"); + dynDisk.Dump(writer, linePrefix + " "); + } + } + + writer.WriteLine(linePrefix + " VOLUMES"); + foreach (var vol in _database.Volumes) + { + writer.WriteLine(linePrefix + " VOLUME (" + vol.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + vol.Name); + writer.WriteLine(linePrefix + " BIOS Type: " + vol.BiosType.ToString("X2", CultureInfo.InvariantCulture) + " [" + BiosPartitionTypes.ToString(vol.BiosType) + "]"); + writer.WriteLine(linePrefix + " Flags: 0x" + (vol.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + vol.Id); + writer.WriteLine(linePrefix + " Guid: " + vol.VolumeGuid); + writer.WriteLine(linePrefix + " State: " + vol.ActiveString); + writer.WriteLine(linePrefix + " Drive Hint: " + vol.MountHint); + writer.WriteLine(linePrefix + " Num Components: " + vol.ComponentCount); + writer.WriteLine(linePrefix + " Link Id: " + vol.PartitionComponentLink); + + writer.WriteLine(linePrefix + " COMPONENTS"); + foreach (var cmpnt in _database.GetVolumeComponents(vol.Id)) + { + writer.WriteLine(linePrefix + " COMPONENT (" + cmpnt.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + cmpnt.Name); + writer.WriteLine(linePrefix + " Flags: 0x" + (cmpnt.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + cmpnt.Id); + writer.WriteLine(linePrefix + " State: " + cmpnt.StatusString); + writer.WriteLine(linePrefix + " Mode: " + cmpnt.MergeType); + writer.WriteLine(linePrefix + " Num Extents: " + cmpnt.NumExtents); + writer.WriteLine(linePrefix + " Link Id: " + cmpnt.LinkId); + writer.WriteLine(linePrefix + " Stripe Size: " + cmpnt.StripeSizeSectors + " (Sectors)"); + writer.WriteLine(linePrefix + " Stripe Stride: " + cmpnt.StripeStride); + + writer.WriteLine(linePrefix + " EXTENTS"); + foreach (var extent in _database.GetComponentExtents(cmpnt.Id)) + { + writer.WriteLine(linePrefix + " EXTENT (" + extent.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + extent.Name); + writer.WriteLine(linePrefix + " Flags: 0x" + (extent.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + extent.Id); + writer.WriteLine(linePrefix + " Disk Offset: " + extent.DiskOffsetLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Volume Offset: " + extent.OffsetInVolumeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Size: " + extent.SizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Component Id: " + extent.ComponentId); + writer.WriteLine(linePrefix + " Disk Id: " + extent.DiskId); + writer.WriteLine(linePrefix + " Link Id: " + extent.PartitionComponentLink); + writer.WriteLine(linePrefix + " Interleave Order: " + extent.InterleaveOrder); + } + } + } + } + #endregion + + internal DynamicVolume[] GetVolumes() + { + List vols = new List(); + foreach (var record in _database.GetVolumes()) + { + vols.Add(new DynamicVolume(this, record.VolumeGuid)); + } + + return vols.ToArray(); + } + + internal VolumeRecord GetVolume(Guid volume) + { + return _database.GetVolume(volume); + } + + internal LogicalVolumeStatus GetVolumeStatus(ulong volumeId) + { + return GetVolumeStatus(_database.GetVolume(volumeId)); + } + + internal SparseStream OpenVolume(ulong volumeId) + { + return OpenVolume(_database.GetVolume(volumeId)); + } + + private static int CompareExtentOffsets(ExtentRecord x, ExtentRecord y) + { + if (x.OffsetInVolumeLba > y.OffsetInVolumeLba) + { + return 1; + } + else if (x.OffsetInVolumeLba < y.OffsetInVolumeLba) + { + return -1; + } + + return 0; + } + + private static int CompareExtentInterleaveOrder(ExtentRecord x, ExtentRecord y) + { + if (x.InterleaveOrder > y.InterleaveOrder) + { + return 1; + } + else if (x.InterleaveOrder < y.InterleaveOrder) + { + return -1; + } + + return 0; + } + + private static LogicalVolumeStatus WorstOf(LogicalVolumeStatus x, LogicalVolumeStatus y) + { + return (LogicalVolumeStatus)Math.Max((int)x, (int)y); + } + + private LogicalVolumeStatus GetVolumeStatus(VolumeRecord volume) + { + int numFailed = 0; + ulong numOK = 0; + LogicalVolumeStatus worst = LogicalVolumeStatus.Healthy; + foreach (var cmpnt in _database.GetVolumeComponents(volume.Id)) + { + LogicalVolumeStatus cmpntStatus = GetComponentStatus(cmpnt); + worst = WorstOf(worst, cmpntStatus); + if (cmpntStatus == LogicalVolumeStatus.Failed) + { + numFailed++; + } + else + { + numOK++; + } + } + + if (numOK < 1) + { + return LogicalVolumeStatus.Failed; + } + else if (numOK == volume.ComponentCount) + { + return worst; + } + else + { + return LogicalVolumeStatus.FailedRedundancy; + } + } + + private LogicalVolumeStatus GetComponentStatus(ComponentRecord cmpnt) + { + // NOTE: no support for RAID, so either valid or failed... + LogicalVolumeStatus status = LogicalVolumeStatus.Healthy; + + foreach (var extent in _database.GetComponentExtents(cmpnt.Id)) + { + DiskRecord disk = _database.GetDisk(extent.DiskId); + if (!_disks.ContainsKey(new Guid(disk.DiskGuidString))) + { + status = LogicalVolumeStatus.Failed; + break; + } + } + + return status; + } + + private SparseStream OpenExtent(ExtentRecord extent) + { + DiskRecord disk = _database.GetDisk(extent.DiskId); + + DynamicDisk diskObj = _disks[new Guid(disk.DiskGuidString)]; + + return new SubStream(diskObj.Content, Ownership.None, (diskObj.DataOffset + extent.DiskOffsetLba) * Sizes.Sector, extent.SizeLba * Sizes.Sector); + } + + private SparseStream OpenComponent(ComponentRecord component) + { + if (component.MergeType == ExtentMergeType.Concatenated) + { + List extents = new List(_database.GetComponentExtents(component.Id)); + extents.Sort(CompareExtentOffsets); + + // Sanity Check... + long pos = 0; + foreach (var extent in extents) + { + if (extent.OffsetInVolumeLba != pos) + { + throw new IOException("Volume extents are non-contiguous"); + } + + pos += extent.SizeLba; + } + + List streams = new List(); + foreach (var extent in extents) + { + streams.Add(OpenExtent(extent)); + } + + return new ConcatStream(Ownership.Dispose, streams.ToArray()); + } + else if (component.MergeType == ExtentMergeType.Interleaved) + { + List extents = new List(_database.GetComponentExtents(component.Id)); + extents.Sort(CompareExtentInterleaveOrder); + + List streams = new List(); + foreach (var extent in extents) + { + streams.Add(OpenExtent(extent)); + } + + return new StripedStream(component.StripeSizeSectors * Sizes.Sector, Ownership.Dispose, streams.ToArray()); + } + else + { + throw new NotImplementedException("Unknown component mode: " + component.MergeType); + } + } + + private SparseStream OpenVolume(VolumeRecord volume) + { + List cmpntStreams = new List(); + foreach (var component in _database.GetVolumeComponents(volume.Id)) + { + if (GetComponentStatus(component) == LogicalVolumeStatus.Healthy) + { + cmpntStreams.Add(OpenComponent(component)); + } + } + + if (cmpntStreams.Count < 1) + { + throw new IOException("Volume with no associated or healthy components"); + } + else if (cmpntStreams.Count == 1) + { + return cmpntStreams[0]; + } + else + { + return new MirrorStream(Ownership.Dispose, cmpntStreams.ToArray()); + } + } + } +} diff --git a/DiscUtils/LogicalDiskManager/DynamicDiskManager.cs b/DiscUtils/LogicalDiskManager/DynamicDiskManager.cs new file mode 100644 index 0000000..43bec6d --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DynamicDiskManager.cs @@ -0,0 +1,153 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System.Collections.Generic; + using System.IO; + using DiscUtils.Partitions; + + /// + /// A class that understands Windows LDM structures, mapping physical volumes to logical volumes. + /// + public class DynamicDiskManager : IDiagnosticTraceable + { + private Dictionary _groups; + + /// + /// Initializes a new instance of the DynamicDiskManager class. + /// + /// The initial set of disks to manage. + public DynamicDiskManager(params VirtualDisk[] disks) + { + _groups = new Dictionary(); + + foreach (var disk in disks) + { + Add(disk); + } + } + + /// + /// Determines if a physical volume contains LDM data. + /// + /// The volume to inspect. + /// true if the physical volume contains LDM data, else false. + public static bool HandlesPhysicalVolume(PhysicalVolumeInfo volumeInfo) + { + PartitionInfo pi = volumeInfo.Partition; + if (pi != null) + { + return IsLdmPartition(pi); + } + + return false; + } + + /// + /// Determines if a disk is 'dynamic' (i.e. contains LDM volumes). + /// + /// The disk to inspect. + /// true if the disk contains LDM volumes, else false. + public static bool IsDynamicDisk(VirtualDisk disk) + { + if (disk.IsPartitioned) + { + foreach (var partition in disk.Partitions.Partitions) + { + if (IsLdmPartition(partition)) + { + return true; + } + } + } + + return false; + } + + /// + /// Adds a new disk to be managed. + /// + /// The disk to manage. + public void Add(VirtualDisk disk) + { + PrivateHeader header = DynamicDisk.GetPrivateHeader(disk); + + DynamicDiskGroup group; + if (_groups.TryGetValue(header.DiskGroupId, out group)) + { + group.Add(disk); + } + else + { + group = new DynamicDiskGroup(disk); + _groups.Add(header.DiskGroupId, group); + } + } + + /// + /// Gets the logical volumes held across the set of managed disks. + /// + /// An array of logical volumes. + public LogicalVolumeInfo[] GetLogicalVolumes() + { + List result = new List(); + foreach (var group in _groups.Values) + { + foreach (var volume in group.GetVolumes()) + { + LogicalVolumeInfo lvi = new LogicalVolumeInfo( + volume.Identity, + null, + volume.Open, + volume.Length, + volume.BiosType, + volume.Status); + result.Add(lvi); + } + } + + return result.ToArray(); + } + + /// + /// Writes a diagnostic report about the state of the disk manager. + /// + /// The writer to send the report to. + /// The prefix to place at the start of each line. + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + "DISK GROUPS"); + foreach (var group in _groups.Values) + { + group.Dump(writer, linePrefix + " "); + } + } + + private static bool IsLdmPartition(PartitionInfo partition) + { + return partition.BiosType == BiosPartitionTypes.WindowsDynamicVolume + || partition.GuidType == GuidPartitionTypes.WindowsLdmMetadata + || partition.GuidType == GuidPartitionTypes.WindowsLdmData; + } + } +} diff --git a/DiscUtils/LogicalDiskManager/DynamicDiskManagerFactory.cs b/DiscUtils/LogicalDiskManager/DynamicDiskManagerFactory.cs new file mode 100644 index 0000000..eafc1d9 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DynamicDiskManagerFactory.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System.Collections.Generic; + + [LogicalVolumeFactory] + internal class DynamicDiskManagerFactory : LogicalVolumeFactory + { + public override bool HandlesPhysicalVolume(PhysicalVolumeInfo volume) + { + return DynamicDiskManager.HandlesPhysicalVolume(volume); + } + + public override void MapDisks(IEnumerable disks, Dictionary result) + { + DynamicDiskManager mgr = new DynamicDiskManager(); + + foreach (var disk in disks) + { + if (DynamicDiskManager.IsDynamicDisk(disk)) + { + mgr.Add(disk); + } + } + + foreach (var vol in mgr.GetLogicalVolumes()) + { + result.Add(vol.Identity, vol); + } + } + } +} diff --git a/DiscUtils/LogicalDiskManager/DynamicVolume.cs b/DiscUtils/LogicalDiskManager/DynamicVolume.cs new file mode 100644 index 0000000..12462e3 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/DynamicVolume.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + using System.IO; + + internal class DynamicVolume + { + private DynamicDiskGroup _group; + private Guid _volumeId; + + internal DynamicVolume(DynamicDiskGroup group, Guid volumeId) + { + _group = group; + _volumeId = volumeId; + } + + public byte BiosType + { + get { return Record.BiosType; } + } + + public Guid Identity + { + get { return _volumeId; } + } + + public long Length + { + get { return Record.Size * Sizes.Sector; } + } + + public LogicalVolumeStatus Status + { + get { return _group.GetVolumeStatus(Record.Id); } + } + + private VolumeRecord Record + { + get { return _group.GetVolume(_volumeId); } + } + + public SparseStream Open() + { + if (Status == LogicalVolumeStatus.Failed) + { + throw new IOException("Attempt to open 'failed' volume"); + } + else + { + return _group.OpenVolume(Record.Id); + } + } + } +} diff --git a/DiscUtils/LogicalDiskManager/ExtentMergeType.cs b/DiscUtils/LogicalDiskManager/ExtentMergeType.cs new file mode 100644 index 0000000..e4fe308 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/ExtentMergeType.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal enum ExtentMergeType : byte + { + None = 0, + Interleaved = 1, + Concatenated = 2 + } +} diff --git a/DiscUtils/LogicalDiskManager/ExtentRecord.cs b/DiscUtils/LogicalDiskManager/ExtentRecord.cs new file mode 100644 index 0000000..ed53760 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/ExtentRecord.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class ExtentRecord : DatabaseRecord + { + public uint Unknown1; + public uint Unknown2; + public uint PartitionComponentLink; + public long DiskOffsetLba; + public long OffsetInVolumeLba; + public long SizeLba; + public ulong ComponentId; + public ulong DiskId; + public ulong InterleaveOrder; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + Unknown1 = ReadUInt(buffer, ref pos); + Unknown2 = ReadUInt(buffer, ref pos); + PartitionComponentLink = ReadUInt(buffer, ref pos); + DiskOffsetLba = ReadLong(buffer, ref pos); + OffsetInVolumeLba = ReadLong(buffer, ref pos); + SizeLba = ReadVarLong(buffer, ref pos); + ComponentId = ReadVarULong(buffer, ref pos); + DiskId = ReadVarULong(buffer, ref pos); + + if ((Flags & 0x0800) != 0) + { + InterleaveOrder = ReadVarULong(buffer, ref pos); + } + } + } +} diff --git a/DiscUtils/LogicalDiskManager/PrivateHeader.cs b/DiscUtils/LogicalDiskManager/PrivateHeader.cs new file mode 100644 index 0000000..bc4bcb8 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/PrivateHeader.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + + internal class PrivateHeader + { + public string Signature; // PRIVHEAD + public uint Checksum; // 00 00 2f 96 + public uint Version; // 2.12 + public DateTime Timestamp; + public long Unknown2; // Active TOC? 00 .. 00 01 + public long Unknown3; // 00 .. 07 ff // 1 sector less than 2MB + public long Unknown4; // 00 .. 07 40 + public string DiskId; // GUID string + public string HostId; // GUID string + public string DiskGroupId; // GUID string + public string DiskGroupName; // MAX_COMPUTER_NAME_LENGTH? + public uint Unknown5; // Sector Size? + public long DataStartLba; // 3F + public long DataSizeLba; // 03 FF F7 C1 + public long ConfigurationStartLba; // 03 FF F8 00 + public long ConfigurationSizeLba; // 08 00 + public long TocSizeLba; + public long NextTocLba; + public long NumberOfConfigs; + public long ConfigSizeLba; + public long NumberOfLogs; + public long LogSizeLba; + + public void ReadFrom(byte[] buffer, int offset) + { + Signature = Utilities.BytesToString(buffer, offset + 0x00, 8); + Checksum = Utilities.ToUInt32BigEndian(buffer, offset + 0x08); + Version = Utilities.ToUInt32BigEndian(buffer, offset + 0x0C); + Timestamp = DateTime.FromFileTimeUtc(Utilities.ToInt64BigEndian(buffer, offset + 0x10)); + Unknown2 = Utilities.ToInt64BigEndian(buffer, offset + 0x18); + Unknown3 = Utilities.ToInt64BigEndian(buffer, offset + 0x20); + Unknown4 = Utilities.ToInt64BigEndian(buffer, offset + 0x28); + DiskId = Utilities.BytesToString(buffer, offset + 0x30, 0x40).Trim('\0'); + HostId = Utilities.BytesToString(buffer, offset + 0x70, 0x40).Trim('\0'); + DiskGroupId = Utilities.BytesToString(buffer, offset + 0xB0, 0x40).Trim('\0'); + DiskGroupName = Utilities.BytesToString(buffer, offset + 0xF0, 31).Trim('\0'); + Unknown5 = Utilities.ToUInt32BigEndian(buffer, offset + 0x10F); + DataStartLba = Utilities.ToInt64BigEndian(buffer, offset + 0x11B); + DataSizeLba = Utilities.ToInt64BigEndian(buffer, offset + 0x123); + ConfigurationStartLba = Utilities.ToInt64BigEndian(buffer, offset + 0x12B); + ConfigurationSizeLba = Utilities.ToInt64BigEndian(buffer, offset + 0x133); + TocSizeLba = Utilities.ToInt64BigEndian(buffer, offset + 0x13B); + NextTocLba = Utilities.ToInt64BigEndian(buffer, offset + 0x143); + + // These two may be reversed + NumberOfConfigs = Utilities.ToInt32BigEndian(buffer, offset + 0x14B); + NumberOfLogs = Utilities.ToInt32BigEndian(buffer, offset + 0x14F); + + ConfigSizeLba = Utilities.ToInt64BigEndian(buffer, offset + 0x153); + LogSizeLba = Utilities.ToInt64BigEndian(buffer, offset + 0x15B); + } + + ////private static int CalcChecksum() + ////{ + //// // Zero checksum bytes (0x08, 4) + //// // Add all byte values for 512 bytes + //// throw new NotImplementedException(); + ////} + } +} diff --git a/DiscUtils/LogicalDiskManager/RecordType.cs b/DiscUtils/LogicalDiskManager/RecordType.cs new file mode 100644 index 0000000..af10ad5 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/RecordType.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal enum RecordType : byte + { + None = 0, + Volume = 1, + Component = 2, + Extent = 3, + Disk = 4, + DiskGroup = 5 + } +} diff --git a/DiscUtils/LogicalDiskManager/TocBlock.cs b/DiscUtils/LogicalDiskManager/TocBlock.cs new file mode 100644 index 0000000..9b9904f --- /dev/null +++ b/DiscUtils/LogicalDiskManager/TocBlock.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + + internal class TocBlock + { + public string Signature; // TOCBLOCK + public uint Checksum; // 00 00 08 B6 + public long SequenceNumber; // 00 .. 01 + public long Unknown1; // 0 + public long Unknown2; // 00 + public string Item1Str; // 'config', length 10 + public long Item1Start; // Sector Offset from ConfigurationStart + public long Item1Size; // Unit? + public uint Unknown3; // 00 06 00 01 (may be two values?) + public uint Unknown4; // 00 00 00 00 + public string Item2Str; // 'log', length 10 + public long Item2Start; // Sector Offset from ConfigurationStart + public long Item2Size; // Unit? + public uint Unknown5; // 00 06 00 01 (may be two values?) + public uint Unknown6; // 00 00 00 00 + + public void ReadFrom(byte[] buffer, int offset) + { + Signature = Utilities.BytesToString(buffer, offset + 0x00, 8); + Checksum = Utilities.ToUInt32BigEndian(buffer, offset + 0x08); + SequenceNumber = Utilities.ToInt64BigEndian(buffer, offset + 0x0C); + Unknown1 = Utilities.ToInt64BigEndian(buffer, offset + 0x14); + Unknown2 = Utilities.ToInt64BigEndian(buffer, offset + 0x1C); + Item1Str = Utilities.BytesToString(buffer, offset + 0x24, 10).Trim('\0'); + Item1Start = Utilities.ToInt64BigEndian(buffer, offset + 0x2E); + Item1Size = Utilities.ToInt64BigEndian(buffer, offset + 0x36); + Unknown3 = Utilities.ToUInt32BigEndian(buffer, offset + 0x3E); + Unknown4 = Utilities.ToUInt32BigEndian(buffer, offset + 0x42); + Item2Str = Utilities.BytesToString(buffer, offset + 0x46, 10).Trim('\0'); + Item2Start = Utilities.ToInt64BigEndian(buffer, offset + 0x50); + Item2Size = Utilities.ToInt64BigEndian(buffer, offset + 0x58); + Unknown5 = Utilities.ToUInt32BigEndian(buffer, offset + 0x60); + Unknown6 = Utilities.ToUInt32BigEndian(buffer, offset + 0x64); + } + + ////private static int CalcChecksum() + ////{ + //// // Zero checksum bytes (0x08, 4) + //// // Add all byte values for ?? bytes + //// throw new NotImplementedException(); + ////} + } +} diff --git a/DiscUtils/LogicalDiskManager/VolumeRecord.cs b/DiscUtils/LogicalDiskManager/VolumeRecord.cs new file mode 100644 index 0000000..c035024 --- /dev/null +++ b/DiscUtils/LogicalDiskManager/VolumeRecord.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.LogicalDiskManager +{ + using System; + + internal sealed class VolumeRecord : DatabaseRecord + { + public string GenString; + public string NumberString; // 8000000000000000 sometimes... + public string ActiveString; + public ulong UnknownA; // Zero + public ulong UnknownB; // 00 .. 03 + public ulong DupCount; // ??Seen once after adding 'foreign disk', from broken mirror (identical links(P/V/C)) + public uint UnknownC; // 00 00 00 11 + public ulong ComponentCount; + public uint UnknownD; // Zero + public uint PartitionComponentLink; + public ulong Unknown1; // Zero + public long Size; + public uint Unknown2; // Zero + public byte BiosType; + public Guid VolumeGuid; + public string MountHint; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + GenString = ReadVarString(buffer, ref pos); + NumberString = ReadVarString(buffer, ref pos); + ActiveString = ReadString(buffer, 6, ref pos); + UnknownA = ReadVarULong(buffer, ref pos); + UnknownB = ReadULong(buffer, ref pos); + DupCount = ReadVarULong(buffer, ref pos); + UnknownC = ReadUInt(buffer, ref pos); + ComponentCount = ReadVarULong(buffer, ref pos); + UnknownD = ReadUInt(buffer, ref pos); + PartitionComponentLink = ReadUInt(buffer, ref pos); + Unknown1 = ReadULong(buffer, ref pos); + Size = ReadVarLong(buffer, ref pos); + Unknown2 = ReadUInt(buffer, ref pos); + BiosType = ReadByte(buffer, ref pos); + VolumeGuid = Utilities.ToGuidBigEndian(buffer, pos); + pos += 16; + + if ((Flags & 0x0200) != 0) + { + MountHint = ReadVarString(buffer, ref pos); + } + } + } +} diff --git a/DiscUtils/LogicalVolumeFactory.cs b/DiscUtils/LogicalVolumeFactory.cs new file mode 100644 index 0000000..b426678 --- /dev/null +++ b/DiscUtils/LogicalVolumeFactory.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Collections.Generic; + + internal abstract class LogicalVolumeFactory + { + public abstract bool HandlesPhysicalVolume(PhysicalVolumeInfo volume); + + public abstract void MapDisks(IEnumerable disks, Dictionary result); + } +} diff --git a/DiscUtils/LogicalVolumeFactoryAttribute.cs b/DiscUtils/LogicalVolumeFactoryAttribute.cs new file mode 100644 index 0000000..71b4870 --- /dev/null +++ b/DiscUtils/LogicalVolumeFactoryAttribute.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + internal sealed class LogicalVolumeFactoryAttribute : Attribute + { + } +} diff --git a/DiscUtils/LogicalVolumeInfo.cs b/DiscUtils/LogicalVolumeInfo.cs new file mode 100644 index 0000000..726e343 --- /dev/null +++ b/DiscUtils/LogicalVolumeInfo.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + /// + /// Enumeration of the health status of a logical volume. + /// + public enum LogicalVolumeStatus + { + /// + /// The volume is healthy and fully functional. + /// + Healthy = 0, + + /// + /// The volume is completely accessible, but at degraded redundancy. + /// + FailedRedundancy = 1, + + /// + /// The volume is wholey, or partly, inaccessible. + /// + Failed = 2 + } + + /// + /// Information about a logical disk volume, which may be backed by one or more physical volumes. + /// + public sealed class LogicalVolumeInfo : VolumeInfo + { + private Guid _guid; + private PhysicalVolumeInfo _physicalVol; + private SparseStreamOpenDelegate _opener; + private long _length; + private LogicalVolumeStatus _status; + private byte _biosType; + + internal LogicalVolumeInfo(Guid guid, PhysicalVolumeInfo physicalVolume, SparseStreamOpenDelegate opener, long length, byte biosType, LogicalVolumeStatus status) + { + _guid = guid; + _physicalVol = physicalVolume; + _opener = opener; + _length = length; + _biosType = biosType; + _status = status; + } + + /// + /// Gets the one-byte BIOS type for this volume, which indicates the content. + /// + public override byte BiosType + { + get { return _biosType; } + } + + /// + /// Gets the status of the logical volume, indicating volume health. + /// + public LogicalVolumeStatus Status + { + get { return _status; } + } + + /// + /// Gets the length of the volume (in bytes). + /// + public override long Length + { + get { return _length; } + } + + /// + /// The stable identity for this logical volume. + /// + /// The stability of the identity depends the disk structure. + /// In some cases the identity may include a simple index, when no other information + /// is available. Best practice is to add disks to the Volume Manager in a stable + /// order, if the stability of this identity is paramount. + public override string Identity + { + get + { + if (_guid != Guid.Empty) + { + return "VLG" + _guid.ToString("B"); + } + else + { + return "VLP:" + _physicalVol.Identity; + } + } + } + + /// + /// Gets the disk geometry of the underlying storage medium, if any (may be Geometry.Null). + /// + public override Geometry PhysicalGeometry + { + get { return (_physicalVol == null) ? Geometry.Null : _physicalVol.PhysicalGeometry; } + } + + /// + /// Gets the disk geometry of the underlying storage medium (as used in BIOS calls), may be null. + /// + public override Geometry BiosGeometry + { + get { return (_physicalVol == null) ? Geometry.Null : _physicalVol.BiosGeometry; } + } + + /// + /// Gets the offset of this volume in the underlying storage medium, if any (may be Zero). + /// + public override long PhysicalStartSector + { + get { return (_physicalVol == null) ? 0 : _physicalVol.PhysicalStartSector; } + } + + /// + /// Opens a stream with access to the content of the logical volume. + /// + /// The volume's content as a stream. + public override SparseStream Open() + { + return _opener(); + } + } +} diff --git a/DiscUtils/MappedStream.cs b/DiscUtils/MappedStream.cs new file mode 100644 index 0000000..fd170fb --- /dev/null +++ b/DiscUtils/MappedStream.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Base class for streams that are essentially a mapping onto a parent stream. + /// + /// + /// This class provides access to the mapping underlying the stream, enabling + /// callers to convert a byte range in this stream into one or more ranges in + /// the parent stream. + /// + public abstract class MappedStream : SparseStream + { + /// + /// Converts any stream into a non-linear stream. + /// + /// The stream to convert. + /// true to have the new stream dispose the wrapped + /// stream when it is disposed. + /// A sparse stream. + /// The wrapped stream is assumed to be a linear stream (such that any byte range + /// maps directly onto the parent stream). + public static new MappedStream FromStream(Stream stream, Ownership takeOwnership) + { + return new WrappingMappedStream(stream, takeOwnership, null); + } + + /// + /// Converts any stream into a non-linear stream. + /// + /// The stream to convert. + /// true to have the new stream dispose the wrapped + /// stream when it is disposed. + /// The set of extents actually stored in stream. + /// A sparse stream. + /// The wrapped stream is assumed to be a linear stream (such that any byte range + /// maps directly onto the parent stream). + public static new MappedStream FromStream(Stream stream, Ownership takeOwnership, IEnumerable extents) + { + return new WrappingMappedStream(stream, takeOwnership, extents); + } + + /// + /// Maps a logical range down to storage locations. + /// + /// The first logical range to map. + /// The length of the range to map. + /// One or more stream extents specifying the storage locations that correspond + /// to the identified logical extent range. + /// + /// As far as possible, the stream extents are returned in logical disk order - + /// however, due to the nature of non-linear streams, not all of the range may actually + /// be stored, or some or all of the range may be compressed - thus reading the + /// returned stream extents is not equivalent to reading the logical disk range. + /// + public abstract IEnumerable MapContent(long start, long length); + } +} diff --git a/DiscUtils/MirrorStream.cs b/DiscUtils/MirrorStream.cs new file mode 100644 index 0000000..a845e28 --- /dev/null +++ b/DiscUtils/MirrorStream.cs @@ -0,0 +1,175 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class MirrorStream : SparseStream + { + private List _wrapped; + private Ownership _ownsWrapped; + private bool _canRead; + private bool _canWrite; + private bool _canSeek; + private long _length; + + public MirrorStream(Ownership ownsWrapped, params SparseStream[] wrapped) + { + _wrapped = new List(wrapped); + _ownsWrapped = ownsWrapped; + + _canRead = _wrapped[0].CanRead; + _canWrite = _wrapped[0].CanWrite; + _canSeek = _wrapped[0].CanSeek; + _length = _wrapped[0].Length; + + foreach (var stream in _wrapped) + { + if (stream.CanRead != _canRead || stream.CanWrite != _canWrite || stream.CanSeek != _canSeek) + { + throw new ArgumentException("All mirrored streams must have the same read/write/seek permissions", "wrapped"); + } + + if (stream.Length != _length) + { + throw new ArgumentException("All mirrored streams must have the same length", "wrapped"); + } + } + } + + public override bool CanRead + { + get { return _canRead; } + } + + public override bool CanSeek + { + get { return _canSeek; } + } + + public override bool CanWrite + { + get { return _canWrite; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get + { + return _wrapped[0].Position; + } + + set + { + _wrapped[0].Position = value; + } + } + + public override IEnumerable Extents + { + get { return _wrapped[0].Extents; } + } + + public override void Flush() + { + _wrapped[0].Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped[0].Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped[0].Seek(offset, origin); + } + + public override void SetLength(long value) + { + if (value != _length) + { + throw new InvalidOperationException("Changing the stream length is not permitted for mirrored streams"); + } + } + + public override void Clear(int count) + { + long pos = _wrapped[0].Position; + + if (pos + count > _length) + { + throw new IOException("Attempt to clear beyond end of mirrored stream"); + } + + foreach (var stream in _wrapped) + { + stream.Position = pos; + stream.Clear(count); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + long pos = _wrapped[0].Position; + + if (pos + count > _length) + { + throw new IOException("Attempt to write beyond end of mirrored stream"); + } + + foreach (var stream in _wrapped) + { + stream.Position = pos; + stream.Write(buffer, offset, count); + } + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsWrapped == Ownership.Dispose && _wrapped != null) + { + foreach (var stream in _wrapped) + { + stream.Dispose(); + } + + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} diff --git a/DiscUtils/Ntfs/AttributeDefinitionRecord.cs b/DiscUtils/Ntfs/AttributeDefinitionRecord.cs new file mode 100644 index 0000000..1899619 --- /dev/null +++ b/DiscUtils/Ntfs/AttributeDefinitionRecord.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Text; + + internal enum AttributeCollationRule : int + { + Binary = 0x00000000, + Filename = 0x00000001, + UnicodeString = 0x00000002, + UnsignedLong = 0x00000010, + Sid = 0x00000011, + SecurityHash = 0x00000012, + MultipleUnsignedLongs = 0x00000013 + } + + [Flags] + internal enum AttributeTypeFlags : int + { + None = 0x00, + Indexed = 0x02, + Multiple = 0x04, + NotZero = 0x08, + IndexedUnique = 0x10, + NamedUnique = 0x20, + MustBeResident = 0x40, + CanBeNonResident = 0x80 + } + + internal sealed class AttributeDefinitionRecord + { + public const int Size = 0xA0; + + public string Name; + public AttributeType Type; + public uint DisplayRule; + public AttributeCollationRule CollationRule; + public AttributeTypeFlags Flags; + public long MinSize; + public long MaxSize; + + internal void Read(byte[] buffer, int offset) + { + Name = Encoding.Unicode.GetString(buffer, offset + 0, 128).Trim('\0'); + Type = (AttributeType)Utilities.ToUInt32LittleEndian(buffer, offset + 0x80); + DisplayRule = Utilities.ToUInt32LittleEndian(buffer, offset + 0x84); + CollationRule = (AttributeCollationRule)Utilities.ToUInt32LittleEndian(buffer, offset + 0x88); + Flags = (AttributeTypeFlags)Utilities.ToUInt32LittleEndian(buffer, offset + 0x8C); + MinSize = Utilities.ToInt64LittleEndian(buffer, offset + 0x90); + MaxSize = Utilities.ToInt64LittleEndian(buffer, offset + 0x98); + } + + internal void Write(byte[] buffer, int offset) + { + Encoding.Unicode.GetBytes(Name, 0, Name.Length, buffer, offset + 0); + Utilities.WriteBytesLittleEndian((uint)Type, buffer, offset + 0x80); + Utilities.WriteBytesLittleEndian(DisplayRule, buffer, offset + 0x84); + Utilities.WriteBytesLittleEndian((uint)CollationRule, buffer, offset + 0x88); + Utilities.WriteBytesLittleEndian((uint)Flags, buffer, offset + 0x8C); + Utilities.WriteBytesLittleEndian(MinSize, buffer, offset + 0x90); + Utilities.WriteBytesLittleEndian(MaxSize, buffer, offset + 0x98); + } + } +} diff --git a/DiscUtils/Ntfs/AttributeDefinitions.cs b/DiscUtils/Ntfs/AttributeDefinitions.cs new file mode 100644 index 0000000..0682956 --- /dev/null +++ b/DiscUtils/Ntfs/AttributeDefinitions.cs @@ -0,0 +1,143 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal sealed class AttributeDefinitions + { + private Dictionary _attrDefs; + + public AttributeDefinitions() + { + _attrDefs = new Dictionary(); + + Add(AttributeType.StandardInformation, "$STANDARD_INFORMATION", AttributeTypeFlags.MustBeResident, 0x30, 0x48); + Add(AttributeType.AttributeList, "$ATTRIBUTE_LIST", AttributeTypeFlags.CanBeNonResident, 0, -1); + Add(AttributeType.FileName, "$FILE_NAME", AttributeTypeFlags.Indexed | AttributeTypeFlags.MustBeResident, 0x44, 0x242); + Add(AttributeType.ObjectId, "$OBJECT_ID", AttributeTypeFlags.MustBeResident, 0, 0x100); + Add(AttributeType.SecurityDescriptor, "$SECURITY_DESCRIPTOR", AttributeTypeFlags.CanBeNonResident, 0x0, -1); + Add(AttributeType.VolumeName, "$VOLUME_NAME", AttributeTypeFlags.MustBeResident, 0x2, 0x100); + Add(AttributeType.VolumeInformation, "$VOLUME_INFORMATION", AttributeTypeFlags.MustBeResident, 0xC, 0xC); + Add(AttributeType.Data, "$DATA", AttributeTypeFlags.None, 0, -1); + Add(AttributeType.IndexRoot, "$INDEX_ROOT", AttributeTypeFlags.MustBeResident, 0, -1); + Add(AttributeType.IndexAllocation, "$INDEX_ALLOCATION", AttributeTypeFlags.CanBeNonResident, 0, -1); + Add(AttributeType.Bitmap, "$BITMAP", AttributeTypeFlags.CanBeNonResident, 0, -1); + Add(AttributeType.ReparsePoint, "$REPARSE_POINT", AttributeTypeFlags.CanBeNonResident, 0, 0x4000); + Add(AttributeType.ExtendedAttributesInformation, "$EA_INFORMATION", AttributeTypeFlags.MustBeResident, 0x8, 0x8); + Add(AttributeType.ExtendedAttributes, "$EA", AttributeTypeFlags.None, 0, 0x10000); + Add(AttributeType.LoggedUtilityStream, "$LOGGED_UTILITY_STREAM", AttributeTypeFlags.CanBeNonResident, 0, 0x10000); + } + + public AttributeDefinitions(File file) + { + _attrDefs = new Dictionary(); + + byte[] buffer = new byte[AttributeDefinitionRecord.Size]; + using (Stream s = file.OpenStream(AttributeType.Data, null, FileAccess.Read)) + { + while (Utilities.ReadFully(s, buffer, 0, buffer.Length) == buffer.Length) + { + AttributeDefinitionRecord record = new AttributeDefinitionRecord(); + record.Read(buffer, 0); + + // NULL terminator record + if (record.Type != AttributeType.None) + { + _attrDefs.Add(record.Type, record); + } + } + } + } + + public void WriteTo(File file) + { + List attribs = new List(_attrDefs.Keys); + attribs.Sort(); + + using (Stream s = file.OpenStream(AttributeType.Data, null, FileAccess.ReadWrite)) + { + byte[] buffer; + for (int i = 0; i < attribs.Count; ++i) + { + buffer = new byte[AttributeDefinitionRecord.Size]; + AttributeDefinitionRecord attrDef = _attrDefs[attribs[i]]; + attrDef.Write(buffer, 0); + + s.Write(buffer, 0, buffer.Length); + } + + buffer = new byte[AttributeDefinitionRecord.Size]; + s.Write(buffer, 0, buffer.Length); + } + } + + internal AttributeDefinitionRecord Lookup(string name) + { + foreach (var record in _attrDefs.Values) + { + if (string.Compare(name, record.Name, StringComparison.OrdinalIgnoreCase) == 0) + { + return record; + } + } + + return null; + } + + internal bool MustBeResident(AttributeType attributeType) + { + AttributeDefinitionRecord record; + if (_attrDefs.TryGetValue(attributeType, out record)) + { + return (record.Flags & AttributeTypeFlags.MustBeResident) != 0; + } + + return false; + } + + internal bool IsIndexed(AttributeType attributeType) + { + AttributeDefinitionRecord record; + if (_attrDefs.TryGetValue(attributeType, out record)) + { + return (record.Flags & AttributeTypeFlags.Indexed) != 0; + } + + return false; + } + + private void Add(AttributeType attributeType, string name, AttributeTypeFlags attributeTypeFlags, int minSize, int maxSize) + { + AttributeDefinitionRecord adr = new AttributeDefinitionRecord(); + adr.Type = attributeType; + adr.Name = name; + adr.Flags = attributeTypeFlags; + adr.MinSize = minSize; + adr.MaxSize = maxSize; + _attrDefs.Add(attributeType, adr); + } + } +} diff --git a/DiscUtils/Ntfs/AttributeList.cs b/DiscUtils/Ntfs/AttributeList.cs new file mode 100644 index 0000000..9c862fe --- /dev/null +++ b/DiscUtils/Ntfs/AttributeList.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.Collections; + using System.Collections.Generic; + using System.IO; + + internal class AttributeList : IByteArraySerializable, IDiagnosticTraceable, ICollection + { + private List _records; + + public AttributeList() + { + _records = new List(); + } + + public int Size + { + get + { + int total = 0; + foreach (var record in _records) + { + total += record.Size; + } + + return total; + } + } + + public int Count + { + get { return _records.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + _records.Clear(); + + int pos = 0; + while (pos < buffer.Length) + { + AttributeListRecord r = new AttributeListRecord(); + pos += r.ReadFrom(buffer, offset + pos); + _records.Add(r); + } + + return pos; + } + + public void WriteTo(byte[] buffer, int offset) + { + int pos = offset; + foreach (var record in _records) + { + record.WriteTo(buffer, offset + pos); + pos += record.Size; + } + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "ATTRIBUTE LIST RECORDS"); + foreach (AttributeListRecord r in _records) + { + r.Dump(writer, indent + " "); + } + } + + public void Add(AttributeListRecord item) + { + _records.Add(item); + _records.Sort(); + } + + public void Clear() + { + _records.Clear(); + } + + public bool Contains(AttributeListRecord item) + { + return _records.Contains(item); + } + + public void CopyTo(AttributeListRecord[] array, int arrayIndex) + { + _records.CopyTo(array, arrayIndex); + } + + public bool Remove(AttributeListRecord item) + { + return _records.Remove(item); + } + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + return _records.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return _records.GetEnumerator(); + } + + #endregion + } +} diff --git a/DiscUtils/Ntfs/AttributeListRecord.cs b/DiscUtils/Ntfs/AttributeListRecord.cs new file mode 100644 index 0000000..df4ae1f --- /dev/null +++ b/DiscUtils/Ntfs/AttributeListRecord.cs @@ -0,0 +1,145 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + using System.Text; + + internal class AttributeListRecord : IDiagnosticTraceable, IByteArraySerializable, IComparable + { + public AttributeType Type; + public ushort RecordLength; + public byte NameLength; + public byte NameOffset; + public string Name; + public ulong StartVcn; + public FileRecordReference BaseFileReference; + public ushort AttributeId; + + public int Size + { + get + { + return Utilities.RoundUp(0x20 + (string.IsNullOrEmpty(Name) ? 0 : Encoding.Unicode.GetByteCount(Name)), 8); + } + } + + public static AttributeListRecord FromAttribute(AttributeRecord attr, FileRecordReference mftRecord) + { + AttributeListRecord newRecord = new AttributeListRecord() + { + Type = attr.AttributeType, + Name = attr.Name, + StartVcn = 0, + BaseFileReference = mftRecord, + AttributeId = attr.AttributeId + }; + + if (attr.IsNonResident) + { + newRecord.StartVcn = (ulong)((NonResidentAttributeRecord)attr).StartVcn; + } + + return newRecord; + } + + public int ReadFrom(byte[] data, int offset) + { + Type = (AttributeType)Utilities.ToUInt32LittleEndian(data, offset + 0x00); + RecordLength = Utilities.ToUInt16LittleEndian(data, offset + 0x04); + NameLength = data[offset + 0x06]; + NameOffset = data[offset + 0x07]; + StartVcn = Utilities.ToUInt64LittleEndian(data, offset + 0x08); + BaseFileReference = new FileRecordReference(Utilities.ToUInt64LittleEndian(data, offset + 0x10)); + AttributeId = Utilities.ToUInt16LittleEndian(data, offset + 0x18); + + if (NameLength > 0) + { + Name = Encoding.Unicode.GetString(data, offset + NameOffset, NameLength * 2); + } + else + { + Name = null; + } + + if (RecordLength < 0x18) + { + throw new InvalidDataException("Malformed AttributeList record"); + } + + return RecordLength; + } + + public void WriteTo(byte[] buffer, int offset) + { + NameOffset = 0x20; + if (string.IsNullOrEmpty(Name)) + { + NameLength = 0; + } + else + { + NameLength = (byte)(Encoding.Unicode.GetBytes(Name, 0, Name.Length, buffer, offset + NameOffset) / 2); + } + + RecordLength = (ushort)Utilities.RoundUp(NameOffset + (NameLength * 2), 8); + + Utilities.WriteBytesLittleEndian((uint)Type, buffer, offset); + Utilities.WriteBytesLittleEndian(RecordLength, buffer, offset + 0x04); + buffer[offset + 0x06] = NameLength; + buffer[offset + 0x07] = NameOffset; + Utilities.WriteBytesLittleEndian(StartVcn, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(BaseFileReference.Value, buffer, offset + 0x10); + Utilities.WriteBytesLittleEndian(AttributeId, buffer, offset + 0x18); + } + + public int CompareTo(AttributeListRecord other) + { + int val = ((int)Type) - (int)other.Type; + if (val != 0) + { + return val; + } + + val = string.Compare(Name, other.Name, StringComparison.OrdinalIgnoreCase); + if (val != 0) + { + return val; + } + + return ((int)StartVcn) - (int)other.StartVcn; + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "ATTRIBUTE LIST RECORD"); + writer.WriteLine(indent + " Type: " + Type); + writer.WriteLine(indent + " Record Length: " + RecordLength); + writer.WriteLine(indent + " Name: " + Name); + writer.WriteLine(indent + " Start VCN: " + StartVcn); + writer.WriteLine(indent + " Base File Reference: " + BaseFileReference); + writer.WriteLine(indent + " Attribute ID: " + AttributeId); + } + } +} diff --git a/DiscUtils/Ntfs/AttributeRecord.cs b/DiscUtils/Ntfs/AttributeRecord.cs new file mode 100644 index 0000000..d7e7dce --- /dev/null +++ b/DiscUtils/Ntfs/AttributeRecord.cs @@ -0,0 +1,199 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + using System.Text; + + [Flags] + internal enum AttributeFlags : ushort + { + None = 0x0000, + Compressed = 0x0001, + Encrypted = 0x4000, + Sparse = 0x8000 + } + + internal abstract class AttributeRecord : IComparable + { + protected AttributeType _type; + protected byte _nonResidentFlag; + protected AttributeFlags _flags; + protected ushort _attributeId; + + protected string _name; + + public AttributeRecord() + { + } + + public AttributeRecord(AttributeType type, string name, ushort id, AttributeFlags flags) + { + _type = type; + _name = name; + _attributeId = id; + _flags = flags; + } + + public AttributeType AttributeType + { + get { return _type; } + } + + public ushort AttributeId + { + get { return _attributeId; } + set { _attributeId = value; } + } + + public abstract long AllocatedLength + { + get; + set; + } + + public abstract long StartVcn + { + get; + } + + public abstract long DataLength + { + get; + set; + } + + public abstract long InitializedDataLength + { + get; + set; + } + + public bool IsNonResident + { + get { return _nonResidentFlag != 0; } + } + + public string Name + { + get { return _name; } + } + + public AttributeFlags Flags + { + get { return _flags; } + set { _flags = value; } + } + + public abstract int Size { get; } + + public static AttributeRecord FromBytes(byte[] buffer, int offset, out int length) + { + if (Utilities.ToUInt32LittleEndian(buffer, offset) == 0xFFFFFFFF) + { + length = 0; + return null; + } + else if (buffer[offset + 0x08] != 0x00) + { + return new NonResidentAttributeRecord(buffer, offset, out length); + } + else + { + return new ResidentAttributeRecord(buffer, offset, out length); + } + } + + public static int CompareStartVcns(AttributeRecord x, AttributeRecord y) + { + if (x.StartVcn < y.StartVcn) + { + return -1; + } + else if (x.StartVcn == y.StartVcn) + { + return 0; + } + else + { + return 1; + } + } + + public abstract Range[] GetClusters(); + + public abstract IBuffer GetReadOnlyDataBuffer(INtfsContext context); + + public int CompareTo(AttributeRecord other) + { + int val = ((int)_type) - (int)other._type; + if (val != 0) + { + return val; + } + + val = string.Compare(_name, other._name, StringComparison.OrdinalIgnoreCase); + if (val != 0) + { + return val; + } + + return ((int)_attributeId) - (int)other._attributeId; + } + + public abstract int Write(byte[] buffer, int offset); + + public virtual void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "ATTRIBUTE RECORD"); + writer.WriteLine(indent + " Type: " + _type); + writer.WriteLine(indent + " Non-Resident: " + _nonResidentFlag); + writer.WriteLine(indent + " Name: " + _name); + writer.WriteLine(indent + " Flags: " + _flags); + writer.WriteLine(indent + " AttributeId: " + _attributeId); + } + + protected virtual void Read(byte[] buffer, int offset, out int length) + { + _type = (AttributeType)Utilities.ToUInt32LittleEndian(buffer, offset + 0x00); + length = Utilities.ToInt32LittleEndian(buffer, offset + 0x04); + + _nonResidentFlag = buffer[offset + 0x08]; + byte nameLength = buffer[offset + 0x09]; + ushort nameOffset = Utilities.ToUInt16LittleEndian(buffer, offset + 0x0A); + _flags = (AttributeFlags)Utilities.ToUInt16LittleEndian(buffer, offset + 0x0C); + _attributeId = Utilities.ToUInt16LittleEndian(buffer, offset + 0x0E); + + if (nameLength != 0x00) + { + if (nameLength + nameOffset > length) + { + throw new IOException("Corrupt attribute, name outside of attribute"); + } + + _name = Encoding.Unicode.GetString(buffer, offset + nameOffset, nameLength * 2); + } + } + } +} diff --git a/DiscUtils/Ntfs/AttributeReference.cs b/DiscUtils/Ntfs/AttributeReference.cs new file mode 100644 index 0000000..6608675 --- /dev/null +++ b/DiscUtils/Ntfs/AttributeReference.cs @@ -0,0 +1,130 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + + /// + /// Fully-qualified reference to an attribute. + /// + internal class AttributeReference : IComparable, IEquatable + { + private FileRecordReference _fileReference; + private ushort _attributeId; + + /// + /// Initializes a new instance of the AttributeReference class. + /// + /// The file containing the attribute. + /// The identity of the attribute within the file record. + public AttributeReference(FileRecordReference fileReference, ushort attributeId) + { + _fileReference = fileReference; + _attributeId = attributeId; + } + + /// + /// Gets the file containing the attribute. + /// + public FileRecordReference File + { + get { return _fileReference; } + } + + /// + /// Gets the identity of the attribute within the file record. + /// + public ushort AttributeId + { + get { return _attributeId; } + } + + /// + /// The reference as a string. + /// + /// String representing the attribute. + public override string ToString() + { + return _fileReference.ToString() + ".attr[" + _attributeId + "]"; + } + + #region IComparable Members + + /// + /// Compares this attribute reference to another. + /// + /// The attribute reference to compare against. + /// Zero if references are identical. + public int CompareTo(AttributeReference other) + { + int refDiff = _fileReference.CompareTo(other._fileReference); + if (refDiff != 0) + { + return refDiff; + } + + return _attributeId.CompareTo(other._attributeId); + } + + #endregion + + #region IEquatable Members + + /// + /// Indicates if two references are equivalent. + /// + /// The attribute reference to compare. + /// true if the references are equivalent. + public bool Equals(AttributeReference other) + { + return CompareTo(other) == 0; + } + + #endregion + + /// + /// Indicates if this reference is equivalent to another object. + /// + /// The object to compare. + /// true if obj is an equivalent attribute reference. + public override bool Equals(object obj) + { + AttributeReference objAsAttrRef = obj as AttributeReference; + if (objAsAttrRef == null) + { + return false; + } + + return Equals(objAsAttrRef); + } + + /// + /// Gets the hash code for this reference. + /// + /// The hash code. + public override int GetHashCode() + { + return _fileReference.GetHashCode() ^ _attributeId.GetHashCode(); + } + } +} diff --git a/DiscUtils/Ntfs/AttributeType.cs b/DiscUtils/Ntfs/AttributeType.cs new file mode 100644 index 0000000..e343b43 --- /dev/null +++ b/DiscUtils/Ntfs/AttributeType.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + /// + /// Enumeration of NTFS file attribute types. + /// + /// Normally applications only create Data attributes. + public enum AttributeType : int + { + /// + /// No type specified. + /// + None = 0x00, + + /// + /// NTFS Standard Information. + /// + StandardInformation = 0x10, + + /// + /// Attribute list, that holds a list of attribute locations for files with a large attribute set. + /// + AttributeList = 0x20, + + /// + /// FileName information, one per hard link. + /// + FileName = 0x30, + + /// + /// Distributed Link Tracking object identity. + /// + ObjectId = 0x40, + + /// + /// Legacy Security Descriptor attribute. + /// + SecurityDescriptor = 0x50, + + /// + /// The name of the NTFS volume. + /// + VolumeName = 0x60, + + /// + /// Information about the NTFS volume. + /// + VolumeInformation = 0x70, + + /// + /// File contents, a file may have multiple data attributes (default is unnamed). + /// + Data = 0x80, + + /// + /// Root information for directories and other NTFS index's. + /// + IndexRoot = 0x90, + + /// + /// For 'large' directories and other NTFS index's, the index contents. + /// + IndexAllocation = 0xA0, + + /// + /// Bitmask of allocated clusters, records, etc - typically used in indexes. + /// + Bitmap = 0xB0, + + /// + /// ReparsePoint information. + /// + ReparsePoint = 0xC0, + + /// + /// Extended Attributes meta-information. + /// + ExtendedAttributesInformation = 0xD0, + + /// + /// Extended Attributes data. + /// + ExtendedAttributes = 0xE0, + + /// + /// Legacy attribute type from NT (not used). + /// + PropertySet = 0xF0, + + /// + /// Encrypted File System (EFS) data. + /// + LoggedUtilityStream = 0x100 + } +} diff --git a/DiscUtils/Ntfs/BiosParameterBlock.cs b/DiscUtils/Ntfs/BiosParameterBlock.cs new file mode 100644 index 0000000..6ec2035 --- /dev/null +++ b/DiscUtils/Ntfs/BiosParameterBlock.cs @@ -0,0 +1,219 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Globalization; + using System.IO; + + internal class BiosParameterBlock + { + public string OemId; + public ushort BytesPerSector; + public byte SectorsPerCluster; + public ushort ReservedSectors; // Must be 0 + public byte NumFats; // Must be 0 + public ushort FatRootEntriesCount; // Must be 0 + public ushort TotalSectors16; // Must be 0 + public byte Media; // Must be 0xF8 + public ushort FatSize16; // Must be 0 + public ushort SectorsPerTrack; // Value: 0x3F 0x00 + public ushort NumHeads; // Value: 0xFF 0x00 + public uint HiddenSectors; // Value: 0x3F 0x00 0x00 0x00 + public uint TotalSectors32; // Must be 0 + public byte BiosDriveNumber; // Value: 0x80 (first hard disk) + public byte ChkDskFlags; // Value: 0x00 + public byte SignatureByte; // Value: 0x80 + public byte PaddingByte; // Value: 0x00 + public long TotalSectors64; + public long MftCluster; + public long MftMirrorCluster; + public byte RawMftRecordSize; + public byte RawIndexBufferSize; + public ulong VolumeSerialNumber; + + public int MftRecordSize + { + get { return CalcRecordSize(RawMftRecordSize); } + } + + public int IndexBufferSize + { + get { return CalcRecordSize(RawIndexBufferSize); } + } + + public int BytesPerCluster + { + get { return ((int)BytesPerSector) * ((int)SectorsPerCluster); } + } + + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + "BIOS PARAMETER BLOCK (BPB)"); + writer.WriteLine(linePrefix + " OEM ID: " + OemId); + writer.WriteLine(linePrefix + " Bytes per Sector: " + BytesPerSector); + writer.WriteLine(linePrefix + " Sectors per Cluster: " + SectorsPerCluster); + writer.WriteLine(linePrefix + " Reserved Sectors: " + ReservedSectors); + writer.WriteLine(linePrefix + " # FATs: " + NumFats); + writer.WriteLine(linePrefix + " # FAT Root Entries: " + FatRootEntriesCount); + writer.WriteLine(linePrefix + " Total Sectors (16b): " + TotalSectors16); + writer.WriteLine(linePrefix + " Media: " + Media.ToString("X", CultureInfo.InvariantCulture) + "h"); + writer.WriteLine(linePrefix + " FAT size (16b): " + FatSize16); + writer.WriteLine(linePrefix + " Sectors per Track: " + SectorsPerTrack); + writer.WriteLine(linePrefix + " # Heads: " + NumHeads); + writer.WriteLine(linePrefix + " Hidden Sectors: " + HiddenSectors); + writer.WriteLine(linePrefix + " Total Sectors (32b): " + TotalSectors32); + writer.WriteLine(linePrefix + " BIOS Drive Number: " + BiosDriveNumber); + writer.WriteLine(linePrefix + " Chkdsk Flags: " + ChkDskFlags); + writer.WriteLine(linePrefix + " Signature Byte: " + SignatureByte); + writer.WriteLine(linePrefix + " Total Sectors (64b): " + TotalSectors64); + writer.WriteLine(linePrefix + " MFT Record Size: " + RawMftRecordSize); + writer.WriteLine(linePrefix + " Index Buffer Size: " + RawIndexBufferSize); + writer.WriteLine(linePrefix + " Volume Serial Number: " + VolumeSerialNumber); + } + + internal static BiosParameterBlock Initialized(Geometry diskGeometry, int clusterSize, uint partitionStartLba, long partitionSizeLba, int mftRecordSize, int indexBufferSize) + { + BiosParameterBlock bpb = new BiosParameterBlock(); + bpb.OemId = "NTFS "; + bpb.BytesPerSector = Sizes.Sector; + bpb.SectorsPerCluster = (byte)(clusterSize / bpb.BytesPerSector); + bpb.ReservedSectors = 0; + bpb.NumFats = 0; + bpb.FatRootEntriesCount = 0; + bpb.TotalSectors16 = 0; + bpb.Media = 0xF8; + bpb.FatSize16 = 0; + bpb.SectorsPerTrack = (ushort)diskGeometry.SectorsPerTrack; + bpb.NumHeads = (ushort)diskGeometry.HeadsPerCylinder; + bpb.HiddenSectors = partitionStartLba; + bpb.TotalSectors32 = 0; + bpb.BiosDriveNumber = 0x80; + bpb.ChkDskFlags = 0; + bpb.SignatureByte = 0x80; + bpb.PaddingByte = 0; + bpb.TotalSectors64 = partitionSizeLba - 1; + bpb.RawMftRecordSize = bpb.CodeRecordSize(mftRecordSize); + bpb.RawIndexBufferSize = bpb.CodeRecordSize(indexBufferSize); + bpb.VolumeSerialNumber = GenSerialNumber(); + + return bpb; + } + + internal static BiosParameterBlock FromBytes(byte[] bytes, int offset) + { + BiosParameterBlock bpb = new BiosParameterBlock(); + bpb.OemId = Utilities.BytesToString(bytes, offset + 0x03, 8); + bpb.BytesPerSector = Utilities.ToUInt16LittleEndian(bytes, offset + 0x0B); + bpb.SectorsPerCluster = bytes[offset + 0x0D]; + bpb.ReservedSectors = Utilities.ToUInt16LittleEndian(bytes, offset + 0x0E); + bpb.NumFats = bytes[offset + 0x10]; + bpb.FatRootEntriesCount = Utilities.ToUInt16LittleEndian(bytes, offset + 0x11); + bpb.TotalSectors16 = Utilities.ToUInt16LittleEndian(bytes, offset + 0x13); + bpb.Media = bytes[offset + 0x15]; + bpb.FatSize16 = Utilities.ToUInt16LittleEndian(bytes, offset + 0x16); + bpb.SectorsPerTrack = Utilities.ToUInt16LittleEndian(bytes, offset + 0x18); + bpb.NumHeads = Utilities.ToUInt16LittleEndian(bytes, offset + 0x1A); + bpb.HiddenSectors = Utilities.ToUInt32LittleEndian(bytes, offset + 0x1C); + bpb.TotalSectors32 = Utilities.ToUInt32LittleEndian(bytes, offset + 0x20); + bpb.BiosDriveNumber = bytes[offset + 0x24]; + bpb.ChkDskFlags = bytes[offset + 0x25]; + bpb.SignatureByte = bytes[offset + 0x26]; + bpb.PaddingByte = bytes[offset + 0x27]; + bpb.TotalSectors64 = Utilities.ToInt64LittleEndian(bytes, offset + 0x28); + bpb.MftCluster = Utilities.ToInt64LittleEndian(bytes, offset + 0x30); + bpb.MftMirrorCluster = Utilities.ToInt64LittleEndian(bytes, offset + 0x38); + bpb.RawMftRecordSize = bytes[offset + 0x40]; + bpb.RawIndexBufferSize = bytes[offset + 0x44]; + bpb.VolumeSerialNumber = Utilities.ToUInt64LittleEndian(bytes, offset + 0x48); + + return bpb; + } + + internal void ToBytes(byte[] buffer, int offset) + { + Utilities.StringToBytes(OemId, buffer, offset + 0x03, 8); + Utilities.WriteBytesLittleEndian(BytesPerSector, buffer, offset + 0x0B); + buffer[offset + 0x0D] = SectorsPerCluster; + Utilities.WriteBytesLittleEndian(ReservedSectors, buffer, offset + 0x0E); + buffer[offset + 0x10] = NumFats; + Utilities.WriteBytesLittleEndian(FatRootEntriesCount, buffer, offset + 0x11); + Utilities.WriteBytesLittleEndian(TotalSectors16, buffer, offset + 0x13); + buffer[offset + 0x15] = Media; + Utilities.WriteBytesLittleEndian(FatSize16, buffer, offset + 0x16); + Utilities.WriteBytesLittleEndian(SectorsPerTrack, buffer, offset + 0x18); + Utilities.WriteBytesLittleEndian(NumHeads, buffer, offset + 0x1A); + Utilities.WriteBytesLittleEndian(HiddenSectors, buffer, offset + 0x1C); + Utilities.WriteBytesLittleEndian(TotalSectors32, buffer, offset + 0x20); + buffer[offset + 0x24] = BiosDriveNumber; + buffer[offset + 0x25] = ChkDskFlags; + buffer[offset + 0x26] = SignatureByte; + buffer[offset + 0x27] = PaddingByte; + Utilities.WriteBytesLittleEndian(TotalSectors64, buffer, offset + 0x28); + Utilities.WriteBytesLittleEndian(MftCluster, buffer, offset + 0x30); + Utilities.WriteBytesLittleEndian(MftMirrorCluster, buffer, offset + 0x38); + buffer[offset + 0x40] = RawMftRecordSize; + buffer[offset + 0x44] = RawIndexBufferSize; + Utilities.WriteBytesLittleEndian(VolumeSerialNumber, buffer, offset + 0x48); + } + + internal int CalcRecordSize(byte rawSize) + { + if ((rawSize & 0x80) != 0) + { + return 1 << (-(sbyte)rawSize); + } + else + { + return rawSize * SectorsPerCluster * BytesPerSector; + } + } + + private static ulong GenSerialNumber() + { + byte[] buffer = new byte[8]; + Random rng = new Random(); + rng.NextBytes(buffer); + return Utilities.ToUInt64LittleEndian(buffer, 0); + } + + private byte CodeRecordSize(int size) + { + if (size >= BytesPerCluster) + { + return (byte)(size / BytesPerCluster); + } + else + { + sbyte val = 0; + while (size != 1) + { + size = (size >> 1) & 0x7FFFFFFF; + val++; + } + + return (byte)-val; + } + } + } +} diff --git a/DiscUtils/Ntfs/Bitmap.cs b/DiscUtils/Ntfs/Bitmap.cs new file mode 100644 index 0000000..8c4a7e4 --- /dev/null +++ b/DiscUtils/Ntfs/Bitmap.cs @@ -0,0 +1,223 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + + internal sealed class Bitmap : IDisposable + { + private Stream _stream; + private long _maxIndex; + + private BlockCacheStream _bitmap; + + private long _nextAvailable; + + public Bitmap(Stream stream, long maxIndex) + { + _stream = stream; + _maxIndex = maxIndex; + _bitmap = new BlockCacheStream(SparseStream.FromStream(stream, Ownership.None), Ownership.None); + } + + public void Dispose() + { + if (_bitmap != null) + { + _bitmap.Dispose(); + _bitmap = null; + } + } + + public bool IsPresent(long index) + { + long byteIdx = index / 8; + int mask = 1 << (int)(index % 8); + return (GetByte(byteIdx) & mask) != 0; + } + + public void MarkPresent(long index) + { + long byteIdx = index / 8; + byte mask = (byte)(1 << (byte)(index % 8)); + + if (byteIdx >= _bitmap.Length) + { + _bitmap.Position = Utilities.RoundUp(byteIdx + 1, 8) - 1; + _bitmap.WriteByte(0); + } + + SetByte(byteIdx, (byte)(GetByte(byteIdx) | mask)); + } + + public void MarkPresentRange(long index, long count) + { + if (count <= 0) + { + return; + } + + long firstByte = index / 8; + long lastByte = (index + count - 1) / 8; + + if (lastByte >= _bitmap.Length) + { + _bitmap.Position = Utilities.RoundUp(lastByte + 1, 8) - 1; + _bitmap.WriteByte(0); + } + + byte[] buffer = new byte[lastByte - firstByte + 1]; + buffer[0] = GetByte(firstByte); + if (buffer.Length != 1) + { + buffer[buffer.Length - 1] = GetByte(lastByte); + } + + for (long i = index; i < index + count; ++i) + { + long byteIdx = (i / 8) - firstByte; + byte mask = (byte)(1 << (byte)(i % 8)); + + buffer[byteIdx] |= mask; + } + + SetBytes(firstByte, buffer); + } + + public void MarkAbsent(long index) + { + long byteIdx = index / 8; + byte mask = (byte)(1 << (byte)(index % 8)); + + if (byteIdx < _stream.Length) + { + SetByte(byteIdx, (byte)(GetByte(byteIdx) & ~mask)); + } + + if (index < _nextAvailable) + { + _nextAvailable = index; + } + } + + internal void MarkAbsentRange(long index, long count) + { + if (count <= 0) + { + return; + } + + long firstByte = index / 8; + long lastByte = (index + count - 1) / 8; + if (lastByte >= _bitmap.Length) + { + _bitmap.Position = Utilities.RoundUp(lastByte + 1, 8) - 1; + _bitmap.WriteByte(0); + } + + byte[] buffer = new byte[lastByte - firstByte + 1]; + buffer[0] = GetByte(firstByte); + if (buffer.Length != 1) + { + buffer[buffer.Length - 1] = GetByte(lastByte); + } + + for (long i = index; i < index + count; ++i) + { + long byteIdx = (i / 8) - firstByte; + byte mask = (byte)(1 << (byte)(i % 8)); + + buffer[byteIdx] &= (byte)(~mask); + } + + SetBytes(firstByte, buffer); + + if (index < _nextAvailable) + { + _nextAvailable = index; + } + } + + internal long AllocateFirstAvailable(long minValue) + { + long i = Math.Max(minValue, _nextAvailable); + while (IsPresent(i) && i < _maxIndex) + { + ++i; + } + + if (i < _maxIndex) + { + MarkPresent(i); + _nextAvailable = i + 1; + return i; + } + else + { + return -1; + } + } + + internal long SetTotalEntries(long numEntries) + { + long length = Utilities.RoundUp(Utilities.Ceil(numEntries, 8), 8); + _stream.SetLength(length); + return length * 8; + } + + private byte GetByte(long index) + { + if (index >= _bitmap.Length) + { + return 0; + } + + byte[] buffer = new byte[1]; + _bitmap.Position = index; + if (_bitmap.Read(buffer, 0, 1) != 0) + { + return buffer[0]; + } + else + { + return 0; + } + } + + private void SetByte(long index, byte value) + { + byte[] buffer = new byte[] { value }; + _bitmap.Position = index; + _bitmap.Write(buffer, 0, 1); + _bitmap.Flush(); + } + + private void SetBytes(long index, byte[] buffer) + { + _bitmap.Position = index; + _bitmap.Write(buffer, 0, buffer.Length); + _bitmap.Flush(); + } + } +} diff --git a/DiscUtils/Ntfs/ClusterBitmap.cs b/DiscUtils/Ntfs/ClusterBitmap.cs new file mode 100644 index 0000000..3214cc5 --- /dev/null +++ b/DiscUtils/Ntfs/ClusterBitmap.cs @@ -0,0 +1,270 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.Collections.Generic; + using System.IO; + + internal class ClusterBitmap : System.IDisposable + { + private File _file; + private Bitmap _bitmap; + + private long _nextDataCluster; + private bool _fragmentedDiskMode; + + public ClusterBitmap(File file) + { + _file = file; + _bitmap = new Bitmap( + _file.OpenStream(AttributeType.Data, null, FileAccess.ReadWrite), + Utilities.Ceil(file.Context.BiosParameterBlock.TotalSectors64, file.Context.BiosParameterBlock.SectorsPerCluster)); + } + + public void Dispose() + { + if (_bitmap != null) + { + _bitmap.Dispose(); + _bitmap = null; + } + } + + /// + /// Allocates clusters from the disk. + /// + /// The number of clusters to allocate. + /// The proposed start cluster (or -1). + /// true if this attribute is the $MFT\$DATA attribute. + /// The total number of clusters in the file, including this allocation. + /// The list of cluster allocations. + public Tuple[] AllocateClusters(long count, long proposedStart, bool isMft, long total) + { + List> result = new List>(); + + long numFound = 0; + + long totalClusters = _file.Context.RawStream.Length / _file.Context.BiosParameterBlock.BytesPerCluster; + + if (isMft) + { + // First, try to extend the existing cluster run (if available) + if (proposedStart >= 0) + { + numFound += ExtendRun(count - numFound, result, proposedStart, totalClusters); + } + + // The MFT grows sequentially across the disk + if (numFound < count && !_fragmentedDiskMode) + { + numFound += FindClusters(count - numFound, result, 0, totalClusters, isMft, true, 0); + } + + if (numFound < count) + { + numFound += FindClusters(count - numFound, result, 0, totalClusters, isMft, false, 0); + } + } + else + { + // First, try to extend the existing cluster run (if available) + if (proposedStart >= 0) + { + numFound += ExtendRun(count - numFound, result, proposedStart, totalClusters); + } + + // Try to find a contiguous range + if (numFound < count && !_fragmentedDiskMode) + { + numFound += FindClusters(count - numFound, result, totalClusters / 8, totalClusters, isMft, true, total / 4); + } + + if (numFound < count) + { + numFound += FindClusters(count - numFound, result, totalClusters / 8, totalClusters, isMft, false, 0); + } + + if (numFound < count) + { + numFound = FindClusters(count - numFound, result, totalClusters / 16, totalClusters / 8, isMft, false, 0); + } + + if (numFound < count) + { + numFound = FindClusters(count - numFound, result, totalClusters / 32, totalClusters / 16, isMft, false, 0); + } + + if (numFound < count) + { + numFound = FindClusters(count - numFound, result, 0, totalClusters / 32, isMft, false, 0); + } + } + + if (numFound < count) + { + FreeClusters(result.ToArray()); + throw new IOException("Out of disk space"); + } + + // If we found more than two clusters, or we have a fragmented result, + // then switch out of trying to allocate contiguous ranges. Similarly, + // switch back if we found a resonable quantity in a single span. + if ((numFound > 4 && result.Count == 1) || result.Count > 1) + { + _fragmentedDiskMode = (numFound / result.Count) < 4; + } + + return result.ToArray(); + } + + internal void MarkAllocated(long first, long count) + { + _bitmap.MarkPresentRange(first, count); + } + + internal void FreeClusters(params Tuple[] runs) + { + foreach (var run in runs) + { + _bitmap.MarkAbsentRange(run.First, run.Second); + } + } + + internal void FreeClusters(params Range[] runs) + { + foreach (var run in runs) + { + _bitmap.MarkAbsentRange(run.Offset, run.Count); + } + } + + /// + /// Sets the total number of clusters managed in the volume. + /// + /// Total number of clusters in the volume. + /// + /// Any clusters represented in the bitmap beyond the total number in the volume are marked as in-use. + /// + internal void SetTotalClusters(long numClusters) + { + long actualClusters = _bitmap.SetTotalEntries(numClusters); + if (actualClusters != numClusters) + { + MarkAllocated(numClusters, actualClusters - numClusters); + } + } + + private long ExtendRun(long count, List> result, long start, long end) + { + long focusCluster = start; + while (!_bitmap.IsPresent(focusCluster) && focusCluster < end && focusCluster - start < count) + { + ++focusCluster; + } + + long numFound = focusCluster - start; + + if (numFound > 0) + { + _bitmap.MarkPresentRange(start, numFound); + result.Add(new Tuple(start, numFound)); + } + + return numFound; + } + + /// + /// Finds one or more free clusters in a range. + /// + /// The number of clusters required. + /// The list of clusters found (i.e. out param). + /// The first cluster in the range to look at. + /// The last cluster in the range to look at (exclusive). + /// Indicates if the clusters are for the MFT. + /// Indicates if contiguous clusters are required. + /// Indicates how many clusters to skip before next allocation, to prevent fragmentation. + /// The number of clusters found in the range. + private long FindClusters(long count, List> result, long start, long end, bool isMft, bool contiguous, long headroom) + { + long numFound = 0; + + long focusCluster; + if (isMft) + { + focusCluster = start; + } + else + { + if (_nextDataCluster < start || _nextDataCluster >= end) + { + _nextDataCluster = start; + } + + focusCluster = _nextDataCluster; + } + + long numInspected = 0; + while (numFound < count && focusCluster >= start && numInspected < end - start) + { + if (!_bitmap.IsPresent(focusCluster)) + { + // Start of a run... + long runStart = focusCluster; + ++focusCluster; + + while (!_bitmap.IsPresent(focusCluster) && focusCluster - runStart < (count - numFound)) + { + ++focusCluster; + ++numInspected; + } + + if (!contiguous || (focusCluster - runStart) == (count - numFound)) + { + _bitmap.MarkPresentRange(runStart, focusCluster - runStart); + + result.Add(new Tuple(runStart, focusCluster - runStart)); + numFound += focusCluster - runStart; + } + } + else + { + ++focusCluster; + } + + ++numInspected; + + if (focusCluster >= end) + { + focusCluster = start; + } + } + + if (!isMft) + { + _nextDataCluster = focusCluster + headroom; + } + + return numFound; + } + } +} diff --git a/DiscUtils/Ntfs/ClusterStream.cs b/DiscUtils/Ntfs/ClusterStream.cs new file mode 100644 index 0000000..2e178ff --- /dev/null +++ b/DiscUtils/Ntfs/ClusterStream.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.Collections.Generic; + + internal abstract class ClusterStream + { + public abstract long AllocatedClusterCount + { + get; + } + + public abstract IEnumerable> StoredClusters + { + get; + } + + public abstract bool IsClusterStored(long vcn); + + public abstract void ExpandToClusters(long numVirtualClusters, NonResidentAttributeRecord extent, bool allocate); + + public abstract void TruncateToClusters(long numVirtualClusters); + + public abstract void ReadClusters(long startVcn, int count, byte[] buffer, int offset); + + public abstract int WriteClusters(long startVcn, int count, byte[] buffer, int offset); + + public abstract int ClearClusters(long startVcn, int count); + } +} diff --git a/DiscUtils/Ntfs/CompressedClusterStream.cs b/DiscUtils/Ntfs/CompressedClusterStream.cs new file mode 100644 index 0000000..e987bd7 --- /dev/null +++ b/DiscUtils/Ntfs/CompressedClusterStream.cs @@ -0,0 +1,257 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + using DiscUtils.Compression; + + internal sealed class CompressedClusterStream : ClusterStream + { + private INtfsContext _context; + private NtfsAttribute _attr; + private RawClusterStream _rawStream; + private int _bytesPerCluster; + + private byte[] _cacheBuffer; + private long _cacheBufferVcn = -1; + private byte[] _ioBuffer; + + public CompressedClusterStream(INtfsContext context, NtfsAttribute attr, RawClusterStream rawStream) + { + _context = context; + _attr = attr; + _rawStream = rawStream; + _bytesPerCluster = _context.BiosParameterBlock.BytesPerCluster; + + _cacheBuffer = new byte[_attr.CompressionUnitSize * context.BiosParameterBlock.BytesPerCluster]; + _ioBuffer = new byte[_attr.CompressionUnitSize * context.BiosParameterBlock.BytesPerCluster]; + } + + public override long AllocatedClusterCount + { + get { return _rawStream.AllocatedClusterCount; } + } + + public override IEnumerable> StoredClusters + { + get + { + return Range.Chunked(_rawStream.StoredClusters, _attr.CompressionUnitSize); + } + } + + public override bool IsClusterStored(long vcn) + { + return _rawStream.IsClusterStored(CompressionStart(vcn)); + } + + public override void ExpandToClusters(long numVirtualClusters, NonResidentAttributeRecord extent, bool allocate) + { + _rawStream.ExpandToClusters(Utilities.RoundUp(numVirtualClusters, _attr.CompressionUnitSize), extent, false); + } + + public override void TruncateToClusters(long numVirtualClusters) + { + long alignedNum = Utilities.RoundUp(numVirtualClusters, _attr.CompressionUnitSize); + _rawStream.TruncateToClusters(alignedNum); + if (alignedNum != numVirtualClusters) + { + _rawStream.ReleaseClusters(numVirtualClusters, (int)(alignedNum - numVirtualClusters)); + } + } + + public override void ReadClusters(long startVcn, int count, byte[] buffer, int offset) + { + if (buffer.Length < (count * _bytesPerCluster) + offset) + { + throw new ArgumentException("Cluster buffer too small", "buffer"); + } + + int totalRead = 0; + while (totalRead < count) + { + long focusVcn = startVcn + totalRead; + LoadCache(focusVcn); + + int cacheOffset = (int)(focusVcn - _cacheBufferVcn); + int toCopy = Math.Min(_attr.CompressionUnitSize - cacheOffset, count - totalRead); + + Array.Copy(_cacheBuffer, cacheOffset * _bytesPerCluster, buffer, offset + (totalRead * _bytesPerCluster), toCopy * _bytesPerCluster); + + totalRead += toCopy; + } + } + + public override int WriteClusters(long startVcn, int count, byte[] buffer, int offset) + { + if (buffer.Length < (count * _bytesPerCluster) + offset) + { + throw new ArgumentException("Cluster buffer too small", "buffer"); + } + + int totalAllocated = 0; + + int totalWritten = 0; + while (totalWritten < count) + { + long focusVcn = startVcn + totalWritten; + long cuStart = CompressionStart(focusVcn); + + if (cuStart == focusVcn && count - totalWritten >= _attr.CompressionUnitSize) + { + // Aligned write... + totalAllocated += CompressAndWriteClusters(focusVcn, _attr.CompressionUnitSize, buffer, offset + (totalWritten * _bytesPerCluster)); + + totalWritten += _attr.CompressionUnitSize; + } + else + { + // Unaligned, so go through cache + LoadCache(focusVcn); + + int cacheOffset = (int)(focusVcn - _cacheBufferVcn); + int toCopy = Math.Min(count - totalWritten, _attr.CompressionUnitSize - cacheOffset); + + Array.Copy(buffer, offset + (totalWritten * _bytesPerCluster), _cacheBuffer, cacheOffset * _bytesPerCluster, toCopy * _bytesPerCluster); + + totalAllocated += CompressAndWriteClusters(_cacheBufferVcn, _attr.CompressionUnitSize, _cacheBuffer, 0); + + totalWritten += toCopy; + } + } + + return totalAllocated; + } + + public override int ClearClusters(long startVcn, int count) + { + int totalReleased = 0; + int totalCleared = 0; + while (totalCleared < count) + { + long focusVcn = startVcn + totalCleared; + if (CompressionStart(focusVcn) == focusVcn && count - totalCleared >= _attr.CompressionUnitSize) + { + // Aligned - so it's a sparse compression unit... + totalReleased += _rawStream.ReleaseClusters(startVcn, _attr.CompressionUnitSize); + totalCleared += _attr.CompressionUnitSize; + } + else + { + int toZero = (int)Math.Min(count - totalCleared, _attr.CompressionUnitSize - (focusVcn - CompressionStart(focusVcn))); + totalReleased -= WriteZeroClusters(focusVcn, toZero); + totalCleared += toZero; + } + } + + return totalReleased; + } + + private int WriteZeroClusters(long focusVcn, int count) + { + int allocatedClusters = 0; + + byte[] zeroBuffer = new byte[16 * _bytesPerCluster]; + int numWritten = 0; + while (numWritten < count) + { + int toWrite = Math.Min(count - numWritten, 16); + + allocatedClusters += WriteClusters(focusVcn + numWritten, toWrite, zeroBuffer, 0); + + numWritten += toWrite; + } + + return allocatedClusters; + } + + private int CompressAndWriteClusters(long focusVcn, int count, byte[] buffer, int offset) + { + BlockCompressor compressor = _context.Options.Compressor; + compressor.BlockSize = _bytesPerCluster; + + int totalAllocated = 0; + + int compressedLength = _ioBuffer.Length; + var result = compressor.Compress(buffer, offset, _attr.CompressionUnitSize * _bytesPerCluster, _ioBuffer, 0, ref compressedLength); + if (result == CompressionResult.AllZeros) + { + totalAllocated -= _rawStream.ReleaseClusters(focusVcn, count); + } + else if (result == CompressionResult.Compressed && (_attr.CompressionUnitSize * _bytesPerCluster) - compressedLength > _bytesPerCluster) + { + int compClusters = Utilities.Ceil(compressedLength, _bytesPerCluster); + totalAllocated += _rawStream.AllocateClusters(focusVcn, compClusters); + totalAllocated += _rawStream.WriteClusters(focusVcn, compClusters, _ioBuffer, 0); + totalAllocated -= _rawStream.ReleaseClusters(focusVcn + compClusters, _attr.CompressionUnitSize - compClusters); + } + else + { + totalAllocated += _rawStream.AllocateClusters(focusVcn, _attr.CompressionUnitSize); + totalAllocated += _rawStream.WriteClusters(focusVcn, _attr.CompressionUnitSize, buffer, offset); + } + + return totalAllocated; + } + + private long CompressionStart(long vcn) + { + return Utilities.RoundDown(vcn, _attr.CompressionUnitSize); + } + + private void LoadCache(long vcn) + { + long cuStart = CompressionStart(vcn); + if (_cacheBufferVcn != cuStart) + { + if (_rawStream.AreAllClustersStored(cuStart, _attr.CompressionUnitSize)) + { + // Uncompressed data - read straight into cache buffer + _rawStream.ReadClusters(cuStart, _attr.CompressionUnitSize, _cacheBuffer, 0); + } + else if (_rawStream.IsClusterStored(cuStart)) + { + // Compressed data - read via IO buffer + _rawStream.ReadClusters(cuStart, _attr.CompressionUnitSize, _ioBuffer, 0); + + int expected = (int)Math.Min(_attr.Length - (vcn * _bytesPerCluster), _attr.CompressionUnitSize * _bytesPerCluster); + + int decomp = _context.Options.Compressor.Decompress(_ioBuffer, 0, _ioBuffer.Length, _cacheBuffer, 0); + if (decomp < expected) + { + throw new IOException("Decompression returned too little data"); + } + } + else + { + // Sparse, wipe cache buffer directly + Array.Clear(_cacheBuffer, 0, _cacheBuffer.Length); + } + + _cacheBufferVcn = cuStart; + } + } + } +} diff --git a/DiscUtils/Ntfs/CookedDataRun.cs b/DiscUtils/Ntfs/CookedDataRun.cs new file mode 100644 index 0000000..d6a7e24 --- /dev/null +++ b/DiscUtils/Ntfs/CookedDataRun.cs @@ -0,0 +1,85 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + + internal class CookedDataRun + { + private long _startVcn; + private long _startLcn; + private DataRun _raw; + private NonResidentAttributeRecord _attributeExtent; + + public CookedDataRun(DataRun raw, long startVcn, long prevLcn, NonResidentAttributeRecord attributeExtent) + { + _raw = raw; + _startVcn = startVcn; + _startLcn = prevLcn + raw.RunOffset; + _attributeExtent = attributeExtent; + + if (startVcn < 0) + { + throw new ArgumentOutOfRangeException("startVcn", startVcn, "VCN must be >= 0"); + } + + if (_startLcn < 0) + { + throw new ArgumentOutOfRangeException("prevLcn", prevLcn, "LCN must be >= 0"); + } + } + + public long StartVcn + { + get { return _startVcn; } + } + + public long StartLcn + { + get { return _startLcn; } + set { _startLcn = value; } + } + + public long Length + { + get { return _raw.RunLength; } + set { _raw.RunLength = value; } + } + + public bool IsSparse + { + get { return _raw.IsSparse; } + } + + public DataRun DataRun + { + get { return _raw; } + } + + public NonResidentAttributeRecord AttributeExtent + { + get { return _attributeExtent; } + } + } +} diff --git a/DiscUtils/Ntfs/CookedDataRuns.cs b/DiscUtils/Ntfs/CookedDataRuns.cs new file mode 100644 index 0000000..971c88b --- /dev/null +++ b/DiscUtils/Ntfs/CookedDataRuns.cs @@ -0,0 +1,321 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class CookedDataRuns + { + private List _runs; + + private int _firstDirty = int.MaxValue; + private int _lastDirty = 0; + + public CookedDataRuns() + { + _runs = new List(); + } + + public CookedDataRuns(IEnumerable rawRuns, NonResidentAttributeRecord attributeExtent) + { + _runs = new List(); + Append(rawRuns, attributeExtent); + } + + public long NextVirtualCluster + { + get + { + if (_runs.Count == 0) + { + return 0; + } + else + { + int lastRun = _runs.Count - 1; + return _runs[lastRun].StartVcn + _runs[lastRun].Length; + } + } + } + + public CookedDataRun Last + { + get + { + if (_runs.Count == 0) + { + return null; + } + else + { + return _runs[_runs.Count - 1]; + } + } + } + + public int Count + { + get { return _runs.Count; } + } + + public CookedDataRun this[int index] + { + get { return _runs[index]; } + } + + public int FindDataRun(long vcn, int startIdx) + { + int numRuns = _runs.Count; + if (numRuns > 0) + { + CookedDataRun run = _runs[numRuns - 1]; + if (vcn >= run.StartVcn) + { + if (run.StartVcn + run.Length > vcn) + { + return numRuns - 1; + } + else + { + throw new IOException("Looking for VCN outside of data runs"); + } + } + + for (int i = startIdx; i < numRuns; ++i) + { + run = _runs[i]; + if (run.StartVcn + run.Length > vcn) + { + return i; + } + } + } + + throw new IOException("Looking for VCN outside of data runs"); + } + + public void Append(DataRun rawRun, NonResidentAttributeRecord attributeExtent) + { + CookedDataRun last = Last; + _runs.Add(new CookedDataRun(rawRun, NextVirtualCluster, last == null ? 0 : last.StartLcn, attributeExtent)); + } + + public void Append(IEnumerable rawRuns, NonResidentAttributeRecord attributeExtent) + { + long vcn = NextVirtualCluster; + long lcn = 0; + foreach (var run in rawRuns) + { + _runs.Add(new CookedDataRun(run, vcn, lcn, attributeExtent)); + vcn += run.RunLength; + lcn += run.RunOffset; + } + } + + public void MakeSparse(int index) + { + if (index < _firstDirty) + { + _firstDirty = index; + } + + if (index > _lastDirty) + { + _lastDirty = index; + } + + long prevLcn = index == 0 ? 0 : _runs[index - 1].StartLcn; + CookedDataRun run = _runs[index]; + + if (run.IsSparse) + { + throw new ArgumentException("Run is already sparse", "index"); + } + + _runs[index] = new CookedDataRun(new DataRun(0, run.Length, true), run.StartVcn, prevLcn, run.AttributeExtent); + run.AttributeExtent.ReplaceRun(run.DataRun, _runs[index].DataRun); + + for (int i = index + 1; i < _runs.Count; ++i) + { + if (!_runs[i].IsSparse) + { + _runs[i].DataRun.RunOffset += run.StartLcn - prevLcn; + break; + } + } + } + + public void MakeNonSparse(int index, IEnumerable rawRuns) + { + if (index < _firstDirty) + { + _firstDirty = index; + } + + if (index > _lastDirty) + { + _lastDirty = index; + } + + long prevLcn = index == 0 ? 0 : _runs[index - 1].StartLcn; + CookedDataRun run = _runs[index]; + + if (!run.IsSparse) + { + throw new ArgumentException("Run is already non-sparse", "index"); + } + + _runs.RemoveAt(index); + int insertIdx = run.AttributeExtent.RemoveRun(run.DataRun); + + CookedDataRun lastNewRun = null; + long lcn = prevLcn; + long vcn = run.StartVcn; + foreach (var rawRun in rawRuns) + { + CookedDataRun newRun = new CookedDataRun(rawRun, vcn, lcn, run.AttributeExtent); + + _runs.Insert(index, newRun); + run.AttributeExtent.InsertRun(insertIdx, rawRun); + + vcn += rawRun.RunLength; + lcn += rawRun.RunOffset; + + lastNewRun = newRun; + insertIdx++; + + index++; + } + + for (int i = index; i < _runs.Count; ++i) + { + if (_runs[i].IsSparse) + { + _runs[i].StartLcn = lastNewRun.StartLcn; + } + else + { + _runs[i].DataRun.RunOffset = _runs[i].StartLcn - lastNewRun.StartLcn; + break; + } + } + } + + public void SplitRun(int runIdx, long vcn) + { + if (runIdx < _firstDirty) + { + _firstDirty = runIdx; + } + + if (runIdx > _lastDirty) + { + _lastDirty = runIdx; + } + + CookedDataRun run = _runs[runIdx]; + + if (run.StartVcn >= vcn || run.StartVcn + run.Length <= vcn) + { + throw new ArgumentException("Attempt to split run outside of it's range", "vcn"); + } + + long distance = vcn - run.StartVcn; + long offset = run.IsSparse ? 0 : distance; + CookedDataRun newRun = new CookedDataRun(new DataRun(offset, run.Length - distance, run.IsSparse), vcn, run.StartLcn, run.AttributeExtent); + + run.Length = distance; + + _runs.Insert(runIdx + 1, newRun); + run.AttributeExtent.InsertRun(run.DataRun, newRun.DataRun); + + for (int i = runIdx + 2; i < _runs.Count; ++i) + { + if (_runs[i].IsSparse) + { + _runs[i].StartLcn += offset; + } + else + { + _runs[i].DataRun.RunOffset -= offset; + break; + } + } + } + + /// + /// Truncates the set of data runs. + /// + /// The first run to be truncated. + public void TruncateAt(int index) + { + while (index < _runs.Count) + { + _runs[index].AttributeExtent.RemoveRun(_runs[index].DataRun); + _runs.RemoveAt(index); + } + } + + internal void CollapseRuns() + { + int i = _firstDirty > 1 ? _firstDirty - 1 : 0; + while (i < _runs.Count - 1 && i <= _lastDirty + 1) + { + if (_runs[i].IsSparse && _runs[i + 1].IsSparse) + { + _runs[i].Length += _runs[i + 1].Length; + _runs[i + 1].AttributeExtent.RemoveRun(_runs[i + 1].DataRun); + _runs.RemoveAt(i + 1); + } + else if (!_runs[i].IsSparse && !_runs[i].IsSparse && _runs[i].StartLcn + _runs[i].Length == _runs[i + 1].StartLcn) + { + _runs[i].Length += _runs[i + 1].Length; + _runs[i + 1].AttributeExtent.RemoveRun(_runs[i + 1].DataRun); + _runs.RemoveAt(i + 1); + + for (int j = i + 1; j < _runs.Count; ++j) + { + if (_runs[j].IsSparse) + { + _runs[j].StartLcn = _runs[i].StartLcn; + } + else + { + _runs[j].DataRun.RunOffset = _runs[j].StartLcn - _runs[i].StartLcn; + break; + } + } + } + else + { + ++i; + } + } + + _firstDirty = int.MaxValue; + _lastDirty = 0; + } + } +} diff --git a/DiscUtils/Ntfs/DataRun.cs b/DiscUtils/Ntfs/DataRun.cs new file mode 100644 index 0000000..99739e2 --- /dev/null +++ b/DiscUtils/Ntfs/DataRun.cs @@ -0,0 +1,173 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.Globalization; + using System.IO; + + internal class DataRun + { + private long _runLength; + private long _runOffset; + private bool _isSparse; + + public DataRun() + { + } + + public DataRun(long offset, long length, bool isSparse) + { + _runOffset = offset; + _runLength = length; + _isSparse = isSparse; + } + + public long RunLength + { + get { return _runLength; } + set { _runLength = value; } + } + + public long RunOffset + { + get { return _runOffset; } + set { _runOffset = value; } + } + + public bool IsSparse + { + get { return _isSparse; } + } + + internal int Size + { + get + { + int runLengthSize = VarLongSize(_runLength); + int runOffsetSize = VarLongSize(_runOffset); + return 1 + runLengthSize + runOffsetSize; + } + } + + public int Read(byte[] buffer, int offset) + { + int runOffsetSize = (buffer[offset] >> 4) & 0x0F; + int runLengthSize = buffer[offset] & 0x0F; + + _runLength = ReadVarLong(buffer, offset + 1, runLengthSize); + _runOffset = ReadVarLong(buffer, offset + 1 + runLengthSize, runOffsetSize); + _isSparse = runOffsetSize == 0; + + return 1 + runLengthSize + runOffsetSize; + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0:+##;-##;0}[+{1}]", _runOffset, _runLength); + } + + internal int Write(byte[] buffer, int offset) + { + int runLengthSize = WriteVarLong(buffer, offset + 1, _runLength); + int runOffsetSize = _isSparse ? 0 : WriteVarLong(buffer, offset + 1 + runLengthSize, _runOffset); + + buffer[offset] = (byte)((runLengthSize & 0x0F) | ((runOffsetSize << 4) & 0xF0)); + + return 1 + runLengthSize + runOffsetSize; + } + + private static long ReadVarLong(byte[] buffer, int offset, int size) + { + ulong val = 0; + bool signExtend = false; + + for (int i = 0; i < size; ++i) + { + byte b = buffer[offset + i]; + val = val | (((ulong)b) << (i * 8)); + signExtend = (b & 0x80) != 0; + } + + if (signExtend) + { + for (int i = size; i < 8; ++i) + { + val = val | (((ulong)0xFF) << (i * 8)); + } + } + + return (long)val; + } + + private static int WriteVarLong(byte[] buffer, int offset, long val) + { + bool isPositive = val >= 0; + + int pos = 0; + do + { + buffer[offset + pos] = (byte)(val & 0xFF); + val >>= 8; + pos++; + } + while (val != 0 && val != -1); + + // Avoid appearing to have a negative number that is actually positive, + // record an extra empty byte if needed. + if (isPositive && (buffer[offset + pos - 1] & 0x80) != 0) + { + buffer[offset + pos] = 0; + pos++; + } + else if (!isPositive && (buffer[offset + pos - 1] & 0x80) != 0x80) + { + buffer[offset + pos] = 0xFF; + pos++; + } + + return pos; + } + + private static int VarLongSize(long val) + { + bool isPositive = val >= 0; + bool lastByteHighBitSet = false; + + int len = 0; + do + { + lastByteHighBitSet = (val & 0x80) != 0; + val >>= 8; + len++; + } + while (val != 0 && val != -1); + + if ((isPositive && lastByteHighBitSet) || (!isPositive && !lastByteHighBitSet)) + { + len++; + } + + return len; + } + } +} diff --git a/DiscUtils/Ntfs/Directory.cs b/DiscUtils/Ntfs/Directory.cs new file mode 100644 index 0000000..5e4d0c8 --- /dev/null +++ b/DiscUtils/Ntfs/Directory.cs @@ -0,0 +1,291 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Text; + using DirectoryIndexEntry = System.Collections.Generic.KeyValuePair; + + internal class Directory : File + { + private IndexView _index; + + public Directory(INtfsContext context, FileRecord baseRecord) + : base(context, baseRecord) + { + } + + public bool IsEmpty + { + get { return Index.Count == 0; } + } + + private IndexView Index + { + get + { + if (_index == null && StreamExists(AttributeType.IndexRoot, "$I30")) + { + _index = new IndexView(GetIndex("$I30")); + } + + return _index; + } + } + + public IEnumerable GetAllEntries(bool filter) + { + IEnumerable entries = filter ? FilterEntries(Index.Entries) : Index.Entries; + + foreach (var entry in entries) + { + yield return new DirectoryEntry(this, entry.Value, entry.Key); + } + } + + public void UpdateEntry(DirectoryEntry entry) + { + Index[entry.Details] = entry.Reference; + UpdateRecordInMft(); + } + + public override void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "DIRECTORY (" + base.ToString() + ")"); + writer.WriteLine(indent + " File Number: " + IndexInMft); + + if (Index != null) + { + foreach (var entry in Index.Entries) + { + writer.WriteLine(indent + " DIRECTORY ENTRY (" + entry.Key.FileName + ")"); + writer.WriteLine(indent + " MFT Ref: " + entry.Value); + entry.Key.Dump(writer, indent + " "); + } + } + } + + public override string ToString() + { + return base.ToString() + @"\"; + } + + internal static new Directory CreateNew(INtfsContext context, FileAttributeFlags parentDirFlags) + { + Directory dir = (Directory)context.AllocateFile(FileRecordFlags.IsDirectory); + + StandardInformation.InitializeNewFile( + dir, + FileAttributeFlags.Archive | (parentDirFlags & FileAttributeFlags.Compressed)); + + // Create the index root attribute by instantiating a new index + dir.CreateIndex("$I30", AttributeType.FileName, AttributeCollationRule.Filename); + + dir.UpdateRecordInMft(); + + return dir; + } + + internal DirectoryEntry GetEntryByName(string name) + { + string searchName = name; + + int streamSepPos = name.IndexOf(':'); + if (streamSepPos >= 0) + { + searchName = name.Substring(0, streamSepPos); + } + + DirectoryIndexEntry entry = Index.FindFirst(new FileNameQuery(searchName, _context.UpperCase)); + if (entry.Key != null) + { + return new DirectoryEntry(this, entry.Value, entry.Key); + } + else + { + return null; + } + } + + internal DirectoryEntry AddEntry(File file, string name, FileNameNamespace nameNamespace) + { + if (name.Length > 255) + { + throw new IOException("Invalid file name, more than 255 characters: " + name); + } + else if (name.IndexOfAny(new char[] { '\0', '/' }) != -1) + { + throw new IOException(@"Invalid file name, contains '\0' or '/': " + name); + } + + FileNameRecord newNameRecord = file.GetFileNameRecord(null, true); + newNameRecord.FileNameNamespace = nameNamespace; + newNameRecord.FileName = name; + newNameRecord.ParentDirectory = MftReference; + + NtfsStream nameStream = file.CreateStream(AttributeType.FileName, null); + nameStream.SetContent(newNameRecord); + + file.HardLinkCount++; + file.UpdateRecordInMft(); + + Index[newNameRecord] = file.MftReference; + + Modified(); + UpdateRecordInMft(); + + return new DirectoryEntry(this, file.MftReference, newNameRecord); + } + + internal void RemoveEntry(DirectoryEntry dirEntry) + { + File file = _context.GetFileByRef(dirEntry.Reference); + + FileNameRecord nameRecord = dirEntry.Details; + + Index.Remove(dirEntry.Details); + + foreach (NtfsStream stream in file.GetStreams(AttributeType.FileName, null)) + { + FileNameRecord streamName = stream.GetContent(); + if (nameRecord.Equals(streamName)) + { + file.RemoveStream(stream); + break; + } + } + + file.HardLinkCount--; + file.UpdateRecordInMft(); + + Modified(); + UpdateRecordInMft(); + } + + internal string CreateShortName(string name) + { + string baseName = string.Empty; + string ext = string.Empty; + + int lastPeriod = name.LastIndexOf('.'); + + int i = 0; + while (baseName.Length < 6 && i < name.Length && i != lastPeriod) + { + char upperChar = Char.ToUpperInvariant(name[i]); + if (Utilities.Is8Dot3Char(upperChar)) + { + baseName += upperChar; + } + + ++i; + } + + if (lastPeriod >= 0) + { + i = lastPeriod + 1; + while (ext.Length < 3 && i < name.Length) + { + char upperChar = Char.ToUpperInvariant(name[i]); + if (Utilities.Is8Dot3Char(upperChar)) + { + ext += upperChar; + } + + ++i; + } + } + + i = 1; + string candidate; + do + { + string suffix = string.Format(CultureInfo.InvariantCulture, "~{0}", i); + candidate = baseName.Substring(0, Math.Min(8 - suffix.Length, baseName.Length)) + suffix + (ext.Length > 0 ? "." + ext : string.Empty); + i++; + } + while (GetEntryByName(candidate) != null); + + return candidate; + } + + private List FilterEntries(IEnumerable entriesIter) + { + List entries = new List(entriesIter); + + // Weed out short-name entries for files and any hidden / system / metadata files. + int i = 0; + while (i < entries.Count) + { + DirectoryIndexEntry entry = entries[i]; + + if (((entry.Key.Flags & FileAttributeFlags.Hidden) != 0) && _context.Options.HideHiddenFiles) + { + entries.RemoveAt(i); + } + else if (((entry.Key.Flags & FileAttributeFlags.System) != 0) && _context.Options.HideSystemFiles) + { + entries.RemoveAt(i); + } + else if (entry.Value.MftIndex < 24 && _context.Options.HideMetafiles) + { + entries.RemoveAt(i); + } + else if (entry.Key.FileNameNamespace == FileNameNamespace.Dos && _context.Options.HideDosFileNames) + { + entries.RemoveAt(i); + } + else + { + ++i; + } + } + + return entries; + } + + private sealed class FileNameQuery : IComparable + { + private byte[] _query; + private UpperCase _upperCase; + + public FileNameQuery(string query, UpperCase upperCase) + { + _query = Encoding.Unicode.GetBytes(query); + _upperCase = upperCase; + } + + public int CompareTo(byte[] buffer) + { + // Note: this is internal knowledge of FileNameRecord structure - but for performance + // reasons, we don't want to decode the entire structure. In fact can avoid the string + // conversion as well. + byte fnLen = buffer[0x40]; + return _upperCase.Compare(_query, 0, _query.Length, buffer, 0x42, fnLen * 2); + } + } + } +} diff --git a/DiscUtils/Ntfs/DirectoryEntry.cs b/DiscUtils/Ntfs/DirectoryEntry.cs new file mode 100644 index 0000000..49a8a8f --- /dev/null +++ b/DiscUtils/Ntfs/DirectoryEntry.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + internal class DirectoryEntry + { + private Directory _directory; + private FileRecordReference _fileReference; + private FileNameRecord _fileDetails; + + public DirectoryEntry(Directory directory, FileRecordReference fileReference, FileNameRecord fileDetails) + { + _directory = directory; + _fileReference = fileReference; + _fileDetails = fileDetails; + } + + public FileRecordReference Reference + { + get { return _fileReference; } + } + + public FileNameRecord Details + { + get { return _fileDetails; } + } + + public bool IsDirectory + { + get { return (_fileDetails.Flags & FileAttributeFlags.Directory) != 0; } + } + + public string SearchName + { + get + { + string fileName = _fileDetails.FileName; + if (fileName.IndexOf('.') == -1) + { + return fileName + "."; + } + else + { + return fileName; + } + } + } + + internal void UpdateFrom(File file) + { + file.FreshenFileName(_fileDetails, true); + _directory.UpdateEntry(this); + } + } +} diff --git a/DiscUtils/Ntfs/File.cs b/DiscUtils/Ntfs/File.cs new file mode 100644 index 0000000..a192a3f --- /dev/null +++ b/DiscUtils/Ntfs/File.cs @@ -0,0 +1,1279 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class File + { + protected INtfsContext _context; + + private MasterFileTable _mft; + private List _records; + private ObjectCache _indexCache; + private List _attributes; + + private bool _dirty; + + public File(INtfsContext context, FileRecord baseRecord) + { + _context = context; + _mft = _context.Mft; + _records = new List(); + _records.Add(baseRecord); + _indexCache = new ObjectCache(); + _attributes = new List(); + + LoadAttributes(); + } + + public uint IndexInMft + { + get { return _records[0].MasterFileTableIndex; } + } + + public uint MaxMftRecordSize + { + get { return _records[0].AllocatedSize; } + } + + public FileRecordReference MftReference + { + get { return _records[0].Reference; } + } + + public List Names + { + get + { + List result = new List(); + + if (IndexInMft == MasterFileTable.RootDirIndex) + { + result.Add(string.Empty); + } + else + { + foreach (StructuredNtfsAttribute attr in GetAttributes(AttributeType.FileName)) + { + string name = attr.Content.FileName; + + Directory parentDir = _context.GetDirectoryByRef(attr.Content.ParentDirectory); + if (parentDir != null) + { + foreach (string dirName in parentDir.Names) + { + result.Add(Utilities.CombinePaths(dirName, name)); + } + } + } + } + + return result; + } + } + + public bool HasWin32OrDosName + { + get + { + foreach (StructuredNtfsAttribute attr in GetAttributes(AttributeType.FileName)) + { + FileNameRecord fnr = attr.Content; + if (fnr.FileNameNamespace != FileNameNamespace.Posix) + { + return true; + } + } + + return false; + } + } + + public bool MftRecordIsDirty + { + get + { + return _dirty; + } + } + + public ushort HardLinkCount + { + get { return _records[0].HardLinkCount; } + set { _records[0].HardLinkCount = value; } + } + + public IEnumerable AllStreams + { + get + { + foreach (var attr in _attributes) + { + yield return new NtfsStream(this, attr); + } + } + } + + public DirectoryEntry DirectoryEntry + { + get + { + if (_context.GetDirectoryByRef == null) + { + return null; + } + + NtfsStream stream = GetStream(AttributeType.FileName, null); + if (stream == null) + { + return null; + } + + FileNameRecord record = stream.GetContent(); + + // Root dir is stored without root directory flag set in FileNameRecord, simulate it. + if (_records[0].MasterFileTableIndex == MasterFileTable.RootDirIndex) + { + record.Flags |= FileAttributeFlags.Directory; + } + + return new DirectoryEntry(_context.GetDirectoryByRef(record.ParentDirectory), MftReference, record); + } + } + + public string BestName + { + get + { + NtfsAttribute[] attrs = GetAttributes(AttributeType.FileName); + + string bestName = null; + + if (attrs != null && attrs.Length != 0) + { + bestName = attrs[0].ToString(); + + for (int i = 1; i < attrs.Length; ++i) + { + string name = attrs[i].ToString(); + + if (Utilities.Is8Dot3(bestName)) + { + bestName = name; + } + } + } + + return bestName; + } + } + + public bool IsDirectory + { + get { return (_records[0].Flags & FileRecordFlags.IsDirectory) != 0; } + } + + public StandardInformation StandardInformation + { + get { return GetStream(AttributeType.StandardInformation, null).GetContent(); } + } + + internal INtfsContext Context + { + get + { + return _context; + } + } + + /// + /// Gets an enumeration of all the attributes. + /// + internal IEnumerable AllAttributes + { + get { return _attributes; } + } + + public static File CreateNew(INtfsContext context, FileAttributeFlags dirFlags) + { + return CreateNew(context, FileRecordFlags.None, dirFlags); + } + + public static File CreateNew(INtfsContext context, FileRecordFlags flags, FileAttributeFlags dirFlags) + { + File newFile = context.AllocateFile(flags); + + FileAttributeFlags fileFlags = + FileAttributeFlags.Archive + | FileRecord.ConvertFlags(flags) + | (dirFlags & FileAttributeFlags.Compressed); + + AttributeFlags dataAttrFlags = AttributeFlags.None; + if ((dirFlags & FileAttributeFlags.Compressed) != 0) + { + dataAttrFlags |= AttributeFlags.Compressed; + } + + StandardInformation.InitializeNewFile(newFile, fileFlags); + + if (context.ObjectIds != null) + { + Guid newId = CreateNewGuid(context); + NtfsStream stream = newFile.CreateStream(AttributeType.ObjectId, null); + ObjectId objId = new ObjectId(); + objId.Id = newId; + stream.SetContent(objId); + context.ObjectIds.Add(newId, newFile.MftReference, newId, Guid.Empty, Guid.Empty); + } + + newFile.CreateAttribute(AttributeType.Data, dataAttrFlags); + + newFile.UpdateRecordInMft(); + + return newFile; + } + + public int MftRecordFreeSpace(AttributeType attrType, string attrName) + { + foreach (var record in _records) + { + if (record.GetAttribute(attrType, attrName) != null) + { + return _mft.RecordSize - record.Size; + } + } + + throw new IOException("Attempt to determine free space for non-existent attribute"); + } + + public void Modified() + { + DateTime now = DateTime.UtcNow; + + NtfsStream siStream = GetStream(AttributeType.StandardInformation, null); + StandardInformation si = siStream.GetContent(); + si.LastAccessTime = now; + si.ModificationTime = now; + siStream.SetContent(si); + + MarkMftRecordDirty(); + } + + public void Accessed() + { + DateTime now = DateTime.UtcNow; + + NtfsStream siStream = GetStream(AttributeType.StandardInformation, null); + StandardInformation si = siStream.GetContent(); + si.LastAccessTime = now; + siStream.SetContent(si); + + MarkMftRecordDirty(); + } + + public void MarkMftRecordDirty() + { + _dirty = true; + } + + public void UpdateRecordInMft() + { + if (_dirty) + { + if (NtfsTransaction.Current != null) + { + NtfsStream stream = GetStream(AttributeType.StandardInformation, null); + StandardInformation si = stream.GetContent(); + si.MftChangedTime = NtfsTransaction.Current.Timestamp; + stream.SetContent(si); + } + + bool fixesApplied = true; + while (fixesApplied) + { + fixesApplied = false; + + for (int i = 0; i < _records.Count; ++i) + { + var record = _records[i]; + + bool fixedAttribute = true; + while (record.Size > _mft.RecordSize && fixedAttribute) + { + fixedAttribute = false; + + if (!fixedAttribute && !record.IsMftRecord) + { + foreach (var attr in record.Attributes) + { + if (!attr.IsNonResident && !_context.AttributeDefinitions.MustBeResident(attr.AttributeType)) + { + MakeAttributeNonResident(new AttributeReference(record.Reference, attr.AttributeId), (int)attr.DataLength); + fixedAttribute = true; + break; + } + } + } + + if (!fixedAttribute) + { + foreach (var attr in record.Attributes) + { + if (attr.AttributeType == AttributeType.IndexRoot + && ShrinkIndexRoot(attr.Name)) + { + fixedAttribute = true; + break; + } + } + } + + if (!fixedAttribute) + { + if (record.Attributes.Count == 1) + { + fixedAttribute = SplitAttribute(record); + } + else + { + if (_records.Count == 1) + { + CreateAttributeList(); + } + + fixedAttribute = ExpelAttribute(record); + } + } + + fixesApplied |= fixedAttribute; + } + } + } + + _dirty = false; + foreach (var record in _records) + { + _mft.WriteRecord(record); + } + } + } + + public Index CreateIndex(string name, AttributeType attrType, AttributeCollationRule collRule) + { + Index.Create(attrType, collRule, this, name); + return GetIndex(name); + } + + public Index GetIndex(string name) + { + Index idx = _indexCache[name]; + + if (idx == null) + { + idx = new Index(this, name, _context.BiosParameterBlock, _context.UpperCase); + _indexCache[name] = idx; + } + + return idx; + } + + public void Delete() + { + if (_records[0].HardLinkCount != 0) + { + throw new InvalidOperationException("Attempt to delete in-use file: " + ToString()); + } + + _context.ForgetFile(this); + + NtfsStream objIdStream = GetStream(AttributeType.ObjectId, null); + if (objIdStream != null) + { + ObjectId objId = objIdStream.GetContent(); + Context.ObjectIds.Remove(objId.Id); + } + + // Truncate attributes, allowing for truncation silently removing the AttributeList attribute + // in some cases (large file with all attributes first extent in the first MFT record). This + // releases all allocated clusters in most cases. + List truncateAttrs = new List(_attributes.Count); + foreach (var attr in _attributes) + { + if (attr.Type != AttributeType.AttributeList) + { + truncateAttrs.Add(attr); + } + } + + foreach (NtfsAttribute attr in truncateAttrs) + { + attr.GetDataBuffer().SetCapacity(0); + } + + // If the attribute list record remains, free any possible clusters it owns. We've now freed + // all clusters. + NtfsAttribute attrList = GetAttribute(AttributeType.AttributeList, null); + if (attrList != null) + { + attrList.GetDataBuffer().SetCapacity(0); + } + + // Now go through the MFT records, freeing them up + foreach (var mftRecord in _records) + { + _context.Mft.RemoveRecord(mftRecord.Reference); + } + + _attributes.Clear(); + _records.Clear(); + } + + public bool StreamExists(AttributeType attrType, string name) + { + return GetStream(attrType, name) != null; + } + + public NtfsStream GetStream(AttributeType attrType, string name) + { + foreach (NtfsStream stream in GetStreams(attrType, name)) + { + return stream; + } + + return null; + } + + public IEnumerable GetStreams(AttributeType attrType, string name) + { + foreach (var attr in _attributes) + { + if (attr.Type == attrType && attr.Name == name) + { + yield return new NtfsStream(this, attr); + } + } + } + + public NtfsStream CreateStream(AttributeType attrType, string name) + { + return new NtfsStream(this, CreateAttribute(attrType, name, AttributeFlags.None)); + } + + public NtfsStream CreateStream(AttributeType attrType, string name, long firstCluster, ulong numClusters, uint bytesPerCluster) + { + return new NtfsStream(this, CreateAttribute(attrType, name, AttributeFlags.None, firstCluster, numClusters, bytesPerCluster)); + } + + public SparseStream OpenStream(AttributeType attrType, string name, FileAccess access) + { + NtfsAttribute attr = GetAttribute(attrType, name); + if (attr != null) + { + return new FileStream(this, attr, access); + } + + return null; + } + + public void RemoveStream(NtfsStream stream) + { + RemoveAttribute(stream.Attribute); + } + + public FileNameRecord GetFileNameRecord(string name, bool freshened) + { + NtfsAttribute[] attrs = GetAttributes(AttributeType.FileName); + StructuredNtfsAttribute attr = null; + if (String.IsNullOrEmpty(name)) + { + if (attrs.Length != 0) + { + attr = (StructuredNtfsAttribute)attrs[0]; + } + } + else + { + foreach (StructuredNtfsAttribute a in attrs) + { + if (_context.UpperCase.Compare(a.Content.FileName, name) == 0) + { + attr = a; + } + } + + if (attr == null) + { + throw new FileNotFoundException("File name not found on file", name); + } + } + + FileNameRecord fnr = attr == null ? new FileNameRecord() : new FileNameRecord(attr.Content); + + if (freshened) + { + FreshenFileName(fnr, false); + } + + return fnr; + } + + public virtual void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "FILE (" + ToString() + ")"); + writer.WriteLine(indent + " File Number: " + _records[0].MasterFileTableIndex); + + _records[0].Dump(writer, indent + " "); + + foreach (AttributeRecord attrRec in _records[0].Attributes) + { + NtfsAttribute.FromRecord(this, MftReference, attrRec).Dump(writer, indent + " "); + } + } + + public override string ToString() + { + string bestName = BestName; + if (bestName == null) + { + return "?????"; + } + else + { + return bestName; + } + } + + internal void RemoveAttributeExtents(NtfsAttribute attr) + { + attr.GetDataBuffer().SetCapacity(0); + + foreach (var extentRef in attr.Extents.Keys) + { + RemoveAttributeExtent(extentRef); + } + } + + internal void RemoveAttributeExtent(AttributeReference extentRef) + { + FileRecord fileRec = GetFileRecord(extentRef.File); + if (fileRec != null) + { + fileRec.RemoveAttribute(extentRef.AttributeId); + + // Remove empty non-primary MFT records + if (fileRec.Attributes.Count == 0 && fileRec.BaseFile.Value != 0) + { + RemoveFileRecord(extentRef.File); + } + } + } + + /// + /// Gets an attribute by reference. + /// + /// Reference to the attribute. + /// The attribute. + internal NtfsAttribute GetAttribute(AttributeReference attrRef) + { + foreach (var attr in _attributes) + { + if (attr.Reference.Equals(attrRef)) + { + return attr; + } + } + + return null; + } + + /// + /// Gets the first (if more than one) instance of a named attribute. + /// + /// The attribute type. + /// The attribute's name. + /// The attribute of null. + internal NtfsAttribute GetAttribute(AttributeType type, string name) + { + foreach (var attr in _attributes) + { + if (attr.PrimaryRecord.AttributeType == type && attr.Name == name) + { + return attr; + } + } + + return null; + } + + /// + /// Gets all instances of an unnamed attribute. + /// + /// The attribute type. + /// The attributes. + internal NtfsAttribute[] GetAttributes(AttributeType type) + { + List matches = new List(); + + foreach (var attr in _attributes) + { + if (attr.PrimaryRecord.AttributeType == type && string.IsNullOrEmpty(attr.Name)) + { + matches.Add(attr); + } + } + + return matches.ToArray(); + } + + internal void MakeAttributeNonResident(AttributeReference attrRef, int maxData) + { + NtfsAttribute attr = GetAttribute(attrRef); + if (attr.IsNonResident) + { + throw new InvalidOperationException("Attribute is already non-resident"); + } + + ushort id = _records[0].CreateNonResidentAttribute(attr.Type, attr.Name, attr.Flags); + AttributeRecord newAttrRecord = _records[0].GetAttribute(id); + + IBuffer attrBuffer = attr.GetDataBuffer(); + byte[] tempData = Utilities.ReadFully(attrBuffer, 0, (int)Math.Min(maxData, attrBuffer.Capacity)); + + RemoveAttributeExtents(attr); + attr.SetExtent(_records[0].Reference, newAttrRecord); + + attr.GetDataBuffer().Write(0, tempData, 0, tempData.Length); + + UpdateAttributeList(); + } + + internal void FreshenFileName(FileNameRecord fileName, bool updateMftRecord) + { + // + // Freshen the record from the definitive info in the other attributes + // + StandardInformation si = StandardInformation; + NtfsAttribute anonDataAttr = GetAttribute(AttributeType.Data, null); + + fileName.CreationTime = si.CreationTime; + fileName.ModificationTime = si.ModificationTime; + fileName.MftChangedTime = si.MftChangedTime; + fileName.LastAccessTime = si.LastAccessTime; + fileName.Flags = si.FileAttributes; + + if (_dirty && NtfsTransaction.Current != null) + { + fileName.MftChangedTime = NtfsTransaction.Current.Timestamp; + } + + // Directories don't have directory flag set in StandardInformation, so set from MFT record + if ((_records[0].Flags & FileRecordFlags.IsDirectory) != 0) + { + fileName.Flags |= FileAttributeFlags.Directory; + } + + if (anonDataAttr != null) + { + fileName.RealSize = (ulong)anonDataAttr.PrimaryRecord.DataLength; + fileName.AllocatedSize = (ulong)anonDataAttr.PrimaryRecord.AllocatedLength; + } + + if (updateMftRecord) + { + foreach (NtfsStream stream in GetStreams(AttributeType.FileName, null)) + { + FileNameRecord fnr = stream.GetContent(); + if (fnr.Equals(fileName)) + { + fnr = new FileNameRecord(fileName); + fnr.Flags &= ~FileAttributeFlags.ReparsePoint; + stream.SetContent(fnr); + } + } + } + } + + internal long GetAttributeOffset(AttributeReference attrRef) + { + long recordOffset = _mft.GetRecordOffset(attrRef.File); + + FileRecord frs = GetFileRecord(attrRef.File); + return recordOffset + frs.GetAttributeOffset(attrRef.AttributeId); + } + + private static Guid CreateNewGuid(INtfsContext context) + { + Random rng = context.Options.RandomNumberGenerator; + if (rng != null) + { + byte[] buffer = new byte[16]; + rng.NextBytes(buffer); + return new Guid(buffer); + } + else + { + return Guid.NewGuid(); + } + } + + private void LoadAttributes() + { + Dictionary extraFileRecords = new Dictionary(); + + AttributeRecord attrListRec = _records[0].GetAttribute(AttributeType.AttributeList); + if (attrListRec != null) + { + NtfsAttribute lastAttr = null; + + StructuredNtfsAttribute attrListAttr = (StructuredNtfsAttribute)NtfsAttribute.FromRecord(this, MftReference, attrListRec); + var attrList = attrListAttr.Content; + _attributes.Add(attrListAttr); + + foreach (var record in attrList) + { + FileRecord attrFileRecord = _records[0]; + if (record.BaseFileReference.MftIndex != _records[0].MasterFileTableIndex) + { + if (!extraFileRecords.TryGetValue(record.BaseFileReference.MftIndex, out attrFileRecord)) + { + attrFileRecord = _context.Mft.GetRecord(record.BaseFileReference); + if (attrFileRecord != null) + { + extraFileRecords[attrFileRecord.MasterFileTableIndex] = attrFileRecord; + } + } + } + + if (attrFileRecord != null) + { + AttributeRecord attrRec = attrFileRecord.GetAttribute(record.AttributeId); + + if (attrRec != null) + { + if (record.StartVcn == 0) + { + lastAttr = NtfsAttribute.FromRecord(this, record.BaseFileReference, attrRec); + _attributes.Add(lastAttr); + } + else + { + lastAttr.AddExtent(record.BaseFileReference, attrRec); + } + } + } + } + + foreach (var extraFileRecord in extraFileRecords) + { + _records.Add(extraFileRecord.Value); + } + } + else + { + foreach (var record in _records[0].Attributes) + { + _attributes.Add(NtfsAttribute.FromRecord(this, MftReference, record)); + } + } + } + + private bool SplitAttribute(FileRecord record) + { + if (record.Attributes.Count != 1) + { + throw new InvalidOperationException("Attempting to split attribute in MFT record containing multiple attributes"); + } + + return SplitAttribute(record, (NonResidentAttributeRecord)record.FirstAttribute, false); + } + + private bool SplitAttribute(FileRecord record, NonResidentAttributeRecord targetAttr, bool atStart) + { + if (targetAttr.DataRuns.Count <= 1) + { + return false; + } + + int splitIndex = 1; + if (!atStart) + { + List runs = targetAttr.DataRuns; + + splitIndex = runs.Count - 1; + int saved = runs[splitIndex].Size; + while (splitIndex > 1 && record.Size - saved > record.AllocatedSize) + { + --splitIndex; + saved += runs[splitIndex].Size; + } + } + + AttributeRecord newAttr = targetAttr.Split(splitIndex); + + // Find a home for the new attribute record + FileRecord newAttrHome = null; + foreach (var targetRecord in _records) + { + if (!targetRecord.IsMftRecord && _mft.RecordSize - targetRecord.Size >= newAttr.Size) + { + targetRecord.AddAttribute(newAttr); + newAttrHome = targetRecord; + } + } + + if (newAttrHome == null) + { + newAttrHome = _mft.AllocateRecord(_records[0].Flags & (~FileRecordFlags.InUse), record.IsMftRecord); + newAttrHome.BaseFile = record.BaseFile.IsNull ? record.Reference : record.BaseFile; + _records.Add(newAttrHome); + newAttrHome.AddAttribute(newAttr); + } + + // Add the new attribute record as an extent on the attribute it split from + bool added = false; + foreach (var attr in _attributes) + { + foreach (var existingRecord in attr.Extents) + { + if (existingRecord.Key.File == record.Reference && existingRecord.Key.AttributeId == targetAttr.AttributeId) + { + attr.AddExtent(newAttrHome.Reference, newAttr); + added = true; + break; + } + } + + if (added) + { + break; + } + } + + UpdateAttributeList(); + + return true; + } + + private bool ExpelAttribute(FileRecord record) + { + if (record.MasterFileTableIndex == MasterFileTable.MftIndex) + { + // Special case for MFT - can't fully expel attributes, instead split most of the data runs off. + List attrs = record.Attributes; + for (int i = attrs.Count - 1; i >= 0; --i) + { + AttributeRecord attr = attrs[i]; + if (attr.AttributeType == AttributeType.Data) + { + if (SplitAttribute(record, (NonResidentAttributeRecord)attr, true)) + { + return true; + } + } + } + } + else + { + List attrs = record.Attributes; + for (int i = attrs.Count - 1; i >= 0; --i) + { + AttributeRecord attr = attrs[i]; + if (attr.AttributeType > AttributeType.AttributeList) + { + foreach (var targetRecord in _records) + { + if (_mft.RecordSize - targetRecord.Size >= attr.Size) + { + MoveAttribute(record, attr, targetRecord); + return true; + } + } + + FileRecord newFileRecord = _mft.AllocateRecord(FileRecordFlags.None, record.IsMftRecord); + newFileRecord.BaseFile = record.Reference; + _records.Add(newFileRecord); + MoveAttribute(record, attr, newFileRecord); + return true; + } + } + } + + return false; + } + + private void MoveAttribute(FileRecord record, AttributeRecord attrRec, FileRecord targetRecord) + { + AttributeReference oldRef = new AttributeReference(record.Reference, attrRec.AttributeId); + + record.RemoveAttribute(attrRec.AttributeId); + targetRecord.AddAttribute(attrRec); + + AttributeReference newRef = new AttributeReference(targetRecord.Reference, attrRec.AttributeId); + + foreach (var attr in _attributes) + { + attr.ReplaceExtent(oldRef, newRef, attrRec); + } + + UpdateAttributeList(); + } + + private void CreateAttributeList() + { + ushort id = _records[0].CreateAttribute(AttributeType.AttributeList, null, false, AttributeFlags.None); + + StructuredNtfsAttribute newAttr = (StructuredNtfsAttribute)NtfsAttribute.FromRecord(this, MftReference, _records[0].GetAttribute(id)); + _attributes.Add(newAttr); + UpdateAttributeList(); + } + + private void UpdateAttributeList() + { + if (_records.Count > 1) + { + AttributeList attrList = new AttributeList(); + + foreach (var attr in _attributes) + { + if (attr.Type != AttributeType.AttributeList) + { + foreach (var extent in attr.Extents) + { + attrList.Add(AttributeListRecord.FromAttribute(extent.Value, extent.Key.File)); + } + } + } + + StructuredNtfsAttribute alAttr; + alAttr = (StructuredNtfsAttribute)GetAttribute(AttributeType.AttributeList, null); + alAttr.Content = attrList; + alAttr.Save(); + } + } + + /// + /// Creates a new unnamed attribute. + /// + /// The type of the new attribute. + /// The flags of the new attribute. + /// The new attribute. + private NtfsAttribute CreateAttribute(AttributeType type, AttributeFlags flags) + { + return CreateAttribute(type, null, flags); + } + + /// + /// Creates a new attribute. + /// + /// The type of the new attribute. + /// The name of the new attribute. + /// The flags of the new attribute. + /// The new attribute. + private NtfsAttribute CreateAttribute(AttributeType type, string name, AttributeFlags flags) + { + bool indexed = _context.AttributeDefinitions.IsIndexed(type); + ushort id = _records[0].CreateAttribute(type, name, indexed, flags); + + AttributeRecord newAttrRecord = _records[0].GetAttribute(id); + NtfsAttribute newAttr = NtfsAttribute.FromRecord(this, MftReference, newAttrRecord); + + _attributes.Add(newAttr); + UpdateAttributeList(); + + MarkMftRecordDirty(); + + return newAttr; + } + + /// + /// Creates a new attribute at a fixed cluster. + /// + /// The type of the new attribute. + /// The name of the new attribute. + /// The flags of the new attribute. + /// The first cluster to assign to the attribute. + /// The number of sequential clusters to assign to the attribute. + /// The number of bytes in each cluster. + /// The new attribute. + private NtfsAttribute CreateAttribute(AttributeType type, string name, AttributeFlags flags, long firstCluster, ulong numClusters, uint bytesPerCluster) + { + bool indexed = _context.AttributeDefinitions.IsIndexed(type); + ushort id = _records[0].CreateNonResidentAttribute(type, name, flags, firstCluster, numClusters, bytesPerCluster); + + AttributeRecord newAttrRecord = _records[0].GetAttribute(id); + NtfsAttribute newAttr = NtfsAttribute.FromRecord(this, MftReference, newAttrRecord); + + _attributes.Add(newAttr); + UpdateAttributeList(); + MarkMftRecordDirty(); + return newAttr; + } + + private void RemoveAttribute(NtfsAttribute attr) + { + if (attr != null) + { + if (attr.PrimaryRecord.AttributeType == AttributeType.IndexRoot) + { + _indexCache.Remove(attr.PrimaryRecord.Name); + } + + RemoveAttributeExtents(attr); + + _attributes.Remove(attr); + + UpdateAttributeList(); + } + } + + private bool ShrinkIndexRoot(string indexName) + { + NtfsAttribute attr = GetAttribute(AttributeType.IndexRoot, indexName); + + // Nothing to win, can't make IndexRoot smaller than this + // 8 = min size of entry that points to IndexAllocation... + if (attr.Length <= IndexRoot.HeaderOffset + IndexHeader.Size + 8) + { + return false; + } + + Index idx = GetIndex(indexName); + return idx.ShrinkRoot(); + } + + private void MakeAttributeResident(AttributeReference attrRef, int maxData) + { + NtfsAttribute attr = GetAttribute(attrRef); + if (!attr.IsNonResident) + { + throw new InvalidOperationException("Attribute is already resident"); + } + + ushort id = _records[0].CreateAttribute(attr.Type, attr.Name, _context.AttributeDefinitions.IsIndexed(attr.Type), attr.Flags); + AttributeRecord newAttrRecord = _records[0].GetAttribute(id); + + IBuffer attrBuffer = attr.GetDataBuffer(); + byte[] tempData = Utilities.ReadFully(attrBuffer, 0, (int)Math.Min(maxData, attrBuffer.Capacity)); + + RemoveAttributeExtents(attr); + attr.SetExtent(_records[0].Reference, newAttrRecord); + + attr.GetDataBuffer().Write(0, tempData, 0, tempData.Length); + + UpdateAttributeList(); + } + + private FileRecord GetFileRecord(FileRecordReference fileReference) + { + foreach (var record in _records) + { + if (record.MasterFileTableIndex == fileReference.MftIndex) + { + return record; + } + } + + return null; + } + + private void RemoveFileRecord(FileRecordReference fileReference) + { + for (int i = 0; i < _records.Count; ++i) + { + if (_records[i].MasterFileTableIndex == fileReference.MftIndex) + { + FileRecord record = _records[i]; + + if (record.Attributes.Count > 0) + { + throw new IOException("Attempting to remove non-empty MFT record"); + } + + _context.Mft.RemoveRecord(fileReference); + _records.Remove(record); + + if (_records.Count == 1) + { + NtfsAttribute attrListAttr = GetAttribute(AttributeType.AttributeList, null); + if (attrListAttr != null) + { + RemoveAttribute(attrListAttr); + } + } + } + } + } + + /// + /// Wrapper for Resident/Non-Resident attribute streams, that remains valid + /// despite the attribute oscillating between resident and not. + /// + private class FileStream : SparseStream + { + private File _file; + private NtfsAttribute _attr; + private SparseStream _wrapped; + + public FileStream(File file, NtfsAttribute attr, FileAccess access) + { + _file = file; + _attr = attr; + _wrapped = attr.Open(access); + } + + public override IEnumerable Extents + { + get + { + return _wrapped.Extents; + } + } + + public override bool CanRead + { + get + { + return _wrapped.CanRead; + } + } + + public override bool CanSeek + { + get + { + return _wrapped.CanSeek; + } + } + + public override bool CanWrite + { + get + { + return _wrapped.CanWrite; + } + } + + public override long Length + { + get + { + return _wrapped.Length; + } + } + + public override long Position + { + get + { + return _wrapped.Position; + } + + set + { + _wrapped.Position = value; + } + } + + public override void Close() + { + base.Close(); + _wrapped.Close(); + } + + public override void Flush() + { + _wrapped.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped.Seek(offset, origin); + } + + public override void SetLength(long value) + { + ChangeAttributeResidencyByLength(value); + _wrapped.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_wrapped.Position + count > Length) + { + ChangeAttributeResidencyByLength(_wrapped.Position + count); + } + + _wrapped.Write(buffer, offset, count); + } + + public override void Clear(int count) + { + if (_wrapped.Position + count > Length) + { + ChangeAttributeResidencyByLength(_wrapped.Position + count); + } + + _wrapped.Clear(count); + } + + public override string ToString() + { + return _file.ToString() + ".attr[" + _attr.Id + "]"; + } + + /// + /// Change attribute residency if it gets too big (or small). + /// + /// The new (anticipated) length of the stream. + /// Has hysteresis - the decision is based on the input and the current + /// state, not the current state alone. + private void ChangeAttributeResidencyByLength(long value) + { + // This is a bit of a hack - but it's really important the bitmap file remains non-resident + if (_file._records[0].MasterFileTableIndex == MasterFileTable.BitmapIndex) + { + return; + } + + if (!_attr.IsNonResident && value >= _file.MaxMftRecordSize) + { + _file.MakeAttributeNonResident(_attr.Reference, (int)Math.Min(value, _wrapped.Length)); + } + else if (_attr.IsNonResident && value <= _file.MaxMftRecordSize / 4) + { + // Use of 1/4 of record size here is just a heuristic - the important thing is not to end up with + // zero-length non-resident attributes + _file.MakeAttributeResident(_attr.Reference, (int)value); + } + } + } + } +} diff --git a/DiscUtils/Ntfs/FileNameRecord.cs b/DiscUtils/Ntfs/FileNameRecord.cs new file mode 100644 index 0000000..cc32f3f --- /dev/null +++ b/DiscUtils/Ntfs/FileNameRecord.cs @@ -0,0 +1,210 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + using System.Text; + + [Flags] + internal enum FileAttributeFlags : uint + { + None = 0x00000000, + ReadOnly = 0x00000001, + Hidden = 0x00000002, + System = 0x00000004, + Archive = 0x00000020, + Device = 0x00000040, + Normal = 0x00000080, + Temporary = 0x00000100, + Sparse = 0x00000200, + ReparsePoint = 0x00000400, + Compressed = 0x00000800, + Offline = 0x00001000, + NotIndexed = 0x00002000, + Encrypted = 0x00004000, + Directory = 0x10000000, + IndexView = 0x20000000 + } + + internal enum FileNameNamespace : byte + { + Posix = 0, + Win32 = 1, + Dos = 2, + Win32AndDos = 3 + } + + internal class FileNameRecord : IByteArraySerializable, IDiagnosticTraceable, IEquatable + { + public FileRecordReference ParentDirectory; + public DateTime CreationTime; + public DateTime ModificationTime; + public DateTime MftChangedTime; + public DateTime LastAccessTime; + public ulong AllocatedSize; + public ulong RealSize; + public FileAttributeFlags Flags; + public uint EASizeOrReparsePointTag; + public FileNameNamespace FileNameNamespace; + public string FileName; + + public FileNameRecord() + { + } + + public FileNameRecord(FileNameRecord toCopy) + { + ParentDirectory = toCopy.ParentDirectory; + CreationTime = toCopy.CreationTime; + ModificationTime = toCopy.ModificationTime; + MftChangedTime = toCopy.MftChangedTime; + LastAccessTime = toCopy.LastAccessTime; + AllocatedSize = toCopy.AllocatedSize; + RealSize = toCopy.RealSize; + Flags = toCopy.Flags; + EASizeOrReparsePointTag = toCopy.EASizeOrReparsePointTag; + FileNameNamespace = toCopy.FileNameNamespace; + FileName = toCopy.FileName; + } + + public FileAttributes FileAttributes + { + get { return ConvertFlags(Flags); } + } + + public int Size + { + get + { + return 0x42 + (FileName.Length * 2); + } + } + + public override string ToString() + { + return FileName; + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "FILE NAME RECORD"); + writer.WriteLine(indent + " Parent Directory: " + ParentDirectory); + writer.WriteLine(indent + " Creation Time: " + CreationTime); + writer.WriteLine(indent + " Modification Time: " + ModificationTime); + writer.WriteLine(indent + " MFT Changed Time: " + MftChangedTime); + writer.WriteLine(indent + " Last Access Time: " + LastAccessTime); + writer.WriteLine(indent + " Allocated Size: " + AllocatedSize); + writer.WriteLine(indent + " Real Size: " + RealSize); + writer.WriteLine(indent + " Flags: " + Flags); + + if ((Flags & FileAttributeFlags.ReparsePoint) != 0) + { + writer.WriteLine(indent + " Reparse Point Tag: " + EASizeOrReparsePointTag); + } + else + { + writer.WriteLine(indent + " Ext Attr Size: " + (EASizeOrReparsePointTag & 0xFFFF)); + } + + writer.WriteLine(indent + " Namespace: " + FileNameNamespace); + writer.WriteLine(indent + " File Name: " + FileName); + } + + public int ReadFrom(byte[] buffer, int offset) + { + ParentDirectory = new FileRecordReference(Utilities.ToUInt64LittleEndian(buffer, offset + 0x00)); + CreationTime = ReadDateTime(buffer, offset + 0x08); + ModificationTime = ReadDateTime(buffer, offset + 0x10); + MftChangedTime = ReadDateTime(buffer, offset + 0x18); + LastAccessTime = ReadDateTime(buffer, offset + 0x20); + AllocatedSize = Utilities.ToUInt64LittleEndian(buffer, offset + 0x28); + RealSize = Utilities.ToUInt64LittleEndian(buffer, offset + 0x30); + Flags = (FileAttributeFlags)Utilities.ToUInt32LittleEndian(buffer, offset + 0x38); + EASizeOrReparsePointTag = Utilities.ToUInt32LittleEndian(buffer, offset + 0x3C); + byte fnLen = buffer[offset + 0x40]; + FileNameNamespace = (FileNameNamespace)buffer[offset + 0x41]; + FileName = Encoding.Unicode.GetString(buffer, offset + 0x42, fnLen * 2); + + return 0x42 + (fnLen * 2); + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian((ulong)ParentDirectory.Value, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian((ulong)CreationTime.ToFileTimeUtc(), buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian((ulong)ModificationTime.ToFileTimeUtc(), buffer, offset + 0x10); + Utilities.WriteBytesLittleEndian((ulong)MftChangedTime.ToFileTimeUtc(), buffer, offset + 0x18); + Utilities.WriteBytesLittleEndian((ulong)LastAccessTime.ToFileTimeUtc(), buffer, offset + 0x20); + Utilities.WriteBytesLittleEndian(AllocatedSize, buffer, offset + 0x28); + Utilities.WriteBytesLittleEndian(RealSize, buffer, offset + 0x30); + Utilities.WriteBytesLittleEndian((uint)Flags, buffer, offset + 0x38); + Utilities.WriteBytesLittleEndian(EASizeOrReparsePointTag, buffer, offset + 0x3C); + buffer[offset + 0x40] = (byte)FileName.Length; + buffer[offset + 0x41] = (byte)FileNameNamespace; + Encoding.Unicode.GetBytes(FileName, 0, FileName.Length, buffer, offset + 0x42); + } + + public bool Equals(FileNameRecord other) + { + if (other == null) + { + return false; + } + + return ParentDirectory == other.ParentDirectory + && FileNameNamespace == other.FileNameNamespace + && FileName == other.FileName; + } + + internal static FileAttributeFlags SetAttributes(FileAttributes attrs, FileAttributeFlags flags) + { + FileAttributes attrMask = ((FileAttributes)0xFFFF) & ~FileAttributes.Directory; + return (FileAttributeFlags)(((uint)flags & 0xFFFF0000) | (uint)(attrs & attrMask)); + } + + internal static FileAttributes ConvertFlags(FileAttributeFlags flags) + { + FileAttributes result = (FileAttributes)(((uint)flags) & 0xFFFF); + + if ((flags & FileAttributeFlags.Directory) != 0) + { + result |= FileAttributes.Directory; + } + + return result; + } + + private static DateTime ReadDateTime(byte[] buffer, int offset) + { + try + { + return DateTime.FromFileTimeUtc(Utilities.ToInt64LittleEndian(buffer, offset)); + } + catch (ArgumentException) + { + return DateTime.MinValue; + } + } + } +} diff --git a/DiscUtils/Ntfs/FileRecord.cs b/DiscUtils/Ntfs/FileRecord.cs new file mode 100644 index 0000000..09125b4 --- /dev/null +++ b/DiscUtils/Ntfs/FileRecord.cs @@ -0,0 +1,468 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + + [Flags] + internal enum FileRecordFlags : ushort + { + None = 0x0000, + InUse = 0x0001, + IsDirectory = 0x0002, + IsMetaFile = 0x0004, + HasViewIndex = 0x0008 + } + + internal class FileRecord : FixupRecordBase + { + private ulong _logFileSequenceNumber; + private ushort _sequenceNumber; + private ushort _hardLinkCount; + private ushort _firstAttributeOffset; + private FileRecordFlags _flags; + private uint _recordRealSize; + private uint _recordAllocatedSize; + private FileRecordReference _baseFile; + private ushort _nextAttributeId; + private uint _index; // Self-reference (on XP+) + private List _attributes; + + private bool _haveIndex; + private uint _loadedIndex; + + public FileRecord(int sectorSize) + : base("FILE", sectorSize) + { + } + + public FileRecord(int sectorSize, int recordLength, uint index) + : base("FILE", sectorSize, recordLength) + { + ReInitialize(sectorSize, recordLength, index); + } + + public uint MasterFileTableIndex + { + get { return _haveIndex ? _index : _loadedIndex; } + } + + public uint LoadedIndex + { + get { return _loadedIndex; } + set { _loadedIndex = value; } + } + + public ulong LogFileSequenceNumber + { + get { return _logFileSequenceNumber; } + } + + public ushort SequenceNumber + { + get { return _sequenceNumber; } + set { _sequenceNumber = value; } + } + + public ushort HardLinkCount + { + get { return _hardLinkCount; } + set { _hardLinkCount = value; } + } + + public uint AllocatedSize + { + get { return _recordAllocatedSize; } + } + + public uint RealSize + { + get { return _recordRealSize; } + } + + public FileRecordReference BaseFile + { + get { return _baseFile; } + set { _baseFile = value; } + } + + public FileRecordFlags Flags + { + get { return _flags; } + set { _flags = value; } + } + + public List Attributes + { + get { return _attributes; } + } + + public AttributeRecord FirstAttribute + { + get { return _attributes.Count > 0 ? _attributes[0] : null; } + } + + public FileRecordReference Reference + { + get { return new FileRecordReference(MasterFileTableIndex, SequenceNumber); } + } + + public ushort NextAttributeId + { + get { return _nextAttributeId; } + } + + public bool IsMftRecord + { + get { return MasterFileTableIndex == MasterFileTable.MftIndex || (_baseFile.MftIndex == MasterFileTable.MftIndex && _baseFile.SequenceNumber != 0); } + } + + public static FileAttributeFlags ConvertFlags(FileRecordFlags source) + { + FileAttributeFlags result = FileAttributeFlags.None; + + if ((source & FileRecordFlags.IsDirectory) != 0) + { + result |= FileAttributeFlags.Directory; + } + + if ((source & FileRecordFlags.HasViewIndex) != 0) + { + result |= FileAttributeFlags.IndexView; + } + + if ((source & FileRecordFlags.IsMetaFile) != 0) + { + result |= FileAttributeFlags.Hidden | FileAttributeFlags.System; + } + + return result; + } + + public void ReInitialize(int sectorSize, int recordLength, uint index) + { + Initialize("FILE", sectorSize, recordLength); + _sequenceNumber++; + _flags = FileRecordFlags.None; + _recordAllocatedSize = (uint)recordLength; + _nextAttributeId = 0; + _index = index; + _hardLinkCount = 0; + _baseFile = new FileRecordReference(0); + + _attributes = new List(); + _haveIndex = true; + } + + /// + /// Gets an attribute by it's id. + /// + /// The attribute's id. + /// The attribute, or null. + public AttributeRecord GetAttribute(ushort id) + { + foreach (AttributeRecord attrRec in _attributes) + { + if (attrRec.AttributeId == id) + { + return attrRec; + } + } + + return null; + } + + /// + /// Gets an unnamed attribute. + /// + /// The attribute type. + /// The attribute, or null. + public AttributeRecord GetAttribute(AttributeType type) + { + return GetAttribute(type, null); + } + + /// + /// Gets an named attribute. + /// + /// The attribute type. + /// The name of the attribute. + /// The attribute, or null. + public AttributeRecord GetAttribute(AttributeType type, string name) + { + foreach (AttributeRecord attrRec in _attributes) + { + if (attrRec.AttributeType == type && attrRec.Name == name) + { + return attrRec; + } + } + + return null; + } + + public override string ToString() + { + foreach (AttributeRecord attr in _attributes) + { + if (attr.AttributeType == AttributeType.FileName) + { + StructuredNtfsAttribute fnAttr = (StructuredNtfsAttribute)NtfsAttribute.FromRecord(null, new FileRecordReference(0), attr); + return fnAttr.Content.FileName; + } + } + + return "No Name"; + } + + /// + /// Creates a new attribute. + /// + /// The type of the new attribute. + /// The name of the new attribute. + /// Whether the attribute is marked as indexed. + /// Flags for the new attribute. + /// The id of the new attribute. + public ushort CreateAttribute(AttributeType type, string name, bool indexed, AttributeFlags flags) + { + ushort id = _nextAttributeId++; + _attributes.Add( + new ResidentAttributeRecord( + type, + name, + id, + indexed, + flags)); + _attributes.Sort(); + return id; + } + + /// + /// Creates a new non-resident attribute. + /// + /// The type of the new attribute. + /// The name of the new attribute. + /// Flags for the new attribute. + /// The id of the new attribute. + public ushort CreateNonResidentAttribute(AttributeType type, string name, AttributeFlags flags) + { + ushort id = _nextAttributeId++; + _attributes.Add( + new NonResidentAttributeRecord( + type, + name, + id, + flags, + 0, + new List())); + _attributes.Sort(); + return id; + } + + /// + /// Creates a new attribute. + /// + /// The type of the new attribute. + /// The name of the new attribute. + /// Flags for the new attribute. + /// The first cluster to assign to the attribute. + /// The number of sequential clusters to assign to the attribute. + /// The number of bytes in each cluster. + /// The id of the new attribute. + public ushort CreateNonResidentAttribute(AttributeType type, string name, AttributeFlags flags, long firstCluster, ulong numClusters, uint bytesPerCluster) + { + ushort id = _nextAttributeId++; + _attributes.Add( + new NonResidentAttributeRecord( + type, + name, + id, + flags, + firstCluster, + numClusters, + bytesPerCluster)); + _attributes.Sort(); + return id; + } + + /// + /// Adds an existing attribute. + /// + /// The attribute to add. + /// The new Id of the attribute. + /// This method is used to move an attribute between different MFT records. + public ushort AddAttribute(AttributeRecord attrRec) + { + attrRec.AttributeId = _nextAttributeId++; + _attributes.Add(attrRec); + _attributes.Sort(); + return attrRec.AttributeId; + } + + /// + /// Removes an attribute by it's id. + /// + /// The attribute's id. + public void RemoveAttribute(ushort id) + { + for (int i = 0; i < _attributes.Count; ++i) + { + if (_attributes[i].AttributeId == id) + { + _attributes.RemoveAt(i); + break; + } + } + } + + public void Reset() + { + _attributes.Clear(); + _flags = FileRecordFlags.None; + _hardLinkCount = 0; + _nextAttributeId = 0; + _recordRealSize = 0; + } + + internal long GetAttributeOffset(ushort id) + { + int firstAttrPos = (ushort)Utilities.RoundUp((_haveIndex ? 0x30 : 0x2A) + UpdateSequenceSize, 8); + + int offset = firstAttrPos; + foreach (var attr in _attributes) + { + if (attr.AttributeId == id) + { + return offset; + } + + offset += attr.Size; + } + + return -1; + } + + internal void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "FILE RECORD (" + ToString() + ")"); + writer.WriteLine(indent + " Magic: " + Magic); + writer.WriteLine(indent + " Update Seq Offset: " + UpdateSequenceOffset); + writer.WriteLine(indent + " Update Seq Count: " + UpdateSequenceCount); + writer.WriteLine(indent + " Update Seq Number: " + UpdateSequenceNumber); + writer.WriteLine(indent + " Log File Seq Num: " + _logFileSequenceNumber); + writer.WriteLine(indent + " Sequence Number: " + _sequenceNumber); + writer.WriteLine(indent + " Hard Link Count: " + _hardLinkCount); + writer.WriteLine(indent + " Flags: " + _flags); + writer.WriteLine(indent + " Record Real Size: " + _recordRealSize); + writer.WriteLine(indent + " Record Alloc Size: " + _recordAllocatedSize); + writer.WriteLine(indent + " Base File: " + _baseFile); + writer.WriteLine(indent + " Next Attribute Id: " + _nextAttributeId); + writer.WriteLine(indent + " Attribute Count: " + _attributes.Count); + writer.WriteLine(indent + " Index (Self Ref): " + _index); + } + + protected override void Read(byte[] buffer, int offset) + { + _logFileSequenceNumber = Utilities.ToUInt64LittleEndian(buffer, offset + 0x08); + _sequenceNumber = Utilities.ToUInt16LittleEndian(buffer, offset + 0x10); + _hardLinkCount = Utilities.ToUInt16LittleEndian(buffer, offset + 0x12); + _firstAttributeOffset = Utilities.ToUInt16LittleEndian(buffer, offset + 0x14); + _flags = (FileRecordFlags)Utilities.ToUInt16LittleEndian(buffer, offset + 0x16); + _recordRealSize = Utilities.ToUInt32LittleEndian(buffer, offset + 0x18); + _recordAllocatedSize = Utilities.ToUInt32LittleEndian(buffer, offset + 0x1C); + _baseFile = new FileRecordReference(Utilities.ToUInt64LittleEndian(buffer, offset + 0x20)); + _nextAttributeId = Utilities.ToUInt16LittleEndian(buffer, offset + 0x28); + + if (UpdateSequenceOffset >= 0x30) + { + _index = Utilities.ToUInt32LittleEndian(buffer, offset + 0x2C); + _haveIndex = true; + } + + _attributes = new List(); + int focus = _firstAttributeOffset; + while (true) + { + int length; + AttributeRecord attr = AttributeRecord.FromBytes(buffer, focus, out length); + if (attr == null) + { + break; + } + + _attributes.Add(attr); + focus += (int)length; + } + } + + protected override ushort Write(byte[] buffer, int offset) + { + ushort headerEnd = (ushort)(_haveIndex ? 0x30 : 0x2A); + + _firstAttributeOffset = (ushort)Utilities.RoundUp(headerEnd + UpdateSequenceSize, 0x08); + _recordRealSize = (uint)CalcSize(); + + Utilities.WriteBytesLittleEndian(_logFileSequenceNumber, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(_sequenceNumber, buffer, offset + 0x10); + Utilities.WriteBytesLittleEndian(_hardLinkCount, buffer, offset + 0x12); + Utilities.WriteBytesLittleEndian(_firstAttributeOffset, buffer, offset + 0x14); + Utilities.WriteBytesLittleEndian((ushort)_flags, buffer, offset + 0x16); + Utilities.WriteBytesLittleEndian(_recordRealSize, buffer, offset + 0x18); + Utilities.WriteBytesLittleEndian(_recordAllocatedSize, buffer, offset + 0x1C); + Utilities.WriteBytesLittleEndian(_baseFile.Value, buffer, offset + 0x20); + Utilities.WriteBytesLittleEndian(_nextAttributeId, buffer, offset + 0x28); + + if (_haveIndex) + { + Utilities.WriteBytesLittleEndian((ushort)0, buffer, offset + 0x2A); // Alignment field + Utilities.WriteBytesLittleEndian(_index, buffer, offset + 0x2C); + } + + int pos = _firstAttributeOffset; + foreach (var attr in _attributes) + { + pos += attr.Write(buffer, offset + pos); + } + + Utilities.WriteBytesLittleEndian(uint.MaxValue, buffer, offset + pos); + + return headerEnd; + } + + protected override int CalcSize() + { + int firstAttrPos = (ushort)Utilities.RoundUp((_haveIndex ? 0x30 : 0x2A) + UpdateSequenceSize, 8); + + int size = firstAttrPos; + foreach (var attr in _attributes) + { + size += attr.Size; + } + + return Utilities.RoundUp(size + 4, 8); // 0xFFFFFFFF terminator on attributes + } + } +} diff --git a/DiscUtils/Ntfs/FileRecordReference.cs b/DiscUtils/Ntfs/FileRecordReference.cs new file mode 100644 index 0000000..595f234 --- /dev/null +++ b/DiscUtils/Ntfs/FileRecordReference.cs @@ -0,0 +1,123 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + + internal struct FileRecordReference : IByteArraySerializable, IComparable + { + private ulong _val; + + public FileRecordReference(ulong val) + { + _val = val; + } + + public FileRecordReference(long mftIndex, ushort sequenceNumber) + { + _val = (ulong)(mftIndex & 0x0000FFFFFFFFFFFFL) | ((ulong)((ulong)sequenceNumber << 48) & 0xFFFF000000000000L); + } + + public ulong Value + { + get { return _val; } + } + + public long MftIndex + { + get { return (long)(_val & 0x0000FFFFFFFFFFFFL); } + } + + public ushort SequenceNumber + { + get { return (ushort)((_val >> 48) & 0xFFFF); } + } + + public int Size + { + get { return 8; } + } + + public bool IsNull + { + get { return SequenceNumber == 0; } + } + + public static bool operator ==(FileRecordReference a, FileRecordReference b) + { + return a._val == b._val; + } + + public static bool operator !=(FileRecordReference a, FileRecordReference b) + { + return a._val != b._val; + } + + public int ReadFrom(byte[] buffer, int offset) + { + _val = Utilities.ToUInt64LittleEndian(buffer, offset); + return 8; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(_val, buffer, offset); + } + + public override bool Equals(object obj) + { + if (obj == null || !(obj is FileRecordReference)) + { + return false; + } + + return _val == ((FileRecordReference)obj)._val; + } + + public override int GetHashCode() + { + return _val.GetHashCode(); + } + + public int CompareTo(FileRecordReference other) + { + if (_val < other._val) + { + return -1; + } + else if (_val > other._val) + { + return 1; + } + else + { + return 0; + } + } + + public override string ToString() + { + return "MFT:" + MftIndex + " (ver: " + SequenceNumber + ")"; + } + } +} diff --git a/DiscUtils/Ntfs/FileSystemFactory.cs b/DiscUtils/Ntfs/FileSystemFactory.cs new file mode 100644 index 0000000..923e640 --- /dev/null +++ b/DiscUtils/Ntfs/FileSystemFactory.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.IO; + using DiscUtils.Vfs; + + [VfsFileSystemFactory] + internal class FileSystemFactory : VfsFileSystemFactory + { + public override DiscUtils.FileSystemInfo[] Detect(Stream stream, VolumeInfo volume) + { + if (NtfsFileSystem.Detect(stream)) + { + return new DiscUtils.FileSystemInfo[] { new VfsFileSystemInfo("NTFS", "Microsoft NTFS", Open) }; + } + + return new DiscUtils.FileSystemInfo[0]; + } + + private DiscFileSystem Open(Stream stream, VolumeInfo volumeInfo, FileSystemParameters parameters) + { + return new NtfsFileSystem(stream); + } + } +} diff --git a/DiscUtils/Ntfs/FixupRecordBase.cs b/DiscUtils/Ntfs/FixupRecordBase.cs new file mode 100644 index 0000000..dffb159 --- /dev/null +++ b/DiscUtils/Ntfs/FixupRecordBase.cs @@ -0,0 +1,190 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + + internal abstract class FixupRecordBase + { + private int _sectorSize; + + private string _magic; + private ushort _updateSequenceOffset; + private ushort _updateSequenceCount; + + private ushort _updateSequenceNumber; + private ushort[] _updateSequenceArray; + + public FixupRecordBase(string magic, int sectorSize) + { + _magic = magic; + _sectorSize = sectorSize; + } + + public FixupRecordBase(string magic, int sectorSize, int recordLength) + { + Initialize(magic, sectorSize, recordLength); + } + + public string Magic + { + get { return _magic; } + } + + public ushort UpdateSequenceOffset + { + get { return _updateSequenceOffset; } + } + + public ushort UpdateSequenceCount + { + get { return _updateSequenceCount; } + } + + public ushort UpdateSequenceNumber + { + get { return _updateSequenceNumber; } + } + + public int UpdateSequenceSize + { + get { return _updateSequenceCount * 2; } + } + + public int Size + { + get + { + return CalcSize(); + } + } + + public void FromBytes(byte[] buffer, int offset) + { + FromBytes(buffer, offset, false); + } + + public void FromBytes(byte[] buffer, int offset, bool ignoreMagic) + { + string diskMagic = Utilities.BytesToString(buffer, offset + 0x00, 4); + if (_magic == null) + { + _magic = diskMagic; + } + else + { + if (diskMagic != _magic && ignoreMagic) + { + return; + } + + if (diskMagic != _magic) + { + throw new IOException("Corrupt record"); + } + } + + _updateSequenceOffset = Utilities.ToUInt16LittleEndian(buffer, offset + 0x04); + _updateSequenceCount = Utilities.ToUInt16LittleEndian(buffer, offset + 0x06); + + _updateSequenceNumber = Utilities.ToUInt16LittleEndian(buffer, offset + _updateSequenceOffset); + _updateSequenceArray = new ushort[_updateSequenceCount - 1]; + for (int i = 0; i < _updateSequenceArray.Length; ++i) + { + _updateSequenceArray[i] = Utilities.ToUInt16LittleEndian(buffer, offset + _updateSequenceOffset + (2 * (i + 1))); + } + + UnprotectBuffer(buffer, offset); + + Read(buffer, offset); + } + + public void ToBytes(byte[] buffer, int offset) + { + _updateSequenceOffset = Write(buffer, offset); + + ProtectBuffer(buffer, offset); + + Utilities.StringToBytes(_magic, buffer, offset + 0x00, 4); + Utilities.WriteBytesLittleEndian(_updateSequenceOffset, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(_updateSequenceCount, buffer, offset + 0x06); + + Utilities.WriteBytesLittleEndian(_updateSequenceNumber, buffer, offset + _updateSequenceOffset); + for (int i = 0; i < _updateSequenceArray.Length; ++i) + { + Utilities.WriteBytesLittleEndian(_updateSequenceArray[i], buffer, offset + _updateSequenceOffset + (2 * (i + 1))); + } + } + + protected void Initialize(string magic, int sectorSize, int recordLength) + { + _magic = magic; + _sectorSize = sectorSize; + _updateSequenceCount = (ushort)(1 + Utilities.Ceil(recordLength, Sizes.Sector)); + _updateSequenceNumber = 1; + _updateSequenceArray = new ushort[_updateSequenceCount - 1]; + } + + protected abstract void Read(byte[] buffer, int offset); + + protected abstract ushort Write(byte[] buffer, int offset); + + protected abstract int CalcSize(); + + private void UnprotectBuffer(byte[] buffer, int offset) + { + // First do validation check - make sure the USN matches on all sectors) + for (int i = 0; i < _updateSequenceArray.Length; ++i) + { + if (_updateSequenceNumber != Utilities.ToUInt16LittleEndian(buffer, offset + (Sizes.Sector * (i + 1)) - 2)) + { + throw new IOException("Corrupt file system record found"); + } + } + + // Now replace the USNs with the actual data from the sequence array + for (int i = 0; i < _updateSequenceArray.Length; ++i) + { + Utilities.WriteBytesLittleEndian(_updateSequenceArray[i], buffer, offset + (Sizes.Sector * (i + 1)) - 2); + } + } + + private void ProtectBuffer(byte[] buffer, int offset) + { + _updateSequenceNumber++; + + // Read in the bytes that are replaced by the USN + for (int i = 0; i < _updateSequenceArray.Length; ++i) + { + _updateSequenceArray[i] = Utilities.ToUInt16LittleEndian(buffer, offset + (Sizes.Sector * (i + 1)) - 2); + } + + // Overwrite the bytes that are replaced with the USN + for (int i = 0; i < _updateSequenceArray.Length; ++i) + { + Utilities.WriteBytesLittleEndian(_updateSequenceNumber, buffer, offset + (Sizes.Sector * (i + 1)) - 2); + } + } + } +} diff --git a/DiscUtils/Ntfs/GenericFixupRecord.cs b/DiscUtils/Ntfs/GenericFixupRecord.cs new file mode 100644 index 0000000..427823c --- /dev/null +++ b/DiscUtils/Ntfs/GenericFixupRecord.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + + internal sealed class GenericFixupRecord : FixupRecordBase + { + private int _bytesPerSector; + private byte[] _content; + + public GenericFixupRecord(int bytesPerSector) + : base(null, bytesPerSector) + { + _bytesPerSector = bytesPerSector; + } + + public byte[] Content + { + get { return _content; } + } + + protected override void Read(byte[] buffer, int offset) + { + _content = new byte[(UpdateSequenceCount - 1) * _bytesPerSector]; + Array.Copy(buffer, offset, _content, 0, _content.Length); + } + + protected override ushort Write(byte[] buffer, int offset) + { + throw new NotImplementedException(); + } + + protected override int CalcSize() + { + throw new NotImplementedException(); + } + } +} diff --git a/DiscUtils/Ntfs/Index.cs b/DiscUtils/Ntfs/Index.cs new file mode 100644 index 0000000..5f80825 --- /dev/null +++ b/DiscUtils/Ntfs/Index.cs @@ -0,0 +1,485 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class Index : IDisposable + { + protected File _file; + protected string _name; + protected BiosParameterBlock _bpb; + private bool _isFileIndex; + + private IComparer _comparer; + + private IndexRoot _root; + private IndexNode _rootNode; + private Stream _indexStream; + private Bitmap _indexBitmap; + + private ObjectCache _blockCache; + + public Index(File file, string name, BiosParameterBlock bpb, UpperCase upCase) + { + _file = file; + _name = name; + _bpb = bpb; + _isFileIndex = name == "$I30"; + + _blockCache = new ObjectCache(); + + _root = _file.GetStream(AttributeType.IndexRoot, _name).GetContent(); + _comparer = _root.GetCollator(upCase); + + using (Stream s = _file.OpenStream(AttributeType.IndexRoot, _name, FileAccess.Read)) + { + byte[] buffer = Utilities.ReadFully(s, (int)s.Length); + _rootNode = new IndexNode(WriteRootNodeToDisk, 0, this, true, buffer, IndexRoot.HeaderOffset); + + // Give the attribute some room to breathe, so long as it doesn't squeeze others out + // BROKEN, BROKEN, BROKEN - how to figure this out? Query at the point of adding entries to the root node? + _rootNode.TotalSpaceAvailable += _file.MftRecordFreeSpace(AttributeType.IndexRoot, _name) - 100; + } + + if (_file.StreamExists(AttributeType.IndexAllocation, _name)) + { + _indexStream = _file.OpenStream(AttributeType.IndexAllocation, _name, FileAccess.ReadWrite); + } + + if (_file.StreamExists(AttributeType.Bitmap, _name)) + { + _indexBitmap = new Bitmap(_file.OpenStream(AttributeType.Bitmap, _name, FileAccess.ReadWrite), long.MaxValue); + } + } + + private Index(AttributeType attrType, AttributeCollationRule collationRule, File file, string name, BiosParameterBlock bpb, UpperCase upCase) + { + _file = file; + _name = name; + _bpb = bpb; + _isFileIndex = name == "$I30"; + + _blockCache = new ObjectCache(); + + _file.CreateStream(AttributeType.IndexRoot, _name); + + _root = new IndexRoot() + { + AttributeType = (uint)attrType, + CollationRule = collationRule, + IndexAllocationSize = (uint)bpb.IndexBufferSize, + RawClustersPerIndexRecord = bpb.RawIndexBufferSize + }; + + _comparer = _root.GetCollator(upCase); + + _rootNode = new IndexNode(WriteRootNodeToDisk, 0, this, true, 32); + } + + public IEnumerable> Entries + { + get + { + foreach (var entry in Enumerate(_rootNode)) + { + yield return new KeyValuePair(entry.KeyBuffer, entry.DataBuffer); + } + } + } + + public int Count + { + get + { + int i = 0; + foreach (var entry in Entries) + { + ++i; + } + + return i; + } + } + + internal Stream AllocationStream + { + get { return _indexStream; } + } + + internal uint IndexBufferSize + { + get { return _root.IndexAllocationSize; } + } + + internal bool IsFileIndex + { + get { return _isFileIndex; } + } + + public byte[] this[byte[] key] + { + get + { + byte[] value; + if (TryGetValue(key, out value)) + { + return value; + } + else + { + throw new KeyNotFoundException(); + } + } + + set + { + IndexEntry oldEntry; + IndexNode node; + _rootNode.TotalSpaceAvailable = _rootNode.CalcSize() + _file.MftRecordFreeSpace(AttributeType.IndexRoot, _name); + if (_rootNode.TryFindEntry(key, out oldEntry, out node)) + { + node.UpdateEntry(key, value); + } + else + { + _rootNode.AddEntry(key, value); + } + } + } + + public static void Create(AttributeType attrType, AttributeCollationRule collationRule, File file, string name) + { + Index idx = new Index(attrType, collationRule, file, name, file.Context.BiosParameterBlock, file.Context.UpperCase); + + idx.WriteRootNodeToDisk(); + } + + public void Dispose() + { + if (_indexBitmap != null) + { + _indexBitmap.Dispose(); + _indexBitmap = null; + } + } + + public IEnumerable> FindAll(IComparable query) + { + foreach (var entry in FindAllIn(query, _rootNode)) + { + yield return new KeyValuePair(entry.KeyBuffer, entry.DataBuffer); + } + } + + public bool ContainsKey(byte[] key) + { + byte[] value; + return TryGetValue(key, out value); + } + + public bool Remove(byte[] key) + { + _rootNode.TotalSpaceAvailable = _rootNode.CalcSize() + _file.MftRecordFreeSpace(AttributeType.IndexRoot, _name); + + IndexEntry overflowEntry; + bool found = _rootNode.RemoveEntry(key, out overflowEntry); + if (overflowEntry != null) + { + throw new IOException("Error removing entry, root overflowed"); + } + + return found; + } + + public bool TryGetValue(byte[] key, out byte[] value) + { + IndexEntry entry; + IndexNode node; + + if (_rootNode.TryFindEntry(key, out entry, out node)) + { + value = entry.DataBuffer; + return true; + } + + value = default(byte[]); + return false; + } + + internal static string EntryAsString(IndexEntry entry, string fileName, string indexName) + { + IByteArraySerializable keyValue = null; + IByteArraySerializable dataValue = null; + + // Try to guess the type of data in the key and data fields from the filename and index name + if (indexName == "$I30") + { + keyValue = new FileNameRecord(); + dataValue = new FileRecordReference(); + } + else if (fileName == "$ObjId" && indexName == "$O") + { + keyValue = new ObjectIds.IndexKey(); + dataValue = new ObjectIdRecord(); + } + else if (fileName == "$Reparse" && indexName == "$R") + { + keyValue = new ReparsePoints.Key(); + dataValue = new ReparsePoints.Data(); + } + else if (fileName == "$Quota") + { + if (indexName == "$O") + { + keyValue = new Quotas.OwnerKey(); + dataValue = new Quotas.OwnerRecord(); + } + else if (indexName == "$Q") + { + keyValue = new Quotas.OwnerRecord(); + dataValue = new Quotas.QuotaRecord(); + } + } + else if (fileName == "$Secure") + { + if (indexName == "$SII") + { + keyValue = new SecurityDescriptors.IdIndexKey(); + dataValue = new SecurityDescriptors.IdIndexData(); + } + else if (indexName == "$SDH") + { + keyValue = new SecurityDescriptors.HashIndexKey(); + dataValue = new SecurityDescriptors.IdIndexData(); + } + } + + try + { + if (keyValue != null && dataValue != null) + { + keyValue.ReadFrom(entry.KeyBuffer, 0); + dataValue.ReadFrom(entry.DataBuffer, 0); + return "{" + keyValue + "-->" + dataValue + "}"; + } + } + catch + { + return "{Parsing-Error}"; + } + + return "{Unknown-Index-Type}"; + } + + internal long IndexBlockVcnToPosition(long vcn) + { + if (vcn % _root.RawClustersPerIndexRecord != 0) + { + throw new NotSupportedException("Unexpected vcn (not a multiple of clusters-per-index-record): vcn=" + vcn + " rcpir=" + _root.RawClustersPerIndexRecord); + } + + if (_bpb.BytesPerCluster <= _root.IndexAllocationSize) + { + return vcn * (long)_bpb.BytesPerCluster; + } + else + { + if (_root.RawClustersPerIndexRecord != 8) + { + throw new NotSupportedException("Unexpected RawClustersPerIndexRecord (multiple index blocks per cluster): " + _root.RawClustersPerIndexRecord); + } + + return (vcn / _root.RawClustersPerIndexRecord) * _root.IndexAllocationSize; + } + } + + internal bool ShrinkRoot() + { + if (_rootNode.Depose()) + { + WriteRootNodeToDisk(); + _rootNode.TotalSpaceAvailable = _rootNode.CalcSize() + _file.MftRecordFreeSpace(AttributeType.IndexRoot, _name); + return true; + } + + return false; + } + + internal IndexBlock GetSubBlock(IndexEntry parentEntry) + { + IndexBlock block = _blockCache[parentEntry.ChildrenVirtualCluster]; + if (block == null) + { + block = new IndexBlock(this, false, parentEntry, _bpb); + _blockCache[parentEntry.ChildrenVirtualCluster] = block; + } + + return block; + } + + internal IndexBlock AllocateBlock(IndexEntry parentEntry) + { + if (_indexStream == null) + { + _file.CreateStream(AttributeType.IndexAllocation, _name); + _indexStream = _file.OpenStream(AttributeType.IndexAllocation, _name, FileAccess.ReadWrite); + } + + if (_indexBitmap == null) + { + _file.CreateStream(AttributeType.Bitmap, _name); + _indexBitmap = new Bitmap(_file.OpenStream(AttributeType.Bitmap, _name, FileAccess.ReadWrite), long.MaxValue); + } + + long idx = _indexBitmap.AllocateFirstAvailable(0); + + parentEntry.ChildrenVirtualCluster = idx * Utilities.Ceil(_bpb.IndexBufferSize, _bpb.SectorsPerCluster * _bpb.BytesPerSector); + parentEntry.Flags |= IndexEntryFlags.Node; + + IndexBlock block = IndexBlock.Initialize(this, false, parentEntry, _bpb); + _blockCache[parentEntry.ChildrenVirtualCluster] = block; + return block; + } + + internal void FreeBlock(long vcn) + { + long idx = vcn / Utilities.Ceil(_bpb.IndexBufferSize, _bpb.SectorsPerCluster * _bpb.BytesPerSector); + _indexBitmap.MarkAbsent(idx); + _blockCache.Remove(vcn); + } + + internal int Compare(byte[] x, byte[] y) + { + return _comparer.Compare(x, y); + } + + internal void Dump(TextWriter writer, string prefix) + { + NodeAsString(writer, prefix, _rootNode, "R"); + } + + protected IEnumerable Enumerate(IndexNode node) + { + foreach (var focus in node.Entries) + { + if ((focus.Flags & IndexEntryFlags.Node) != 0) + { + IndexBlock block = GetSubBlock(focus); + foreach (var subEntry in Enumerate(block.Node)) + { + yield return subEntry; + } + } + + if ((focus.Flags & IndexEntryFlags.End) == 0) + { + yield return focus; + } + } + } + + private IEnumerable FindAllIn(IComparable query, IndexNode node) + { + foreach (var focus in node.Entries) + { + bool searchChildren = true; + bool matches = false; + bool keepIterating = true; + + if ((focus.Flags & IndexEntryFlags.End) == 0) + { + int compVal = query.CompareTo(focus.KeyBuffer); + if (compVal == 0) + { + matches = true; + } + else if (compVal > 0) + { + searchChildren = false; + } + else if (compVal < 0) + { + keepIterating = false; + } + } + + if (searchChildren && (focus.Flags & IndexEntryFlags.Node) != 0) + { + IndexBlock block = GetSubBlock(focus); + foreach (var entry in FindAllIn(query, block.Node)) + { + yield return entry; + } + } + + if (matches) + { + yield return focus; + } + + if (!keepIterating) + { + yield break; + } + } + } + + private void WriteRootNodeToDisk() + { + _rootNode.Header.AllocatedSizeOfEntries = (uint)_rootNode.CalcSize(); + byte[] buffer = new byte[_rootNode.Header.AllocatedSizeOfEntries + _root.Size]; + _root.WriteTo(buffer, 0); + _rootNode.WriteTo(buffer, _root.Size); + using (Stream s = _file.OpenStream(AttributeType.IndexRoot, _name, FileAccess.Write)) + { + s.Position = 0; + s.Write(buffer, 0, buffer.Length); + s.SetLength(s.Position); + } + } + + private void NodeAsString(TextWriter writer, string prefix, IndexNode node, string id) + { + writer.WriteLine(prefix + id + ":"); + foreach (var entry in node.Entries) + { + if ((entry.Flags & IndexEntryFlags.End) != 0) + { + writer.WriteLine(prefix + " E"); + } + else + { + writer.WriteLine(prefix + " " + EntryAsString(entry, _file.BestName, _name)); + } + + if ((entry.Flags & IndexEntryFlags.Node) != 0) + { + NodeAsString(writer, prefix + " ", GetSubBlock(entry).Node, ":i" + entry.ChildrenVirtualCluster); + } + } + } + } +} diff --git a/DiscUtils/Ntfs/IndexBlock.cs b/DiscUtils/Ntfs/IndexBlock.cs new file mode 100644 index 0000000..d7dbfdc --- /dev/null +++ b/DiscUtils/Ntfs/IndexBlock.cs @@ -0,0 +1,114 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class IndexBlock : FixupRecordBase + { + /// + /// Size of meta-data placed at start of a block. + /// + private const int FieldSize = 0x18; + + private ulong _logSequenceNumber; + private ulong _indexBlockVcn; // Virtual Cluster Number (maybe in sectors sometimes...?) + + private IndexNode _node; + + private Index _index; + private bool _isRoot; + private long _streamPosition; + + public IndexBlock(Index index, bool isRoot, IndexEntry parentEntry, BiosParameterBlock bpb) + : base("INDX", bpb.BytesPerSector) + { + _index = index; + _isRoot = isRoot; + + Stream stream = index.AllocationStream; + _streamPosition = index.IndexBlockVcnToPosition(parentEntry.ChildrenVirtualCluster); + stream.Position = _streamPosition; + byte[] buffer = Utilities.ReadFully(stream, (int)index.IndexBufferSize); + FromBytes(buffer, 0); + } + + private IndexBlock(Index index, bool isRoot, long vcn, BiosParameterBlock bpb) + : base("INDX", bpb.BytesPerSector, bpb.IndexBufferSize) + { + _index = index; + _isRoot = isRoot; + + _indexBlockVcn = (ulong)vcn; + + _streamPosition = vcn * bpb.BytesPerSector * bpb.SectorsPerCluster; + + _node = new IndexNode(WriteToDisk, UpdateSequenceSize, _index, isRoot, (uint)bpb.IndexBufferSize - FieldSize); + + WriteToDisk(); + } + + public IndexNode Node + { + get { return _node; } + } + + internal static IndexBlock Initialize(Index index, bool isRoot, IndexEntry parentEntry, BiosParameterBlock bpb) + { + return new IndexBlock(index, isRoot, parentEntry.ChildrenVirtualCluster, bpb); + } + + internal void WriteToDisk() + { + byte[] buffer = new byte[_index.IndexBufferSize]; + ToBytes(buffer, 0); + + Stream stream = _index.AllocationStream; + stream.Position = _streamPosition; + stream.Write(buffer, 0, buffer.Length); + stream.Flush(); + } + + protected override void Read(byte[] buffer, int offset) + { + // Skip FixupRecord fields... + _logSequenceNumber = Utilities.ToUInt64LittleEndian(buffer, offset + 0x08); + _indexBlockVcn = Utilities.ToUInt64LittleEndian(buffer, offset + 0x10); + _node = new IndexNode(WriteToDisk, UpdateSequenceSize, _index, _isRoot, buffer, offset + FieldSize); + } + + protected override ushort Write(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(_logSequenceNumber, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(_indexBlockVcn, buffer, offset + 0x10); + return (ushort)(FieldSize + Node.WriteTo(buffer, offset + FieldSize)); + } + + protected override int CalcSize() + { + throw new NotImplementedException(); + } + } +} diff --git a/DiscUtils/Ntfs/IndexEntry.cs b/DiscUtils/Ntfs/IndexEntry.cs new file mode 100644 index 0000000..cddac84 --- /dev/null +++ b/DiscUtils/Ntfs/IndexEntry.cs @@ -0,0 +1,193 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + + [Flags] + internal enum IndexEntryFlags : ushort + { + None = 0x00, + Node = 0x01, + End = 0x02 + } + + internal class IndexEntry + { + public const int EndNodeSize = 0x18; + + protected IndexEntryFlags _flags; + protected long _vcn; // Only valid if Node flag set + + protected byte[] _keyBuffer; + protected byte[] _dataBuffer; + + private bool _isFileIndexEntry; + + public IndexEntry(bool isFileIndexEntry) + { + _isFileIndexEntry = isFileIndexEntry; + } + + public IndexEntry(IndexEntry toCopy, byte[] newKey, byte[] newData) + { + _isFileIndexEntry = toCopy._isFileIndexEntry; + _flags = toCopy._flags; + _vcn = toCopy._vcn; + _keyBuffer = newKey; + _dataBuffer = newData; + } + + public IndexEntry(byte[] key, byte[] data, bool isFileIndexEntry) + { + _isFileIndexEntry = isFileIndexEntry; + _flags = IndexEntryFlags.None; + _keyBuffer = key; + _dataBuffer = data; + } + + public byte[] KeyBuffer + { + get { return _keyBuffer; } + set { _keyBuffer = value; } + } + + public byte[] DataBuffer + { + get { return _dataBuffer; } + set { _dataBuffer = value; } + } + + public IndexEntryFlags Flags + { + get { return _flags; } + set { _flags = value; } + } + + public long ChildrenVirtualCluster + { + get { return _vcn; } + set { _vcn = value; } + } + + public virtual int Size + { + get + { + int size = 0x10; // start of variable data + + if ((_flags & IndexEntryFlags.End) == 0) + { + size += _keyBuffer.Length; + size += IsFileIndexEntry ? 0 : _dataBuffer.Length; + } + + size = Utilities.RoundUp(size, 8); + + if ((_flags & IndexEntryFlags.Node) != 0) + { + size += 8; + } + + return size; + } + } + + protected bool IsFileIndexEntry + { + get { return _isFileIndexEntry; } + } + + public virtual void Read(byte[] buffer, int offset) + { + ushort dataOffset = Utilities.ToUInt16LittleEndian(buffer, offset + 0x00); + ushort dataLength = Utilities.ToUInt16LittleEndian(buffer, offset + 0x02); + ushort length = Utilities.ToUInt16LittleEndian(buffer, offset + 0x08); + ushort keyLength = Utilities.ToUInt16LittleEndian(buffer, offset + 0x0A); + _flags = (IndexEntryFlags)Utilities.ToUInt16LittleEndian(buffer, offset + 0x0C); + + if ((_flags & IndexEntryFlags.End) == 0) + { + _keyBuffer = new byte[keyLength]; + Array.Copy(buffer, offset + 0x10, _keyBuffer, 0, keyLength); + + if (IsFileIndexEntry) + { + // Special case, for file indexes, the MFT ref is held where the data offset & length go + _dataBuffer = new byte[8]; + Array.Copy(buffer, offset + 0x00, _dataBuffer, 0, 8); + } + else + { + _dataBuffer = new byte[dataLength]; + Array.Copy(buffer, offset + 0x10 + keyLength, _dataBuffer, 0, dataLength); + } + } + + if ((_flags & IndexEntryFlags.Node) != 0) + { + _vcn = Utilities.ToInt64LittleEndian(buffer, offset + length - 8); + } + } + + public virtual void WriteTo(byte[] buffer, int offset) + { + ushort length = (ushort)Size; + + if ((_flags & IndexEntryFlags.End) == 0) + { + ushort keyLength = (ushort)_keyBuffer.Length; + + if (IsFileIndexEntry) + { + Array.Copy(_dataBuffer, 0, buffer, offset + 0x00, 8); + } + else + { + ushort dataOffset = (ushort)(IsFileIndexEntry ? 0 : (0x10 + keyLength)); + ushort dataLength = (ushort)_dataBuffer.Length; + + Utilities.WriteBytesLittleEndian(dataOffset, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian(dataLength, buffer, offset + 0x02); + Array.Copy(_dataBuffer, 0, buffer, offset + dataOffset, _dataBuffer.Length); + } + + Utilities.WriteBytesLittleEndian(keyLength, buffer, offset + 0x0A); + Array.Copy(_keyBuffer, 0, buffer, offset + 0x10, _keyBuffer.Length); + } + else + { + Utilities.WriteBytesLittleEndian((ushort)0, buffer, offset + 0x00); // dataOffset + Utilities.WriteBytesLittleEndian((ushort)0, buffer, offset + 0x02); // dataLength + Utilities.WriteBytesLittleEndian((ushort)0, buffer, offset + 0x0A); // keyLength + } + + Utilities.WriteBytesLittleEndian(length, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian((ushort)_flags, buffer, offset + 0x0C); + if ((_flags & IndexEntryFlags.Node) != 0) + { + Utilities.WriteBytesLittleEndian(_vcn, buffer, offset + length - 8); + } + } + } +} diff --git a/DiscUtils/Ntfs/IndexHeader.cs b/DiscUtils/Ntfs/IndexHeader.cs new file mode 100644 index 0000000..1798846 --- /dev/null +++ b/DiscUtils/Ntfs/IndexHeader.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + internal class IndexHeader + { + public const int Size = 0x10; + + public uint OffsetToFirstEntry; + public uint TotalSizeOfEntries; + public uint AllocatedSizeOfEntries; + public byte HasChildNodes; + + public IndexHeader(uint allocatedSize) + { + AllocatedSizeOfEntries = allocatedSize; + } + + public IndexHeader(byte[] data, int offset) + { + OffsetToFirstEntry = Utilities.ToUInt32LittleEndian(data, offset + 0x00); + TotalSizeOfEntries = Utilities.ToUInt32LittleEndian(data, offset + 0x04); + AllocatedSizeOfEntries = Utilities.ToUInt32LittleEndian(data, offset + 0x08); + HasChildNodes = data[offset + 0x0C]; + } + + internal void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(OffsetToFirstEntry, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian(TotalSizeOfEntries, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(AllocatedSizeOfEntries, buffer, offset + 0x08); + buffer[offset + 0x0C] = HasChildNodes; + buffer[offset + 0x0D] = 0; + buffer[offset + 0x0E] = 0; + buffer[offset + 0x0F] = 0; + } + } +} diff --git a/DiscUtils/Ntfs/IndexNode.cs b/DiscUtils/Ntfs/IndexNode.cs new file mode 100644 index 0000000..4a43707 --- /dev/null +++ b/DiscUtils/Ntfs/IndexNode.cs @@ -0,0 +1,588 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal delegate void IndexNodeSaveFn(); + + internal class IndexNode + { + private IndexNodeSaveFn _store; + private int _storageOverhead; + private long _totalSpaceAvailable; + + private IndexHeader _header; + + private Index _index; + private bool _isRoot; + + private List _entries; + + public IndexNode(IndexNodeSaveFn store, int storeOverhead, Index index, bool isRoot, uint allocatedSize) + { + _store = store; + _storageOverhead = storeOverhead; + _index = index; + _isRoot = isRoot; + _header = new IndexHeader(allocatedSize); + _totalSpaceAvailable = allocatedSize; + + IndexEntry endEntry = new IndexEntry(_index.IsFileIndex); + endEntry.Flags |= IndexEntryFlags.End; + + _entries = new List(); + _entries.Add(endEntry); + + _header.OffsetToFirstEntry = (uint)(IndexHeader.Size + storeOverhead); + _header.TotalSizeOfEntries = (uint)(_header.OffsetToFirstEntry + endEntry.Size); + } + + public IndexNode(IndexNodeSaveFn store, int storeOverhead, Index index, bool isRoot, byte[] buffer, int offset) + { + _store = store; + _storageOverhead = storeOverhead; + _index = index; + _isRoot = isRoot; + _header = new IndexHeader(buffer, offset + 0); + _totalSpaceAvailable = _header.AllocatedSizeOfEntries; + + _entries = new List(); + int pos = (int)_header.OffsetToFirstEntry; + while (pos < _header.TotalSizeOfEntries) + { + IndexEntry entry = new IndexEntry(index.IsFileIndex); + entry.Read(buffer, offset + pos); + _entries.Add(entry); + + if ((entry.Flags & IndexEntryFlags.End) != 0) + { + break; + } + + pos += entry.Size; + } + } + + public IndexHeader Header + { + get { return _header; } + } + + public IEnumerable Entries + { + get { return _entries; } + } + + internal long TotalSpaceAvailable + { + get { return _totalSpaceAvailable; } + set { _totalSpaceAvailable = value; } + } + + private long SpaceFree + { + get + { + long entriesTotal = 0; + for (int i = 0; i < _entries.Count; ++i) + { + entriesTotal += _entries[i].Size; + } + + int firstEntryOffset = Utilities.RoundUp(IndexHeader.Size + _storageOverhead, 8); + + return _totalSpaceAvailable - (entriesTotal + firstEntryOffset); + } + } + + public void AddEntry(byte[] key, byte[] data) + { + IndexEntry overflowEntry = AddEntry(new IndexEntry(key, data, _index.IsFileIndex)); + if (overflowEntry != null) + { + throw new IOException("Error adding entry - root overflowed"); + } + } + + public void UpdateEntry(byte[] key, byte[] data) + { + for (int i = 0; i < _entries.Count; ++i) + { + var focus = _entries[i]; + int compVal = _index.Compare(key, focus.KeyBuffer); + if (compVal == 0) + { + IndexEntry newEntry = new IndexEntry(focus, key, data); + if (_entries[i].Size != newEntry.Size) + { + throw new NotImplementedException("Changing index entry sizes"); + } + + _entries[i] = newEntry; + _store(); + return; + } + } + + throw new IOException("No such index entry"); + } + + public bool TryFindEntry(byte[] key, out IndexEntry entry, out IndexNode node) + { + foreach (var focus in _entries) + { + if ((focus.Flags & IndexEntryFlags.End) != 0) + { + if ((focus.Flags & IndexEntryFlags.Node) != 0) + { + IndexBlock subNode = _index.GetSubBlock(focus); + return subNode.Node.TryFindEntry(key, out entry, out node); + } + + break; + } + else + { + int compVal = _index.Compare(key, focus.KeyBuffer); + if (compVal == 0) + { + entry = focus; + node = this; + return true; + } + else if (compVal < 0 && (focus.Flags & (IndexEntryFlags.End | IndexEntryFlags.Node)) != 0) + { + IndexBlock subNode = _index.GetSubBlock(focus); + return subNode.Node.TryFindEntry(key, out entry, out node); + } + } + } + + entry = null; + node = null; + return false; + } + + public virtual ushort WriteTo(byte[] buffer, int offset) + { + bool haveSubNodes = false; + uint totalEntriesSize = 0; + foreach (var entry in _entries) + { + totalEntriesSize += (uint)entry.Size; + haveSubNodes |= (entry.Flags & IndexEntryFlags.Node) != 0; + } + + _header.OffsetToFirstEntry = (uint)Utilities.RoundUp(IndexHeader.Size + _storageOverhead, 8); + _header.TotalSizeOfEntries = totalEntriesSize + _header.OffsetToFirstEntry; + _header.HasChildNodes = (byte)(haveSubNodes ? 1 : 0); + _header.WriteTo(buffer, offset + 0); + + int pos = (int)_header.OffsetToFirstEntry; + foreach (var entry in _entries) + { + entry.WriteTo(buffer, offset + pos); + pos += entry.Size; + } + + return IndexHeader.Size; + } + + public int CalcEntriesSize() + { + int totalEntriesSize = 0; + foreach (var entry in _entries) + { + totalEntriesSize += entry.Size; + } + + return totalEntriesSize; + } + + public virtual int CalcSize() + { + int firstEntryOffset = Utilities.RoundUp(IndexHeader.Size + _storageOverhead, 8); + return firstEntryOffset + CalcEntriesSize(); + } + + public int GetEntry(byte[] key, out bool exactMatch) + { + for (int i = 0; i < _entries.Count; ++i) + { + var focus = _entries[i]; + int compVal; + + if ((focus.Flags & IndexEntryFlags.End) != 0) + { + exactMatch = false; + return i; + } + else + { + compVal = _index.Compare(key, focus.KeyBuffer); + if (compVal <= 0) + { + exactMatch = compVal == 0; + return i; + } + } + } + + throw new IOException("Corrupt index node - no End entry"); + } + + public bool RemoveEntry(byte[] key, out IndexEntry newParentEntry) + { + bool exactMatch; + int entryIndex = GetEntry(key, out exactMatch); + IndexEntry entry = _entries[entryIndex]; + + if (exactMatch) + { + if ((entry.Flags & IndexEntryFlags.Node) != 0) + { + IndexNode childNode = _index.GetSubBlock(entry).Node; + IndexEntry rLeaf = childNode.FindLargestLeaf(); + + byte[] newKey = rLeaf.KeyBuffer; + byte[] newData = rLeaf.DataBuffer; + + IndexEntry newEntry; + childNode.RemoveEntry(newKey, out newEntry); + entry.KeyBuffer = newKey; + entry.DataBuffer = newData; + + if (newEntry != null) + { + InsertEntryThisNode(newEntry); + } + + newEntry = LiftNode(entryIndex); + if (newEntry != null) + { + InsertEntryThisNode(newEntry); + } + + newEntry = PopulateEnd(); + if (newEntry != null) + { + InsertEntryThisNode(newEntry); + } + + // New entry could be larger than old, so may need + // to divide this node... + newParentEntry = EnsureNodeSize(); + } + else + { + _entries.RemoveAt(entryIndex); + newParentEntry = null; + } + + _store(); + return true; + } + else if ((entry.Flags & IndexEntryFlags.Node) != 0) + { + IndexNode childNode = _index.GetSubBlock(entry).Node; + IndexEntry newEntry; + if (childNode.RemoveEntry(key, out newEntry)) + { + if (newEntry != null) + { + InsertEntryThisNode(newEntry); + } + + newEntry = LiftNode(entryIndex); + if (newEntry != null) + { + InsertEntryThisNode(newEntry); + } + + newEntry = PopulateEnd(); + if (newEntry != null) + { + InsertEntryThisNode(newEntry); + } + + // New entry could be larger than old, so may need + // to divide this node... + newParentEntry = EnsureNodeSize(); + + _store(); + return true; + } + } + + newParentEntry = null; + return false; + } + + /// + /// Only valid on the root node, this method moves all entries into a + /// single child node. + /// + /// Whether any changes were made. + internal bool Depose() + { + if (!_isRoot) + { + throw new InvalidOperationException("Only valid on root node"); + } + + if (_entries.Count == 1) + { + return false; + } + + IndexEntry newRootEntry = new IndexEntry(_index.IsFileIndex); + newRootEntry.Flags = IndexEntryFlags.End; + + IndexBlock newBlock = _index.AllocateBlock(newRootEntry); + + // Set the deposed entries into the new node. Note we updated the parent + // pointers first, because it's possible SetEntries may need to further + // divide the entries to fit into nodes. We mustn't overwrite any changes. + newBlock.Node.SetEntries(_entries, 0, _entries.Count); + + _entries.Clear(); + _entries.Add(newRootEntry); + + return true; + } + + /// + /// Removes redundant nodes (that contain only an 'End' entry). + /// + /// The index of the entry that may have a redundant child. + /// An entry that needs to be promoted to the parent node (if any). + private IndexEntry LiftNode(int entryIndex) + { + if ((_entries[entryIndex].Flags & IndexEntryFlags.Node) != 0) + { + IndexNode childNode = _index.GetSubBlock(_entries[entryIndex]).Node; + if (childNode._entries.Count == 1) + { + long freeBlock = _entries[entryIndex].ChildrenVirtualCluster; + _entries[entryIndex].Flags = (_entries[entryIndex].Flags & ~IndexEntryFlags.Node) | (childNode._entries[0].Flags & IndexEntryFlags.Node); + _entries[entryIndex].ChildrenVirtualCluster = childNode._entries[0].ChildrenVirtualCluster; + + _index.FreeBlock(freeBlock); + } + + if ((_entries[entryIndex].Flags & (IndexEntryFlags.Node | IndexEntryFlags.End)) == 0) + { + IndexEntry entry = _entries[entryIndex]; + _entries.RemoveAt(entryIndex); + + IndexNode nextNode = _index.GetSubBlock(_entries[entryIndex]).Node; + return nextNode.AddEntry(entry); + } + } + + return null; + } + + private IndexEntry PopulateEnd() + { + if (_entries.Count > 1 + && _entries[_entries.Count - 1].Flags == IndexEntryFlags.End + && (_entries[_entries.Count - 2].Flags & IndexEntryFlags.Node) != 0) + { + IndexEntry old = _entries[_entries.Count - 2]; + _entries.RemoveAt(_entries.Count - 2); + _entries[_entries.Count - 1].ChildrenVirtualCluster = old.ChildrenVirtualCluster; + _entries[_entries.Count - 1].Flags |= IndexEntryFlags.Node; + old.ChildrenVirtualCluster = 0; + old.Flags = IndexEntryFlags.None; + return _index.GetSubBlock(_entries[_entries.Count - 1]).Node.AddEntry(old); + } + + return null; + } + + private void InsertEntryThisNode(IndexEntry newEntry) + { + bool exactMatch; + int index = GetEntry(newEntry.KeyBuffer, out exactMatch); + + if (exactMatch) + { + throw new InvalidOperationException("Entry already exists"); + } + else + { + _entries.Insert(index, newEntry); + } + } + + private IndexEntry AddEntry(IndexEntry newEntry) + { + bool exactMatch; + int index = GetEntry(newEntry.KeyBuffer, out exactMatch); + + if (exactMatch) + { + throw new InvalidOperationException("Entry already exists"); + } + + if ((_entries[index].Flags & IndexEntryFlags.Node) != 0) + { + IndexEntry ourNewEntry = _index.GetSubBlock(_entries[index]).Node.AddEntry(newEntry); + if (ourNewEntry == null) + { + // No change to this node + return null; + } + + InsertEntryThisNode(ourNewEntry); + } + else + { + _entries.Insert(index, newEntry); + } + + // If there wasn't enough space, we may need to + // divide this node + IndexEntry newParentEntry = EnsureNodeSize(); + + _store(); + + return newParentEntry; + } + + private IndexEntry EnsureNodeSize() + { + // While the node is too small to hold the entries, we need to reduce + // the number of entries. + if (SpaceFree < 0) + { + if (_isRoot) + { + Depose(); + } + else + { + return Divide(); + } + } + + return null; + } + + /// + /// Finds the largest leaf entry in this tree. + /// + /// The index entry of the largest leaf. + private IndexEntry FindLargestLeaf() + { + if ((_entries[_entries.Count - 1].Flags & IndexEntryFlags.Node) != 0) + { + return _index.GetSubBlock(_entries[_entries.Count - 1]).Node.FindLargestLeaf(); + } + else if (_entries.Count > 1 && (_entries[_entries.Count - 2].Flags & IndexEntryFlags.Node) == 0) + { + return _entries[_entries.Count - 2]; + } + else + { + throw new IOException("Invalid index node found"); + } + } + + /// + /// Only valid on non-root nodes, this method divides the node in two, + /// adding the new node to the current parent. + /// + /// An entry that needs to be promoted to the parent node (if any). + private IndexEntry Divide() + { + int midEntryIdx = _entries.Count / 2; + IndexEntry midEntry = _entries[midEntryIdx]; + + // The terminating entry (aka end) for the new node + IndexEntry newTerm = new IndexEntry(_index.IsFileIndex); + newTerm.Flags |= IndexEntryFlags.End; + + // The set of entries in the new node + List newEntries = new List(midEntryIdx + 1); + for (int i = 0; i < midEntryIdx; ++i) + { + newEntries.Add(_entries[i]); + } + + newEntries.Add(newTerm); + + // Copy the node pointer from the elected 'mid' entry to the new node + if ((midEntry.Flags & IndexEntryFlags.Node) != 0) + { + newTerm.ChildrenVirtualCluster = midEntry.ChildrenVirtualCluster; + newTerm.Flags |= IndexEntryFlags.Node; + } + + // Set the new entries into the new node + IndexBlock newBlock = _index.AllocateBlock(midEntry); + + // Set the entries into the new node. Note we updated the parent + // pointers first, because it's possible SetEntries may need to further + // divide the entries to fit into nodes. We mustn't overwrite any changes. + newBlock.Node.SetEntries(newEntries, 0, newEntries.Count); + + // Forget about the entries moved into the new node, and the entry about + // to be promoted as the new node's pointer + _entries.RemoveRange(0, midEntryIdx + 1); + + // Promote the old mid entry + return midEntry; + } + + private void SetEntries(IList newEntries, int offset, int count) + { + _entries.Clear(); + for (int i = 0; i < count; ++i) + { + _entries.Add(newEntries[i + offset]); + } + + // Add an end entry, if not present + if (count == 0 || (_entries[_entries.Count - 1].Flags & IndexEntryFlags.End) == 0) + { + IndexEntry end = new IndexEntry(_index.IsFileIndex); + end.Flags = IndexEntryFlags.End; + _entries.Add(end); + } + + // Ensure the node isn't over-filled + if (SpaceFree < 0) + { + throw new IOException("Error setting node entries - oversized for node"); + } + + // Persist the new entries to disk + _store(); + } + } +} diff --git a/DiscUtils/Ntfs/IndexRoot.cs b/DiscUtils/Ntfs/IndexRoot.cs new file mode 100644 index 0000000..f308689 --- /dev/null +++ b/DiscUtils/Ntfs/IndexRoot.cs @@ -0,0 +1,297 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal sealed class IndexRoot : IByteArraySerializable, IDiagnosticTraceable + { + public const int HeaderOffset = 0x10; + + private uint _attrType; + private AttributeCollationRule _collationRule; + private uint _indexAllocationEntrySize; + private byte _rawClustersPerIndexRecord; + + public uint AttributeType + { + get { return _attrType; } + set { _attrType = value; } + } + + public AttributeCollationRule CollationRule + { + get { return _collationRule; } + set { _collationRule = value; } + } + + public uint IndexAllocationSize + { + get { return _indexAllocationEntrySize; } + set { _indexAllocationEntrySize = value; } + } + + public byte RawClustersPerIndexRecord + { + get { return _rawClustersPerIndexRecord; } + set { _rawClustersPerIndexRecord = value; } + } + + public int Size + { + get { return 16; } + } + + public IComparer GetCollator(UpperCase upCase) + { + switch (_collationRule) + { + case AttributeCollationRule.Filename: + return new FileNameComparer(upCase); + case AttributeCollationRule.SecurityHash: + return new SecurityHashComparer(); + case AttributeCollationRule.UnsignedLong: + return new UnsignedLongComparer(); + case AttributeCollationRule.MultipleUnsignedLongs: + return new MultipleUnsignedLongComparer(); + case AttributeCollationRule.Sid: + return new SidComparer(); + default: + throw new NotImplementedException(); + } + } + + public int ReadFrom(byte[] buffer, int offset) + { + _attrType = Utilities.ToUInt32LittleEndian(buffer, 0x00); + _collationRule = (AttributeCollationRule)Utilities.ToUInt32LittleEndian(buffer, 0x04); + _indexAllocationEntrySize = Utilities.ToUInt32LittleEndian(buffer, 0x08); + _rawClustersPerIndexRecord = buffer[0x0C]; + return 16; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(_attrType, buffer, 0); + Utilities.WriteBytesLittleEndian((uint)_collationRule, buffer, 0x04); + Utilities.WriteBytesLittleEndian(_indexAllocationEntrySize, buffer, 0x08); + Utilities.WriteBytesLittleEndian(_rawClustersPerIndexRecord, buffer, 0x0C); + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + " Attr Type: " + _attrType); + writer.WriteLine(indent + " Collation Rule: " + _collationRule); + writer.WriteLine(indent + " Index Alloc Size: " + _indexAllocationEntrySize); + writer.WriteLine(indent + " Raw Clusters Per Record: " + _rawClustersPerIndexRecord); + } + + private sealed class SecurityHashComparer : IComparer + { + public int Compare(byte[] x, byte[] y) + { + if (x == null && y == null) + { + return 0; + } + else if (y == null) + { + return -1; + } + else if (x == null) + { + return 1; + } + + uint xHash = Utilities.ToUInt32LittleEndian(x, 0); + uint yHash = Utilities.ToUInt32LittleEndian(y, 0); + + if (xHash < yHash) + { + return -1; + } + else if (xHash > yHash) + { + return 1; + } + + uint xId = Utilities.ToUInt32LittleEndian(x, 4); + uint yId = Utilities.ToUInt32LittleEndian(y, 4); + if (xId < yId) + { + return -1; + } + else if (xId > yId) + { + return 1; + } + else + { + return 0; + } + } + } + + private sealed class UnsignedLongComparer : IComparer + { + public int Compare(byte[] x, byte[] y) + { + if (x == null && y == null) + { + return 0; + } + else if (y == null) + { + return -1; + } + else if (x == null) + { + return 1; + } + + uint xVal = Utilities.ToUInt32LittleEndian(x, 0); + uint yVal = Utilities.ToUInt32LittleEndian(y, 0); + + if (xVal < yVal) + { + return -1; + } + else if (xVal > yVal) + { + return 1; + } + + return 0; + } + } + + private sealed class MultipleUnsignedLongComparer : IComparer + { + public int Compare(byte[] x, byte[] y) + { + for (int i = 0; i < x.Length / 4; ++i) + { + if (x == null && y == null) + { + return 0; + } + else if (y == null) + { + return -1; + } + else if (x == null) + { + return 1; + } + + uint xVal = Utilities.ToUInt32LittleEndian(x, i * 4); + uint yVal = Utilities.ToUInt32LittleEndian(y, i * 4); + + if (xVal < yVal) + { + return -1; + } + else if (xVal > yVal) + { + return 1; + } + } + + return 0; + } + } + + private sealed class FileNameComparer : IComparer + { + private UpperCase _stringComparer; + + public FileNameComparer(UpperCase upCase) + { + _stringComparer = upCase; + } + + public int Compare(byte[] x, byte[] y) + { + if (x == null && y == null) + { + return 0; + } + else if (y == null) + { + return -1; + } + else if (x == null) + { + return 1; + } + + byte xFnLen = x[0x40]; + byte yFnLen = y[0x40]; + + return _stringComparer.Compare(x, 0x42, xFnLen * 2, y, 0x42, yFnLen * 2); + } + } + + private sealed class SidComparer : IComparer + { + public int Compare(byte[] x, byte[] y) + { + if (x == null && y == null) + { + return 0; + } + else if (y == null) + { + return -1; + } + else if (x == null) + { + return 1; + } + + int toComp = Math.Min(x.Length, y.Length); + for (int i = 0; i < toComp; ++i) + { + int val = ((int)x[i]) - ((int)y[i]); + if (val != 0) + { + return val; + } + } + + if (x.Length < y.Length) + { + return -1; + } + else if (x.Length > y.Length) + { + return 1; + } + + return 0; + } + } + } +} diff --git a/DiscUtils/Ntfs/IndexView.cs b/DiscUtils/Ntfs/IndexView.cs new file mode 100644 index 0000000..7222a95 --- /dev/null +++ b/DiscUtils/Ntfs/IndexView.cs @@ -0,0 +1,160 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + + internal class IndexView + where K : IByteArraySerializable, new() + where D : IByteArraySerializable, new() + { + private Index _index; + + public IndexView(Index index) + { + _index = index; + } + + public int Count + { + get { return _index.Count; } + } + + public IEnumerable> Entries + { + get + { + foreach (var entry in _index.Entries) + { + yield return new KeyValuePair(Convert(entry.Key), Convert(entry.Value)); + } + } + } + + public D this[K key] + { + get + { + return Convert(_index[Unconvert(key)]); + } + + set + { + _index[Unconvert(key)] = Unconvert(value); + } + } + + public IEnumerable> FindAll(IComparable query) + { + foreach (var entry in _index.FindAll(query)) + { + yield return new KeyValuePair(Convert(entry.Key), Convert(entry.Value)); + } + } + + public KeyValuePair FindFirst(IComparable query) + { + foreach (var entry in FindAll(query)) + { + return entry; + } + + return default(KeyValuePair); + } + + public IEnumerable> FindAll(IComparable query) + { + foreach (var entry in _index.FindAll(new ComparableConverter(query))) + { + yield return new KeyValuePair(Convert(entry.Key), Convert(entry.Value)); + } + } + + public KeyValuePair FindFirst(IComparable query) + { + foreach (var entry in FindAll(query)) + { + return entry; + } + + return default(KeyValuePair); + } + + public bool TryGetValue(K key, out D data) + { + byte[] value; + if (_index.TryGetValue(Unconvert(key), out value)) + { + data = Convert(value); + return true; + } + else + { + data = default(D); + return false; + } + } + + public bool ContainsKey(K key) + { + return _index.ContainsKey(Unconvert(key)); + } + + public void Remove(K key) + { + _index.Remove(Unconvert(key)); + } + + private static T Convert(byte[] data) + where T : IByteArraySerializable, new() + { + T result = new T(); + result.ReadFrom(data, 0); + return result; + } + + private static byte[] Unconvert(T value) + where T : IByteArraySerializable, new() + { + byte[] buffer = new byte[value.Size]; + value.WriteTo(buffer, 0); + return buffer; + } + + private class ComparableConverter : IComparable + { + private IComparable _wrapped; + + public ComparableConverter(IComparable toWrap) + { + _wrapped = toWrap; + } + + public int CompareTo(byte[] other) + { + return _wrapped.CompareTo(Convert(other)); + } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/AttributeFlags.cs b/DiscUtils/Ntfs/Internals/AttributeFlags.cs new file mode 100644 index 0000000..1ceca8a --- /dev/null +++ b/DiscUtils/Ntfs/Internals/AttributeFlags.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System; + + /// + /// Flags indicating how an attribute's content is stored on disk. + /// + [Flags] + public enum AttributeFlags + { + /// + /// The data is stored in linear form. + /// + None = 0x0000, + + /// + /// The data is compressed. + /// + Compressed = 0x0001, + + /// + /// The data is encrypted. + /// + Encrypted = 0x4000, + + /// + /// The data is stored in sparse form. + /// + Sparse = 0x8000 + } +} diff --git a/DiscUtils/Ntfs/Internals/AttributeListAttribute.cs b/DiscUtils/Ntfs/Internals/AttributeListAttribute.cs new file mode 100644 index 0000000..a115e3a --- /dev/null +++ b/DiscUtils/Ntfs/Internals/AttributeListAttribute.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System.Collections.Generic; + + /// + /// List of attributes for files that are split over multiple Master File Table entries. + /// + /// + /// + /// Files with lots of attribute data (for example that have become very fragmented) contain + /// this attribute in their 'base' Master File Table entry. This attribute acts as an index, + /// indicating for each attribute in the file, which Master File Table entry contains the + /// attribute. + /// + /// + public sealed class AttributeListAttribute : GenericAttribute + { + private AttributeList _list; + + internal AttributeListAttribute(INtfsContext context, AttributeRecord record) + : base(context, record) + { + byte[] content = Utilities.ReadAll(Content); + _list = new AttributeList(); + _list.ReadFrom(content, 0); + } + + /// + /// Gets the entries in this attribute list. + /// + public ICollection Entries + { + get + { + List entries = new List(); + foreach (var record in _list) + { + entries.Add(new AttributeListEntry(record)); + } + + return entries; + } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/AttributeListEntry.cs b/DiscUtils/Ntfs/Internals/AttributeListEntry.cs new file mode 100644 index 0000000..790d8e6 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/AttributeListEntry.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + /// + /// Represents an entry in an AttributeList attribute. + /// + /// Each instance of this class points to the actual Master File Table + /// entry that contains the attribute. It is used for files split over multiple + /// Master File Table entries. + public sealed class AttributeListEntry + { + private AttributeListRecord _record; + + internal AttributeListEntry(AttributeListRecord record) + { + _record = record; + } + + /// + /// Gets the type of the attribute. + /// + public AttributeType AttributeType + { + get { return _record.Type; } + } + + /// + /// Gets the name of the attribute (if any). + /// + public string AttributeName + { + get { return _record.Name; } + } + + /// + /// Gets the first cluster represented in this attribute (normally 0). + /// + /// + /// + /// For very fragmented files, it can be necessary to split a single attribute + /// over multiple Master File Table entries. This is achieved with multiple attributes + /// with the same name and type (one per Master File Table entry), with this field + /// determining the logical order of the attributes. + /// + /// + /// The number is the first 'virtual' cluster present (i.e. divide the file's content + /// into 'cluster' sized chunks, this is the first of those clusters logically + /// represented in the attribute). + /// + /// + public long FirstFileCluster + { + get { return (long)_record.StartVcn; } + } + + /// + /// Gets the Master File Table entry that contains the attribute. + /// + public MasterFileTableReference MasterFileTableEntry + { + get { return new MasterFileTableReference(_record.BaseFileReference); } + } + + /// + /// Gets the identifier of the attribute. + /// + public int AttributeIdentifier + { + get { return _record.AttributeId; } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/EntryState.cs b/DiscUtils/Ntfs/Internals/EntryState.cs new file mode 100644 index 0000000..ad05f7f --- /dev/null +++ b/DiscUtils/Ntfs/Internals/EntryState.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +using System; + +namespace DiscUtils.Ntfs.Internals +{ + /// + /// Flags indicating the state of a Master File Table entry. + /// + /// + /// Used to filter entries in the Master File Table. + /// + [Flags] + public enum EntryState + { + /// + /// No entries match. + /// + None = 0, + + /// + /// The entry is currently in use. + /// + InUse = 1, + + /// + /// The entry is currently not in use. + /// + NotInUse = 2, + + /// + /// All entries match. + /// + All = 3 + } +} diff --git a/DiscUtils/Ntfs/Internals/EntryStates.cs b/DiscUtils/Ntfs/Internals/EntryStates.cs new file mode 100644 index 0000000..b85b8a4 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/EntryStates.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System; + + /// + /// Flags indicating the state of a Master File Table entry. + /// + /// + /// Used to filter entries in the Master File Table. + /// + [Flags] + public enum EntryStates + { + /// + /// No entries match. + /// + None = 0, + + /// + /// The entry is currently in use. + /// + InUse = 1, + + /// + /// The entry is currently not in use. + /// + NotInUse = 2, + + /// + /// All entries match. + /// + All = 3 + } +} diff --git a/DiscUtils/Ntfs/Internals/FileNameAttribute.cs b/DiscUtils/Ntfs/Internals/FileNameAttribute.cs new file mode 100644 index 0000000..de7d5d3 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/FileNameAttribute.cs @@ -0,0 +1,145 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System; + + /// + /// Representation of an NTFS File Name attribute. + /// + /// + /// + /// Each Master File Table entry (MFT Entry) has one of these attributes for each + /// hard link. Files with a long name and a short name will have at least two of + /// these attributes. + /// + /// The details in this attribute may be inconsistent with similar information in + /// the StandardInformationAttribute for a file. The StandardInformation is + /// definitive, this attribute holds a 'cache' of the information. + /// + /// + public sealed class FileNameAttribute : GenericAttribute + { + private FileNameRecord _fnr; + + internal FileNameAttribute(INtfsContext context, AttributeRecord record) + : base(context, record) + { + byte[] content = Utilities.ReadAll(Content); + _fnr = new FileNameRecord(); + _fnr.ReadFrom(content, 0); + } + + /// + /// Gets the reference to the parent directory. + /// + /// + /// This attribute stores the name of a file within a directory, this field + /// provides the link back to the directory. + /// + public MasterFileTableReference ParentDirectory + { + get { return new MasterFileTableReference(_fnr.ParentDirectory); } + } + + /// + /// Gets the creation time of the file. + /// + public DateTime CreationTime + { + get { return _fnr.CreationTime; } + } + + /// + /// Gets the modification time of the file. + /// + public DateTime ModificationTime + { + get { return _fnr.ModificationTime; } + } + + /// + /// Gets the last time the Master File Table entry for the file was changed. + /// + public DateTime MasterFileTableChangedTime + { + get { return _fnr.MftChangedTime; } + } + + /// + /// Gets the last access time of the file. + /// + public DateTime LastAccessTime + { + get { return _fnr.LastAccessTime; } + } + + /// + /// Gets the amount of disk space allocated for the file. + /// + public long AllocatedSize + { + get { return (long)_fnr.AllocatedSize; } + } + + /// + /// Gets the amount of data stored in the file. + /// + public long RealSize + { + get { return (long)_fnr.RealSize; } + } + + /// + /// Gets the attributes of the file, as stored by NTFS. + /// + public NtfsFileAttributes FileAttributes + { + get { return (NtfsFileAttributes)_fnr.Flags; } + } + + /// + /// Gets the extended attributes size, or a reparse tag, depending on the nature of the file. + /// + public long ExtendedAttributesSizeOrReparsePointTag + { + get { return (long)_fnr.EASizeOrReparsePointTag; } + } + + /// + /// Gets the namespace of the FileName property. + /// + public NtfsNamespace FileNameNamespace + { + get { return (NtfsNamespace)_fnr.FileNameNamespace; } + } + + /// + /// Gets the name of the file within the parent directory. + /// + public string FileName + { + get { return _fnr.FileName; } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/GenericAttribute.cs b/DiscUtils/Ntfs/Internals/GenericAttribute.cs new file mode 100644 index 0000000..2699cec --- /dev/null +++ b/DiscUtils/Ntfs/Internals/GenericAttribute.cs @@ -0,0 +1,117 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + /// + /// Base class for all attributes within Master File Table entries. + /// + /// + /// More specialized base classes are provided for known attribute types. + /// + public abstract class GenericAttribute + { + private INtfsContext _context; + private AttributeRecord _record; + + internal GenericAttribute(INtfsContext context, AttributeRecord record) + { + _context = context; + _record = record; + } + + /// + /// Gets the name of the attribute (if any). + /// + public string Name + { + get { return _record.Name; } + } + + /// + /// Gets the type of the attribute. + /// + public AttributeType AttributeType + { + get { return _record.AttributeType; } + } + + /// + /// Gets the unique id of the attribute. + /// + public int Identifier + { + get { return _record.AttributeId; } + } + + /// + /// Gets a value indicating whether the attribute content is stored in the MFT record itself. + /// + public bool IsResident + { + get { return !_record.IsNonResident; } + } + + /// + /// Gets the flags indicating how the content of the attribute is stored. + /// + public AttributeFlags Flags + { + get { return (AttributeFlags)_record.Flags; } + } + + /// + /// Gets the amount of valid data in the attribute's content. + /// + public long ContentLength + { + get { return _record.DataLength; } + } + + /// + /// Gets a buffer that can access the content of the attribute. + /// + public IBuffer Content + { + get + { + IBuffer rawBuffer = _record.GetReadOnlyDataBuffer(_context); + return new SubBuffer(rawBuffer, 0, _record.DataLength); + } + } + + internal static GenericAttribute FromAttributeRecord(INtfsContext context, AttributeRecord record) + { + switch (record.AttributeType) + { + case AttributeType.AttributeList: + return new AttributeListAttribute(context, record); + case AttributeType.FileName: + return new FileNameAttribute(context, record); + case AttributeType.StandardInformation: + return new StandardInformationAttribute(context, record); + default: + return new UnknownAttribute(context, record); + } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/MasterFileTable.cs b/DiscUtils/Ntfs/Internals/MasterFileTable.cs new file mode 100644 index 0000000..03d98de --- /dev/null +++ b/DiscUtils/Ntfs/Internals/MasterFileTable.cs @@ -0,0 +1,154 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System.Collections.Generic; + using InternalMasterFileTable = DiscUtils.Ntfs.MasterFileTable; + + /// + /// Provides read-only access to the Master File Table of an NTFS file system. + /// + public sealed class MasterFileTable + { + /// + /// Index of the Master File Table itself. + /// + public const long MasterFileTableIndex = 0; + + /// + /// Index of the Master File Table Mirror file. + /// + public const long MasterFileTableMirrorIndex = 1; + + /// + /// Index of the Log file. + /// + public const long LogFileIndex = 2; + + /// + /// Index of the Volume file. + /// + public const long VolumeIndex = 3; + + /// + /// Index of the Attribute Definition file. + /// + public const long AttributeDefinitionIndex = 4; + + /// + /// Index of the Root Directory. + /// + public const long RootDirectoryIndex = 5; + + /// + /// Index of the Bitmap file. + /// + public const long BitmapIndex = 6; + + /// + /// Index of the Boot sector(s). + /// + public const long BootIndex = 7; + + /// + /// Index of the Bad Cluster file. + /// + public const long BadClusterIndex = 8; + + /// + /// Index of the Security Descriptor file. + /// + public const long SecureIndex = 9; + + /// + /// Index of the Uppercase mapping file. + /// + public const long UppercaseIndex = 10; + + /// + /// Index of the Optional Extensions directory. + /// + public const long ExtendDirectoryIndex = 11; + + /// + /// First index available for 'normal' files. + /// + private const uint FirstNormalFileIndex = 24; + + private INtfsContext _context; + private InternalMasterFileTable _mft; + + internal MasterFileTable(INtfsContext context, InternalMasterFileTable mft) + { + _context = context; + _mft = mft; + } + + /// + /// Gets an entry by index. + /// + /// The index of the entry. + /// The entry. + public MasterFileTableEntry this[long index] + { + get + { + FileRecord mftRecord = _mft.GetRecord(index, true, true); + if (mftRecord != null) + { + return new MasterFileTableEntry(_context, mftRecord); + } + else + { + return null; + } + } + } + + /// + /// Enumerates all entries. + /// + /// Filter controlling which entries are returned. + /// An enumeration of entries matching the filter. + public IEnumerable GetEntries(EntryStates filter) + { + foreach (var record in _mft.Records) + { + EntryStates state; + if ((record.Flags & FileRecordFlags.InUse) != 0) + { + state = EntryStates.InUse; + } + else + { + state = EntryStates.NotInUse; + } + + if ((state & filter) != 0) + { + yield return new MasterFileTableEntry(_context, record); + } + } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/MasterFileTableAttribute.cs b/DiscUtils/Ntfs/Internals/MasterFileTableAttribute.cs new file mode 100644 index 0000000..234752d --- /dev/null +++ b/DiscUtils/Ntfs/Internals/MasterFileTableAttribute.cs @@ -0,0 +1,10 @@ +namespace DiscUtils.Ntfs.Internals +{ + using System; + using System.Collections.Generic; + using System.Text; + + public sealed class MasterFileTableAttribute + { + } +} diff --git a/DiscUtils/Ntfs/Internals/MasterFileTableEntry.cs b/DiscUtils/Ntfs/Internals/MasterFileTableEntry.cs new file mode 100644 index 0000000..47acbc3 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/MasterFileTableEntry.cs @@ -0,0 +1,135 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System.Collections.Generic; + + /// + /// An entry within the Master File Table. + /// + public sealed class MasterFileTableEntry + { + private INtfsContext _context; + private FileRecord _fileRecord; + + internal MasterFileTableEntry(INtfsContext context, FileRecord fileRecord) + { + _context = context; + _fileRecord = fileRecord; + } + + /// + /// Gets the index of this entry in the Master File Table. + /// + public long Index + { + get { return _fileRecord.LoadedIndex; } + } + + /// + /// Gets the change identifier that is updated each time the file is modified by Windows, relates to the NTFS log file. + /// + /// + /// The NTFS log file provides journalling, preventing meta-data corruption in the event of a system crash. + /// + public long LogFileSequenceNumber + { + get { return (long)_fileRecord.LogFileSequenceNumber; } + } + + /// + /// Gets the revision number of the entry. + /// + /// + /// Each time an entry is allocated or de-allocated, this number is incremented by one. + /// + public int SequenceNumber + { + get { return _fileRecord.SequenceNumber; } + } + + /// + /// Gets the number of hard links referencing this file. + /// + public int HardLinkCount + { + get { return _fileRecord.HardLinkCount; } + } + + /// + /// Gets the flags indicating the nature of the entry. + /// + public MasterFileTableEntryFlags Flags + { + get { return (MasterFileTableEntryFlags)_fileRecord.Flags; } + } + + /// + /// Gets the identity of the base entry for files split over multiple entries. + /// + /// + /// All entries that form part of the same file have the same value for + /// this property. + /// + public MasterFileTableReference BaseRecordReference + { + get { return new MasterFileTableReference(_fileRecord.BaseFile); } + } + + /// + /// Gets the next attribute identity that will be allocated. + /// + public int NextAttributeId + { + get { return _fileRecord.NextAttributeId; } + } + + /// + /// Gets the index of this entry in the Master File Table (as stored in the entry itself). + /// + /// + /// Note - older versions of Windows did not store this value, so it may be Zero. + /// + public long SelfIndex + { + get { return _fileRecord.MasterFileTableIndex; } + } + + /// + /// Gets the attributes contained in this entry. + /// + public ICollection Attributes + { + get + { + List result = new List(); + foreach (var attr in _fileRecord.Attributes) + { + result.Add(GenericAttribute.FromAttributeRecord(_context, attr)); + } + + return result; + } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/MasterFileTableEntryFlags.cs b/DiscUtils/Ntfs/Internals/MasterFileTableEntryFlags.cs new file mode 100644 index 0000000..e91b097 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/MasterFileTableEntryFlags.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System; + + /// + /// Flags indicating the nature of a Master File Table entry. + /// + [Flags] + public enum MasterFileTableEntryFlags : int + { + /// + /// Default value. + /// + None = 0x0000, + + /// + /// The entry is currently in use. + /// + InUse = 0x0001, + + /// + /// The entry is for a directory (rather than a file). + /// + IsDirectory = 0x0002, + + /// + /// The entry is for a file that forms parts of the NTFS meta-data. + /// + IsMetaFile = 0x0004, + + /// + /// The entry contains index attributes. + /// + HasViewIndex = 0x0008 + } +} diff --git a/DiscUtils/Ntfs/Internals/MasterFileTableRecord.cs b/DiscUtils/Ntfs/Internals/MasterFileTableRecord.cs new file mode 100644 index 0000000..37f76bb --- /dev/null +++ b/DiscUtils/Ntfs/Internals/MasterFileTableRecord.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System.Collections; + using System.Collections.Generic; + using System.IO; + + public sealed class MasterFileTableRecord + { + private FileRecord _fileRecord; + + internal MasterFileTableRecord(FileRecord fileRecord) + { + _fileRecord = fileRecord; + } + + /// + /// Changes each time the file is modified by Windows, relates to the NTFS journal. + /// + public long JournalSequenceNumber + { + get { return (long)_fileRecord.LogFileSequenceNumber; } + } + + public int SequenceNumber + { + get { return _fileRecord.SequenceNumber; } + } + + public int HardLinkCount + { + get { return _fileRecord.HardLinkCount; } + } + + public MasterFileTableRecordFlags Flags + { + get { return (MasterFileTableRecordFlags)_fileRecord.Flags; } + } + + public MasterFileTableReference BaseRecordReference + { + get { return new MasterFileTableReference(_fileRecord.BaseFile); } + } + + public int NextAttributeId + { + get { return _fileRecord.NextAttributeId; } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/MasterFileTableRecordFlags.cs b/DiscUtils/Ntfs/Internals/MasterFileTableRecordFlags.cs new file mode 100644 index 0000000..15d46d4 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/MasterFileTableRecordFlags.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System; + + [Flags] + public enum MasterFileTableRecordFlags : int + { + None = 0x0000, + InUse = 0x0001, + IsDirectory = 0x0002, + IsMetaFile = 0x0004, + HasViewIndex = 0x0008 + } +} diff --git a/DiscUtils/Ntfs/Internals/MasterFileTableReference.cs b/DiscUtils/Ntfs/Internals/MasterFileTableReference.cs new file mode 100644 index 0000000..8ac7ec8 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/MasterFileTableReference.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + /// + /// A reference to a Master File Table entry. + /// + public struct MasterFileTableReference + { + private FileRecordReference _ref; + + internal MasterFileTableReference(FileRecordReference recordRef) + { + _ref = recordRef; + } + + /// + /// Gets the index of the referred entry in the Master File Table. + /// + public long RecordIndex + { + get { return _ref.MftIndex; } + } + + /// + /// Gets the revision number of the entry. + /// + /// + /// This value prevents accidental reference to an entry - it will get out + /// of sync with the actual entry if the entry is re-allocated or de-allocated. + /// + public int RecordSequenceNumber + { + get { return _ref.SequenceNumber; } + } + + /// + /// Compares to instances for equality. + /// + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equivalent, else false. + public static bool operator ==(MasterFileTableReference a, MasterFileTableReference b) + { + return a._ref == b._ref; + } + + /// + /// Compares to instances for equality. + /// + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equivalent, else false. + public static bool operator !=(MasterFileTableReference a, MasterFileTableReference b) + { + return a._ref != b._ref; + } + + /// + /// Compares another object for equality. + /// + /// The object to compare. + /// true if the other object is equivalent, else false. + public override bool Equals(object obj) + { + if (obj == null || !(obj is MasterFileTableReference)) + { + return false; + } + + return _ref == ((MasterFileTableReference)obj)._ref; + } + + /// + /// Gets a hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + return _ref.GetHashCode(); + } + } +} diff --git a/DiscUtils/Ntfs/Internals/NtfsFileAttributes.cs b/DiscUtils/Ntfs/Internals/NtfsFileAttributes.cs new file mode 100644 index 0000000..1b71731 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/NtfsFileAttributes.cs @@ -0,0 +1,113 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System; + + /// + /// File attributes as stored natively by NTFS. + /// + [Flags] + public enum NtfsFileAttributes + { + /// + /// No attributes. + /// + None = 0x00000000, + + /// + /// The file is read-only. + /// + ReadOnly = 0x00000001, + + /// + /// The file is hidden. + /// + Hidden = 0x00000002, + + /// + /// The file is part of the Operating System. + /// + System = 0x00000004, + + /// + /// The file should be archived. + /// + Archive = 0x00000020, + + /// + /// The file is actually a device. + /// + Device = 0x00000040, + + /// + /// The file is a 'normal' file. + /// + Normal = 0x00000080, + + /// + /// The file is a temporary file. + /// + Temporary = 0x00000100, + + /// + /// The file content is stored in sparse form. + /// + Sparse = 0x00000200, + + /// + /// The file has a reparse point attached. + /// + ReparsePoint = 0x00000400, + + /// + /// The file content is stored compressed. + /// + Compressed = 0x00000800, + + /// + /// The file is an 'offline' file. + /// + Offline = 0x00001000, + + /// + /// The file is not indexed. + /// + NotIndexed = 0x00002000, + + /// + /// The file content is encrypted. + /// + Encrypted = 0x00004000, + + /// + /// The file is actually a directory. + /// + Directory = 0x10000000, + + /// + /// The file has an index attribute. + /// + IndexView = 0x20000000 + } +} diff --git a/DiscUtils/Ntfs/Internals/NtfsNamespace.cs b/DiscUtils/Ntfs/Internals/NtfsNamespace.cs new file mode 100644 index 0000000..7058168 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/NtfsNamespace.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System; + + /// + /// The known NTFS namespaces. + /// + /// + /// NTFS has multiple namespaces, indicating whether a name is the + /// long name for a file, the short name for a file, both, or none. + /// + public enum NtfsNamespace + { + /// + /// Posix namespace (i.e. long name). + /// + Posix = 0, + + /// + /// Windows long file name. + /// + Win32 = 1, + + /// + /// DOS (8.3) file name. + /// + Dos = 2, + + /// + /// File name that is both the long name and the DOS (8.3) name. + /// + Win32AndDos = 3 + } +} diff --git a/DiscUtils/Ntfs/Internals/StandardInformationAttribute.cs b/DiscUtils/Ntfs/Internals/StandardInformationAttribute.cs new file mode 100644 index 0000000..04f0d35 --- /dev/null +++ b/DiscUtils/Ntfs/Internals/StandardInformationAttribute.cs @@ -0,0 +1,146 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + using System; + + /// + /// Representation of an NTFS File Name attribute. + /// + /// + /// The details in this attribute may be inconsistent with similar information in + /// the FileNameAttribute(s) for a file. This attribute is definitive, the + /// FileNameAttribute attribute holds a 'cache' of some of the information. + /// + public sealed class StandardInformationAttribute : GenericAttribute + { + private StandardInformation _si; + + internal StandardInformationAttribute(INtfsContext context, AttributeRecord record) + : base(context, record) + { + byte[] content = Utilities.ReadAll(Content); + _si = new StandardInformation(); + _si.ReadFrom(content, 0); + } + + /// + /// Gets the creation time of the file. + /// + public DateTime CreationTime + { + get { return _si.CreationTime; } + } + + /// + /// Gets the modification time of the file. + /// + public DateTime ModificationTime + { + get { return _si.ModificationTime; } + } + + /// + /// Gets the last time the Master File Table entry for the file was changed. + /// + public DateTime MasterFileTableChangedTime + { + get { return _si.MftChangedTime; } + } + + /// + /// Gets the last access time of the file. + /// + public DateTime LastAccessTime + { + get { return _si.LastAccessTime; } + } + + /// + /// Gets the attributes of the file, as stored by NTFS. + /// + public NtfsFileAttributes FileAttributes + { + get { return (NtfsFileAttributes)_si.FileAttributes; } + } + + /// + /// Gets the maximum number of file versions (normally 0). + /// + public long MaxVersions + { + get { return _si.MaxVersions; } + } + + /// + /// Gets the version number of the file (normally 0). + /// + public long Version + { + get { return _si.Version; } + } + + /// + /// Gets the Unknown. + /// + public long ClassId + { + get { return _si.ClassId; } + } + + /// + /// Gets the owner identity, for the purposes of quota allocation. + /// + public long OwnerId + { + get { return _si.OwnerId; } + } + + /// + /// Gets the identifier of the Security Descriptor for this file. + /// + /// + /// Security Descriptors are stored in the \$Secure meta-data file. + /// + public long SecurityId + { + get { return _si.SecurityId; } + } + + /// + /// Gets the amount charged to the owners quota for this file. + /// + public long QuotaCharged + { + get { return (long)_si.QuotaCharged; } + } + + /// + /// Gets the last update sequence number of the file (relates to the user-readable journal). + /// + public long JournalSequenceNumber + { + get { return (long)_si.UpdateSequenceNumber; } + } + } +} diff --git a/DiscUtils/Ntfs/Internals/UnknownAttribute.cs b/DiscUtils/Ntfs/Internals/UnknownAttribute.cs new file mode 100644 index 0000000..25c765a --- /dev/null +++ b/DiscUtils/Ntfs/Internals/UnknownAttribute.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs.Internals +{ + internal sealed class UnknownAttribute : GenericAttribute + { + public UnknownAttribute(INtfsContext context, AttributeRecord record) + : base(context, record) + { + } + } +} diff --git a/DiscUtils/Ntfs/LZNT1.cs b/DiscUtils/Ntfs/LZNT1.cs new file mode 100644 index 0000000..963db10 --- /dev/null +++ b/DiscUtils/Ntfs/LZNT1.cs @@ -0,0 +1,314 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Contributions by bsobel: +// - Compression algorithm distantly derived from Puyo tools (BSD license)* +// - Decompression adjusted to support variety of block sizes +// +// (*) Puyo tools implements a different LZ-style algorithm +// + +namespace DiscUtils.Ntfs +{ + using System; + using DiscUtils.Compression; + + /// + /// Implementation of the LZNT1 algorithm used for compressing NTFS files. + /// + /// + /// Due to apparent bugs in Window's LZNT1 decompressor, it is strongly recommended that + /// only the block size of 4096 is used. Other block sizes corrupt data on decompression. + /// + internal sealed class LZNT1 : BlockCompressor + { + private const ushort SubBlockIsCompressedFlag = 0x8000; + private const ushort SubBlockSizeMask = 0x0fff; + + // LZNT1 appears to ignore the actual block size requested, most likely due to + // a bug in the decompressor, which assumes 4KB block size. To be bug-compatible, + // we assume each block is 4KB on decode also. + private const int FixedBlockSize = 0x1000; + + private static byte[] s_compressionBits = CalcCompressionBits(); + + public LZNT1() + { + BlockSize = 4096; + } + + public override CompressionResult Compress(byte[] source, int sourceOffset, int sourceLength, byte[] compressed, int compressedOffset, ref int compressedLength) + { + uint sourcePointer = 0; + uint sourceCurrentBlock = 0; + uint destPointer = 0; + + // Set up the Lz Compression Dictionary + LzWindowDictionary lzDictionary = new LzWindowDictionary(); + bool nonZeroDataFound = false; + + for (int subBlock = 0; subBlock < sourceLength; subBlock += BlockSize) + { + lzDictionary.MinMatchAmount = 3; + sourceCurrentBlock = sourcePointer; + + uint decompressedSize = (uint)Math.Min(sourceLength - subBlock, BlockSize); + uint compressedSize = 0; + + // Start compression + uint headerPosition = destPointer; + compressed[compressedOffset + destPointer] = compressed[compressedOffset + destPointer + 1] = 0; + destPointer += 2; + + while (sourcePointer - subBlock < decompressedSize) + { + if (destPointer + 1 >= compressedLength) + { + return CompressionResult.Incompressible; + } + + byte bitFlag = 0x0; + uint flagPosition = destPointer; + + compressed[compressedOffset + destPointer] = bitFlag; // It will be filled in later + compressedSize++; + destPointer++; + + for (int i = 0; i < 8; i++) + { + int lengthBits = 16 - s_compressionBits[sourcePointer - subBlock]; + ushort lengthMask = (ushort)((1 << s_compressionBits[sourcePointer - subBlock]) - 1); + + lzDictionary.MaxMatchAmount = Math.Min(1 << lengthBits, BlockSize - 1); + + int[] lzSearchMatch = lzDictionary.Search(source, sourceOffset + subBlock, (uint)(sourcePointer - subBlock), decompressedSize); + if (lzSearchMatch[1] > 0) + { + // There is a compression match + if (destPointer + 2 >= compressedLength) + { + return CompressionResult.Incompressible; + } + + bitFlag |= (byte)(1 << i); + + int rawOffset = lzSearchMatch[0]; + int rawLength = lzSearchMatch[1]; + + int convertedOffset = (rawOffset - 1) << lengthBits; + int convertedSize = (rawLength - 3) & ((1 << lengthMask) - 1); + + ushort convertedData = (ushort)(convertedOffset | convertedSize); + Utilities.WriteBytesLittleEndian(convertedData, compressed, compressedOffset + (int)destPointer); + + lzDictionary.AddEntryRange(source, sourceOffset + subBlock, (int)(sourcePointer - subBlock), lzSearchMatch[1]); + sourcePointer += (uint)lzSearchMatch[1]; + destPointer += 2; + compressedSize += 2; + } + else + { + // There wasn't a match + if (destPointer + 1 >= compressedLength) + { + return CompressionResult.Incompressible; + } + + bitFlag |= (byte)(0 << i); + + if (source[sourceOffset + sourcePointer] != 0) + { + nonZeroDataFound = true; + } + + compressed[compressedOffset + destPointer] = source[sourceOffset + sourcePointer]; + lzDictionary.AddEntry(source, sourceOffset + subBlock, (int)(sourcePointer - subBlock)); + + sourcePointer++; + destPointer++; + compressedSize++; + } + + // Check for out of bounds + if (sourcePointer - subBlock >= decompressedSize) + { + break; + } + } + + // Write the real flag. + compressed[compressedOffset + flagPosition] = bitFlag; + } + + // If compressed size >= block size just store block + if (compressedSize >= BlockSize) + { + // Set the header to indicate non-compressed block + Utilities.WriteBytesLittleEndian((ushort)(0x3000 | (BlockSize - 1)), compressed, compressedOffset + (int)headerPosition); + + Array.Copy(source, (int)sourceOffset + sourceCurrentBlock, compressed, compressedOffset + headerPosition + 2, BlockSize); + destPointer = (uint)(headerPosition + 2 + BlockSize); + + // Make sure decompression stops by setting the next two bytes to null, prevents us from having to + // clear the rest of the array. + compressed[destPointer] = 0; + compressed[destPointer + 1] = 0; + } + else + { + // Set the header to indicate compressed and the right length + Utilities.WriteBytesLittleEndian((ushort)(0xb000 | (compressedSize - 1)), compressed, compressedOffset + (int)headerPosition); + } + + lzDictionary.Reset(); + } + + if (destPointer >= sourceLength) + { + compressedLength = 0; + return CompressionResult.Incompressible; + } + else if (nonZeroDataFound) + { + compressedLength = (int)destPointer; + return CompressionResult.Compressed; + } + else + { + compressedLength = 0; + return CompressionResult.AllZeros; + } + } + + public override int Decompress(byte[] source, int sourceOffset, int sourceLength, byte[] decompressed, int decompressedOffset) + { + int sourceIdx = 0; + int destIdx = 0; + + while (sourceIdx < sourceLength) + { + ushort header = Utilities.ToUInt16LittleEndian(source, sourceOffset + sourceIdx); + sourceIdx += 2; + + // Look for null-terminating sub-block header + if (header == 0) + { + break; + } + + if ((header & SubBlockIsCompressedFlag) == 0) + { + int blockSize = (header & SubBlockSizeMask) + 1; + Array.Copy(source, sourceOffset + sourceIdx, decompressed, decompressedOffset + destIdx, blockSize); + sourceIdx += blockSize; + destIdx += blockSize; + } + else + { + // compressed + int destSubBlockStart = destIdx; + int srcSubBlockEnd = sourceIdx + (header & SubBlockSizeMask) + 1; + while (sourceIdx < srcSubBlockEnd) + { + byte tag = source[sourceOffset + sourceIdx]; + ++sourceIdx; + + for (int token = 0; token < 8; ++token) + { + // We might have hit the end of the sub block whilst still working though + // a tag - abort if we have... + if (sourceIdx >= srcSubBlockEnd) + { + break; + } + + if ((tag & 1) == 0) + { + if (decompressedOffset + destIdx >= decompressed.Length) + { + return destIdx; + } + + decompressed[decompressedOffset + destIdx] = source[sourceOffset + sourceIdx]; + ++destIdx; + ++sourceIdx; + } + else + { + ushort lengthBits = (ushort)(16 - s_compressionBits[destIdx - destSubBlockStart]); + ushort lengthMask = (ushort)((1 << lengthBits) - 1); + + ushort phraseToken = Utilities.ToUInt16LittleEndian(source, sourceOffset + sourceIdx); + sourceIdx += 2; + + int destBackAddr = destIdx - (phraseToken >> lengthBits) - 1; + int length = (phraseToken & lengthMask) + 3; + + for (int i = 0; i < length; ++i) + { + decompressed[decompressedOffset + destIdx++] = decompressed[decompressedOffset + destBackAddr++]; + } + } + + tag >>= 1; + } + } + + // Bug-compatible - if we decompressed less than 4KB, jump to next 4KB boundary. If + // that would leave less than a 4KB remaining, abort with data decompressed so far. + if (decompressedOffset + destIdx + FixedBlockSize > decompressed.Length) + { + return destIdx; + } + else if (destIdx < destSubBlockStart + FixedBlockSize) + { + int skip = (destSubBlockStart + FixedBlockSize) - destIdx; + Array.Clear(decompressed, decompressedOffset + destIdx, skip); + destIdx += skip; + } + } + } + + return destIdx; + } + + private static byte[] CalcCompressionBits() + { + byte[] result = new byte[4096]; + byte offsetBits = 0; + + int y = 0x10; + for (int x = 0; x < result.Length; x++) + { + result[x] = (byte)(4 + offsetBits); + if (x == y) + { + y <<= 1; + offsetBits++; + } + } + + return result; + } + } +} diff --git a/DiscUtils/Ntfs/LzWindowDictionary.cs b/DiscUtils/Ntfs/LzWindowDictionary.cs new file mode 100644 index 0000000..0be74f1 --- /dev/null +++ b/DiscUtils/Ntfs/LzWindowDictionary.cs @@ -0,0 +1,142 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +// +// Contributed by bsobel: +// - Derived from Puyo tools (BSD license) +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + + internal sealed class LzWindowDictionary + { + /// + /// Index of locations of each possible byte value within the compression window. + /// + private List[] _offsetList; + + public LzWindowDictionary() + { + Initalize(); + + // Build the index list, so Lz compression will become significantly faster + _offsetList = new List[0x100]; + for (int i = 0; i < _offsetList.Length; i++) + { + _offsetList[i] = new List(); + } + } + + public int MinMatchAmount { get; set; } + + public int MaxMatchAmount { get; set; } + + private int BlockSize { get; set; } + + public void Reset() + { + Initalize(); + + for (int i = 0; i < _offsetList.Length; i++) + { + _offsetList[i].Clear(); + } + } + + public int[] Search(byte[] decompressedData, int decompressedDataOffset, uint index, uint length) + { + RemoveOldEntries(decompressedData[decompressedDataOffset + index]); // Remove old entries for this index + + int[] match = new int[] { 0, 0 }; + + if (index < 1 || length - index < MinMatchAmount) + { + // Can't find matches if there isn't enough data + return match; + } + + for (int i = 0; i < _offsetList[decompressedData[decompressedDataOffset + index]].Count; i++) + { + int matchStart = _offsetList[decompressedData[decompressedDataOffset + index]][i]; + int matchSize = 1; + + if (index - matchStart > BlockSize) + { + break; + } + + int maxMatchSize = (int)Math.Min(Math.Min(MaxMatchAmount, BlockSize), Math.Min(length - index, length - matchStart)); + while (matchSize < maxMatchSize && decompressedData[decompressedDataOffset + index + matchSize] == decompressedData[decompressedDataOffset + matchStart + matchSize]) + { + matchSize++; + } + + if (matchSize >= MinMatchAmount && matchSize > match[1]) + { + // This is a good match + match = new int[] { (int)(index - matchStart), matchSize }; + + if (matchSize == MaxMatchAmount) + { + // Don't look for more matches + break; + } + } + } + + // Return the real match (or the default 0:0 match). + return match; + } + + // Add entries + public void AddEntry(byte[] decompressedData, int decompressedDataOffset, int index) + { + _offsetList[decompressedData[decompressedDataOffset + index]].Add(index); + } + + public void AddEntryRange(byte[] decompressedData, int decompressedDataOffset, int index, int length) + { + for (int i = 0; i < length; i++) + { + AddEntry(decompressedData, decompressedDataOffset, index + i); + } + } + + private void Initalize() + { + MinMatchAmount = 3; + MaxMatchAmount = 18; + BlockSize = 4096; + } + + private void RemoveOldEntries(byte index) + { + while (_offsetList[index].Count > 256) + { + _offsetList[index].RemoveAt(0); + } + } + } +} diff --git a/DiscUtils/Ntfs/MasterFileTable.cs b/DiscUtils/Ntfs/MasterFileTable.cs new file mode 100644 index 0000000..f9d092e --- /dev/null +++ b/DiscUtils/Ntfs/MasterFileTable.cs @@ -0,0 +1,522 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + + /// + /// Class representing the $MFT file on disk, including mirror. + /// + /// This class only understands basic record structure, and is + /// ignorant of files that span multiple records. This class should only + /// be used by the NtfsFileSystem and File classes. + internal class MasterFileTable : IDiagnosticTraceable, IDisposable + { + /// + /// MFT index of the MFT file itself. + /// + public const long MftIndex = 0; + + /// + /// MFT index of the MFT Mirror file. + /// + public const long MftMirrorIndex = 1; + + /// + /// MFT Index of the Log file. + /// + public const long LogFileIndex = 2; + + /// + /// MFT Index of the Volume file. + /// + public const long VolumeIndex = 3; + + /// + /// MFT Index of the Attribute Definition file. + /// + public const long AttrDefIndex = 4; + + /// + /// MFT Index of the Root Directory. + /// + public const long RootDirIndex = 5; + + /// + /// MFT Index of the Bitmap file. + /// + public const long BitmapIndex = 6; + + /// + /// MFT Index of the Boot sector(s). + /// + public const long BootIndex = 7; + + /// + /// MFT Index of the Bad Bluster file. + /// + public const long BadClusIndex = 8; + + /// + /// MFT Index of the Security Descriptor file. + /// + public const long SecureIndex = 9; + + /// + /// MFT Index of the Uppercase mapping file. + /// + public const long UpCaseIndex = 10; + + /// + /// MFT Index of the Optional Extensions directory. + /// + public const long ExtendIndex = 11; + + /// + /// First MFT Index available for 'normal' files. + /// + private const uint FirstAvailableMftIndex = 24; + + private File _self; + private Bitmap _bitmap; + private Stream _recordStream; + private ObjectCache _recordCache; + + private int _recordLength; + private int _bytesPerSector; + + public MasterFileTable(INtfsContext context) + { + BiosParameterBlock bpb = context.BiosParameterBlock; + + _recordCache = new ObjectCache(); + _recordLength = bpb.MftRecordSize; + _bytesPerSector = bpb.BytesPerSector; + + // Temporary record stream - until we've bootstrapped the MFT properly + _recordStream = new SubStream(context.RawStream, bpb.MftCluster * bpb.SectorsPerCluster * bpb.BytesPerSector, 24 * _recordLength); + } + + public int RecordSize + { + get { return _recordLength; } + } + + /// + /// Gets the MFT records directly from the MFT stream - bypassing the record cache. + /// + public IEnumerable Records + { + get + { + using (Stream mftStream = _self.OpenStream(AttributeType.Data, null, FileAccess.Read)) + { + uint index = 0; + while (mftStream.Position < mftStream.Length) + { + byte[] recordData = Utilities.ReadFully(mftStream, _recordLength); + + if (Utilities.BytesToString(recordData, 0, 4) != "FILE") + { + continue; + } + + FileRecord record = new FileRecord(_bytesPerSector); + record.FromBytes(recordData, 0); + record.LoadedIndex = index; + + yield return record; + + index++; + } + } + } + } + + public void Dispose() + { + if (_recordStream != null) + { + _recordStream.Dispose(); + _recordStream = null; + } + + if (_bitmap != null) + { + _bitmap.Dispose(); + _bitmap = null; + } + + GC.SuppressFinalize(this); + } + + public FileRecord GetBootstrapRecord() + { + _recordStream.Position = 0; + byte[] mftSelfRecordData = Utilities.ReadFully(_recordStream, _recordLength); + FileRecord mftSelfRecord = new FileRecord(_bytesPerSector); + mftSelfRecord.FromBytes(mftSelfRecordData, 0); + _recordCache[MftIndex] = mftSelfRecord; + return mftSelfRecord; + } + + public void Initialize(File file) + { + _self = file; + + if (_recordStream != null) + { + _recordStream.Dispose(); + } + + NtfsStream bitmapStream = _self.GetStream(AttributeType.Bitmap, null); + _bitmap = new Bitmap(bitmapStream.Open(FileAccess.ReadWrite), long.MaxValue); + + NtfsStream recordsStream = _self.GetStream(AttributeType.Data, null); + _recordStream = recordsStream.Open(FileAccess.ReadWrite); + } + + public File InitializeNew(INtfsContext context, long firstBitmapCluster, ulong numBitmapClusters, long firstRecordsCluster, ulong numRecordsClusters) + { + BiosParameterBlock bpb = context.BiosParameterBlock; + + FileRecord fileRec = new FileRecord(bpb.BytesPerSector, bpb.MftRecordSize, (uint)MftIndex); + fileRec.Flags = FileRecordFlags.InUse; + fileRec.SequenceNumber = 1; + _recordCache[MftIndex] = fileRec; + + _self = new File(context, fileRec); + + StandardInformation.InitializeNewFile(_self, FileAttributeFlags.Hidden | FileAttributeFlags.System); + + NtfsStream recordsStream = _self.CreateStream(AttributeType.Data, null, firstRecordsCluster, numRecordsClusters, (uint)bpb.BytesPerCluster); + _recordStream = recordsStream.Open(FileAccess.ReadWrite); + Wipe(_recordStream); + + NtfsStream bitmapStream = _self.CreateStream(AttributeType.Bitmap, null, firstBitmapCluster, numBitmapClusters, (uint)bpb.BytesPerCluster); + using (Stream s = bitmapStream.Open(FileAccess.ReadWrite)) + { + Wipe(s); + s.SetLength(8); + _bitmap = new Bitmap(s, long.MaxValue); + } + + _recordLength = context.BiosParameterBlock.MftRecordSize; + _bytesPerSector = context.BiosParameterBlock.BytesPerSector; + + _bitmap.MarkPresentRange(0, 1); + + // Write the MFT's own record to itself + byte[] buffer = new byte[_recordLength]; + fileRec.ToBytes(buffer, 0); + _recordStream.Position = 0; + _recordStream.Write(buffer, 0, _recordLength); + _recordStream.Flush(); + + return _self; + } + + public FileRecord AllocateRecord(FileRecordFlags flags, bool isMft) + { + long index; + if (isMft) + { + // Have to take a lot of care extending the MFT itself, to ensure we never end up unable to + // bootstrap the file system via the MFT itself - hence why special records are reserved + // for MFT's own MFT record overflow. + for (int i = 15; i > 11; --i) + { + FileRecord r = GetRecord(i, false); + if (r.BaseFile.SequenceNumber == 0) + { + r.Reset(); + r.Flags |= FileRecordFlags.InUse; + WriteRecord(r); + return r; + } + } + + throw new IOException("MFT too fragmented - unable to allocate MFT overflow record"); + } + else + { + index = _bitmap.AllocateFirstAvailable(FirstAvailableMftIndex); + } + + if (index * _recordLength >= _recordStream.Length) + { + // Note: 64 is significant, since bitmap extends by 8 bytes (=64 bits) at a time. + long newEndIndex = Utilities.RoundUp(index + 1, 64); + _recordStream.SetLength(newEndIndex * _recordLength); + for (long i = index; i < newEndIndex; ++i) + { + FileRecord record = new FileRecord(_bytesPerSector, _recordLength, (uint)i); + WriteRecord(record); + } + } + + FileRecord newRecord = GetRecord(index, true); + newRecord.ReInitialize(_bytesPerSector, _recordLength, (uint)index); + + _recordCache[index] = newRecord; + + newRecord.Flags = FileRecordFlags.InUse | flags; + + WriteRecord(newRecord); + _self.UpdateRecordInMft(); + + return newRecord; + } + + public FileRecord AllocateRecord(long index, FileRecordFlags flags) + { + _bitmap.MarkPresent(index); + + FileRecord newRecord = new FileRecord(_bytesPerSector, _recordLength, (uint)index); + _recordCache[index] = newRecord; + newRecord.Flags = FileRecordFlags.InUse | flags; + + WriteRecord(newRecord); + _self.UpdateRecordInMft(); + return newRecord; + } + + public void RemoveRecord(FileRecordReference fileRef) + { + FileRecord record = GetRecord(fileRef.MftIndex, false); + record.Reset(); + WriteRecord(record); + + _recordCache.Remove(fileRef.MftIndex); + _bitmap.MarkAbsent(fileRef.MftIndex); + _self.UpdateRecordInMft(); + } + + public FileRecord GetRecord(FileRecordReference fileReference) + { + FileRecord result = GetRecord(fileReference.MftIndex, false); + + if (result != null) + { + if (fileReference.SequenceNumber != 0 && result.SequenceNumber != 0) + { + if (fileReference.SequenceNumber != result.SequenceNumber) + { + throw new IOException("Attempt to get an MFT record with an old reference"); + } + } + } + + return result; + } + + public FileRecord GetRecord(long index, bool ignoreMagic) + { + return GetRecord(index, ignoreMagic, false); + } + + public FileRecord GetRecord(long index, bool ignoreMagic, bool ignoreBitmap) + { + if (ignoreBitmap || _bitmap == null || _bitmap.IsPresent(index)) + { + FileRecord result = _recordCache[index]; + if (result != null) + { + return result; + } + + if ((index + 1) * _recordLength <= _recordStream.Length) + { + _recordStream.Position = index * _recordLength; + byte[] recordBuffer = Utilities.ReadFully(_recordStream, _recordLength); + + result = new FileRecord(_bytesPerSector); + result.FromBytes(recordBuffer, 0, ignoreMagic); + result.LoadedIndex = (uint)index; + } + else + { + result = new FileRecord(_bytesPerSector, _recordLength, (uint)index); + } + + _recordCache[index] = result; + return result; + } + + return null; + } + + public void WriteRecord(FileRecord record) + { + int recordSize = record.Size; + if (recordSize > _recordLength) + { + throw new IOException("Attempting to write over-sized MFT record"); + } + + byte[] buffer = new byte[_recordLength]; + record.ToBytes(buffer, 0); + + _recordStream.Position = record.MasterFileTableIndex * (long)_recordLength; + _recordStream.Write(buffer, 0, _recordLength); + _recordStream.Flush(); + + // We may have modified our own meta-data by extending the data stream, so + // make sure our records are up-to-date. + if (_self.MftRecordIsDirty) + { + DirectoryEntry dirEntry = _self.DirectoryEntry; + if (dirEntry != null) + { + dirEntry.UpdateFrom(_self); + } + + _self.UpdateRecordInMft(); + } + + // Need to update Mirror. OpenRaw is OK because this is short duration, and we don't + // extend or otherwise modify any meta-data, just the content of the Data stream. + if (record.MasterFileTableIndex < 4 && _self.Context.GetFileByIndex != null) + { + File mftMirror = _self.Context.GetFileByIndex(MftMirrorIndex); + if (mftMirror != null) + { + using (Stream s = mftMirror.OpenStream(AttributeType.Data, null, FileAccess.ReadWrite)) + { + s.Position = record.MasterFileTableIndex * (long)_recordLength; + s.Write(buffer, 0, _recordLength); + } + } + } + } + + public long GetRecordOffset(FileRecordReference fileReference) + { + return fileReference.MftIndex * _recordLength; + } + + public ClusterMap GetClusterMap() + { + int totalClusters = (int)Utilities.Ceil(_self.Context.BiosParameterBlock.TotalSectors64, _self.Context.BiosParameterBlock.SectorsPerCluster); + + ClusterRoles[] clusterToRole = new ClusterRoles[totalClusters]; + object[] clusterToFile = new object[totalClusters]; + Dictionary fileToPaths = new Dictionary(); + + for (int i = 0; i < totalClusters; ++i) + { + clusterToRole[i] = ClusterRoles.Free; + } + + foreach (FileRecord fr in Records) + { + if (fr.BaseFile.Value != 0 || (fr.Flags & FileRecordFlags.InUse) == 0) + { + continue; + } + + File f = new File(_self.Context, fr); + + foreach (var stream in f.AllStreams) + { + string fileId; + + if (stream.AttributeType == AttributeType.Data && !string.IsNullOrEmpty(stream.Name)) + { + fileId = f.IndexInMft.ToString(CultureInfo.InvariantCulture) + ":" + stream.Name; + fileToPaths[fileId] = Utilities.Map(f.Names, n => n + ":" + stream.Name); + } + else + { + fileId = f.IndexInMft.ToString(CultureInfo.InvariantCulture); + fileToPaths[fileId] = f.Names.ToArray(); + } + + ClusterRoles roles = ClusterRoles.None; + if (f.IndexInMft < MasterFileTable.FirstAvailableMftIndex) + { + roles |= ClusterRoles.SystemFile; + + if (f.IndexInMft == MasterFileTable.BootIndex) + { + roles |= ClusterRoles.BootArea; + } + } + else + { + roles |= ClusterRoles.DataFile; + } + + if (stream.AttributeType != AttributeType.Data) + { + roles |= ClusterRoles.Metadata; + } + + foreach (var range in stream.GetClusters()) + { + for (long cluster = range.Offset; cluster < range.Offset + range.Count; ++cluster) + { + clusterToRole[cluster] = roles; + clusterToFile[cluster] = fileId; + } + } + } + } + + return new ClusterMap(clusterToRole, clusterToFile, fileToPaths); + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "MASTER FILE TABLE"); + writer.WriteLine(indent + " Record Length: " + _recordLength); + + foreach (var record in Records) + { + record.Dump(writer, indent + " "); + + foreach (AttributeRecord attr in record.Attributes) + { + attr.Dump(writer, indent + " "); + } + } + } + + private static void Wipe(Stream s) + { + s.Position = 0; + + byte[] buffer = new byte[64 * Sizes.OneKiB]; + int numWiped = 0; + while (numWiped < s.Length) + { + int toWrite = (int)Math.Min(buffer.Length, s.Length - s.Position); + s.Write(buffer, 0, toWrite); + numWiped += toWrite; + } + } + } +} diff --git a/DiscUtils/Ntfs/NewFileOptions.cs b/DiscUtils/Ntfs/NewFileOptions.cs new file mode 100644 index 0000000..8668f32 --- /dev/null +++ b/DiscUtils/Ntfs/NewFileOptions.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.Security.AccessControl; + + /// + /// Options controlling how new NTFS files are created. + /// + public sealed class NewFileOptions + { + /// + /// Initializes a new instance of the NewFileOptions class. + /// + public NewFileOptions() + { + Compressed = null; + CreateShortNames = null; + SecurityDescriptor = null; + } + + /// + /// Gets or sets whether the new file should be compressed. + /// + /// The default (null) value indicates the file system default behaviour applies. + public bool? Compressed { get; set; } + + /// + /// Gets or sets whether a short name should be created for the file. + /// + /// The default (null) value indicates the file system default behaviour applies. + public bool? CreateShortNames { get; set; } + + /// + /// Gets or sets the security descriptor that to set for the new file. + /// + /// The default (null) value indicates the security descriptor is inherited. + public RawSecurityDescriptor SecurityDescriptor { get; set; } + } +} diff --git a/DiscUtils/Ntfs/NonResidentAttributeBuffer.cs b/DiscUtils/Ntfs/NonResidentAttributeBuffer.cs new file mode 100644 index 0000000..f064b7a --- /dev/null +++ b/DiscUtils/Ntfs/NonResidentAttributeBuffer.cs @@ -0,0 +1,353 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class NonResidentAttributeBuffer : NonResidentDataBuffer + { + private File _file; + private NtfsAttribute _attribute; + + public NonResidentAttributeBuffer(File file, NtfsAttribute attribute) + : base(file.Context, CookRuns(attribute), file.IndexInMft == MasterFileTable.MftIndex) + { + _file = file; + _attribute = attribute; + + switch (attribute.Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) + { + case AttributeFlags.Sparse: + _activeStream = new SparseClusterStream(_attribute, _rawStream); + break; + + case AttributeFlags.Compressed: + _activeStream = new CompressedClusterStream(_context, _attribute, _rawStream); + break; + + case AttributeFlags.None: + _activeStream = _rawStream; + break; + + default: + throw new NotImplementedException("Unhandled attribute type '" + attribute.Flags + "'"); + } + } + + public override bool CanWrite + { + get { return _context.RawStream.CanWrite && _file != null; } + } + + public override long Capacity + { + get { return PrimaryAttributeRecord.DataLength; } + } + + private NonResidentAttributeRecord PrimaryAttributeRecord + { + get { return _attribute.PrimaryRecord as NonResidentAttributeRecord; } + } + + public void AlignVirtualClusterCount() + { + _file.MarkMftRecordDirty(); + _activeStream.ExpandToClusters(Utilities.Ceil(_attribute.Length, _bytesPerCluster), (NonResidentAttributeRecord)_attribute.LastExtent, false); + } + + public override void SetCapacity(long value) + { + if (!CanWrite) + { + throw new IOException("Attempt to change length of file not opened for write"); + } + + if (value == Capacity) + { + return; + } + + _file.MarkMftRecordDirty(); + + long newClusterCount = Utilities.Ceil(value, _bytesPerCluster); + + if (value < Capacity) + { + Truncate(value); + } + else + { + _activeStream.ExpandToClusters(newClusterCount, (NonResidentAttributeRecord)_attribute.LastExtent, true); + + PrimaryAttributeRecord.AllocatedLength = _cookedRuns.NextVirtualCluster * _bytesPerCluster; + } + + PrimaryAttributeRecord.DataLength = value; + + if (PrimaryAttributeRecord.InitializedDataLength > value) + { + PrimaryAttributeRecord.InitializedDataLength = value; + } + + _cookedRuns.CollapseRuns(); + } + + public override void Write(long pos, byte[] buffer, int offset, int count) + { + if (!CanWrite) + { + throw new IOException("Attempt to write to file not opened for write"); + } + + if (count == 0) + { + return; + } + + if (pos + count > Capacity) + { + SetCapacity(pos + count); + } + + // Write zeros from end of current initialized data to the start of the new write + if (pos > PrimaryAttributeRecord.InitializedDataLength) + { + InitializeData(pos); + } + + int allocatedClusters = 0; + + long focusPos = pos; + while (focusPos < pos + count) + { + long vcn = focusPos / _bytesPerCluster; + long remaining = (pos + count) - focusPos; + long clusterOffset = focusPos - (vcn * _bytesPerCluster); + + if (vcn * _bytesPerCluster != focusPos || remaining < _bytesPerCluster) + { + // Unaligned or short write + int toWrite = (int)Math.Min(remaining, _bytesPerCluster - clusterOffset); + + _activeStream.ReadClusters(vcn, 1, _ioBuffer, 0); + Array.Copy(buffer, offset + (focusPos - pos), _ioBuffer, clusterOffset, toWrite); + allocatedClusters += _activeStream.WriteClusters(vcn, 1, _ioBuffer, 0); + + focusPos += toWrite; + } + else + { + // Aligned, full cluster writes... + int fullClusters = (int)(remaining / _bytesPerCluster); + allocatedClusters += _activeStream.WriteClusters(vcn, fullClusters, buffer, (int)(offset + (focusPos - pos))); + + focusPos += fullClusters * _bytesPerCluster; + } + } + + if (pos + count > PrimaryAttributeRecord.InitializedDataLength) + { + _file.MarkMftRecordDirty(); + + PrimaryAttributeRecord.InitializedDataLength = pos + count; + } + + if (pos + count > PrimaryAttributeRecord.DataLength) + { + _file.MarkMftRecordDirty(); + + PrimaryAttributeRecord.DataLength = pos + count; + } + + if ((_attribute.Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) + { + PrimaryAttributeRecord.CompressedDataSize += allocatedClusters * _bytesPerCluster; + } + + _cookedRuns.CollapseRuns(); + } + + public override void Clear(long pos, int count) + { + if (!CanWrite) + { + throw new IOException("Attempt to erase bytes from file not opened for write"); + } + + if (count == 0) + { + return; + } + + if (pos + count > Capacity) + { + SetCapacity(pos + count); + } + + _file.MarkMftRecordDirty(); + + // Write zeros from end of current initialized data to the start of the new write + if (pos > PrimaryAttributeRecord.InitializedDataLength) + { + InitializeData(pos); + } + + int releasedClusters = 0; + + long focusPos = pos; + while (focusPos < pos + count) + { + long vcn = focusPos / _bytesPerCluster; + long remaining = (pos + count) - focusPos; + long clusterOffset = focusPos - (vcn * _bytesPerCluster); + + if (vcn * _bytesPerCluster != focusPos || remaining < _bytesPerCluster) + { + // Unaligned or short write + int toClear = (int)Math.Min(remaining, _bytesPerCluster - clusterOffset); + + if (_activeStream.IsClusterStored(vcn)) + { + _activeStream.ReadClusters(vcn, 1, _ioBuffer, 0); + Array.Clear(_ioBuffer, (int)clusterOffset, toClear); + releasedClusters -= _activeStream.WriteClusters(vcn, 1, _ioBuffer, 0); + } + + focusPos += toClear; + } + else + { + // Aligned, full cluster clears... + int fullClusters = (int)(remaining / _bytesPerCluster); + releasedClusters += _activeStream.ClearClusters(vcn, fullClusters); + + focusPos += fullClusters * _bytesPerCluster; + } + } + + if (pos + count > PrimaryAttributeRecord.InitializedDataLength) + { + PrimaryAttributeRecord.InitializedDataLength = pos + count; + } + + if (pos + count > PrimaryAttributeRecord.DataLength) + { + PrimaryAttributeRecord.DataLength = pos + count; + } + + if ((_attribute.Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) + { + PrimaryAttributeRecord.CompressedDataSize -= releasedClusters * _bytesPerCluster; + } + + _cookedRuns.CollapseRuns(); + } + + private static CookedDataRuns CookRuns(NtfsAttribute attribute) + { + CookedDataRuns result = new CookedDataRuns(); + + foreach (NonResidentAttributeRecord record in attribute.Records) + { + if (record.StartVcn != result.NextVirtualCluster) + { + throw new IOException("Invalid NTFS attribute - non-contiguous data runs"); + } + + result.Append(record.DataRuns, record); + } + + return result; + } + + private void InitializeData(long pos) + { + long initDataLen = PrimaryAttributeRecord.InitializedDataLength; + _file.MarkMftRecordDirty(); + + int clustersAllocated = 0; + + while (initDataLen < pos) + { + long vcn = initDataLen / _bytesPerCluster; + if (initDataLen % _bytesPerCluster != 0 || pos - initDataLen < _bytesPerCluster) + { + int clusterOffset = (int)(initDataLen - (vcn * _bytesPerCluster)); + int toClear = (int)Math.Min(_bytesPerCluster - clusterOffset, pos - initDataLen); + + if (_activeStream.IsClusterStored(vcn)) + { + _activeStream.ReadClusters(vcn, 1, _ioBuffer, 0); + Array.Clear(_ioBuffer, clusterOffset, toClear); + clustersAllocated += _activeStream.WriteClusters(vcn, 1, _ioBuffer, 0); + } + + initDataLen += toClear; + } + else + { + int numClusters = (int)((pos / _bytesPerCluster) - vcn); + clustersAllocated -= _activeStream.ClearClusters(vcn, numClusters); + + initDataLen += numClusters * _bytesPerCluster; + } + } + + PrimaryAttributeRecord.InitializedDataLength = pos; + + if ((_attribute.Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) + { + PrimaryAttributeRecord.CompressedDataSize += clustersAllocated * _bytesPerCluster; + } + } + + private void Truncate(long value) + { + long endVcn = Utilities.Ceil(value, _bytesPerCluster); + + // Release the clusters + _activeStream.TruncateToClusters(endVcn); + + // First, remove any extents that are now redundant. + Dictionary extentCache = new Dictionary(_attribute.Extents); + foreach (var extent in extentCache) + { + if (extent.Value.StartVcn >= endVcn) + { + NonResidentAttributeRecord record = (NonResidentAttributeRecord)extent.Value; + _file.RemoveAttributeExtent(extent.Key); + _attribute.RemoveExtentCacheSafe(extent.Key); + } + } + + PrimaryAttributeRecord.LastVcn = Math.Max(0, endVcn - 1); + PrimaryAttributeRecord.AllocatedLength = endVcn * _bytesPerCluster; + PrimaryAttributeRecord.DataLength = value; + PrimaryAttributeRecord.InitializedDataLength = Math.Min(PrimaryAttributeRecord.InitializedDataLength, value); + + _file.MarkMftRecordDirty(); + } + } +} diff --git a/DiscUtils/Ntfs/NonResidentAttributeRecord.cs b/DiscUtils/Ntfs/NonResidentAttributeRecord.cs new file mode 100644 index 0000000..6a1eb5e --- /dev/null +++ b/DiscUtils/Ntfs/NonResidentAttributeRecord.cs @@ -0,0 +1,401 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + + internal sealed class NonResidentAttributeRecord : AttributeRecord + { + private const ushort DefaultCompressionUnitSize = 4; + + private ulong _startingVCN; + private ulong _lastVCN; + private ushort _dataRunsOffset; + private ushort _compressionUnitSize; + private ulong _dataAllocatedSize; + private ulong _dataRealSize; + private ulong _initializedDataSize; + private ulong _compressedSize; + + private List _dataRuns; + + public NonResidentAttributeRecord(byte[] buffer, int offset, out int length) + { + Read(buffer, offset, out length); + } + + public NonResidentAttributeRecord(AttributeType type, string name, ushort id, AttributeFlags flags, long firstCluster, ulong numClusters, uint bytesPerCluster) + : base(type, name, id, flags) + { + _nonResidentFlag = 1; + _dataRuns = new List(); + _dataRuns.Add(new DataRun(firstCluster, (long)numClusters, false)); + _lastVCN = numClusters - 1; + _dataAllocatedSize = bytesPerCluster * numClusters; + _dataRealSize = bytesPerCluster * numClusters; + _initializedDataSize = bytesPerCluster * numClusters; + + if ((flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) + { + _compressionUnitSize = DefaultCompressionUnitSize; + } + } + + public NonResidentAttributeRecord(AttributeType type, string name, ushort id, AttributeFlags flags, long startVcn, List dataRuns) + : base(type, name, id, flags) + { + _nonResidentFlag = 1; + _dataRuns = dataRuns; + _startingVCN = (ulong)startVcn; + + if ((flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) + { + _compressionUnitSize = DefaultCompressionUnitSize; + } + + if (dataRuns != null && dataRuns.Count != 0) + { + _lastVCN = _startingVCN; + foreach (var run in dataRuns) + { + _lastVCN += (ulong)run.RunLength; + } + + _lastVCN -= 1; + } + } + + /// + /// The amount of space occupied by the attribute (in bytes). + /// + public override long AllocatedLength + { + get { return (long)_dataAllocatedSize; } + set { _dataAllocatedSize = (ulong)value; } + } + + /// + /// The amount of data in the attribute (in bytes). + /// + public override long DataLength + { + get { return (long)_dataRealSize; } + set { _dataRealSize = (ulong)value; } + } + + /// + /// The amount of initialized data in the attribute (in bytes). + /// + public override long InitializedDataLength + { + get { return (long)_initializedDataSize; } + set { _initializedDataSize = (ulong)value; } + } + + public long CompressedDataSize + { + get { return (long)_compressedSize; } + set { _compressedSize = (ulong)value; } + } + + public override long StartVcn + { + get { return (long)_startingVCN; } + } + + public long LastVcn + { + get { return (long)_lastVCN; } + set { _lastVCN = (ulong)value; } + } + + /// + /// Gets or sets the size of a compression unit (in clusters). + /// + public int CompressionUnitSize + { + get { return 1 << _compressionUnitSize; } + set { _compressionUnitSize = (ushort)Utilities.Log2(value); } + } + + public List DataRuns + { + get { return _dataRuns; } + } + + public override int Size + { + get + { + byte nameLength = 0; + ushort nameOffset = (ushort)(((Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) ? 0x48 : 0x40); + if (Name != null) + { + nameLength = (byte)Name.Length; + } + + ushort dataOffset = (ushort)Utilities.RoundUp(nameOffset + (nameLength * 2), 8); + + // Write out data first, since we know where it goes... + int dataLen = 0; + foreach (var run in _dataRuns) + { + dataLen += run.Size; + } + + dataLen++; // NULL terminator + + return Utilities.RoundUp(dataOffset + dataLen, 8); + } + } + + public void ReplaceRun(DataRun oldRun, DataRun newRun) + { + int idx = _dataRuns.IndexOf(oldRun); + if (idx < 0) + { + throw new ArgumentException("Attempt to replace non-existant run", "oldRun"); + } + + _dataRuns[idx] = newRun; + } + + public int RemoveRun(DataRun run) + { + int idx = _dataRuns.IndexOf(run); + if (idx < 0) + { + throw new ArgumentException("Attempt to remove non-existant run", "run"); + } + + _dataRuns.RemoveAt(idx); + return idx; + } + + public void InsertRun(DataRun existingRun, DataRun newRun) + { + int idx = _dataRuns.IndexOf(existingRun); + if (idx < 0) + { + throw new ArgumentException("Attempt to replace non-existant run", "existingRun"); + } + + _dataRuns.Insert(idx + 1, newRun); + } + + public void InsertRun(int index, DataRun newRun) + { + _dataRuns.Insert(index, newRun); + } + + public override Range[] GetClusters() + { + var cookedRuns = _dataRuns; + + long start = 0; + List> result = new List>(_dataRuns.Count); + foreach (var run in cookedRuns) + { + if (!run.IsSparse) + { + start += run.RunOffset; + result.Add(new Range(start, run.RunLength)); + } + } + + return result.ToArray(); + } + + public override IBuffer GetReadOnlyDataBuffer(INtfsContext context) + { + return new NonResidentDataBuffer(context, this); + } + + public override int Write(byte[] buffer, int offset) + { + ushort headerLength = 0x40; + if ((Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) + { + headerLength += 0x08; + } + + byte nameLength = 0; + ushort nameOffset = headerLength; + if (Name != null) + { + nameLength = (byte)Name.Length; + } + + ushort dataOffset = (ushort)Utilities.RoundUp(headerLength + (nameLength * 2), 8); + + // Write out data first, since we know where it goes... + int dataLen = 0; + foreach (var run in _dataRuns) + { + dataLen += run.Write(buffer, offset + dataOffset + dataLen); + } + + buffer[offset + dataOffset + dataLen] = 0; // NULL terminator + dataLen++; + + int length = (int)Utilities.RoundUp(dataOffset + dataLen, 8); + + Utilities.WriteBytesLittleEndian((uint)_type, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian(length, buffer, offset + 0x04); + buffer[offset + 0x08] = _nonResidentFlag; + buffer[offset + 0x09] = nameLength; + Utilities.WriteBytesLittleEndian(nameOffset, buffer, offset + 0x0A); + Utilities.WriteBytesLittleEndian((ushort)_flags, buffer, offset + 0x0C); + Utilities.WriteBytesLittleEndian(_attributeId, buffer, offset + 0x0E); + + Utilities.WriteBytesLittleEndian(_startingVCN, buffer, offset + 0x10); + Utilities.WriteBytesLittleEndian(_lastVCN, buffer, offset + 0x18); + Utilities.WriteBytesLittleEndian(dataOffset, buffer, offset + 0x20); + Utilities.WriteBytesLittleEndian(_compressionUnitSize, buffer, offset + 0x22); + Utilities.WriteBytesLittleEndian((uint)0, buffer, offset + 0x24); // Padding + Utilities.WriteBytesLittleEndian(_dataAllocatedSize, buffer, offset + 0x28); + Utilities.WriteBytesLittleEndian(_dataRealSize, buffer, offset + 0x30); + Utilities.WriteBytesLittleEndian(_initializedDataSize, buffer, offset + 0x38); + if ((Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) + { + Utilities.WriteBytesLittleEndian(_compressedSize, buffer, offset + 0x40); + } + + if (Name != null) + { + Array.Copy(Encoding.Unicode.GetBytes(Name), 0, buffer, offset + nameOffset, nameLength * 2); + } + + return length; + } + + public AttributeRecord Split(int suggestedSplitIdx) + { + int splitIdx; + if (suggestedSplitIdx <= 0 || suggestedSplitIdx >= _dataRuns.Count) + { + splitIdx = _dataRuns.Count / 2; + } + else + { + splitIdx = suggestedSplitIdx; + } + + long splitVcn = (long)_startingVCN; + long splitLcn = 0; + for (int i = 0; i < splitIdx; ++i) + { + splitVcn += _dataRuns[i].RunLength; + splitLcn += _dataRuns[i].RunOffset; + } + + List newRecordRuns = new List(); + while (_dataRuns.Count > splitIdx) + { + DataRun run = _dataRuns[splitIdx]; + + _dataRuns.RemoveAt(splitIdx); + newRecordRuns.Add(run); + } + + // Each extent has implicit start LCN=0, so have to make stored runs match reality. + // However, take care not to stomp on 'sparse' runs that may be at the start of the + // new extent (indicated by Zero run offset). + for (int i = 0; i < newRecordRuns.Count; ++i) + { + if (!newRecordRuns[i].IsSparse) + { + newRecordRuns[i].RunOffset += splitLcn; + break; + } + } + + _lastVCN = (ulong)splitVcn - 1; + + return new NonResidentAttributeRecord(_type, _name, 0, _flags, splitVcn, newRecordRuns); + } + + public override void Dump(TextWriter writer, string indent) + { + base.Dump(writer, indent); + writer.WriteLine(indent + " Starting VCN: " + _startingVCN); + writer.WriteLine(indent + " Last VCN: " + _lastVCN); + writer.WriteLine(indent + " Comp Unit Size: " + _compressionUnitSize); + writer.WriteLine(indent + " Allocated Size: " + _dataAllocatedSize); + writer.WriteLine(indent + " Real Size: " + _dataRealSize); + writer.WriteLine(indent + " Init Data Size: " + _initializedDataSize); + if ((Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0) + { + writer.WriteLine(indent + " Compressed Size: " + _compressedSize); + } + + string runStr = string.Empty; + + foreach (DataRun run in _dataRuns) + { + runStr += " " + run.ToString(); + } + + writer.WriteLine(indent + " Data Runs:" + runStr); + } + + protected override void Read(byte[] buffer, int offset, out int length) + { + _dataRuns = null; + + base.Read(buffer, offset, out length); + + _startingVCN = Utilities.ToUInt64LittleEndian(buffer, offset + 0x10); + _lastVCN = Utilities.ToUInt64LittleEndian(buffer, offset + 0x18); + _dataRunsOffset = Utilities.ToUInt16LittleEndian(buffer, offset + 0x20); + _compressionUnitSize = Utilities.ToUInt16LittleEndian(buffer, offset + 0x22); + _dataAllocatedSize = Utilities.ToUInt64LittleEndian(buffer, offset + 0x28); + _dataRealSize = Utilities.ToUInt64LittleEndian(buffer, offset + 0x30); + _initializedDataSize = Utilities.ToUInt64LittleEndian(buffer, offset + 0x38); + if ((Flags & (AttributeFlags.Compressed | AttributeFlags.Sparse)) != 0 && _dataRunsOffset > 0x40) + { + _compressedSize = Utilities.ToUInt64LittleEndian(buffer, offset + 0x40); + } + + _dataRuns = new List(); + int pos = _dataRunsOffset; + while (pos < length) + { + DataRun run = new DataRun(); + int len = run.Read(buffer, offset + pos); + + // Length 1 means there was only a header byte (i.e. terminator) + if (len == 1) + { + break; + } + + _dataRuns.Add(run); + pos += len; + } + } + } +} diff --git a/DiscUtils/Ntfs/NonResidentDataBuffer.cs b/DiscUtils/Ntfs/NonResidentDataBuffer.cs new file mode 100644 index 0000000..04ec896 --- /dev/null +++ b/DiscUtils/Ntfs/NonResidentDataBuffer.cs @@ -0,0 +1,169 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + using DiscUtils.Compression; + + internal class NonResidentDataBuffer : DiscUtils.Buffer, IMappedBuffer + { + protected INtfsContext _context; + protected CookedDataRuns _cookedRuns; + + protected long _bytesPerCluster; + protected RawClusterStream _rawStream; + protected ClusterStream _activeStream; + + protected byte[] _ioBuffer; + + public NonResidentDataBuffer(INtfsContext context, NonResidentAttributeRecord record) + : this(context, new CookedDataRuns(record.DataRuns, record), false) + { + } + + public NonResidentDataBuffer(INtfsContext context, CookedDataRuns cookedRuns, bool isMft) + { + _context = context; + _cookedRuns = cookedRuns; + + _rawStream = new RawClusterStream(_context, _cookedRuns, isMft); + _activeStream = _rawStream; + + _bytesPerCluster = _context.BiosParameterBlock.BytesPerCluster; + _ioBuffer = new byte[_bytesPerCluster]; + } + + public override bool CanRead + { + get { return _context.RawStream.CanRead; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Capacity + { + get { return VirtualClusterCount * _bytesPerCluster; } + } + + public long VirtualClusterCount + { + get { return _cookedRuns.NextVirtualCluster; } + } + + public override IEnumerable Extents + { + get + { + List extents = new List(); + foreach (var range in _activeStream.StoredClusters) + { + extents.Add(new StreamExtent(range.Offset * _bytesPerCluster, range.Count * _bytesPerCluster)); + } + + return StreamExtent.Intersect(extents, new StreamExtent(0, Capacity)); + } + } + + public override IEnumerable GetExtentsInRange(long start, long count) + { + return StreamExtent.Intersect(Extents, new StreamExtent(start, count)); + } + + public long MapPosition(long pos) + { + long vcn = pos / _bytesPerCluster; + int dataRunIdx = _cookedRuns.FindDataRun(vcn, 0); + + if (_cookedRuns[dataRunIdx].IsSparse) + { + return -1; + } + else + { + return (_cookedRuns[dataRunIdx].StartLcn * _bytesPerCluster) + (pos - (_cookedRuns[dataRunIdx].StartVcn * _bytesPerCluster)); + } + } + + public override int Read(long pos, byte[] buffer, int offset, int count) + { + if (!CanRead) + { + throw new IOException("Attempt to read from file not opened for read"); + } + + Utilities.AssertBufferParameters(buffer, offset, count); + + // Limit read to length of attribute + int totalToRead = (int)Math.Min(count, Capacity - pos); + if (totalToRead <= 0) + { + return 0; + } + + long focusPos = pos; + while (focusPos < pos + totalToRead) + { + long vcn = focusPos / _bytesPerCluster; + long remaining = (pos + totalToRead) - focusPos; + long clusterOffset = focusPos - (vcn * _bytesPerCluster); + + if (vcn * _bytesPerCluster != focusPos || remaining < _bytesPerCluster) + { + // Unaligned or short read + _activeStream.ReadClusters(vcn, 1, _ioBuffer, 0); + + int toRead = (int)Math.Min(remaining, _bytesPerCluster - clusterOffset); + + Array.Copy(_ioBuffer, clusterOffset, buffer, offset + (focusPos - pos), toRead); + + focusPos += toRead; + } + else + { + // Aligned, full cluster reads... + int fullClusters = (int)(remaining / _bytesPerCluster); + _activeStream.ReadClusters(vcn, fullClusters, buffer, (int)(offset + (focusPos - pos))); + + focusPos += fullClusters * _bytesPerCluster; + } + } + + return totalToRead; + } + + public override void Write(long pos, byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override void SetCapacity(long value) + { + throw new NotSupportedException(); + } + } +} diff --git a/DiscUtils/Ntfs/NtfsAttribute.cs b/DiscUtils/Ntfs/NtfsAttribute.cs new file mode 100644 index 0000000..ed61d53 --- /dev/null +++ b/DiscUtils/Ntfs/NtfsAttribute.cs @@ -0,0 +1,414 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + + internal class NtfsAttribute : IDiagnosticTraceable + { + protected File _file; + protected FileRecordReference _containingFile; + protected AttributeRecord _primaryRecord; + protected Dictionary _extents; + private IBuffer _cachedRawBuffer; + + protected NtfsAttribute(File file, FileRecordReference containingFile, AttributeRecord record) + { + _file = file; + _containingFile = containingFile; + _primaryRecord = record; + _extents = new Dictionary(); + _extents.Add(new AttributeReference(containingFile, record.AttributeId), _primaryRecord); + } + + public AttributeReference Reference + { + get + { + return new AttributeReference(_containingFile, _primaryRecord.AttributeId); + } + } + + public AttributeType Type + { + get { return _primaryRecord.AttributeType; } + } + + public string Name + { + get { return _primaryRecord.Name; } + } + + public AttributeFlags Flags + { + get + { + return _primaryRecord.Flags; + } + + set + { + _primaryRecord.Flags = value; + _cachedRawBuffer = null; + } + } + + public ushort Id + { + get { return _primaryRecord.AttributeId; } + } + + public long Length + { + get { return _primaryRecord.DataLength; } + } + + public AttributeRecord PrimaryRecord + { + get + { + return _primaryRecord; + } + } + + public int CompressionUnitSize + { + get + { + NonResidentAttributeRecord firstExtent = FirstExtent as NonResidentAttributeRecord; + if (firstExtent == null) + { + return 0; + } + else + { + return firstExtent.CompressionUnitSize; + } + } + + set + { + NonResidentAttributeRecord firstExtent = FirstExtent as NonResidentAttributeRecord; + if (firstExtent != null) + { + firstExtent.CompressionUnitSize = value; + } + } + } + + public long CompressedDataSize + { + get + { + NonResidentAttributeRecord firstExtent = FirstExtent as NonResidentAttributeRecord; + if (firstExtent == null) + { + return FirstExtent.AllocatedLength; + } + else + { + return firstExtent.CompressedDataSize; + } + } + + set + { + NonResidentAttributeRecord firstExtent = FirstExtent as NonResidentAttributeRecord; + if (firstExtent != null) + { + firstExtent.CompressedDataSize = value; + } + } + } + + public List Records + { + get + { + List records = new List(_extents.Values); + records.Sort(AttributeRecord.CompareStartVcns); + return records; + } + } + + public IBuffer RawBuffer + { + get + { + if (_cachedRawBuffer == null) + { + if (_primaryRecord.IsNonResident) + { + _cachedRawBuffer = new NonResidentAttributeBuffer(_file, this); + } + else + { + _cachedRawBuffer = ((ResidentAttributeRecord)_primaryRecord).DataBuffer; + } + } + + return _cachedRawBuffer; + } + } + + public IDictionary Extents + { + get { return _extents; } + } + + public AttributeRecord LastExtent + { + get + { + AttributeRecord last = null; + + if (_extents != null) + { + long lastVcn = 0; + foreach (var extent in _extents) + { + NonResidentAttributeRecord nonResident = extent.Value as NonResidentAttributeRecord; + if (nonResident == null) + { + // Resident attribute, so there can only be one... + return extent.Value; + } + + if (nonResident.LastVcn >= lastVcn) + { + last = extent.Value; + lastVcn = nonResident.LastVcn; + } + } + } + + return last; + } + } + + public AttributeRecord FirstExtent + { + get + { + if (_extents != null) + { + foreach (var extent in _extents) + { + NonResidentAttributeRecord nonResident = extent.Value as NonResidentAttributeRecord; + if (nonResident == null) + { + // Resident attribute, so there can only be one... + return extent.Value; + } + else if (nonResident.StartVcn == 0) + { + return extent.Value; + } + } + } + + throw new InvalidDataException("Attribute with no initial extent"); + } + } + + public bool IsNonResident + { + get { return _primaryRecord.IsNonResident; } + } + + protected string AttributeTypeName + { + get + { + switch (_primaryRecord.AttributeType) + { + case AttributeType.StandardInformation: + return "STANDARD INFORMATION"; + case AttributeType.FileName: + return "FILE NAME"; + case AttributeType.SecurityDescriptor: + return "SECURITY DESCRIPTOR"; + case AttributeType.Data: + return "DATA"; + case AttributeType.Bitmap: + return "BITMAP"; + case AttributeType.VolumeName: + return "VOLUME NAME"; + case AttributeType.VolumeInformation: + return "VOLUME INFORMATION"; + case AttributeType.IndexRoot: + return "INDEX ROOT"; + case AttributeType.IndexAllocation: + return "INDEX ALLOCATION"; + case AttributeType.ObjectId: + return "OBJECT ID"; + case AttributeType.ReparsePoint: + return "REPARSE POINT"; + case AttributeType.AttributeList: + return "ATTRIBUTE LIST"; + default: + return "UNKNOWN"; + } + } + } + + public static NtfsAttribute FromRecord(File file, FileRecordReference recordFile, AttributeRecord record) + { + switch (record.AttributeType) + { + case AttributeType.StandardInformation: + return new StructuredNtfsAttribute(file, recordFile, record); + case AttributeType.FileName: + return new StructuredNtfsAttribute(file, recordFile, record); + case AttributeType.SecurityDescriptor: + return new StructuredNtfsAttribute(file, recordFile, record); + case AttributeType.Data: + return new NtfsAttribute(file, recordFile, record); + case AttributeType.Bitmap: + return new NtfsAttribute(file, recordFile, record); + case AttributeType.VolumeName: + return new StructuredNtfsAttribute(file, recordFile, record); + case AttributeType.VolumeInformation: + return new StructuredNtfsAttribute(file, recordFile, record); + case AttributeType.IndexRoot: + return new NtfsAttribute(file, recordFile, record); + case AttributeType.IndexAllocation: + return new NtfsAttribute(file, recordFile, record); + case AttributeType.ObjectId: + return new StructuredNtfsAttribute(file, recordFile, record); + case AttributeType.ReparsePoint: + return new StructuredNtfsAttribute(file, recordFile, record); + case AttributeType.AttributeList: + return new StructuredNtfsAttribute(file, recordFile, record); + default: + return new NtfsAttribute(file, recordFile, record); + } + } + + public void SetExtent(FileRecordReference containingFile, AttributeRecord record) + { + _cachedRawBuffer = null; + _containingFile = containingFile; + _primaryRecord = record; + _extents.Clear(); + _extents.Add(new AttributeReference(containingFile, record.AttributeId), record); + } + + public void AddExtent(FileRecordReference containingFile, AttributeRecord record) + { + _cachedRawBuffer = null; + _extents.Add(new AttributeReference(containingFile, record.AttributeId), record); + } + + public void RemoveExtentCacheSafe(AttributeReference reference) + { + _extents.Remove(reference); + } + + public bool ReplaceExtent(AttributeReference oldRef, AttributeReference newRef, AttributeRecord record) + { + _cachedRawBuffer = null; + + if (!_extents.Remove(oldRef)) + { + return false; + } + else + { + if (oldRef.Equals(Reference) || _extents.Count == 0) + { + _primaryRecord = record; + _containingFile = newRef.File; + } + + _extents.Add(newRef, record); + return true; + } + } + + public Range[] GetClusters() + { + List> result = new List>(); + + foreach (var extent in _extents) + { + result.AddRange(extent.Value.GetClusters()); + } + + return result.ToArray(); + } + + public virtual void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + AttributeTypeName + " ATTRIBUTE (" + (Name == null ? "No Name" : Name) + ")"); + + writer.WriteLine(indent + " Length: " + _primaryRecord.DataLength + " bytes"); + if (_primaryRecord.DataLength == 0) + { + writer.WriteLine(indent + " Data: "); + } + else + { + try + { + using (Stream s = Open(FileAccess.Read)) + { + string hex = string.Empty; + byte[] buffer = new byte[32]; + int numBytes = s.Read(buffer, 0, buffer.Length); + for (int i = 0; i < numBytes; ++i) + { + hex = hex + string.Format(CultureInfo.InvariantCulture, " {0:X2}", buffer[i]); + } + + writer.WriteLine(indent + " Data: " + hex + ((numBytes < s.Length) ? "..." : string.Empty)); + } + } + catch + { + writer.WriteLine(indent + " Data: "); + } + } + + _primaryRecord.Dump(writer, indent + " "); + } + + internal SparseStream Open(FileAccess access) + { + return new BufferStream(GetDataBuffer(), access); + } + + internal IMappedBuffer GetDataBuffer() + { + return new NtfsAttributeBuffer(_file, this); + } + + internal long OffsetToAbsolutePos(long offset) + { + return GetDataBuffer().MapPosition(offset); + } + } +} diff --git a/DiscUtils/Ntfs/NtfsAttributeBuffer.cs b/DiscUtils/Ntfs/NtfsAttributeBuffer.cs new file mode 100644 index 0000000..33eb64f --- /dev/null +++ b/DiscUtils/Ntfs/NtfsAttributeBuffer.cs @@ -0,0 +1,198 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class NtfsAttributeBuffer : DiscUtils.Buffer, IMappedBuffer + { + private File _file; + private NtfsAttribute _attribute; + + public NtfsAttributeBuffer(File file, NtfsAttribute attribute) + { + _file = file; + _attribute = attribute; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanWrite + { + get { return _file.Context.RawStream.CanWrite; } + } + + public override long Capacity + { + get + { + return _attribute.PrimaryRecord.DataLength; + } + } + + public long MapPosition(long pos) + { + if (_attribute.IsNonResident) + { + return ((IMappedBuffer)_attribute.RawBuffer).MapPosition(pos); + } + else + { + AttributeReference attrRef = new AttributeReference(_file.MftReference, _attribute.PrimaryRecord.AttributeId); + ResidentAttributeRecord attrRecord = (ResidentAttributeRecord)_file.GetAttribute(attrRef).PrimaryRecord; + + long attrStart = _file.GetAttributeOffset(attrRef); + long mftPos = attrStart + attrRecord.DataOffset + pos; + + return _file.Context.GetFileByIndex(MasterFileTable.MftIndex).GetAttribute(AttributeType.Data, null).OffsetToAbsolutePos(mftPos); + } + } + + public override int Read(long pos, byte[] buffer, int offset, int count) + { + var record = _attribute.PrimaryRecord; + + if (!CanRead) + { + throw new IOException("Attempt to read from file not opened for read"); + } + + Utilities.AssertBufferParameters(buffer, offset, count); + + if (pos >= Capacity) + { + return 0; + } + + // Limit read to length of attribute + int totalToRead = (int)Math.Min(count, Capacity - pos); + int toRead = totalToRead; + + // Handle uninitialized bytes at end of attribute + if (pos + totalToRead > record.InitializedDataLength) + { + if (pos >= record.InitializedDataLength) + { + // We're just reading zero bytes from the uninitialized area + Array.Clear(buffer, offset, totalToRead); + pos += totalToRead; + return totalToRead; + } + else + { + // Partial read of uninitialized area + Array.Clear(buffer, offset + (int)(record.InitializedDataLength - pos), (int)((pos + toRead) - record.InitializedDataLength)); + toRead = (int)(record.InitializedDataLength - pos); + } + } + + int numRead = 0; + while (numRead < toRead) + { + IBuffer extentBuffer = _attribute.RawBuffer; + + int justRead = extentBuffer.Read(pos + numRead, buffer, offset + numRead, toRead - numRead); + if (justRead == 0) + { + break; + } + + numRead += justRead; + } + + return totalToRead; + } + + public override void SetCapacity(long value) + { + if (!CanWrite) + { + throw new IOException("Attempt to change length of file not opened for write"); + } + + if (value == Capacity) + { + return; + } + + _attribute.RawBuffer.SetCapacity(value); + _file.MarkMftRecordDirty(); + } + + public override void Write(long pos, byte[] buffer, int offset, int count) + { + var record = _attribute.PrimaryRecord; + + if (!CanWrite) + { + throw new IOException("Attempt to write to file not opened for write"); + } + + Utilities.AssertBufferParameters(buffer, offset, count); + + if (count == 0) + { + return; + } + + _attribute.RawBuffer.Write(pos, buffer, offset, count); + + if (!record.IsNonResident) + { + _file.MarkMftRecordDirty(); + } + } + + public override void Clear(long pos, int count) + { + var record = _attribute.PrimaryRecord; + + if (!CanWrite) + { + throw new IOException("Attempt to write to file not opened for write"); + } + + if (count == 0) + { + return; + } + + _attribute.RawBuffer.Clear(pos, count); + + if (!record.IsNonResident) + { + _file.MarkMftRecordDirty(); + } + } + + public override IEnumerable GetExtentsInRange(long start, long count) + { + return StreamExtent.Intersect(_attribute.RawBuffer.GetExtentsInRange(start, count), new StreamExtent(0, Capacity)); + } + } +} diff --git a/DiscUtils/Ntfs/NtfsContext.cs b/DiscUtils/Ntfs/NtfsContext.cs new file mode 100644 index 0000000..21aa5db --- /dev/null +++ b/DiscUtils/Ntfs/NtfsContext.cs @@ -0,0 +1,261 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.IO; + + internal delegate File GetFileByIndexFn(long index); + + internal delegate File GetFileByRefFn(FileRecordReference reference); + + internal delegate Directory GetDirectoryByIndexFn(long index); + + internal delegate Directory GetDirectoryByRefFn(FileRecordReference reference); + + internal delegate File AllocateFileFn(FileRecordFlags flags); + + internal delegate void ForgetFileFn(File file); + + internal interface INtfsContext + { + Stream RawStream + { + get; + } + + AttributeDefinitions AttributeDefinitions + { + get; + } + + UpperCase UpperCase + { + get; + } + + BiosParameterBlock BiosParameterBlock + { + get; + } + + MasterFileTable Mft + { + get; + } + + ClusterBitmap ClusterBitmap + { + get; + } + + SecurityDescriptors SecurityDescriptors + { + get; + } + + ObjectIds ObjectIds + { + get; + } + + ReparsePoints ReparsePoints + { + get; + } + + Quotas Quotas + { + get; + } + + NtfsOptions Options + { + get; + } + + GetFileByIndexFn GetFileByIndex + { + get; + } + + GetFileByRefFn GetFileByRef + { + get; + } + + GetDirectoryByIndexFn GetDirectoryByIndex + { + get; + } + + GetDirectoryByRefFn GetDirectoryByRef + { + get; + } + + AllocateFileFn AllocateFile + { + get; + } + + ForgetFileFn ForgetFile + { + get; + } + + bool ReadOnly + { + get; + } + } + + internal sealed class NtfsContext : INtfsContext + { + private Stream _rawStream; + private AttributeDefinitions _attrDefs; + private UpperCase _upperCase; + private BiosParameterBlock _bpb; + private MasterFileTable _mft; + private ClusterBitmap _bitmap; + private SecurityDescriptors _securityDescriptors; + private ObjectIds _objectIds; + private ReparsePoints _reparsePoints; + private Quotas _quotas; + private NtfsOptions _options; + private GetFileByIndexFn _getFileByIndexFn; + private GetFileByRefFn _getFileByRefFn; + private GetDirectoryByIndexFn _getDirByIndexFn; + private GetDirectoryByRefFn _getDirByRefFn; + private AllocateFileFn _allocateFileFn; + private ForgetFileFn _forgetFileFn; + private bool _readOnly; + + public Stream RawStream + { + get { return _rawStream; } + set { _rawStream = value; } + } + + public AttributeDefinitions AttributeDefinitions + { + get { return _attrDefs; } + set { _attrDefs = value; } + } + + public UpperCase UpperCase + { + get { return _upperCase; } + set { _upperCase = value; } + } + + public BiosParameterBlock BiosParameterBlock + { + get { return _bpb; } + set { _bpb = value; } + } + + public MasterFileTable Mft + { + get { return _mft; } + set { _mft = value; } + } + + public ClusterBitmap ClusterBitmap + { + get { return _bitmap; } + set { _bitmap = value; } + } + + public SecurityDescriptors SecurityDescriptors + { + get { return _securityDescriptors; } + set { _securityDescriptors = value; } + } + + public ObjectIds ObjectIds + { + get { return _objectIds; } + set { _objectIds = value; } + } + + public ReparsePoints ReparsePoints + { + get { return _reparsePoints; } + set { _reparsePoints = value; } + } + + public Quotas Quotas + { + get { return _quotas; } + set { _quotas = value; } + } + + public NtfsOptions Options + { + get { return _options; } + set { _options = value; } + } + + public GetFileByIndexFn GetFileByIndex + { + get { return _getFileByIndexFn; } + set { _getFileByIndexFn = value; } + } + + public GetFileByRefFn GetFileByRef + { + get { return _getFileByRefFn; } + set { _getFileByRefFn = value; } + } + + public GetDirectoryByIndexFn GetDirectoryByIndex + { + get { return _getDirByIndexFn; } + set { _getDirByIndexFn = value; } + } + + public GetDirectoryByRefFn GetDirectoryByRef + { + get { return _getDirByRefFn; } + set { _getDirByRefFn = value; } + } + + public AllocateFileFn AllocateFile + { + get { return _allocateFileFn; } + set { _allocateFileFn = value; } + } + + public ForgetFileFn ForgetFile + { + get { return _forgetFileFn; } + set { _forgetFileFn = value; } + } + + public bool ReadOnly + { + get { return _readOnly; } + set { _readOnly = value; } + } + } +} diff --git a/DiscUtils/Ntfs/NtfsFileStream.cs b/DiscUtils/Ntfs/NtfsFileStream.cs new file mode 100644 index 0000000..91b813f --- /dev/null +++ b/DiscUtils/Ntfs/NtfsFileStream.cs @@ -0,0 +1,224 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal sealed class NtfsFileStream : SparseStream + { + private DirectoryEntry _entry; + + private File _file; + private SparseStream _baseStream; + + private bool _isDirty; + + public NtfsFileStream(NtfsFileSystem fileSystem, DirectoryEntry entry, AttributeType attrType, string attrName, FileAccess access) + { + _entry = entry; + + _file = fileSystem.GetFile(entry.Reference); + _baseStream = _file.OpenStream(attrType, attrName, access); + } + + public override bool CanRead + { + get + { + AssertOpen(); + return _baseStream.CanRead; + } + } + + public override bool CanSeek + { + get + { + AssertOpen(); + return _baseStream.CanSeek; + } + } + + public override bool CanWrite + { + get + { + AssertOpen(); + return _baseStream.CanWrite; + } + } + + public override long Length + { + get + { + AssertOpen(); + return _baseStream.Length; + } + } + + public override long Position + { + get + { + AssertOpen(); + return _baseStream.Position; + } + + set + { + AssertOpen(); + using (new NtfsTransaction()) + { + _baseStream.Position = value; + } + } + } + + public override IEnumerable Extents + { + get + { + AssertOpen(); + return _baseStream.Extents; + } + } + + public override void Close() + { + if (_baseStream == null) + { + return; + } + + using (new NtfsTransaction()) + { + base.Close(); + _baseStream.Close(); + + UpdateMetadata(); + + _baseStream = null; + } + } + + public override void Flush() + { + AssertOpen(); + using (new NtfsTransaction()) + { + _baseStream.Flush(); + + UpdateMetadata(); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + AssertOpen(); + Utilities.AssertBufferParameters(buffer, offset, count); + + using (new NtfsTransaction()) + { + return _baseStream.Read(buffer, offset, count); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + AssertOpen(); + using (new NtfsTransaction()) + { + return _baseStream.Seek(offset, origin); + } + } + + public override void SetLength(long value) + { + AssertOpen(); + using (new NtfsTransaction()) + { + if (value != Length) + { + _isDirty = true; + _baseStream.SetLength(value); + } + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + AssertOpen(); + Utilities.AssertBufferParameters(buffer, offset, count); + + using (new NtfsTransaction()) + { + _isDirty = true; + _baseStream.Write(buffer, offset, count); + } + } + + public override void Clear(int count) + { + AssertOpen(); + using (new NtfsTransaction()) + { + _isDirty = true; + _baseStream.Clear(count); + } + } + + private void UpdateMetadata() + { + if (!_file.Context.ReadOnly) + { + // Update the standard information attribute - so it reflects the actual file state + if (_isDirty) + { + _file.Modified(); + } + else + { + _file.Accessed(); + } + + // Update the directory entry used to open the file, so it's accurate + _entry.UpdateFrom(_file); + + // Write attribute changes back to the Master File Table + _file.UpdateRecordInMft(); + _isDirty = false; + } + } + + private void AssertOpen() + { + if (_baseStream == null) + { + throw new ObjectDisposedException(_entry.Details.FileName, "Attempt to use closed stream"); + } + } + } +} diff --git a/DiscUtils/Ntfs/NtfsFileSystem.cs b/DiscUtils/Ntfs/NtfsFileSystem.cs new file mode 100644 index 0000000..97aa3ae --- /dev/null +++ b/DiscUtils/Ntfs/NtfsFileSystem.cs @@ -0,0 +1,2440 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Security.AccessControl; + using System.Text.RegularExpressions; + + /// + /// Class for accessing NTFS file systems. + /// + public sealed class NtfsFileSystem : DiscFileSystem, IClusterBasedFileSystem, IWindowsFileSystem, IDiagnosticTraceable + { + private const FileAttributes NonSettableFileAttributes = FileAttributes.Directory | FileAttributes.Offline | FileAttributes.ReparsePoint; + + private NtfsContext _context; + private VolumeInformation _volumeInfo; + + // Top-level file system structures + + // Working state + private ObjectCache _fileCache; + + /// + /// Initializes a new instance of the NtfsFileSystem class. + /// + /// The stream containing the NTFS file system. + public NtfsFileSystem(Stream stream) + : base(new NtfsOptions()) + { + _context = new NtfsContext(); + _context.RawStream = stream; + _context.Options = NtfsOptions; + + _context.GetFileByIndex = GetFile; + _context.GetFileByRef = GetFile; + _context.GetDirectoryByRef = GetDirectory; + _context.GetDirectoryByIndex = GetDirectory; + _context.AllocateFile = AllocateFile; + _context.ForgetFile = ForgetFile; + _context.ReadOnly = !stream.CanWrite; + + _fileCache = new ObjectCache(); + + stream.Position = 0; + byte[] bytes = Utilities.ReadFully(stream, 512); + + _context.BiosParameterBlock = BiosParameterBlock.FromBytes(bytes, 0); + if (!IsValidBPB(_context.BiosParameterBlock, stream.Length)) + { + throw new InvalidFileSystemException("BIOS Parameter Block is invalid for an NTFS file system"); + } + + if (NtfsOptions.ReadCacheEnabled) + { + BlockCacheSettings cacheSettings = new BlockCacheSettings(); + cacheSettings.BlockSize = _context.BiosParameterBlock.BytesPerCluster; + _context.RawStream = new BlockCacheStream(SparseStream.FromStream(stream, Ownership.None), Ownership.None, cacheSettings); + } + + // Bootstrap the Master File Table + _context.Mft = new MasterFileTable(_context); + File mftFile = new File(_context, _context.Mft.GetBootstrapRecord()); + _fileCache[MasterFileTable.MftIndex] = mftFile; + _context.Mft.Initialize(mftFile); + + // Get volume information (includes version number) + File volumeInfoFile = GetFile(MasterFileTable.VolumeIndex); + _volumeInfo = volumeInfoFile.GetStream(AttributeType.VolumeInformation, null).GetContent(); + + // Initialize access to the other well-known metadata files + _context.ClusterBitmap = new ClusterBitmap(GetFile(MasterFileTable.BitmapIndex)); + _context.AttributeDefinitions = new AttributeDefinitions(GetFile(MasterFileTable.AttrDefIndex)); + _context.UpperCase = new UpperCase(GetFile(MasterFileTable.UpCaseIndex)); + + if (_volumeInfo.Version >= VolumeInformation.VersionW2k) + { + _context.SecurityDescriptors = new SecurityDescriptors(GetFile(MasterFileTable.SecureIndex)); + _context.ObjectIds = new ObjectIds(GetFile(GetDirectoryEntry(@"$Extend\$ObjId").Reference)); + _context.ReparsePoints = new ReparsePoints(GetFile(GetDirectoryEntry(@"$Extend\$Reparse").Reference)); + _context.Quotas = new Quotas(GetFile(GetDirectoryEntry(@"$Extend\$Quota").Reference)); + } +#if false + byte[] buffer = new byte[1024]; + for (int i = 0; i < buffer.Length; ++i) + { + buffer[i] = 0xFF; + } + + using (Stream s = OpenFile("$LogFile", FileMode.Open, FileAccess.ReadWrite)) + { + while (s.Position != s.Length) + { + s.Write(buffer, 0, (int)Math.Min(buffer.Length, s.Length - s.Position)); + } + } +#endif + } + + private delegate void StandardInformationModifier(StandardInformation si); + + /// + /// Gets the options that control how the file system is interpreted. + /// + public NtfsOptions NtfsOptions + { + get { return (NtfsOptions)Options; } + } + + /// + /// Gets the friendly name for the file system. + /// + public override string FriendlyName + { + get { return "Microsoft NTFS"; } + } + + /// + /// Indicates if the file system supports write operations. + /// + public override bool CanWrite + { + // For now, we don't... + get { return !_context.ReadOnly; } + } + + /// + /// Gets the size of each cluster (in bytes). + /// + public long ClusterSize + { + get { return _context.BiosParameterBlock.BytesPerCluster; } + } + + /// + /// Gets the total number of clusters managed by the file system. + /// + public long TotalClusters + { + get { return Utilities.Ceil(_context.BiosParameterBlock.TotalSectors64, _context.BiosParameterBlock.SectorsPerCluster); } + } + + /// + /// Gets the volume label. + /// + public override string VolumeLabel + { + get + { + File volumeFile = GetFile(MasterFileTable.VolumeIndex); + NtfsStream volNameStream = volumeFile.GetStream(AttributeType.VolumeName, null); + return volNameStream.GetContent().Name; + } + } + + private bool CreateShortNames + { + get + { + return _context.Options.ShortNameCreation == ShortFileNameOption.Enabled + || (_context.Options.ShortNameCreation == ShortFileNameOption.UseVolumeFlag + && (_volumeInfo.Flags & VolumeInformationFlags.DisableShortNameCreation) == 0); + } + } + + /// + /// Initializes a new NTFS file system. + /// + /// The stream to write the new file system to. + /// The label for the new file system. + /// The disk geometry of the disk containing the new file system. + /// The first sector of the new file system on the disk. + /// The number of sectors allocated to the new file system on the disk. + /// The newly-initialized file system. + public static NtfsFileSystem Format( + Stream stream, + string label, + Geometry diskGeometry, + long firstSector, + long sectorCount) + { + NtfsFormatter formatter = new NtfsFormatter(); + formatter.Label = label; + formatter.DiskGeometry = diskGeometry; + formatter.FirstSector = firstSector; + formatter.SectorCount = sectorCount; + return formatter.Format(stream); + } + + /// + /// Initializes a new NTFS file system. + /// + /// The stream to write the new file system to. + /// The label for the new file system. + /// The disk geometry of the disk containing the new file system. + /// The first sector of the new file system on the disk. + /// The number of sectors allocated to the new file system on the disk. + /// The Operating System's boot code. + /// The newly-initialized file system. + public static NtfsFileSystem Format( + Stream stream, + string label, + Geometry diskGeometry, + long firstSector, + long sectorCount, + byte[] bootCode) + { + NtfsFormatter formatter = new NtfsFormatter(); + formatter.Label = label; + formatter.DiskGeometry = diskGeometry; + formatter.FirstSector = firstSector; + formatter.SectorCount = sectorCount; + formatter.BootCode = bootCode; + return formatter.Format(stream); + } + + /// + /// Initializes a new NTFS file system. + /// + /// The stream to write the new file system to. + /// The label for the new file system. + /// The disk geometry of the disk containing the new file system. + /// The first sector of the new file system on the disk. + /// The number of sectors allocated to the new file system on the disk. + /// The formatting options. + /// The newly-initialized file system. + public static NtfsFileSystem Format( + Stream stream, + string label, + Geometry diskGeometry, + long firstSector, + long sectorCount, + NtfsFormatOptions options) + { + NtfsFormatter formatter = new NtfsFormatter(); + formatter.Label = label; + formatter.DiskGeometry = diskGeometry; + formatter.FirstSector = firstSector; + formatter.SectorCount = sectorCount; + formatter.BootCode = options.BootCode; + formatter.ComputerAccount = options.ComputerAccount; + return formatter.Format(stream); + } + + /// + /// Initializes a new NTFS file system. + /// + /// The volume to format. + /// The label for the new file system. + /// The newly-initialized file system. + public static NtfsFileSystem Format( + VolumeInfo volume, + string label) + { + NtfsFormatter formatter = new NtfsFormatter(); + formatter.Label = label; + formatter.DiskGeometry = volume.BiosGeometry ?? Geometry.Null; + formatter.FirstSector = volume.PhysicalStartSector; + formatter.SectorCount = volume.Length / Sizes.Sector; + return formatter.Format(volume.Open()); + } + + /// + /// Initializes a new NTFS file system. + /// + /// The volume to format. + /// The label for the new file system. + /// The Operating System's boot code. + /// The newly-initialized file system. + public static NtfsFileSystem Format( + VolumeInfo volume, + string label, + byte[] bootCode) + { + NtfsFormatter formatter = new NtfsFormatter(); + formatter.Label = label; + formatter.DiskGeometry = volume.BiosGeometry ?? Geometry.Null; + formatter.FirstSector = volume.PhysicalStartSector; + formatter.SectorCount = volume.Length / Sizes.Sector; + formatter.BootCode = bootCode; + return formatter.Format(volume.Open()); + } + + /// + /// Initializes a new NTFS file system. + /// + /// The volume to format. + /// The label for the new file system. + /// The formatting options. + /// The newly-initialized file system. + public static NtfsFileSystem Format( + VolumeInfo volume, + string label, + NtfsFormatOptions options) + { + NtfsFormatter formatter = new NtfsFormatter(); + formatter.Label = label; + formatter.DiskGeometry = volume.BiosGeometry ?? Geometry.Null; + formatter.FirstSector = volume.PhysicalStartSector; + formatter.SectorCount = volume.Length / Sizes.Sector; + formatter.BootCode = options.BootCode; + formatter.ComputerAccount = options.ComputerAccount; + return formatter.Format(volume.Open()); + } + + /// + /// Detects if a stream contains an NTFS file system. + /// + /// The stream to inspect. + /// true if NTFS is detected, else false. + public static bool Detect(Stream stream) + { + if (stream.Length < 512) + { + return false; + } + + stream.Position = 0; + byte[] bytes = Utilities.ReadFully(stream, 512); + BiosParameterBlock bpb = BiosParameterBlock.FromBytes(bytes, 0); + + return IsValidBPB(bpb, stream.Length); + } + + /// + /// Gets the Master File Table for this file system. + /// + /// + /// Use the returned object to explore the internals of the file system - most people will + /// never need to use this. + /// + /// The Master File Table. + public Internals.MasterFileTable GetMasterFileTable() + { + return new Internals.MasterFileTable(_context, _context.Mft); + } + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + using (new NtfsTransaction()) + { + DirectoryEntry sourceParentDirEntry = GetDirectoryEntry(Utilities.GetDirectoryFromPath(sourceFile)); + if (sourceParentDirEntry == null || !sourceParentDirEntry.IsDirectory) + { + throw new FileNotFoundException("No such file", sourceFile); + } + + Directory sourceParentDir = GetDirectory(sourceParentDirEntry.Reference); + + DirectoryEntry sourceEntry = sourceParentDir.GetEntryByName(Utilities.GetFileFromPath(sourceFile)); + if (sourceEntry == null || sourceEntry.IsDirectory) + { + throw new FileNotFoundException("No such file", sourceFile); + } + + File origFile = GetFile(sourceEntry.Reference); + + DirectoryEntry destParentDirEntry = GetDirectoryEntry(Utilities.GetDirectoryFromPath(destinationFile)); + if (destParentDirEntry == null || !destParentDirEntry.IsDirectory) + { + throw new FileNotFoundException("Destination directory not found", destinationFile); + } + + Directory destParentDir = GetDirectory(destParentDirEntry.Reference); + + DirectoryEntry destDirEntry = destParentDir.GetEntryByName(Utilities.GetFileFromPath(destinationFile)); + if (destDirEntry != null && !destDirEntry.IsDirectory) + { + if (overwrite) + { + if (destDirEntry.Reference.MftIndex == sourceEntry.Reference.MftIndex) + { + throw new IOException("Destination file already exists and is the source file"); + } + + File oldFile = GetFile(destDirEntry.Reference); + destParentDir.RemoveEntry(destDirEntry); + if (oldFile.HardLinkCount == 0) + { + oldFile.Delete(); + } + } + else + { + throw new IOException("Destination file already exists"); + } + } + + File newFile = File.CreateNew(_context, destParentDir.StandardInformation.FileAttributes); + foreach (var origStream in origFile.AllStreams) + { + NtfsStream newStream = newFile.GetStream(origStream.AttributeType, origStream.Name); + + switch (origStream.AttributeType) + { + case AttributeType.Data: + if (newStream == null) + { + newStream = newFile.CreateStream(origStream.AttributeType, origStream.Name); + } + + using (SparseStream s = origStream.Open(FileAccess.Read)) + using (SparseStream d = newStream.Open(FileAccess.Write)) + { + byte[] buffer = new byte[64 * Sizes.OneKiB]; + int numRead; + + do + { + numRead = s.Read(buffer, 0, buffer.Length); + d.Write(buffer, 0, numRead); + } + while (numRead != 0); + } + + break; + + case AttributeType.StandardInformation: + StandardInformation newSi = origStream.GetContent(); + newStream.SetContent(newSi); + break; + } + } + + AddFileToDirectory(newFile, destParentDir, Utilities.GetFileFromPath(destinationFile), null); + destParentDirEntry.UpdateFrom(destParentDir); + } + } + + /// + /// Creates a directory. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + CreateDirectory(path, null); + } + + /// + /// Creates a directory. + /// + /// The path of the new directory. + /// Options controlling attributes of the new Director, or null for defaults. + public void CreateDirectory(string path, NewFileOptions options) + { + using (new NtfsTransaction()) + { + string[] pathElements = path.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + + Directory focusDir = GetDirectory(MasterFileTable.RootDirIndex); + DirectoryEntry focusDirEntry = focusDir.DirectoryEntry; + + for (int i = 0; i < pathElements.Length; ++i) + { + DirectoryEntry childDirEntry = focusDir.GetEntryByName(pathElements[i]); + if (childDirEntry == null) + { + FileAttributeFlags newDirAttrs = focusDir.StandardInformation.FileAttributes; + if (options != null && options.Compressed.HasValue) + { + if (options.Compressed.Value) + { + newDirAttrs |= FileAttributeFlags.Compressed; + } + else + { + newDirAttrs &= ~FileAttributeFlags.Compressed; + } + } + + Directory childDir = Directory.CreateNew(_context, newDirAttrs); + try + { + childDirEntry = AddFileToDirectory(childDir, focusDir, pathElements[i], options); + + RawSecurityDescriptor parentSd = DoGetSecurity(focusDir); + RawSecurityDescriptor newSd; + if (options != null && options.SecurityDescriptor != null) + { + newSd = options.SecurityDescriptor; + } + else + { + newSd = SecurityDescriptor.CalcNewObjectDescriptor(parentSd, false); + } + + DoSetSecurity(childDir, newSd); + childDirEntry.UpdateFrom(childDir); + + // Update the directory entry by which we found the directory we've just modified + focusDirEntry.UpdateFrom(focusDir); + + focusDir = childDir; + } + finally + { + if (childDir.HardLinkCount == 0) + { + childDir.Delete(); + } + } + } + else + { + focusDir = GetDirectory(childDirEntry.Reference); + } + + focusDirEntry = childDirEntry; + } + } + } + + /// + /// Deletes a directory. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + using (new NtfsTransaction()) + { + if (string.IsNullOrEmpty(path)) + { + throw new IOException("Unable to delete root directory"); + } + + string parent = Utilities.GetDirectoryFromPath(path); + + DirectoryEntry parentDirEntry = GetDirectoryEntry(parent); + if (parentDirEntry == null || !parentDirEntry.IsDirectory) + { + throw new DirectoryNotFoundException("No such directory: " + path); + } + + Directory parentDir = GetDirectory(parentDirEntry.Reference); + + DirectoryEntry dirEntry = parentDir.GetEntryByName(Utilities.GetFileFromPath(path)); + if (dirEntry == null || !dirEntry.IsDirectory) + { + throw new DirectoryNotFoundException("No such directory: " + path); + } + + Directory dir = GetDirectory(dirEntry.Reference); + + if (!dir.IsEmpty) + { + throw new IOException("Unable to delete non-empty directory"); + } + + if ((dirEntry.Details.FileAttributes & FileAttributes.ReparsePoint) != 0) + { + RemoveReparsePoint(dir); + } + + RemoveFileFromDirectory(parentDir, dir, Utilities.GetFileFromPath(path)); + + if (dir.HardLinkCount == 0) + { + dir.Delete(); + } + } + } + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + using (new NtfsTransaction()) + { + string attributeName; + AttributeType attributeType; + string dirEntryPath = ParsePath(path, out attributeName, out attributeType); + + string parentDirPath = Utilities.GetDirectoryFromPath(dirEntryPath); + + DirectoryEntry parentDirEntry = GetDirectoryEntry(parentDirPath); + if (parentDirEntry == null || !parentDirEntry.IsDirectory) + { + throw new FileNotFoundException("No such file", path); + } + + Directory parentDir = GetDirectory(parentDirEntry.Reference); + + DirectoryEntry dirEntry = parentDir.GetEntryByName(Utilities.GetFileFromPath(dirEntryPath)); + if (dirEntry == null || dirEntry.IsDirectory) + { + throw new FileNotFoundException("No such file", path); + } + + File file = GetFile(dirEntry.Reference); + + if (string.IsNullOrEmpty(attributeName) && attributeType == AttributeType.Data) + { + if ((dirEntry.Details.FileAttributes & FileAttributes.ReparsePoint) != 0) + { + RemoveReparsePoint(file); + } + + RemoveFileFromDirectory(parentDir, file, Utilities.GetFileFromPath(path)); + + if (file.HardLinkCount == 0) + { + file.Delete(); + } + } + else + { + NtfsStream attrStream = file.GetStream(attributeType, attributeName); + if (attrStream == null) + { + throw new FileNotFoundException("No such attribute: " + attributeName, path); + } + else + { + file.RemoveStream(attrStream); + } + } + } + } + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public override bool DirectoryExists(string path) + { + using (new NtfsTransaction()) + { + // Special case - root directory + if (String.IsNullOrEmpty(path)) + { + return true; + } + else + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + return dirEntry != null && (dirEntry.Details.FileAttributes & FileAttributes.Directory) != 0; + } + } + } + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public override bool FileExists(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + return dirEntry != null && (dirEntry.Details.FileAttributes & FileAttributes.Directory) == 0; + } + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) + { + using (new NtfsTransaction()) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + List dirs = new List(); + DoSearch(dirs, path, re, searchOption == SearchOption.AllDirectories, true, false); + return dirs.ToArray(); + } + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern, SearchOption searchOption) + { + using (new NtfsTransaction()) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + List results = new List(); + DoSearch(results, path, re, searchOption == SearchOption.AllDirectories, false, true); + return results.ToArray(); + } + } + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories. + public override string[] GetFileSystemEntries(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry parentDirEntry = GetDirectoryEntry(path); + if (parentDirEntry == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' does not exist", path)); + } + + Directory parentDir = GetDirectory(parentDirEntry.Reference); + + return Utilities.Map(parentDir.GetAllEntries(true), (m) => Utilities.CombinePaths(path, m.Details.FileName)); + } + } + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path, string searchPattern) + { + using (new NtfsTransaction()) + { + // TODO: Be smarter, use the B*Tree for better performance when the start of the pattern is known + // characters + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + DirectoryEntry parentDirEntry = GetDirectoryEntry(path); + if (parentDirEntry == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' does not exist", path)); + } + + Directory parentDir = GetDirectory(parentDirEntry.Reference); + + List result = new List(); + foreach (DirectoryEntry dirEntry in parentDir.GetAllEntries(true)) + { + if (re.IsMatch(dirEntry.Details.FileName)) + { + result.Add(Utilities.CombinePaths(path, dirEntry.Details.FileName)); + } + } + + return result.ToArray(); + } + } + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + using (new NtfsTransaction()) + { + using (new NtfsTransaction()) + { + DirectoryEntry sourceParentDirEntry = GetDirectoryEntry(Utilities.GetDirectoryFromPath(sourceDirectoryName)); + if (sourceParentDirEntry == null || !sourceParentDirEntry.IsDirectory) + { + throw new DirectoryNotFoundException("No such directory: " + sourceDirectoryName); + } + + Directory sourceParentDir = GetDirectory(sourceParentDirEntry.Reference); + + DirectoryEntry sourceEntry = sourceParentDir.GetEntryByName(Utilities.GetFileFromPath(sourceDirectoryName)); + if (sourceEntry == null || !sourceEntry.IsDirectory) + { + throw new DirectoryNotFoundException("No such directory: " + sourceDirectoryName); + } + + File file = GetFile(sourceEntry.Reference); + + DirectoryEntry destParentDirEntry = GetDirectoryEntry(Utilities.GetDirectoryFromPath(destinationDirectoryName)); + if (destParentDirEntry == null || !destParentDirEntry.IsDirectory) + { + throw new DirectoryNotFoundException("Destination directory not found: " + destinationDirectoryName); + } + + Directory destParentDir = GetDirectory(destParentDirEntry.Reference); + + DirectoryEntry destDirEntry = destParentDir.GetEntryByName(Utilities.GetFileFromPath(destinationDirectoryName)); + if (destDirEntry != null) + { + throw new IOException("Destination directory already exists"); + } + + RemoveFileFromDirectory(sourceParentDir, file, sourceEntry.Details.FileName); + AddFileToDirectory(file, destParentDir, Utilities.GetFileFromPath(destinationDirectoryName), null); + } + } + } + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + using (new NtfsTransaction()) + { + DirectoryEntry sourceParentDirEntry = GetDirectoryEntry(Utilities.GetDirectoryFromPath(sourceName)); + if (sourceParentDirEntry == null || !sourceParentDirEntry.IsDirectory) + { + throw new FileNotFoundException("No such file", sourceName); + } + + Directory sourceParentDir = GetDirectory(sourceParentDirEntry.Reference); + + DirectoryEntry sourceEntry = sourceParentDir.GetEntryByName(Utilities.GetFileFromPath(sourceName)); + if (sourceEntry == null || sourceEntry.IsDirectory) + { + throw new FileNotFoundException("No such file", sourceName); + } + + File file = GetFile(sourceEntry.Reference); + + DirectoryEntry destParentDirEntry = GetDirectoryEntry(Utilities.GetDirectoryFromPath(destinationName)); + if (destParentDirEntry == null || !destParentDirEntry.IsDirectory) + { + throw new FileNotFoundException("Destination directory not found", destinationName); + } + + Directory destParentDir = GetDirectory(destParentDirEntry.Reference); + + DirectoryEntry destDirEntry = destParentDir.GetEntryByName(Utilities.GetFileFromPath(destinationName)); + if (destDirEntry != null && !destDirEntry.IsDirectory) + { + if (overwrite) + { + if (destDirEntry.Reference.MftIndex == sourceEntry.Reference.MftIndex) + { + throw new IOException("Destination file already exists and is the source file"); + } + + File oldFile = GetFile(destDirEntry.Reference); + destParentDir.RemoveEntry(destDirEntry); + if (oldFile.HardLinkCount == 0) + { + oldFile.Delete(); + } + } + else + { + throw new IOException("Destination file already exists"); + } + } + + RemoveFileFromDirectory(sourceParentDir, file, sourceEntry.Details.FileName); + AddFileToDirectory(file, destParentDir, Utilities.GetFileFromPath(destinationName), null); + } + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the returned stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) + { + return OpenFile(path, mode, access, null); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the returned stream. + /// Options controlling attributes of a new file, or null for defaults (ignored if file exists). + /// The new stream. + public SparseStream OpenFile(string path, FileMode mode, FileAccess access, NewFileOptions options) + { + using (new NtfsTransaction()) + { + string attributeName; + AttributeType attributeType; + string dirEntryPath = ParsePath(path, out attributeName, out attributeType); + + DirectoryEntry entry = GetDirectoryEntry(dirEntryPath); + if (entry == null) + { + if (mode == FileMode.Open) + { + throw new FileNotFoundException("No such file", path); + } + else + { + entry = CreateNewFile(dirEntryPath, options); + } + } + else if (mode == FileMode.CreateNew) + { + throw new IOException("File already exists"); + } + + if ((entry.Details.FileAttributes & FileAttributes.Directory) != 0 && attributeType == AttributeType.Data) + { + throw new IOException("Attempt to open directory as a file"); + } + else + { + File file = GetFile(entry.Reference); + NtfsStream ntfsStream = file.GetStream(attributeType, attributeName); + + if (ntfsStream == null) + { + if (mode == FileMode.Create || mode == FileMode.OpenOrCreate) + { + ntfsStream = file.CreateStream(attributeType, attributeName); + } + else + { + throw new FileNotFoundException("No such attribute on file", path); + } + } + + SparseStream stream = new NtfsFileStream(this, entry, attributeType, attributeName, access); + + if (mode == FileMode.Create || mode == FileMode.Truncate) + { + stream.SetLength(0); + } + + return stream; + } + } + } + + /// + /// Opens an existing file stream. + /// + /// The file containing the stream. + /// The type of the stream. + /// The name of the stream. + /// The desired access to the stream. + /// A stream that can be used to access the file stream. + [Obsolete(@"Use OpenFile with filename:attributename:$attributetype syntax (e.g. \FILE.TXT:STREAM:$DATA)", false)] + public SparseStream OpenRawStream(string file, AttributeType type, string name, FileAccess access) + { + using (new NtfsTransaction()) + { + DirectoryEntry entry = GetDirectoryEntry(file); + if (entry == null) + { + throw new FileNotFoundException("No such file", file); + } + + File fileObj = GetFile(entry.Reference); + return fileObj.OpenStream(type, name, access); + } + } + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public override FileAttributes GetAttributes(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + return dirEntry.Details.FileAttributes; + } + } + } + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + + FileAttributes oldValue = dirEntry.Details.FileAttributes; + FileAttributes changedAttribs = oldValue ^ newValue; + + if (changedAttribs == 0) + { + // Abort - nothing changed + return; + } + + if ((changedAttribs & NonSettableFileAttributes) != 0) + { + throw new ArgumentException("Attempt to change attributes that are read-only", "newValue"); + } + + File file = GetFile(dirEntry.Reference); + + if ((changedAttribs & FileAttributes.SparseFile) != 0) + { + if (dirEntry.IsDirectory) + { + throw new ArgumentException("Attempt to change sparse attribute on a directory", "newValue"); + } + + if ((newValue & FileAttributes.SparseFile) == 0) + { + throw new ArgumentException("Attempt to remove sparse attribute from file", "newValue"); + } + else + { + NtfsAttribute ntfsAttr = file.GetAttribute(AttributeType.Data, null); + if ((ntfsAttr.Flags & AttributeFlags.Compressed) != 0) + { + throw new ArgumentException("Attempt to mark compressed file as sparse", "newValue"); + } + + ntfsAttr.Flags |= AttributeFlags.Sparse; + if (ntfsAttr.IsNonResident) + { + ntfsAttr.CompressedDataSize = ntfsAttr.PrimaryRecord.AllocatedLength; + ntfsAttr.CompressionUnitSize = 16; + ((NonResidentAttributeBuffer)ntfsAttr.RawBuffer).AlignVirtualClusterCount(); + } + } + } + + if ((changedAttribs & FileAttributes.Compressed) != 0 && !dirEntry.IsDirectory) + { + if ((newValue & FileAttributes.Compressed) == 0) + { + throw new ArgumentException("Attempt to remove compressed attribute from file", "newValue"); + } + else + { + NtfsAttribute ntfsAttr = file.GetAttribute(AttributeType.Data, null); + if ((ntfsAttr.Flags & AttributeFlags.Sparse) != 0) + { + throw new ArgumentException("Attempt to mark sparse file as compressed", "newValue"); + } + + ntfsAttr.Flags |= AttributeFlags.Compressed; + if (ntfsAttr.IsNonResident) + { + ntfsAttr.CompressedDataSize = ntfsAttr.PrimaryRecord.AllocatedLength; + ntfsAttr.CompressionUnitSize = 16; + ((NonResidentAttributeBuffer)ntfsAttr.RawBuffer).AlignVirtualClusterCount(); + } + } + } + + UpdateStandardInformation(dirEntry, file, delegate(StandardInformation si) { si.FileAttributes = FileNameRecord.SetAttributes(newValue, si.FileAttributes); }); + } + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTimeUtc(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + return dirEntry.Details.CreationTime; + } + } + } + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + using (new NtfsTransaction()) + { + UpdateStandardInformation(path, delegate(StandardInformation si) { si.CreationTime = newTime; }); + } + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTimeUtc(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + return dirEntry.Details.LastAccessTime; + } + } + } + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + using (new NtfsTransaction()) + { + UpdateStandardInformation(path, delegate(StandardInformation si) { si.LastAccessTime = newTime; }); + } + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTimeUtc(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + return dirEntry.Details.ModificationTime; + } + } + } + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + using (new NtfsTransaction()) + { + UpdateStandardInformation(path, delegate(StandardInformation si) { si.ModificationTime = newTime; }); + } + } + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public override long GetFileLength(string path) + { + using (new NtfsTransaction()) + { + string attributeName; + AttributeType attributeType; + string dirEntryPath = ParsePath(path, out attributeName, out attributeType); + + DirectoryEntry dirEntry = GetDirectoryEntry(dirEntryPath); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + + // Ordinary file length request, use info from directory entry for efficiency - if allowed + if (NtfsOptions.FileLengthFromDirectoryEntries && attributeName == null && attributeType == AttributeType.Data) + { + return (long)dirEntry.Details.RealSize; + } + + // Alternate stream / attribute, pull info from attribute record + File file = GetFile(dirEntry.Reference); + var attr = file.GetAttribute(attributeType, attributeName); + if (attr == null) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "No such attribute '{0}({1})'", attributeName, attributeType)); + } + + return attr.Length; + } + } + + /// + /// Converts a cluster (index) into an absolute byte position in the underlying stream. + /// + /// The cluster to convert. + /// The corresponding absolute byte position. + public long ClusterToOffset(long cluster) + { + return cluster * ClusterSize; + } + + /// + /// Converts an absolute byte position in the underlying stream to a cluster (index). + /// + /// The byte position to convert. + /// The cluster containing the specified byte. + public long OffsetToCluster(long offset) + { + return offset / ClusterSize; + } + + /// + /// Converts a file name to the list of clusters occupied by the file's data. + /// + /// The path to inspect. + /// The clusters as a list of cluster ranges. + /// Note that in some file systems, small files may not have dedicated + /// clusters. Only dedicated clusters will be returned. + public Range[] PathToClusters(string path) + { + string plainPath; + string attributeName; + SplitPath(path, out plainPath, out attributeName); + + DirectoryEntry dirEntry = GetDirectoryEntry(plainPath); + if (dirEntry == null || dirEntry.IsDirectory) + { + throw new FileNotFoundException("No such file", path); + } + + File file = GetFile(dirEntry.Reference); + + NtfsStream stream = file.GetStream(AttributeType.Data, attributeName); + if (stream == null) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "File does not contain '{0}' data attribute", attributeName), path); + } + + return stream.GetClusters(); + } + + /// + /// Converts a file name to the extents containing its data. + /// + /// The path to inspect. + /// The file extents, as absolute byte positions in the underlying stream. + /// Use this method with caution - NTFS supports encrypted, sparse and compressed files + /// where bytes are not directly stored in extents. Small files may be entirely stored in the + /// Master File Table, where corruption protection algorithms mean that some bytes do not contain + /// the expected values. This method merely indicates where file data is stored, + /// not what's stored. To access the contents of a file, use OpenFile. + public StreamExtent[] PathToExtents(string path) + { + string plainPath; + string attributeName; + SplitPath(path, out plainPath, out attributeName); + + DirectoryEntry dirEntry = GetDirectoryEntry(plainPath); + if (dirEntry == null || dirEntry.IsDirectory) + { + throw new FileNotFoundException("No such file", path); + } + + File file = GetFile(dirEntry.Reference); + + NtfsStream stream = file.GetStream(AttributeType.Data, attributeName); + if (stream == null) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "File does not contain '{0}' data attribute", attributeName), path); + } + + return stream.GetAbsoluteExtents(); + } + + /// + /// Gets an object that can convert between clusters and files. + /// + /// The cluster map. + public ClusterMap BuildClusterMap() + { + return _context.Mft.GetClusterMap(); + } + + /// + /// Creates an NTFS hard link to an existing file. + /// + /// An existing name of the file. + /// The name of the new hard link to the file. + public void CreateHardLink(string sourceName, string destinationName) + { + using (new NtfsTransaction()) + { + DirectoryEntry sourceDirEntry = GetDirectoryEntry(sourceName); + if (sourceDirEntry == null) + { + throw new FileNotFoundException("Source file not found", sourceName); + } + + string destinationDirName = Utilities.GetDirectoryFromPath(destinationName); + DirectoryEntry destinationDirSelfEntry = GetDirectoryEntry(destinationDirName); + if (destinationDirSelfEntry == null || (destinationDirSelfEntry.Details.FileAttributes & FileAttributes.Directory) == 0) + { + throw new FileNotFoundException("Destination directory not found", destinationDirName); + } + + Directory destinationDir = GetDirectory(destinationDirSelfEntry.Reference); + if (destinationDir == null) + { + throw new FileNotFoundException("Destination directory not found", destinationDirName); + } + + DirectoryEntry destinationDirEntry = GetDirectoryEntry(destinationDir, Utilities.GetFileFromPath(destinationName)); + if (destinationDirEntry != null) + { + throw new IOException("A file with this name already exists: " + destinationName); + } + + File file = GetFile(sourceDirEntry.Reference); + destinationDir.AddEntry(file, Utilities.GetFileFromPath(destinationName), FileNameNamespace.Posix); + destinationDirSelfEntry.UpdateFrom(destinationDir); + } + } + + /// + /// Gets the number of hard links to a given file or directory. + /// + /// The path of the file or directory. + /// The number of hard links. + /// All files have at least one hard link. + public int GetHardLinkCount(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + + File file = GetFile(dirEntry.Reference); + + if (!_context.Options.HideDosFileNames) + { + return file.HardLinkCount; + } + else + { + int numHardLinks = 0; + + foreach (var fnStream in file.GetStreams(AttributeType.FileName, null)) + { + var fnr = fnStream.GetContent(); + if (fnr.FileNameNamespace != FileNameNamespace.Dos) + { + ++numHardLinks; + } + } + + return numHardLinks; + } + } + } + + /// + /// Indicates whether the file is known by other names. + /// + /// The file to inspect. + /// true if the file has other names, else false. + public bool HasHardLinks(string path) + { + return GetHardLinkCount(path) > 1; + } + + /// + /// Gets the security descriptor associated with the file or directory. + /// + /// The file or directory to inspect. + /// The security descriptor. + public RawSecurityDescriptor GetSecurity(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + File file = GetFile(dirEntry.Reference); + return DoGetSecurity(file); + } + } + } + + /// + /// Sets the security descriptor associated with the file or directory. + /// + /// The file or directory to change. + /// The new security descriptor. + public void SetSecurity(string path, RawSecurityDescriptor securityDescriptor) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + File file = GetFile(dirEntry.Reference); + DoSetSecurity(file, securityDescriptor); + + // Update the directory entry used to open the file + dirEntry.UpdateFrom(file); + } + } + } + + /// + /// Sets the reparse point data on a file or directory. + /// + /// The file to set the reparse point on. + /// The new reparse point. + public void SetReparsePoint(string path, ReparsePoint reparsePoint) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + File file = GetFile(dirEntry.Reference); + + NtfsStream stream = file.GetStream(AttributeType.ReparsePoint, null); + if (stream != null) + { + // If there's an existing reparse point, unhook it. + using (Stream contentStream = stream.Open(FileAccess.Read)) + { + byte[] oldRpBuffer = Utilities.ReadFully(contentStream, (int)contentStream.Length); + ReparsePointRecord rp = new ReparsePointRecord(); + rp.ReadFrom(oldRpBuffer, 0); + _context.ReparsePoints.Remove(rp.Tag, dirEntry.Reference); + } + } + else + { + stream = file.CreateStream(AttributeType.ReparsePoint, null); + } + + // Set the new content + ReparsePointRecord newRp = new ReparsePointRecord(); + newRp.Tag = (uint)reparsePoint.Tag; + newRp.Content = reparsePoint.Content; + + byte[] contentBuffer = new byte[newRp.Size]; + newRp.WriteTo(contentBuffer, 0); + using (Stream contentStream = stream.Open(FileAccess.ReadWrite)) + { + contentStream.Write(contentBuffer, 0, contentBuffer.Length); + contentStream.SetLength(contentBuffer.Length); + } + + // Update the standard information attribute - so it reflects the actual file state + NtfsStream stdInfoStream = file.GetStream(AttributeType.StandardInformation, null); + StandardInformation si = stdInfoStream.GetContent(); + si.FileAttributes = si.FileAttributes | FileAttributeFlags.ReparsePoint; + stdInfoStream.SetContent(si); + + // Update the directory entry used to open the file, so it's accurate + dirEntry.Details.EASizeOrReparsePointTag = newRp.Tag; + dirEntry.UpdateFrom(file); + + // Write attribute changes back to the Master File Table + file.UpdateRecordInMft(); + + // Add the reparse point to the index + _context.ReparsePoints.Add(newRp.Tag, dirEntry.Reference); + } + } + } + + /// + /// Gets the reparse point data associated with a file or directory. + /// + /// The file to query. + /// The reparse point information. + public ReparsePoint GetReparsePoint(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + File file = GetFile(dirEntry.Reference); + + NtfsStream stream = file.GetStream(AttributeType.ReparsePoint, null); + if (stream != null) + { + ReparsePointRecord rp = new ReparsePointRecord(); + + using (Stream contentStream = stream.Open(FileAccess.Read)) + { + byte[] buffer = Utilities.ReadFully(contentStream, (int)contentStream.Length); + rp.ReadFrom(buffer, 0); + return new ReparsePoint((int)rp.Tag, rp.Content); + } + } + } + } + + return null; + } + + /// + /// Removes a reparse point from a file or directory, without deleting the file or directory. + /// + /// The path to the file or directory to remove the reparse point from. + public void RemoveReparsePoint(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + File file = GetFile(dirEntry.Reference); + RemoveReparsePoint(file); + + // Update the directory entry used to open the file, so it's accurate + dirEntry.UpdateFrom(file); + + // Write attribute changes back to the Master File Table + file.UpdateRecordInMft(); + } + } + } + + /// + /// Gets the short name for a given path. + /// + /// The path to convert. + /// The short name. + /// + /// This method only gets the short name for the final part of the path, to + /// convert a complete path, call this method repeatedly, once for each path + /// segment. If there is no short name for the given path,null is + /// returned. + /// + public string GetShortName(string path) + { + using (new NtfsTransaction()) + { + string parentPath = Utilities.GetDirectoryFromPath(path); + DirectoryEntry parentEntry = GetDirectoryEntry(parentPath); + if (parentEntry == null || (parentEntry.Details.FileAttributes & FileAttributes.Directory) == 0) + { + throw new DirectoryNotFoundException("Parent directory not found"); + } + + Directory dir = GetDirectory(parentEntry.Reference); + if (dir == null) + { + throw new DirectoryNotFoundException("Parent directory not found"); + } + + DirectoryEntry givenEntry = dir.GetEntryByName(Utilities.GetFileFromPath(path)); + if (givenEntry == null) + { + throw new FileNotFoundException("Path not found", path); + } + + if (givenEntry.Details.FileNameNamespace == FileNameNamespace.Dos) + { + return givenEntry.Details.FileName; + } + else if (givenEntry.Details.FileNameNamespace == FileNameNamespace.Win32) + { + File file = GetFile(givenEntry.Reference); + + foreach (var stream in file.GetStreams(AttributeType.FileName, null)) + { + FileNameRecord fnr = stream.GetContent(); + if (fnr.ParentDirectory.Equals(givenEntry.Details.ParentDirectory) + && fnr.FileNameNamespace == FileNameNamespace.Dos) + { + return fnr.FileName; + } + } + } + + return null; + } + } + + /// + /// Sets the short name for a given file or directory. + /// + /// The full path to the file or directory to change. + /// The shortName, which should not include a path. + public void SetShortName(string path, string shortName) + { + if (!Utilities.Is8Dot3(shortName)) + { + throw new ArgumentException("Short name is not a valid 8.3 file name", "shortName"); + } + + using (new NtfsTransaction()) + { + string parentPath = Utilities.GetDirectoryFromPath(path); + DirectoryEntry parentEntry = GetDirectoryEntry(parentPath); + if (parentEntry == null || (parentEntry.Details.FileAttributes & FileAttributes.Directory) == 0) + { + throw new DirectoryNotFoundException("Parent directory not found"); + } + + Directory dir = GetDirectory(parentEntry.Reference); + if (dir == null) + { + throw new DirectoryNotFoundException("Parent directory not found"); + } + + DirectoryEntry givenEntry = dir.GetEntryByName(Utilities.GetFileFromPath(path)); + if (givenEntry == null) + { + throw new FileNotFoundException("Path not found", path); + } + + FileNameNamespace givenNamespace = givenEntry.Details.FileNameNamespace; + File file = GetFile(givenEntry.Reference); + + if (givenNamespace == FileNameNamespace.Posix && file.HasWin32OrDosName) + { + throw new InvalidOperationException("Cannot set a short name on hard links"); + } + + // Convert Posix/Win32AndDos to just Win32 + if (givenEntry.Details.FileNameNamespace != FileNameNamespace.Win32) + { + dir.RemoveEntry(givenEntry); + dir.AddEntry(file, givenEntry.Details.FileName, FileNameNamespace.Win32); + } + + // Remove any existing Dos names, and set the new one + List nameStreams = new List(file.GetStreams(AttributeType.FileName, null)); + foreach (var stream in nameStreams) + { + FileNameRecord fnr = stream.GetContent(); + if (fnr.ParentDirectory.Equals(givenEntry.Details.ParentDirectory) + && fnr.FileNameNamespace == FileNameNamespace.Dos) + { + DirectoryEntry oldEntry = dir.GetEntryByName(fnr.FileName); + dir.RemoveEntry(oldEntry); + } + } + + dir.AddEntry(file, shortName, FileNameNamespace.Dos); + + parentEntry.UpdateFrom(dir); + } + } + + /// + /// Gets the standard file information for a file. + /// + /// The full path to the file or directory to query. + /// The standard file information. + public WindowsFileInformation GetFileStandardInformation(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + + File file = GetFile(dirEntry.Reference); + StandardInformation si = file.StandardInformation; + + return new WindowsFileInformation + { + CreationTime = si.CreationTime, + LastAccessTime = si.LastAccessTime, + ChangeTime = si.MftChangedTime, + LastWriteTime = si.ModificationTime, + FileAttributes = StandardInformation.ConvertFlags(si.FileAttributes, file.IsDirectory) + }; + } + } + + /// + /// Sets the standard file information for a file. + /// + /// The full path to the file or directory to query. + /// The standard file information. + public void SetFileStandardInformation(string path, WindowsFileInformation info) + { + using (new NtfsTransaction()) + { + UpdateStandardInformation( + path, + delegate(StandardInformation si) + { + si.CreationTime = info.CreationTime; + si.LastAccessTime = info.LastAccessTime; + si.MftChangedTime = info.ChangeTime; + si.ModificationTime = info.LastWriteTime; + si.FileAttributes = StandardInformation.SetFileAttributes(info.FileAttributes, si.FileAttributes); + }); + } + } + + /// + /// Gets the file id for a given path. + /// + /// The path to get the id of. + /// The file id. + /// + /// The returned file id includes the MFT index of the primary file record for the file. + /// The file id can be used to determine if two paths refer to the same actual file. + /// The MFT index is held in the lower 48 bits of the id. + /// + public long GetFileId(string path) + { + using (new NtfsTransaction()) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + return (long)dirEntry.Reference.Value; + } + } + } + + /// + /// Gets the names of the alternate data streams for a file. + /// + /// The path to the file. + /// + /// The list of alternate data streams (or empty, if none). To access the contents + /// of the alternate streams, use OpenFile(path + ":" + name, ...). + /// + public string[] GetAlternateDataStreams(string path) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + + File file = GetFile(dirEntry.Reference); + + List names = new List(); + foreach (var attr in file.AllStreams) + { + if (attr.AttributeType == AttributeType.Data && !string.IsNullOrEmpty(attr.Name)) + { + names.Add(attr.Name); + } + } + + return names.ToArray(); + } + + /// + /// Reads the boot code of the file system into a byte array. + /// + /// The boot code, or null if not available. + public override byte[] ReadBootCode() + { + using (Stream s = OpenFile(@"\$Boot", FileMode.Open)) + { + return Utilities.ReadFully(s, (int)s.Length); + } + } + + /// + /// Updates the BIOS Parameter Block (BPB) of the file system to reflect a new disk geometry. + /// + /// The disk's new BIOS geometry. + /// Having an accurate geometry in the BPB is essential for booting some Operating Systems (e.g. Windows XP). + public void UpdateBiosGeometry(Geometry geometry) + { + _context.BiosParameterBlock.SectorsPerTrack = (ushort)geometry.SectorsPerTrack; + _context.BiosParameterBlock.NumHeads = (ushort)geometry.HeadsPerCylinder; + + _context.RawStream.Position = 0; + byte[] bpbSector = Utilities.ReadFully(_context.RawStream, 512); + + _context.BiosParameterBlock.ToBytes(bpbSector, 0); + + _context.RawStream.Position = 0; + _context.RawStream.Write(bpbSector, 0, bpbSector.Length); + } + + /// + /// Writes a diagnostic dump of key NTFS structures. + /// + /// The writer to receive the dump. + /// The indent to apply to the start of each line of output. + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + "NTFS File System Dump"); + writer.WriteLine(linePrefix + "====================="); + + ////_context.Mft.Dump(writer, linePrefix); + writer.WriteLine(linePrefix); + _context.BiosParameterBlock.Dump(writer, linePrefix); + + if (_context.SecurityDescriptors != null) + { + writer.WriteLine(linePrefix); + _context.SecurityDescriptors.Dump(writer, linePrefix); + } + + if (_context.ObjectIds != null) + { + writer.WriteLine(linePrefix); + _context.ObjectIds.Dump(writer, linePrefix); + } + + if (_context.ReparsePoints != null) + { + writer.WriteLine(linePrefix); + _context.ReparsePoints.Dump(writer, linePrefix); + } + + if (_context.Quotas != null) + { + writer.WriteLine(linePrefix); + _context.Quotas.Dump(writer, linePrefix); + } + + writer.WriteLine(linePrefix); + GetDirectory(MasterFileTable.RootDirIndex).Dump(writer, linePrefix); + + writer.WriteLine(linePrefix); + writer.WriteLine(linePrefix + "FULL FILE LISTING"); + foreach (var record in _context.Mft.Records) + { + // Don't go through cache - these are short-lived, and this is (just!) diagnostics + File f = new File(_context, record); + f.Dump(writer, linePrefix); + + foreach (var stream in f.AllStreams) + { + if (stream.AttributeType == AttributeType.IndexRoot) + { + try + { + writer.WriteLine(linePrefix + " INDEX (" + stream.Name + ")"); + f.GetIndex(stream.Name).Dump(writer, linePrefix + " "); + } + catch (Exception e) + { + writer.WriteLine(linePrefix + "!Exception: " + e); + } + } + } + } + + writer.WriteLine(linePrefix); + writer.WriteLine(linePrefix + "DIRECTORY TREE"); + writer.WriteLine(linePrefix + @"\ (5)"); + DumpDirectory(GetDirectory(MasterFileTable.RootDirIndex), writer, linePrefix); // 5 = Root Dir + } + + #region Internal File access methods (exposed via NtfsContext) + internal Directory GetDirectory(long index) + { + return (Directory)GetFile(index); + } + + internal Directory GetDirectory(FileRecordReference fileReference) + { + return (Directory)GetFile(fileReference); + } + + internal File GetFile(FileRecordReference fileReference) + { + FileRecord record = _context.Mft.GetRecord(fileReference); + if (record == null) + { + return null; + } + + // Don't create file objects for file record segments that are part of another + // logical file. + if (record.BaseFile.Value != 0) + { + return null; + } + + File file = _fileCache[fileReference.MftIndex]; + + if (file != null && file.MftReference.SequenceNumber != fileReference.SequenceNumber) + { + file = null; + } + + if (file == null) + { + if ((record.Flags & FileRecordFlags.IsDirectory) != 0) + { + file = new Directory(_context, record); + } + else + { + file = new File(_context, record); + } + + _fileCache[fileReference.MftIndex] = file; + } + + return file; + } + + internal File GetFile(long index) + { + FileRecord record = _context.Mft.GetRecord(index, false); + if (record == null) + { + return null; + } + + // Don't create file objects for file record segments that are part of another + // logical file. + if (record.BaseFile.Value != 0) + { + return null; + } + + File file = _fileCache[index]; + + if (file != null && file.MftReference.SequenceNumber != record.SequenceNumber) + { + file = null; + } + + if (file == null) + { + if ((record.Flags & FileRecordFlags.IsDirectory) != 0) + { + file = new Directory(_context, record); + } + else + { + file = new File(_context, record); + } + + _fileCache[index] = file; + } + + return file; + } + + internal File AllocateFile(FileRecordFlags flags) + { + File result = null; + if ((flags & FileRecordFlags.IsDirectory) != 0) + { + result = new Directory(_context, _context.Mft.AllocateRecord(flags, false)); + } + else + { + result = new File(_context, _context.Mft.AllocateRecord(flags, false)); + } + + _fileCache[result.MftReference.MftIndex] = result; + return result; + } + + internal void ForgetFile(File file) + { + _fileCache.Remove(file.IndexInMft); + } + + #endregion + + internal DirectoryEntry GetDirectoryEntry(string path) + { + return GetDirectoryEntry(GetDirectory(MasterFileTable.RootDirIndex), path); + } + + /// + /// Disposes of this instance. + /// + /// Whether called from Dispose or from a finalizer. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_context != null && _context.Mft != null) + { + _context.Mft.Dispose(); + _context.Mft = null; + } + + IDisposable disposableCompressor = _context.Options.Compressor as IDisposable; + if (disposableCompressor != null) + { + disposableCompressor.Dispose(); + _context.Options.Compressor = null; + } + } + + base.Dispose(disposing); + } + + private static bool IsValidBPB(BiosParameterBlock bpb, long volumeSize) + { + if (bpb.SignatureByte != 0x80 || bpb.TotalSectors16 != 0 || bpb.TotalSectors32 != 0 + || bpb.TotalSectors64 == 0 || bpb.MftRecordSize == 0 || bpb.MftCluster == 0 || bpb.BytesPerSector == 0) + { + return false; + } + + long mftPos = bpb.MftCluster * bpb.SectorsPerCluster * bpb.BytesPerSector; + return mftPos < bpb.TotalSectors64 * bpb.BytesPerSector && mftPos < volumeSize; + } + + private static void RemoveFileFromDirectory(Directory dir, File file, string name) + { + List aliases = new List(); + + DirectoryEntry dirEntry = dir.GetEntryByName(name); + if (dirEntry.Details.FileNameNamespace == FileNameNamespace.Dos + || dirEntry.Details.FileNameNamespace == FileNameNamespace.Win32) + { + foreach (var fnStream in file.GetStreams(AttributeType.FileName, null)) + { + var fnr = fnStream.GetContent(); + if ((fnr.FileNameNamespace == FileNameNamespace.Win32 || fnr.FileNameNamespace == FileNameNamespace.Dos) + && fnr.ParentDirectory.Value == dir.MftReference.Value) + { + aliases.Add(fnr.FileName); + } + } + } + else + { + aliases.Add(name); + } + + foreach (var alias in aliases) + { + DirectoryEntry de = dir.GetEntryByName(alias); + dir.RemoveEntry(de); + } + } + + private static void SplitPath(string path, out string plainPath, out string attributeName) + { + plainPath = path; + string fileName = Utilities.GetFileFromPath(path); + attributeName = null; + + int streamSepPos = fileName.IndexOf(':'); + if (streamSepPos >= 0) + { + attributeName = fileName.Substring(streamSepPos + 1); + plainPath = plainPath.Substring(0, path.Length - (fileName.Length - streamSepPos)); + } + } + + private static void UpdateStandardInformation(DirectoryEntry dirEntry, File file, StandardInformationModifier modifier) + { + // Update the standard information attribute - so it reflects the actual file state + NtfsStream stream = file.GetStream(AttributeType.StandardInformation, null); + StandardInformation si = stream.GetContent(); + modifier(si); + stream.SetContent(si); + + // Update the directory entry used to open the file, so it's accurate + dirEntry.UpdateFrom(file); + + // Write attribute changes back to the Master File Table + file.UpdateRecordInMft(); + } + + private DirectoryEntry CreateNewFile(string path, NewFileOptions options) + { + DirectoryEntry result; + DirectoryEntry parentDirEntry = GetDirectoryEntry(Utilities.GetDirectoryFromPath(path)); + Directory parentDir = GetDirectory(parentDirEntry.Reference); + + FileAttributeFlags newFileAttrs = parentDir.StandardInformation.FileAttributes; + if (options != null && options.Compressed.HasValue) + { + if (options.Compressed.Value) + { + newFileAttrs |= FileAttributeFlags.Compressed; + } + else + { + newFileAttrs &= ~FileAttributeFlags.Compressed; + } + } + + File file = File.CreateNew(_context, newFileAttrs); + try + { + result = AddFileToDirectory(file, parentDir, Utilities.GetFileFromPath(path), options); + + RawSecurityDescriptor parentSd = DoGetSecurity(parentDir); + RawSecurityDescriptor newSd; + if (options != null && options.SecurityDescriptor != null) + { + newSd = options.SecurityDescriptor; + } + else + { + newSd = SecurityDescriptor.CalcNewObjectDescriptor(parentSd, false); + } + + DoSetSecurity(file, newSd); + result.UpdateFrom(file); + + parentDirEntry.UpdateFrom(parentDir); + } + finally + { + if (file.HardLinkCount == 0) + { + file.Delete(); + } + } + + return result; + } + + private DirectoryEntry GetDirectoryEntry(Directory dir, string path) + { + string[] pathElements = path.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + return GetDirectoryEntry(dir, pathElements, 0); + } + + private void DoSearch(List results, string path, Regex regex, bool subFolders, bool dirs, bool files) + { + DirectoryEntry parentDirEntry = GetDirectoryEntry(path); + if (parentDirEntry == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' was not found", path)); + } + + Directory parentDir = GetDirectory(parentDirEntry.Reference); + if (parentDir == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' was not found", path)); + } + + foreach (DirectoryEntry de in parentDir.GetAllEntries(true)) + { + bool isDir = (de.Details.FileAttributes & FileAttributes.Directory) != 0; + + if ((isDir && dirs) || (!isDir && files)) + { + if (regex.IsMatch(de.SearchName)) + { + results.Add(Utilities.CombinePaths(path, de.Details.FileName)); + } + } + + if (subFolders && isDir) + { + DoSearch(results, Utilities.CombinePaths(path, de.Details.FileName), regex, subFolders, dirs, files); + } + } + } + + private DirectoryEntry GetDirectoryEntry(Directory dir, string[] pathEntries, int pathOffset) + { + DirectoryEntry entry; + + if (pathEntries.Length == 0) + { + return dir.DirectoryEntry; + } + else + { + entry = dir.GetEntryByName(pathEntries[pathOffset]); + if (entry != null) + { + if (pathOffset == pathEntries.Length - 1) + { + return entry; + } + else if ((entry.Details.FileAttributes & FileAttributes.Directory) != 0) + { + return GetDirectoryEntry(GetDirectory(entry.Reference), pathEntries, pathOffset + 1); + } + else + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, "{0} is a file, not a directory", pathEntries[pathOffset])); + } + } + else + { + return null; + } + } + } + + private DirectoryEntry AddFileToDirectory(File file, Directory dir, string name, NewFileOptions options) + { + DirectoryEntry entry; + + bool createShortNames; + if (options != null && options.CreateShortNames.HasValue) + { + createShortNames = options.CreateShortNames.Value; + } + else + { + createShortNames = CreateShortNames; + } + + if (createShortNames) + { + if (Utilities.Is8Dot3(name.ToUpperInvariant())) + { + entry = dir.AddEntry(file, name, FileNameNamespace.Win32AndDos); + } + else + { + entry = dir.AddEntry(file, name, FileNameNamespace.Win32); + dir.AddEntry(file, dir.CreateShortName(name), FileNameNamespace.Dos); + } + } + else + { + entry = dir.AddEntry(file, name, FileNameNamespace.Posix); + } + + return entry; + } + + private void RemoveReparsePoint(File file) + { + NtfsStream stream = file.GetStream(AttributeType.ReparsePoint, null); + if (stream != null) + { + ReparsePointRecord rp = new ReparsePointRecord(); + + using (Stream contentStream = stream.Open(FileAccess.Read)) + { + byte[] buffer = Utilities.ReadFully(contentStream, (int)contentStream.Length); + rp.ReadFrom(buffer, 0); + } + + file.RemoveStream(stream); + + // Update the standard information attribute - so it reflects the actual file state + NtfsStream stdInfoStream = file.GetStream(AttributeType.StandardInformation, null); + StandardInformation si = stdInfoStream.GetContent(); + si.FileAttributes = si.FileAttributes & ~FileAttributeFlags.ReparsePoint; + stdInfoStream.SetContent(si); + + // Remove the reparse point from the index + _context.ReparsePoints.Remove(rp.Tag, file.MftReference); + } + } + + private RawSecurityDescriptor DoGetSecurity(File file) + { + NtfsStream legacyStream = file.GetStream(AttributeType.SecurityDescriptor, null); + if (legacyStream != null) + { + return legacyStream.GetContent().Descriptor; + } + + StandardInformation si = file.StandardInformation; + return _context.SecurityDescriptors.GetDescriptorById(si.SecurityId); + } + + private void DoSetSecurity(File file, RawSecurityDescriptor securityDescriptor) + { + NtfsStream legacyStream = file.GetStream(AttributeType.SecurityDescriptor, null); + if (legacyStream != null) + { + SecurityDescriptor sd = new SecurityDescriptor(); + sd.Descriptor = securityDescriptor; + legacyStream.SetContent(sd); + } + else + { + uint id = _context.SecurityDescriptors.AddDescriptor(securityDescriptor); + + // Update the standard information attribute - so it reflects the actual file state + NtfsStream stream = file.GetStream(AttributeType.StandardInformation, null); + StandardInformation si = stream.GetContent(); + si.SecurityId = id; + stream.SetContent(si); + + // Write attribute changes back to the Master File Table + file.UpdateRecordInMft(); + } + } + + private void DumpDirectory(Directory dir, TextWriter writer, string indent) + { + foreach (DirectoryEntry dirEntry in dir.GetAllEntries(true)) + { + File file = GetFile(dirEntry.Reference); + Directory asDir = file as Directory; + writer.WriteLine(indent + "+-" + file.ToString() + " (" + file.IndexInMft + ")"); + + // Recurse - but avoid infinite recursion via the root dir... + if (asDir != null && file.IndexInMft != 5) + { + DumpDirectory(asDir, writer, indent + "| "); + } + } + } + + private void UpdateStandardInformation(string path, StandardInformationModifier modifier) + { + DirectoryEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + else + { + File file = GetFile(dirEntry.Reference); + + UpdateStandardInformation(dirEntry, file, modifier); + } + } + + private string ParsePath(string path, out string attributeName, out AttributeType attributeType) + { + string fileName = Utilities.GetFileFromPath(path); + attributeName = null; + attributeType = AttributeType.Data; + + string[] fileNameElements = fileName.Split(new char[] { ':' }, 3); + fileName = fileNameElements[0]; + + if (fileNameElements.Length > 1) + { + attributeName = fileNameElements[1]; + if (string.IsNullOrEmpty(attributeName)) + { + attributeName = null; + } + } + + if (fileNameElements.Length > 2) + { + string typeName = fileNameElements[2]; + AttributeDefinitionRecord typeDefn = _context.AttributeDefinitions.Lookup(typeName); + if (typeDefn == null) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "No such attribute type '{0}'", typeName), path); + } + + attributeType = typeDefn.Type; + } + + try + { + string dirName = Utilities.GetDirectoryFromPath(path); + return Utilities.CombinePaths(dirName, fileName); + } + catch (ArgumentException) + { + throw new IOException("Invalid path: " + path); + } + } + } +} diff --git a/DiscUtils/Ntfs/NtfsFileSystemChecker.cs b/DiscUtils/Ntfs/NtfsFileSystemChecker.cs new file mode 100644 index 0000000..6ba0d6a --- /dev/null +++ b/DiscUtils/Ntfs/NtfsFileSystemChecker.cs @@ -0,0 +1,618 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Runtime.Serialization; + using System.Text; + + /// + /// Class that checks NTFS file system integrity. + /// + /// Poor relation of chkdsk/fsck. + public sealed class NtfsFileSystemChecker : DiscFileSystemChecker + { + private Stream _target; + + private NtfsContext _context; + private TextWriter _report; + private ReportLevels _reportLevels; + + private ReportLevels _levelsDetected; + private ReportLevels _levelsConsideredFail = ReportLevels.Errors; + + /// + /// Initializes a new instance of the NtfsFileSystemChecker class. + /// + /// The file system to check. + public NtfsFileSystemChecker(Stream diskData) + { + SnapshotStream protectiveStream = new SnapshotStream(diskData, Ownership.None); + protectiveStream.Snapshot(); + protectiveStream.Freeze(); + _target = protectiveStream; + } + + /// + /// Checks the integrity of an NTFS file system held in a stream. + /// + /// A report on issues found. + /// The amount of detail to report. + /// true if the file system appears valid, else false. + public override bool Check(TextWriter reportOutput, ReportLevels levels) + { + _context = new NtfsContext(); + _context.RawStream = _target; + _context.Options = new NtfsOptions(); + + _report = reportOutput; + _reportLevels = levels; + _levelsDetected = ReportLevels.None; + + try + { + DoCheck(); + } + catch (AbortException ae) + { + ReportError("File system check aborted: " + ae.ToString()); + return false; + } + catch (Exception e) + { + ReportError("File system check aborted with exception: " + e.ToString()); + return false; + } + + return (_levelsDetected & _levelsConsideredFail) == 0; + } + + /// + /// Gets an object that can convert between clusters and files. + /// + /// The cluster map. + public ClusterMap BuildClusterMap() + { + _context = new NtfsContext(); + _context.RawStream = _target; + _context.Options = new NtfsOptions(); + + _context.RawStream.Position = 0; + byte[] bytes = Utilities.ReadFully(_context.RawStream, 512); + + _context.BiosParameterBlock = BiosParameterBlock.FromBytes(bytes, 0); + + _context.Mft = new MasterFileTable(_context); + File mftFile = new File(_context, _context.Mft.GetBootstrapRecord()); + _context.Mft.Initialize(mftFile); + return _context.Mft.GetClusterMap(); + } + + private static void Abort() + { + throw new AbortException(); + } + + private void DoCheck() + { + _context.RawStream.Position = 0; + byte[] bytes = Utilities.ReadFully(_context.RawStream, 512); + + _context.BiosParameterBlock = BiosParameterBlock.FromBytes(bytes, 0); + + //----------------------------------------------------------------------- + // MASTER FILE TABLE + // + + // Bootstrap the Master File Table + _context.Mft = new MasterFileTable(_context); + File mftFile = new File(_context, _context.Mft.GetBootstrapRecord()); + + // Verify basic MFT records before initializing the Master File Table + PreVerifyMft(mftFile); + _context.Mft.Initialize(mftFile); + + // Now the MFT is up and running, do more detailed analysis of it's contents - double-accounted clusters, etc + VerifyMft(); + _context.Mft.Dump(_report, "INFO: "); + + //----------------------------------------------------------------------- + // INDEXES + // + + // Need UpperCase in order to verify some indexes (i.e. directories). + File ucFile = new File(_context, _context.Mft.GetRecord(MasterFileTable.UpCaseIndex, false)); + _context.UpperCase = new UpperCase(ucFile); + + SelfCheckIndexes(); + + //----------------------------------------------------------------------- + // DIRECTORIES + // + VerifyDirectories(); + + //----------------------------------------------------------------------- + // WELL KNOWN FILES + // + VerifyWellKnownFilesExist(); + + //----------------------------------------------------------------------- + // OBJECT IDS + // + VerifyObjectIds(); + + //----------------------------------------------------------------------- + // FINISHED + // + + // Temporary... + using (NtfsFileSystem fs = new NtfsFileSystem(_context.RawStream)) + { + if ((_reportLevels & ReportLevels.Information) != 0) + { + ReportDump(fs); + } + } + } + + private void VerifyWellKnownFilesExist() + { + Directory rootDir = new Directory(_context, _context.Mft.GetRecord(MasterFileTable.RootDirIndex, false)); + + DirectoryEntry extendDirEntry = rootDir.GetEntryByName("$Extend"); + if (extendDirEntry == null) + { + ReportError("$Extend does not exist in root directory"); + Abort(); + } + + Directory extendDir = new Directory(_context, _context.Mft.GetRecord(extendDirEntry.Reference)); + + DirectoryEntry objIdDirEntry = extendDir.GetEntryByName("$ObjId"); + if (objIdDirEntry == null) + { + ReportError("$ObjId does not exist in $Extend directory"); + Abort(); + } + + // Stash ObjectIds + _context.ObjectIds = new ObjectIds(new File(_context, _context.Mft.GetRecord(objIdDirEntry.Reference))); + + DirectoryEntry sysVolInfDirEntry = rootDir.GetEntryByName("System Volume Information"); + if (sysVolInfDirEntry == null) + { + ReportError("'System Volume Information' does not exist in root directory"); + Abort(); + } + ////Directory sysVolInfDir = new Directory(_context, _context.Mft.GetRecord(sysVolInfDirEntry.Reference)); + } + + private void VerifyObjectIds() + { + foreach (FileRecord fr in _context.Mft.Records) + { + if (fr.BaseFile.Value != 0) + { + File f = new File(_context, fr); + foreach (var stream in f.AllStreams) + { + if (stream.AttributeType == AttributeType.ObjectId) + { + ObjectId objId = stream.GetContent(); + ObjectIdRecord objIdRec; + if (!_context.ObjectIds.TryGetValue(objId.Id, out objIdRec)) + { + ReportError("ObjectId {0} for file {1} is not indexed", objId.Id, f.BestName); + } + else if (objIdRec.MftReference != f.MftReference) + { + ReportError("ObjectId {0} for file {1} points to {2}", objId.Id, f.BestName, objIdRec.MftReference); + } + } + } + } + } + + foreach (var objIdRec in _context.ObjectIds.All) + { + if (_context.Mft.GetRecord(objIdRec.Value.MftReference) == null) + { + ReportError("ObjectId {0} refers to non-existant file {1}", objIdRec.Key, objIdRec.Value.MftReference); + } + } + } + + private void VerifyDirectories() + { + foreach (FileRecord fr in _context.Mft.Records) + { + if (fr.BaseFile.Value != 0) + { + continue; + } + + File f = new File(_context, fr); + foreach (var stream in f.AllStreams) + { + if (stream.AttributeType == AttributeType.IndexRoot && stream.Name == "$I30") + { + IndexView dir = new IndexView(f.GetIndex("$I30")); + foreach (var entry in dir.Entries) + { + FileRecord refFile = _context.Mft.GetRecord(entry.Value); + + // Make sure each referenced file actually exists... + if (refFile == null) + { + ReportError("Directory {0} references non-existent file {1}", f, entry.Key); + } + + File referencedFile = new File(_context, refFile); + StandardInformation si = referencedFile.StandardInformation; + if (si.CreationTime != entry.Key.CreationTime || si.MftChangedTime != entry.Key.MftChangedTime + || si.ModificationTime != entry.Key.ModificationTime) + { + ReportInfo("Directory entry {0} in {1} is out of date", entry.Key, f); + } + } + } + } + } + } + + private void SelfCheckIndexes() + { + foreach (FileRecord fr in _context.Mft.Records) + { + File f = new File(_context, fr); + foreach (var stream in f.AllStreams) + { + if (stream.AttributeType == AttributeType.IndexRoot) + { + SelfCheckIndex(f, stream.Name); + } + } + } + } + + private void SelfCheckIndex(File file, string name) + { + ReportInfo("About to self-check index {0} in file {1} (MFT:{2})", name, file.BestName, file.IndexInMft); + + IndexRoot root = file.GetStream(AttributeType.IndexRoot, name).GetContent(); + + byte[] rootBuffer; + using (Stream s = file.OpenStream(AttributeType.IndexRoot, name, FileAccess.Read)) + { + rootBuffer = Utilities.ReadFully(s, (int)s.Length); + } + + Bitmap indexBitmap = null; + if (file.GetStream(AttributeType.Bitmap, name) != null) + { + indexBitmap = new Bitmap(file.OpenStream(AttributeType.Bitmap, name, FileAccess.Read), long.MaxValue); + } + + if (!SelfCheckIndexNode(rootBuffer, IndexRoot.HeaderOffset, indexBitmap, root, file.BestName, name)) + { + ReportError("Index {0} in file {1} (MFT:{2}) has corrupt IndexRoot attribute", name, file.BestName, file.IndexInMft); + } + else + { + ReportInfo("Self-check of index {0} in file {1} (MFT:{2}) complete", name, file.BestName, file.IndexInMft); + } + } + + private bool SelfCheckIndexNode(byte[] buffer, int offset, Bitmap bitmap, IndexRoot root, string fileName, string indexName) + { + bool ok = true; + + IndexHeader header = new IndexHeader(buffer, offset); + + IndexEntry lastEntry = null; + + IComparer collator = root.GetCollator(_context.UpperCase); + + int pos = (int)header.OffsetToFirstEntry; + while (pos < header.TotalSizeOfEntries) + { + IndexEntry entry = new IndexEntry(indexName == "$I30"); + entry.Read(buffer, offset + pos); + pos += entry.Size; + + if ((entry.Flags & IndexEntryFlags.Node) != 0) + { + long bitmapIdx = entry.ChildrenVirtualCluster / Utilities.Ceil(root.IndexAllocationSize, _context.BiosParameterBlock.SectorsPerCluster * _context.BiosParameterBlock.BytesPerSector); + if (!bitmap.IsPresent(bitmapIdx)) + { + ReportError("Index entry {0} is non-leaf, but child vcn {1} is not in bitmap at index {2}", Index.EntryAsString(entry, fileName, indexName), entry.ChildrenVirtualCluster, bitmapIdx); + } + } + + if ((entry.Flags & IndexEntryFlags.End) != 0) + { + if (pos != header.TotalSizeOfEntries) + { + ReportError("Found END index entry {0}, but not at end of node", Index.EntryAsString(entry, fileName, indexName)); + ok = false; + } + } + + if (lastEntry != null && collator.Compare(lastEntry.KeyBuffer, entry.KeyBuffer) >= 0) + { + ReportError("Found entries out of order {0} was before {1}", Index.EntryAsString(lastEntry, fileName, indexName), Index.EntryAsString(entry, fileName, indexName)); + ok = false; + } + + lastEntry = entry; + } + + return ok; + } + + private void PreVerifyMft(File file) + { + int recordLength = _context.BiosParameterBlock.MftRecordSize; + int bytesPerSector = _context.BiosParameterBlock.BytesPerSector; + + // Check out the MFT's clusters + foreach (var range in file.GetAttribute(AttributeType.Data, null).GetClusters()) + { + if (!VerifyClusterRange(range)) + { + ReportError("Corrupt cluster range in MFT data attribute {0}", range.ToString()); + Abort(); + } + } + + foreach (var range in file.GetAttribute(AttributeType.Bitmap, null).GetClusters()) + { + if (!VerifyClusterRange(range)) + { + ReportError("Corrupt cluster range in MFT bitmap attribute {0}", range.ToString()); + Abort(); + } + } + + using (Stream mftStream = file.OpenStream(AttributeType.Data, null, FileAccess.Read)) + using (Stream bitmapStream = file.OpenStream(AttributeType.Bitmap, null, FileAccess.Read)) + { + Bitmap bitmap = new Bitmap(bitmapStream, long.MaxValue); + + long index = 0; + while (mftStream.Position < mftStream.Length) + { + byte[] recordData = Utilities.ReadFully(mftStream, recordLength); + + string magic = Utilities.BytesToString(recordData, 0, 4); + if (magic != "FILE") + { + if (bitmap.IsPresent(index)) + { + ReportError("Invalid MFT record magic at index {0} - was ({2},{3},{4},{5}) \"{1}\"", index, magic.Trim('\0'), (int)magic[0], (int)magic[1], (int)magic[2], (int)magic[3]); + } + } + else + { + if (!VerifyMftRecord(recordData, bitmap.IsPresent(index), bytesPerSector)) + { + ReportError("Invalid MFT record at index {0}", index); + StringBuilder bldr = new StringBuilder(); + for (int i = 0; i < recordData.Length; ++i) + { + bldr.Append(string.Format(CultureInfo.InvariantCulture, " {0:X2}", recordData[i])); + } + + ReportInfo("MFT record binary data for index {0}:{1}", index, bldr.ToString()); + } + } + + index++; + } + } + } + + private void VerifyMft() + { + // Cluster allocation check - check for double allocations + Dictionary clusterMap = new Dictionary(); + foreach (FileRecord fr in _context.Mft.Records) + { + if ((fr.Flags & FileRecordFlags.InUse) != 0) + { + File f = new File(_context, fr); + foreach (NtfsAttribute attr in f.AllAttributes) + { + string attrKey = fr.MasterFileTableIndex + ":" + attr.Id; + + foreach (var range in attr.GetClusters()) + { + if (!VerifyClusterRange(range)) + { + ReportError("Attribute {0} contains bad cluster range {1}", attrKey, range); + } + + for (long cluster = range.Offset; cluster < range.Offset + range.Count; ++cluster) + { + string existingKey; + if (clusterMap.TryGetValue(cluster, out existingKey)) + { + ReportError("Two attributes referencing cluster {0} (0x{0:X16}) - {1} and {2} (as MftIndex:AttrId)", cluster, existingKey, attrKey); + } + } + } + } + } + } + } + + private bool VerifyMftRecord(byte[] recordData, bool presentInBitmap, int bytesPerSector) + { + bool ok = true; + + // + // Verify the attributes seem OK... + // + byte[] tempBuffer = new byte[recordData.Length]; + Array.Copy(recordData, tempBuffer, tempBuffer.Length); + GenericFixupRecord genericRecord = new GenericFixupRecord(bytesPerSector); + genericRecord.FromBytes(tempBuffer, 0); + + int pos = Utilities.ToUInt16LittleEndian(genericRecord.Content, 0x14); + while (Utilities.ToUInt32LittleEndian(genericRecord.Content, pos) != 0xFFFFFFFF) + { + int attrLen; + try + { + AttributeRecord ar = AttributeRecord.FromBytes(genericRecord.Content, pos, out attrLen); + if (attrLen != ar.Size) + { + ReportError("Attribute size is different to calculated size. AttrId={0}", ar.AttributeId); + ok = false; + } + + if (ar.IsNonResident) + { + NonResidentAttributeRecord nrr = (NonResidentAttributeRecord)ar; + if (nrr.DataRuns.Count > 0) + { + long totalVcn = 0; + foreach (var run in nrr.DataRuns) + { + totalVcn += run.RunLength; + } + + if (totalVcn != nrr.LastVcn - nrr.StartVcn + 1) + { + ReportError("Declared VCNs doesn't match data runs. AttrId={0}", ar.AttributeId); + ok = false; + } + } + } + } + catch + { + ReportError("Failure parsing attribute at pos={0}", pos); + return false; + } + + pos += attrLen; + } + + // + // Now consider record as a whole + // + FileRecord record = new FileRecord(bytesPerSector); + record.FromBytes(recordData, 0); + + bool inUse = (record.Flags & FileRecordFlags.InUse) != 0; + if (inUse != presentInBitmap) + { + ReportError("MFT bitmap and record in-use flag don't agree. Mft={0}, Record={1}", presentInBitmap ? "InUse" : "Free", inUse ? "InUse" : "Free"); + ok = false; + } + + if (record.Size != record.RealSize) + { + ReportError("MFT record real size is different to calculated size. Stored in MFT={0}, Calculated={1}", record.RealSize, record.Size); + ok = false; + } + + if (Utilities.ToUInt32LittleEndian(recordData, (int)record.RealSize - 8) != uint.MaxValue) + { + ReportError("MFT record is not correctly terminated with 0xFFFFFFFF"); + ok = false; + } + + return ok; + } + + private bool VerifyClusterRange(Range range) + { + bool ok = true; + if (range.Offset < 0) + { + ReportError("Invalid cluster range {0} - negative start", range); + ok = false; + } + + if (range.Count <= 0) + { + ReportError("Invalid cluster range {0} - negative/zero count", range); + ok = false; + } + + if ((range.Offset + range.Count) * _context.BiosParameterBlock.BytesPerCluster > _context.RawStream.Length) + { + ReportError("Invalid cluster range {0} - beyond end of disk", range); + ok = false; + } + + return ok; + } + + private void ReportDump(IDiagnosticTraceable toDump) + { + _levelsDetected |= ReportLevels.Information; + if ((_reportLevels & ReportLevels.Information) != 0) + { + toDump.Dump(_report, "INFO: "); + } + } + + private void ReportInfo(string str, params object[] args) + { + _levelsDetected |= ReportLevels.Information; + if ((_reportLevels & ReportLevels.Information) != 0) + { + _report.WriteLine("INFO: " + str, args); + } + } + + private void ReportError(string str, params object[] args) + { + _levelsDetected |= ReportLevels.Errors; + if ((_reportLevels & ReportLevels.Errors) != 0) + { + _report.WriteLine("ERROR: " + str, args); + } + } + + [Serializable] + private sealed class AbortException : InvalidFileSystemException + { + public AbortException() + : base() + { + } + + private AbortException(SerializationInfo info, StreamingContext ctxt) + : base(info, ctxt) + { + } + } + } +} diff --git a/DiscUtils/Ntfs/NtfsFormatOptions.cs b/DiscUtils/Ntfs/NtfsFormatOptions.cs new file mode 100644 index 0000000..af4f02e --- /dev/null +++ b/DiscUtils/Ntfs/NtfsFormatOptions.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.Security.Principal; + + /// + /// Class representing NTFS formatting options. + /// + public sealed class NtfsFormatOptions + { + /// + /// Gets or sets the NTFS bootloader code to put in the formatted file system. + /// + public byte[] BootCode { get; set; } + + /// + /// Gets or sets the SID of the computer account that notionally formatted the file system. + /// + /// + /// Certain ACLs in the file system will refer to the 'local' administrator of the indicated + /// computer account. + /// + public SecurityIdentifier ComputerAccount { get; set; } + } +} diff --git a/DiscUtils/Ntfs/NtfsFormatter.cs b/DiscUtils/Ntfs/NtfsFormatter.cs new file mode 100644 index 0000000..b4ecfae --- /dev/null +++ b/DiscUtils/Ntfs/NtfsFormatter.cs @@ -0,0 +1,341 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + using System.Security.AccessControl; + using System.Security.Principal; + + internal class NtfsFormatter + { + private int _clusterSize; + private int _mftRecordSize; + private int _indexBufferSize; + private long _bitmapCluster; + private long _mftMirrorCluster; + private long _mftCluster; + + private NtfsContext _context; + + public string Label { get; set; } + + public Geometry DiskGeometry { get; set; } + + public long FirstSector { get; set; } + + public long SectorCount { get; set; } + + public byte[] BootCode { get; set; } + + public SecurityIdentifier ComputerAccount { get; set; } + + public NtfsFileSystem Format(Stream stream) + { + _context = new NtfsContext(); + _context.Options = new NtfsOptions(); + _context.RawStream = stream; + _context.AttributeDefinitions = new AttributeDefinitions(); + + string localAdminString = (ComputerAccount == null) + ? "LA" + : new SecurityIdentifier(WellKnownSidType.AccountAdministratorSid, ComputerAccount).ToString(); + + using (new NtfsTransaction()) + { + _clusterSize = 4096; + _mftRecordSize = 1024; + _indexBufferSize = 4096; + + long totalClusters = ((SectorCount - 1) * Sizes.Sector) / _clusterSize; + + // Allocate a minimum of 8KB for the boot loader, but allow for more + int numBootClusters = Utilities.Ceil(Math.Max((int)(8 * Sizes.OneKiB), BootCode == null ? 0 : BootCode.Length), _clusterSize); + + // Place MFT mirror in the middle of the volume + _mftMirrorCluster = totalClusters / 2; + uint numMftMirrorClusters = 1; + + // The bitmap is also near the middle + _bitmapCluster = _mftMirrorCluster + 13; + int numBitmapClusters = (int)Utilities.Ceil(totalClusters / 8, _clusterSize); + + // The MFT bitmap goes 'near' the start - approx 10% in - but ensure we avoid the bootloader + long mftBitmapCluster = Math.Max(3 + (totalClusters / 10), numBootClusters); + int numMftBitmapClusters = 1; + + // The MFT follows it's bitmap + _mftCluster = mftBitmapCluster + numMftBitmapClusters; + int numMftClusters = 8; + + if (_mftCluster + numMftClusters > _mftMirrorCluster + || _bitmapCluster + numBitmapClusters >= totalClusters) + { + throw new IOException("Unable to determine initial layout of NTFS metadata - disk may be too small"); + } + + CreateBiosParameterBlock(stream, numBootClusters * _clusterSize); + + _context.Mft = new MasterFileTable(_context); + File mftFile = _context.Mft.InitializeNew(_context, mftBitmapCluster, (ulong)numMftBitmapClusters, (long)_mftCluster, (ulong)numMftClusters); + + File bitmapFile = CreateFixedSystemFile(MasterFileTable.BitmapIndex, _bitmapCluster, (ulong)numBitmapClusters, true); + _context.ClusterBitmap = new ClusterBitmap(bitmapFile); + _context.ClusterBitmap.MarkAllocated(0, numBootClusters); + _context.ClusterBitmap.MarkAllocated(_bitmapCluster, numBitmapClusters); + _context.ClusterBitmap.MarkAllocated(mftBitmapCluster, numMftBitmapClusters); + _context.ClusterBitmap.MarkAllocated(_mftCluster, numMftClusters); + _context.ClusterBitmap.SetTotalClusters(totalClusters); + bitmapFile.UpdateRecordInMft(); + + File mftMirrorFile = CreateFixedSystemFile(MasterFileTable.MftMirrorIndex, _mftMirrorCluster, numMftMirrorClusters, true); + + File logFile = CreateSystemFile(MasterFileTable.LogFileIndex); + using (Stream s = logFile.OpenStream(AttributeType.Data, null, FileAccess.ReadWrite)) + { + s.SetLength(Math.Min(Math.Max(2 * Sizes.OneMiB, (totalClusters / 500) * (long)_clusterSize), 64 * Sizes.OneMiB)); + byte[] buffer = new byte[1024 * 1024]; + for (int i = 0; i < buffer.Length; ++i) + { + buffer[i] = 0xFF; + } + + long totalWritten = 0; + while (totalWritten < s.Length) + { + int toWrite = (int)Math.Min(s.Length - totalWritten, buffer.Length); + s.Write(buffer, 0, toWrite); + totalWritten += toWrite; + } + } + + File volumeFile = CreateSystemFile(MasterFileTable.VolumeIndex); + NtfsStream volNameStream = volumeFile.CreateStream(AttributeType.VolumeName, null); + volNameStream.SetContent(new VolumeName(Label ?? "New Volume")); + NtfsStream volInfoStream = volumeFile.CreateStream(AttributeType.VolumeInformation, null); + volInfoStream.SetContent(new VolumeInformation(3, 1, VolumeInformationFlags.None)); + SetSecurityAttribute(volumeFile, "O:" + localAdminString + "G:BAD:(A;;0x12019f;;;SY)(A;;0x12019f;;;BA)"); + volumeFile.UpdateRecordInMft(); + + _context.GetFileByIndex = delegate(long index) { return new File(_context, _context.Mft.GetRecord(index, false)); }; + _context.AllocateFile = delegate(FileRecordFlags frf) { return new File(_context, _context.Mft.AllocateRecord(frf, false)); }; + + File attrDefFile = CreateSystemFile(MasterFileTable.AttrDefIndex); + _context.AttributeDefinitions.WriteTo(attrDefFile); + SetSecurityAttribute(attrDefFile, "O:" + localAdminString + "G:BAD:(A;;FR;;;SY)(A;;FR;;;BA)"); + attrDefFile.UpdateRecordInMft(); + + File bootFile = CreateFixedSystemFile(MasterFileTable.BootIndex, 0, (uint)numBootClusters, false); + SetSecurityAttribute(bootFile, "O:" + localAdminString + "G:BAD:(A;;FR;;;SY)(A;;FR;;;BA)"); + bootFile.UpdateRecordInMft(); + + File badClusFile = CreateSystemFile(MasterFileTable.BadClusIndex); + badClusFile.CreateStream(AttributeType.Data, "$Bad"); + badClusFile.UpdateRecordInMft(); + + File secureFile = CreateSystemFile(MasterFileTable.SecureIndex, FileRecordFlags.HasViewIndex); + secureFile.RemoveStream(secureFile.GetStream(AttributeType.Data, null)); + _context.SecurityDescriptors = SecurityDescriptors.Initialize(secureFile); + secureFile.UpdateRecordInMft(); + + File upcaseFile = CreateSystemFile(MasterFileTable.UpCaseIndex); + _context.UpperCase = UpperCase.Initialize(upcaseFile); + upcaseFile.UpdateRecordInMft(); + + File objIdFile = File.CreateNew(_context, FileRecordFlags.IsMetaFile | FileRecordFlags.HasViewIndex, FileAttributeFlags.None); + objIdFile.RemoveStream(objIdFile.GetStream(AttributeType.Data, null)); + objIdFile.CreateIndex("$O", (AttributeType)0, AttributeCollationRule.MultipleUnsignedLongs); + objIdFile.UpdateRecordInMft(); + + File reparseFile = File.CreateNew(_context, FileRecordFlags.IsMetaFile | FileRecordFlags.HasViewIndex, FileAttributeFlags.None); + reparseFile.CreateIndex("$R", (AttributeType)0, AttributeCollationRule.MultipleUnsignedLongs); + reparseFile.UpdateRecordInMft(); + + File quotaFile = File.CreateNew(_context, FileRecordFlags.IsMetaFile | FileRecordFlags.HasViewIndex, FileAttributeFlags.None); + Quotas.Initialize(quotaFile); + + Directory extendDir = CreateSystemDirectory(MasterFileTable.ExtendIndex); + extendDir.AddEntry(objIdFile, "$ObjId", FileNameNamespace.Win32AndDos); + extendDir.AddEntry(reparseFile, "$Reparse", FileNameNamespace.Win32AndDos); + extendDir.AddEntry(quotaFile, "$Quota", FileNameNamespace.Win32AndDos); + extendDir.UpdateRecordInMft(); + + Directory rootDir = CreateSystemDirectory(MasterFileTable.RootDirIndex); + rootDir.AddEntry(mftFile, "$MFT", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(mftMirrorFile, "$MFTMirr", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(logFile, "$LogFile", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(volumeFile, "$Volume", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(attrDefFile, "$AttrDef", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(rootDir, ".", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(bitmapFile, "$Bitmap", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(bootFile, "$Boot", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(badClusFile, "$BadClus", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(secureFile, "$Secure", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(upcaseFile, "$UpCase", FileNameNamespace.Win32AndDos); + rootDir.AddEntry(extendDir, "$Extend", FileNameNamespace.Win32AndDos); + SetSecurityAttribute(rootDir, "O:" + localAdminString + "G:BUD:(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;OICIIO;GA;;;CO)(A;OICI;0x1200a9;;;BU)(A;CI;LC;;;BU)(A;CIIO;DC;;;BU)(A;;0x1200a9;;;WD)"); + rootDir.UpdateRecordInMft(); + + // A number of records are effectively 'reserved' + for (long i = MasterFileTable.ExtendIndex + 1; i <= 15; i++) + { + File f = CreateSystemFile(i); + SetSecurityAttribute(f, "O:S-1-5-21-1708537768-746137067-1060284298-1003G:BAD:(A;;0x12019f;;;SY)(A;;0x12019f;;;BA)"); + f.UpdateRecordInMft(); + } + } + + // XP-style security permissions setup + NtfsFileSystem ntfs = new NtfsFileSystem(stream); + + ntfs.SetSecurity(@"$MFT", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;FR;;;SY)(A;;FR;;;BA)")); + ntfs.SetSecurity(@"$MFTMirr", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;FR;;;SY)(A;;FR;;;BA)")); + ntfs.SetSecurity(@"$LogFile", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;FR;;;SY)(A;;FR;;;BA)")); + ntfs.SetSecurity(@"$Bitmap", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;FR;;;SY)(A;;FR;;;BA)")); + ntfs.SetSecurity(@"$BadClus", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;FR;;;SY)(A;;FR;;;BA)")); + ntfs.SetSecurity(@"$UpCase", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;FR;;;SY)(A;;FR;;;BA)")); + ntfs.SetSecurity(@"$Secure", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;0x12019f;;;SY)(A;;0x12019f;;;BA)")); + ntfs.SetSecurity(@"$Extend", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;0x12019f;;;SY)(A;;0x12019f;;;BA)")); + ntfs.SetSecurity(@"$Extend\$Quota", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;0x12019f;;;SY)(A;;0x12019f;;;BA)")); + ntfs.SetSecurity(@"$Extend\$ObjId", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;0x12019f;;;SY)(A;;0x12019f;;;BA)")); + ntfs.SetSecurity(@"$Extend\$Reparse", new RawSecurityDescriptor("O:" + localAdminString + "G:BAD:(A;;0x12019f;;;SY)(A;;0x12019f;;;BA)")); + + ntfs.CreateDirectory("System Volume Information"); + ntfs.SetAttributes("System Volume Information", FileAttributes.Hidden | FileAttributes.System | FileAttributes.Directory); + ntfs.SetSecurity("System Volume Information", new RawSecurityDescriptor("O:BAG:SYD:(A;OICI;FA;;;SY)")); + + using (Stream s = ntfs.OpenFile(@"System Volume Information\MountPointManagerRemoteDatabase", FileMode.Create)) + { + } + + ntfs.SetAttributes(@"System Volume Information\MountPointManagerRemoteDatabase", FileAttributes.Hidden | FileAttributes.System | FileAttributes.Archive); + ntfs.SetSecurity(@"System Volume Information\MountPointManagerRemoteDatabase", new RawSecurityDescriptor("O:BAG:SYD:(A;;FA;;;SY)")); + return ntfs; + } + + private static void SetSecurityAttribute(File file, string secDesc) + { + NtfsStream rootSecurityStream = file.CreateStream(AttributeType.SecurityDescriptor, null); + SecurityDescriptor sd = new SecurityDescriptor(); + sd.Descriptor = new RawSecurityDescriptor(secDesc); + rootSecurityStream.SetContent(sd); + } + + private File CreateFixedSystemFile(long mftIndex, long firstCluster, ulong numClusters, bool wipe) + { + BiosParameterBlock bpb = _context.BiosParameterBlock; + + if (wipe) + { + byte[] wipeBuffer = new byte[bpb.BytesPerCluster]; + _context.RawStream.Position = firstCluster * bpb.BytesPerCluster; + for (ulong i = 0; i < numClusters; ++i) + { + _context.RawStream.Write(wipeBuffer, 0, wipeBuffer.Length); + } + } + + FileRecord fileRec = _context.Mft.AllocateRecord((uint)mftIndex, FileRecordFlags.None); + fileRec.Flags = FileRecordFlags.InUse; + fileRec.SequenceNumber = (ushort)mftIndex; + + File file = new File(_context, fileRec); + + StandardInformation.InitializeNewFile(file, FileAttributeFlags.Hidden | FileAttributeFlags.System); + + file.CreateStream(AttributeType.Data, null, firstCluster, numClusters, (uint)bpb.BytesPerCluster); + + file.UpdateRecordInMft(); + + if (_context.ClusterBitmap != null) + { + _context.ClusterBitmap.MarkAllocated(firstCluster, (long)numClusters); + } + + return file; + } + + private File CreateSystemFile(long mftIndex) + { + return CreateSystemFile(mftIndex, FileRecordFlags.None); + } + + private File CreateSystemFile(long mftIndex, FileRecordFlags flags) + { + FileRecord fileRec = _context.Mft.AllocateRecord((uint)mftIndex, flags); + fileRec.SequenceNumber = (ushort)mftIndex; + + File file = new File(_context, fileRec); + + StandardInformation.InitializeNewFile(file, FileAttributeFlags.Hidden | FileAttributeFlags.System | FileRecord.ConvertFlags(flags)); + + file.CreateStream(AttributeType.Data, null); + + file.UpdateRecordInMft(); + + return file; + } + + private Directory CreateSystemDirectory(long mftIndex) + { + FileRecord fileRec = _context.Mft.AllocateRecord((uint)mftIndex, FileRecordFlags.None); + fileRec.Flags = FileRecordFlags.InUse | FileRecordFlags.IsDirectory; + fileRec.SequenceNumber = (ushort)mftIndex; + + Directory dir = new Directory(_context, fileRec); + + StandardInformation.InitializeNewFile(dir, FileAttributeFlags.Hidden | FileAttributeFlags.System); + + dir.CreateIndex("$I30", AttributeType.FileName, AttributeCollationRule.Filename); + + dir.UpdateRecordInMft(); + + return dir; + } + + private void CreateBiosParameterBlock(Stream stream, int bootFileSize) + { + byte[] bootSectors = new byte[bootFileSize]; + + if (BootCode != null) + { + Array.Copy(BootCode, 0, bootSectors, 0, BootCode.Length); + } + + BiosParameterBlock bpb = BiosParameterBlock.Initialized(DiskGeometry, _clusterSize, (uint)FirstSector, SectorCount, _mftRecordSize, _indexBufferSize); + bpb.MftCluster = _mftCluster; + bpb.MftMirrorCluster = _mftMirrorCluster; + bpb.ToBytes(bootSectors, 0); + + // Primary goes at the start of the partition + stream.Position = 0; + stream.Write(bootSectors, 0, bootSectors.Length); + + // Backup goes at the end of the data in the partition + stream.Position = (SectorCount - 1) * Sizes.Sector; + stream.Write(bootSectors, 0, Sizes.Sector); + + _context.BiosParameterBlock = bpb; + } + } +} diff --git a/DiscUtils/Ntfs/NtfsOptions.cs b/DiscUtils/Ntfs/NtfsOptions.cs new file mode 100644 index 0000000..7ac8989 --- /dev/null +++ b/DiscUtils/Ntfs/NtfsOptions.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using DiscUtils.Compression; + + /// + /// Class whose instances hold options controlling how works. + /// + public sealed class NtfsOptions : DiscFileSystemOptions + { + private bool _hideMetaFiles; + private bool _hideHiddenFiles; + private bool _hideSystemFiles; + private bool _hideDosFileNames; + private ShortFileNameOption _shortNameCreation; + private BlockCompressor _compressor; + private bool _readCache; + private bool _fileLengthFromDirectoryEntries; + + internal NtfsOptions() + { + _hideMetaFiles = true; + _hideHiddenFiles = true; + _hideSystemFiles = true; + _hideDosFileNames = true; + _compressor = new LZNT1(); + _readCache = true; + _fileLengthFromDirectoryEntries = true; + } + + /// + /// Gets or sets a value indicating whether to include file system meta-files when enumerating directories. + /// + /// Meta-files are those with an MFT (Master File Table) index less than 24. + public bool HideMetafiles + { + get { return _hideMetaFiles; } + set { _hideMetaFiles = value; } + } + + /// + /// Gets or sets a value indicating whether to include hidden files when enumerating directories. + /// + public bool HideHiddenFiles + { + get { return _hideHiddenFiles; } + set { _hideHiddenFiles = value; } + } + + /// + /// Gets or sets a value indicating whether to include system files when enumerating directories. + /// + public bool HideSystemFiles + { + get { return _hideSystemFiles; } + set { _hideSystemFiles = value; } + } + + /// + /// Gets or sets a value indicating whether to hide DOS (8.3-style) file names when enumerating directories. + /// + public bool HideDosFileNames + { + get { return _hideDosFileNames; } + set { _hideDosFileNames = value; } + } + + /// + /// Gets or sets a value indicating whether short (8.3) file names are created automatically. + /// + public ShortFileNameOption ShortNameCreation + { + get { return _shortNameCreation; } + set { _shortNameCreation = value; } + } + + /// + /// Gets or sets the compression algorithm used for compressing files. + /// + public BlockCompressor Compressor + { + get { return _compressor; } + set { _compressor = value; } + } + + /// + /// Gets or sets a value indicating whether NTFS-level read caching is used. + /// + public bool ReadCacheEnabled + { + get { return _readCache; } + set { _readCache = value; } + } + + /// + /// Gets or sets a value indicating whether file length information comes from directory entries or file data. + /// + /// + /// The default (true) is that file length information is supplied by the directory entry + /// for a file. In some circumstances that information may be inaccurate - specifically for files with multiple + /// hard links, the directory entries are only updated for the hard link used to open the file. + /// Setting this value to false, will always retrieve the latest information from the underlying + /// NTFS attribute information, which reflects the true size of the file. + /// + public bool FileLengthFromDirectoryEntries + { + get { return _fileLengthFromDirectoryEntries; } + set { _fileLengthFromDirectoryEntries = value; } + } + + /// + /// Returns a string representation of the file system options. + /// + /// A string of the form Show: XX XX XX. + public override string ToString() + { + return "Show: Normal " + (HideMetafiles ? string.Empty : "Meta ") + (HideHiddenFiles ? string.Empty : "Hidden ") + (HideSystemFiles ? string.Empty : "System ") + (HideDosFileNames ? string.Empty : "ShortNames "); + } + } +} diff --git a/DiscUtils/Ntfs/NtfsStream.cs b/DiscUtils/Ntfs/NtfsStream.cs new file mode 100644 index 0000000..fff00c9 --- /dev/null +++ b/DiscUtils/Ntfs/NtfsStream.cs @@ -0,0 +1,122 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class NtfsStream + { + private File _file; + private NtfsAttribute _attr; + + public NtfsStream(File file, NtfsAttribute attr) + { + _file = file; + _attr = attr; + } + + public NtfsAttribute Attribute + { + get { return _attr; } + } + + public AttributeType AttributeType + { + get { return _attr.Type; } + } + + public string Name + { + get { return _attr.Name; } + } + + /// + /// Gets the content of a stream. + /// + /// The stream's content structure. + /// The content. + public T GetContent() + where T : IByteArraySerializable, IDiagnosticTraceable, new() + { + byte[] buffer; + using (Stream s = Open(FileAccess.Read)) + { + buffer = Utilities.ReadFully(s, (int)s.Length); + } + + T value = new T(); + value.ReadFrom(buffer, 0); + return value; + } + + /// + /// Sets the content of a stream. + /// + /// The stream's content structure. + /// The new value for the stream. + public void SetContent(T value) + where T : IByteArraySerializable, IDiagnosticTraceable, new() + { + byte[] buffer = new byte[value.Size]; + value.WriteTo(buffer, 0); + using (Stream s = Open(FileAccess.Write)) + { + s.Write(buffer, 0, buffer.Length); + s.SetLength(buffer.Length); + } + } + + public SparseStream Open(FileAccess access) + { + return _attr.Open(access); + } + + internal Range[] GetClusters() + { + return _attr.GetClusters(); + } + + internal StreamExtent[] GetAbsoluteExtents() + { + List result = new List(); + + long clusterSize = _file.Context.BiosParameterBlock.BytesPerCluster; + if (_attr.IsNonResident) + { + Range[] clusters = _attr.GetClusters(); + foreach (var clusterRange in clusters) + { + result.Add(new StreamExtent(clusterRange.Offset * clusterSize, clusterRange.Count * clusterSize)); + } + } + else + { + result.Add(new StreamExtent(_attr.OffsetToAbsolutePos(0), _attr.Length)); + } + + return result.ToArray(); + } + } +} diff --git a/DiscUtils/Ntfs/NtfsTransaction.cs b/DiscUtils/Ntfs/NtfsTransaction.cs new file mode 100644 index 0000000..7d268d4 --- /dev/null +++ b/DiscUtils/Ntfs/NtfsTransaction.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Text; + + internal sealed class NtfsTransaction : IDisposable + { + [ThreadStatic] + private static NtfsTransaction s_instance; + + private bool _ownRecord; + + private DateTime _timestamp; + + public NtfsTransaction() + { + if (s_instance == null) + { + s_instance = this; + _timestamp = DateTime.UtcNow; + _ownRecord = true; + } + } + + public static NtfsTransaction Current + { + get { return s_instance; } + } + + public DateTime Timestamp + { + get { return _timestamp; } + } + + public void Dispose() + { + if (_ownRecord) + { + s_instance = null; + } + } + } +} diff --git a/DiscUtils/Ntfs/ObjectId.cs b/DiscUtils/Ntfs/ObjectId.cs new file mode 100644 index 0000000..df484b0 --- /dev/null +++ b/DiscUtils/Ntfs/ObjectId.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + + internal sealed class ObjectId : IByteArraySerializable, IDiagnosticTraceable + { + public Guid Id; + + public int Size + { + get { return 16; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Id = Utilities.ToGuidLittleEndian(buffer, offset); + return 16; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Id, buffer, offset); + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + " Object ID: " + Id); + } + } +} diff --git a/DiscUtils/Ntfs/ObjectIdRecord.cs b/DiscUtils/Ntfs/ObjectIdRecord.cs new file mode 100644 index 0000000..59dd958 --- /dev/null +++ b/DiscUtils/Ntfs/ObjectIdRecord.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Globalization; + + internal sealed class ObjectIdRecord : IByteArraySerializable + { + public FileRecordReference MftReference; + public Guid BirthVolumeId; + public Guid BirthObjectId; + public Guid BirthDomainId; + + public int Size + { + get { return 0x38; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + MftReference = new FileRecordReference(); + MftReference.ReadFrom(buffer, offset); + + BirthVolumeId = Utilities.ToGuidLittleEndian(buffer, offset + 0x08); + BirthObjectId = Utilities.ToGuidLittleEndian(buffer, offset + 0x18); + BirthDomainId = Utilities.ToGuidLittleEndian(buffer, offset + 0x28); + return 0x38; + } + + public void WriteTo(byte[] buffer, int offset) + { + MftReference.WriteTo(buffer, offset); + Utilities.WriteBytesLittleEndian(BirthVolumeId, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(BirthObjectId, buffer, offset + 0x18); + Utilities.WriteBytesLittleEndian(BirthDomainId, buffer, offset + 0x28); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[Data-MftRef:{0},BirthVolId:{1},BirthObjId:{2},BirthDomId:{3}]", MftReference, BirthVolumeId, BirthObjectId, BirthDomainId); + } + } +} diff --git a/DiscUtils/Ntfs/ObjectIds.cs b/DiscUtils/Ntfs/ObjectIds.cs new file mode 100644 index 0000000..cda68cf --- /dev/null +++ b/DiscUtils/Ntfs/ObjectIds.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + + internal sealed class ObjectIds + { + private IndexView _index; + private File _file; + + public ObjectIds(File file) + { + _file = file; + _index = new IndexView(file.GetIndex("$O")); + } + + internal IEnumerable> All + { + get + { + foreach (var record in _index.Entries) + { + yield return new KeyValuePair(record.Key.Id, record.Value); + } + } + } + + internal void Add(Guid objId, FileRecordReference mftRef, Guid birthId, Guid birthVolumeId, Guid birthDomainId) + { + IndexKey newKey = new IndexKey(); + newKey.Id = objId; + + ObjectIdRecord newData = new ObjectIdRecord(); + newData.MftReference = mftRef; + newData.BirthObjectId = birthId; + newData.BirthVolumeId = birthVolumeId; + newData.BirthDomainId = birthDomainId; + + _index[newKey] = newData; + _file.UpdateRecordInMft(); + } + + internal void Remove(Guid objId) + { + IndexKey key = new IndexKey(); + key.Id = objId; + + _index.Remove(key); + _file.UpdateRecordInMft(); + } + + internal bool TryGetValue(Guid objId, out ObjectIdRecord value) + { + IndexKey key = new IndexKey(); + key.Id = objId; + + return _index.TryGetValue(key, out value); + } + + internal void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "OBJECT ID INDEX"); + + foreach (var entry in _index.Entries) + { + writer.WriteLine(indent + " OBJECT ID INDEX ENTRY"); + writer.WriteLine(indent + " Id: " + entry.Key.Id); + writer.WriteLine(indent + " MFT Reference: " + entry.Value.MftReference); + writer.WriteLine(indent + " Birth Volume: " + entry.Value.BirthVolumeId); + writer.WriteLine(indent + " Birth Id: " + entry.Value.BirthObjectId); + writer.WriteLine(indent + " Birth Domain: " + entry.Value.BirthDomainId); + } + } + + internal sealed class IndexKey : IByteArraySerializable + { + public Guid Id; + + public int Size + { + get { return 16; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Id = Utilities.ToGuidLittleEndian(buffer, offset + 0); + return 16; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Id, buffer, offset + 0); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[Key-Id:{0}]", Id); + } + } + } +} diff --git a/DiscUtils/Ntfs/Quotas.cs b/DiscUtils/Ntfs/Quotas.cs new file mode 100644 index 0000000..f568f20 --- /dev/null +++ b/DiscUtils/Ntfs/Quotas.cs @@ -0,0 +1,227 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Globalization; + using System.IO; + using System.Security.Principal; + + internal sealed class Quotas + { + private IndexView _ownerIndex; + private IndexView _quotaIndex; + + public Quotas(File file) + { + _ownerIndex = new IndexView(file.GetIndex("$O")); + _quotaIndex = new IndexView(file.GetIndex("$Q")); + } + + public static Quotas Initialize(File file) + { + Index ownerIndex = file.CreateIndex("$O", (AttributeType)0, AttributeCollationRule.Sid); + Index quotaIndox = file.CreateIndex("$Q", (AttributeType)0, AttributeCollationRule.UnsignedLong); + + IndexView ownerIndexView = new IndexView(ownerIndex); + IndexView quotaIndexView = new IndexView(quotaIndox); + + OwnerKey adminSid = new OwnerKey(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null)); + OwnerRecord adminOwnerId = new OwnerRecord(256); + + ownerIndexView[adminSid] = adminOwnerId; + + quotaIndexView[new OwnerRecord(1)] = new QuotaRecord(null); + quotaIndexView[adminOwnerId] = new QuotaRecord(adminSid.Sid); + + return new Quotas(file); + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "QUOTAS"); + + writer.WriteLine(indent + " OWNER INDEX"); + foreach (var entry in _ownerIndex.Entries) + { + writer.WriteLine(indent + " OWNER INDEX ENTRY"); + writer.WriteLine(indent + " SID: " + entry.Key.Sid); + writer.WriteLine(indent + " Owner Id: " + entry.Value.OwnerId); + } + + writer.WriteLine(indent + " QUOTA INDEX"); + foreach (var entry in _quotaIndex.Entries) + { + writer.WriteLine(indent + " QUOTA INDEX ENTRY"); + writer.WriteLine(indent + " Owner Id: " + entry.Key.OwnerId); + writer.WriteLine(indent + " User SID: " + entry.Value.Sid); + writer.WriteLine(indent + " Changed: " + entry.Value.ChangeTime); + writer.WriteLine(indent + " Exceeded: " + entry.Value.ExceededTime); + writer.WriteLine(indent + " Bytes Used: " + entry.Value.BytesUsed); + writer.WriteLine(indent + " Flags: " + entry.Value.Flags); + writer.WriteLine(indent + " Hard Limit: " + entry.Value.HardLimit); + writer.WriteLine(indent + " Warning Limit: " + entry.Value.WarningLimit); + writer.WriteLine(indent + " Version: " + entry.Value.Version); + } + } + + internal sealed class OwnerKey : IByteArraySerializable + { + public SecurityIdentifier Sid; + + public OwnerKey() + { + } + + public OwnerKey(SecurityIdentifier sid) + { + Sid = sid; + } + + public int Size + { + get { return Sid.BinaryLength; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Sid = new SecurityIdentifier(buffer, offset); + return Sid.BinaryLength; + } + + public void WriteTo(byte[] buffer, int offset) + { + Sid.GetBinaryForm(buffer, offset); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[Sid:{0}]", Sid); + } + } + + internal sealed class OwnerRecord : IByteArraySerializable + { + public int OwnerId; + + public OwnerRecord() + { + } + + public OwnerRecord(int ownerId) + { + OwnerId = ownerId; + } + + public int Size + { + get { return 4; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + OwnerId = Utilities.ToInt32LittleEndian(buffer, offset); + return 4; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(OwnerId, buffer, offset); + } + + public override string ToString() + { + return "[OwnerId:" + OwnerId + "]"; + } + } + + internal sealed class QuotaRecord : IByteArraySerializable + { + public int Version; + public int Flags; + public long BytesUsed; + public DateTime ChangeTime; + public long WarningLimit; + public long HardLimit; + public long ExceededTime; + public SecurityIdentifier Sid; + + public QuotaRecord() + { + } + + public QuotaRecord(SecurityIdentifier sid) + { + Version = 2; + Flags = 1; + ChangeTime = DateTime.UtcNow; + WarningLimit = -1; + HardLimit = -1; + Sid = sid; + } + + public int Size + { + get { return 0x30 + (Sid == null ? 0 : Sid.BinaryLength); } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Version = Utilities.ToInt32LittleEndian(buffer, offset); + Flags = Utilities.ToInt32LittleEndian(buffer, offset + 0x04); + BytesUsed = Utilities.ToInt64LittleEndian(buffer, offset + 0x08); + ChangeTime = DateTime.FromFileTimeUtc(Utilities.ToInt64LittleEndian(buffer, offset + 0x10)); + WarningLimit = Utilities.ToInt64LittleEndian(buffer, offset + 0x18); + HardLimit = Utilities.ToInt64LittleEndian(buffer, offset + 0x20); + ExceededTime = Utilities.ToInt64LittleEndian(buffer, offset + 0x28); + if (buffer.Length - offset > 0x30) + { + Sid = new SecurityIdentifier(buffer, offset + 0x30); + return 0x30 + Sid.BinaryLength; + } + + return 0x30; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Version, buffer, offset); + Utilities.WriteBytesLittleEndian(Flags, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(BytesUsed, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(ChangeTime.ToFileTimeUtc(), buffer, offset + 0x10); + Utilities.WriteBytesLittleEndian(WarningLimit, buffer, offset + 0x18); + Utilities.WriteBytesLittleEndian(HardLimit, buffer, offset + 0x20); + Utilities.WriteBytesLittleEndian(ExceededTime, buffer, offset + 0x28); + if (Sid != null) + { + Sid.GetBinaryForm(buffer, offset + 0x30); + } + } + + public override string ToString() + { + return "[V:" + Version + ",F:" + Flags + ",BU:" + BytesUsed + ",CT:" + ChangeTime + ",WL:" + WarningLimit + ",HL:" + HardLimit + ",ET:" + ExceededTime + ",SID:" + Sid + "]"; + } + } + } +} diff --git a/DiscUtils/Ntfs/RawClusterStream.cs b/DiscUtils/Ntfs/RawClusterStream.cs new file mode 100644 index 0000000..899a67b --- /dev/null +++ b/DiscUtils/Ntfs/RawClusterStream.cs @@ -0,0 +1,368 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using DiscUtils; + + /// + /// Low-level non-resident attribute operations. + /// + /// + /// Responsible for: + /// * Cluster Allocation / Release + /// * Reading clusters from disk + /// * Writing clusters to disk + /// * Substituting zeros for 'sparse'/'unallocated' clusters + /// Not responsible for: + /// * Compression / Decompression + /// * Extending attributes. + /// + internal sealed class RawClusterStream : ClusterStream + { + private INtfsContext _context; + private Stream _fsStream; + private int _bytesPerCluster; + private CookedDataRuns _cookedRuns; + private bool _isMft; + + public RawClusterStream(INtfsContext context, CookedDataRuns cookedRuns, bool isMft) + { + _context = context; + _cookedRuns = cookedRuns; + _isMft = isMft; + + _fsStream = _context.RawStream; + _bytesPerCluster = context.BiosParameterBlock.BytesPerCluster; + } + + public override long AllocatedClusterCount + { + get + { + long total = 0; + for (int i = 0; i < _cookedRuns.Count; ++i) + { + CookedDataRun run = _cookedRuns[i]; + total += run.IsSparse ? 0 : run.Length; + } + + return total; + } + } + + public override IEnumerable> StoredClusters + { + get + { + Range lastVcnRange = null; + List> ranges = new List>(); + + int runCount = _cookedRuns.Count; + for (int i = 0; i < runCount; i++) + { + CookedDataRun cookedRun = _cookedRuns[i]; + if (!cookedRun.IsSparse) + { + long startPos = cookedRun.StartVcn; + if (lastVcnRange != null && lastVcnRange.Offset + lastVcnRange.Count == startPos) + { + lastVcnRange = new Range(lastVcnRange.Offset, lastVcnRange.Count + cookedRun.Length); + ranges[ranges.Count - 1] = lastVcnRange; + } + else + { + lastVcnRange = new Range(cookedRun.StartVcn, cookedRun.Length); + ranges.Add(lastVcnRange); + } + } + } + + return ranges; + } + } + + public override bool IsClusterStored(long vcn) + { + int runIdx = _cookedRuns.FindDataRun(vcn, 0); + return !_cookedRuns[runIdx].IsSparse; + } + + public bool AreAllClustersStored(long vcn, int count) + { + int runIdx = 0; + long focusVcn = vcn; + while (focusVcn < vcn + count) + { + runIdx = _cookedRuns.FindDataRun(focusVcn, runIdx); + + CookedDataRun run = _cookedRuns[runIdx]; + if (run.IsSparse) + { + return false; + } + + focusVcn = run.StartVcn + run.Length; + } + + return true; + } + + public override void ExpandToClusters(long numVirtualClusters, NonResidentAttributeRecord extent, bool allocate) + { + long totalVirtualClusters = _cookedRuns.NextVirtualCluster; + if (totalVirtualClusters < numVirtualClusters) + { + NonResidentAttributeRecord realExtent = extent; + if (realExtent == null) + { + realExtent = _cookedRuns.Last.AttributeExtent; + } + + DataRun newRun = new DataRun(0, numVirtualClusters - totalVirtualClusters, true); + realExtent.DataRuns.Add(newRun); + _cookedRuns.Append(newRun, extent); + realExtent.LastVcn = numVirtualClusters - 1; + } + + if (allocate) + { + AllocateClusters(totalVirtualClusters, (int)(numVirtualClusters - totalVirtualClusters)); + } + } + + public override void TruncateToClusters(long numVirtualClusters) + { + if (numVirtualClusters < _cookedRuns.NextVirtualCluster) + { + ReleaseClusters(numVirtualClusters, (int)(_cookedRuns.NextVirtualCluster - numVirtualClusters)); + + int runIdx = _cookedRuns.FindDataRun(numVirtualClusters, 0); + + if (numVirtualClusters != _cookedRuns[runIdx].StartVcn) + { + _cookedRuns.SplitRun(runIdx, numVirtualClusters); + runIdx++; + } + + _cookedRuns.TruncateAt(runIdx); + } + } + + public int AllocateClusters(long startVcn, int count) + { + if (startVcn + count > _cookedRuns.NextVirtualCluster) + { + throw new IOException("Attempt to allocate unknown clusters"); + } + + int totalAllocated = 0; + int runIdx = 0; + + long focus = startVcn; + while (focus < startVcn + count) + { + runIdx = _cookedRuns.FindDataRun(focus, runIdx); + CookedDataRun run = _cookedRuns[runIdx]; + + if (run.IsSparse) + { + if (focus != run.StartVcn) + { + _cookedRuns.SplitRun(runIdx, focus); + runIdx++; + run = _cookedRuns[runIdx]; + } + + long numClusters = Math.Min((startVcn + count) - focus, run.Length); + if (numClusters != run.Length) + { + _cookedRuns.SplitRun(runIdx, focus + numClusters); + run = _cookedRuns[runIdx]; + } + + long nextCluster = -1; + for (int i = runIdx - 1; i >= 0; --i) + { + if (!_cookedRuns[i].IsSparse) + { + nextCluster = _cookedRuns[i].StartLcn + _cookedRuns[i].Length; + break; + } + } + + var alloced = _context.ClusterBitmap.AllocateClusters(numClusters, nextCluster, _isMft, AllocatedClusterCount); + + List runs = new List(); + + long lcn = runIdx == 0 ? 0 : _cookedRuns[runIdx - 1].StartLcn; + foreach (var allocation in alloced) + { + runs.Add(new DataRun(allocation.First - lcn, allocation.Second, false)); + lcn = allocation.First; + } + + _cookedRuns.MakeNonSparse(runIdx, runs); + + totalAllocated += (int)numClusters; + focus += numClusters; + } + else + { + focus = run.StartVcn + run.Length; + } + } + + return totalAllocated; + } + + public int ReleaseClusters(long startVcn, int count) + { + int runIdx = 0; + + int totalReleased = 0; + + long focus = startVcn; + while (focus < startVcn + count) + { + runIdx = _cookedRuns.FindDataRun(focus, runIdx); + CookedDataRun run = _cookedRuns[runIdx]; + + if (run.IsSparse) + { + focus += run.Length; + } + else + { + if (focus != run.StartVcn) + { + _cookedRuns.SplitRun(runIdx, focus); + runIdx++; + run = _cookedRuns[runIdx]; + } + + long numClusters = Math.Min((startVcn + count) - focus, run.Length); + if (numClusters != run.Length) + { + _cookedRuns.SplitRun(runIdx, focus + numClusters); + run = _cookedRuns[runIdx]; + } + + _context.ClusterBitmap.FreeClusters(new Range(run.StartLcn, run.Length)); + _cookedRuns.MakeSparse(runIdx); + totalReleased += (int)run.Length; + + focus += numClusters; + } + } + + return totalReleased; + } + + public override void ReadClusters(long startVcn, int count, byte[] buffer, int offset) + { + Utilities.AssertBufferParameters(buffer, offset, count * _bytesPerCluster); + + int runIdx = 0; + int totalRead = 0; + while (totalRead < count) + { + long focusVcn = startVcn + totalRead; + + runIdx = _cookedRuns.FindDataRun(focusVcn, runIdx); + CookedDataRun run = _cookedRuns[runIdx]; + + int toRead = (int)Math.Min(count - totalRead, run.Length - (focusVcn - run.StartVcn)); + + if (run.IsSparse) + { + Array.Clear(buffer, offset + (totalRead * _bytesPerCluster), toRead * _bytesPerCluster); + } + else + { + long lcn = _cookedRuns[runIdx].StartLcn + (focusVcn - run.StartVcn); + _fsStream.Position = lcn * _bytesPerCluster; + int numRead = Utilities.ReadFully(_fsStream, buffer, offset + (totalRead * _bytesPerCluster), toRead * _bytesPerCluster); + if (numRead != toRead * _bytesPerCluster) + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, "Short read, reading {0} clusters starting at LCN {1}", toRead, lcn)); + } + } + + totalRead += toRead; + } + } + + public override int WriteClusters(long startVcn, int count, byte[] buffer, int offset) + { + Utilities.AssertBufferParameters(buffer, offset, count * _bytesPerCluster); + + int runIdx = 0; + int totalWritten = 0; + while (totalWritten < count) + { + long focusVcn = startVcn + totalWritten; + + runIdx = _cookedRuns.FindDataRun(focusVcn, runIdx); + CookedDataRun run = _cookedRuns[runIdx]; + + if (run.IsSparse) + { + throw new NotImplementedException("Writing to sparse datarun"); + } + + int toWrite = (int)Math.Min(count - totalWritten, run.Length - (focusVcn - run.StartVcn)); + + long lcn = _cookedRuns[runIdx].StartLcn + (focusVcn - run.StartVcn); + _fsStream.Position = lcn * _bytesPerCluster; + _fsStream.Write(buffer, offset + (totalWritten * _bytesPerCluster), toWrite * _bytesPerCluster); + + totalWritten += toWrite; + } + + return 0; + } + + public override int ClearClusters(long startVcn, int count) + { + byte[] zeroBuffer = new byte[16 * _bytesPerCluster]; + + int clustersAllocated = 0; + + int numWritten = 0; + while (numWritten < count) + { + int toWrite = Math.Min(count - numWritten, 16); + + clustersAllocated += WriteClusters(startVcn + numWritten, toWrite, zeroBuffer, 0); + + numWritten += toWrite; + } + + return -clustersAllocated; + } + } +} diff --git a/DiscUtils/Ntfs/ReparsePointRecord.cs b/DiscUtils/Ntfs/ReparsePointRecord.cs new file mode 100644 index 0000000..2647450 --- /dev/null +++ b/DiscUtils/Ntfs/ReparsePointRecord.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Globalization; + using System.IO; + + internal sealed class ReparsePointRecord : IByteArraySerializable, IDiagnosticTraceable + { + public uint Tag; + public byte[] Content; + + public int Size + { + get { return 8 + Content.Length; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Tag = Utilities.ToUInt32LittleEndian(buffer, offset); + ushort length = Utilities.ToUInt16LittleEndian(buffer, offset + 4); + Content = new byte[length]; + Array.Copy(buffer, offset + 8, Content, 0, length); + return 8 + length; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Tag, buffer, offset); + Utilities.WriteBytesLittleEndian((ushort)Content.Length, buffer, offset + 4); + Utilities.WriteBytesLittleEndian((ushort)0, buffer, offset + 6); + Array.Copy(Content, 0, buffer, offset + 8, Content.Length); + } + + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + " Tag: " + Tag.ToString("x", CultureInfo.InvariantCulture)); + + string hex = string.Empty; + for (int i = 0; i < Math.Min(Content.Length, 32); ++i) + { + hex = hex + string.Format(CultureInfo.InvariantCulture, " {0:X2}", Content[i]); + } + + writer.WriteLine(linePrefix + " Data:" + hex + (Content.Length > 32 ? "..." : string.Empty)); + } + } +} diff --git a/DiscUtils/Ntfs/ReparsePoints.cs b/DiscUtils/Ntfs/ReparsePoints.cs new file mode 100644 index 0000000..37f78aa --- /dev/null +++ b/DiscUtils/Ntfs/ReparsePoints.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.Globalization; + using System.IO; + + internal class ReparsePoints + { + private IndexView _index; + private File _file; + + public ReparsePoints(File file) + { + _file = file; + _index = new IndexView(file.GetIndex("$R")); + } + + internal void Add(uint tag, FileRecordReference file) + { + Key newKey = new Key(); + newKey.Tag = tag; + newKey.File = file; + + Data data = new Data(); + + _index[newKey] = data; + _file.UpdateRecordInMft(); + } + + internal void Remove(uint tag, FileRecordReference file) + { + Key key = new Key(); + key.Tag = tag; + key.File = file; + + _index.Remove(key); + _file.UpdateRecordInMft(); + } + + internal void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "REPARSE POINT INDEX"); + + foreach (var entry in _index.Entries) + { + writer.WriteLine(indent + " REPARSE POINT INDEX ENTRY"); + writer.WriteLine(indent + " Tag: " + entry.Key.Tag.ToString("x", CultureInfo.InvariantCulture)); + writer.WriteLine(indent + " MFT Reference: " + entry.Key.File); + } + } + + internal sealed class Key : IByteArraySerializable + { + public uint Tag; + public FileRecordReference File; + + public int Size + { + get { return 12; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Tag = Utilities.ToUInt32LittleEndian(buffer, offset); + File = new FileRecordReference(Utilities.ToUInt64LittleEndian(buffer, offset + 4)); + return 12; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Tag, buffer, offset); + Utilities.WriteBytesLittleEndian(File.Value, buffer, offset + 4); + ////Utilities.WriteBytesLittleEndian((uint)0, buffer, offset + 12); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "{0:x}:", Tag) + File; + } + } + + internal sealed class Data : IByteArraySerializable + { + public int Size + { + get { return 0; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + return 0; + } + + public void WriteTo(byte[] buffer, int offset) + { + } + + public override string ToString() + { + return ""; + } + } + } +} diff --git a/DiscUtils/Ntfs/ResidentAttributeRecord.cs b/DiscUtils/Ntfs/ResidentAttributeRecord.cs new file mode 100644 index 0000000..1b6b710 --- /dev/null +++ b/DiscUtils/Ntfs/ResidentAttributeRecord.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + using System.Text; + + internal sealed class ResidentAttributeRecord : AttributeRecord + { + private byte _indexedFlag; + private SparseMemoryBuffer _memoryBuffer; + + public ResidentAttributeRecord(byte[] buffer, int offset, out int length) + { + Read(buffer, offset, out length); + } + + public ResidentAttributeRecord(AttributeType type, string name, ushort id, bool indexed, AttributeFlags flags) + : base(type, name, id, flags) + { + _nonResidentFlag = 0; + _indexedFlag = (byte)(indexed ? 1 : 0); + _memoryBuffer = new SparseMemoryBuffer(1024); + } + + public override long AllocatedLength + { + get { return Utilities.RoundUp(DataLength, 8); } + set { throw new NotSupportedException(); } + } + + public override long StartVcn + { + get { return 0; } + } + + public override long DataLength + { + get { return _memoryBuffer.Capacity; } + set { throw new NotSupportedException(); } + } + + /// + /// The amount of initialized data in the attribute (in bytes). + /// + public override long InitializedDataLength + { + get { return (long)DataLength; } + set { throw new NotSupportedException(); } + } + + public override int Size + { + get + { + byte nameLength = 0; + ushort nameOffset = 0x18; + if (Name != null) + { + nameLength = (byte)Name.Length; + } + + ushort dataOffset = (ushort)Utilities.RoundUp(nameOffset + (nameLength * 2), 8); + return (int)Utilities.RoundUp(dataOffset + _memoryBuffer.Capacity, 8); + } + } + + public int DataOffset + { + get + { + byte nameLength = 0; + if (Name != null) + { + nameLength = (byte)Name.Length; + } + + return Utilities.RoundUp(0x18 + (nameLength * 2), 8); + } + } + + public IBuffer DataBuffer + { + get { return _memoryBuffer; } + } + + public override IBuffer GetReadOnlyDataBuffer(INtfsContext context) + { + return _memoryBuffer; + } + + public override Range[] GetClusters() + { + return new Range[0]; + } + + public override int Write(byte[] buffer, int offset) + { + byte nameLength = 0; + ushort nameOffset = 0; + if (Name != null) + { + nameOffset = 0x18; + nameLength = (byte)Name.Length; + } + + ushort dataOffset = (ushort)Utilities.RoundUp(0x18 + (nameLength * 2), 8); + int length = (int)Utilities.RoundUp(dataOffset + _memoryBuffer.Capacity, 8); + + Utilities.WriteBytesLittleEndian((uint)_type, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian(length, buffer, offset + 0x04); + buffer[offset + 0x08] = _nonResidentFlag; + buffer[offset + 0x09] = nameLength; + Utilities.WriteBytesLittleEndian(nameOffset, buffer, offset + 0x0A); + Utilities.WriteBytesLittleEndian((ushort)_flags, buffer, offset + 0x0C); + Utilities.WriteBytesLittleEndian(_attributeId, buffer, offset + 0x0E); + Utilities.WriteBytesLittleEndian((int)_memoryBuffer.Capacity, buffer, offset + 0x10); + Utilities.WriteBytesLittleEndian(dataOffset, buffer, offset + 0x14); + buffer[offset + 0x16] = _indexedFlag; + buffer[offset + 0x17] = 0; // Padding + + if (Name != null) + { + Array.Copy(Encoding.Unicode.GetBytes(Name), 0, buffer, offset + nameOffset, nameLength * 2); + } + + _memoryBuffer.Read(0, buffer, offset + dataOffset, (int)_memoryBuffer.Capacity); + + return (int)length; + } + + public override void Dump(TextWriter writer, string indent) + { + base.Dump(writer, indent); + writer.WriteLine(indent + " Data Length: " + DataLength); + writer.WriteLine(indent + " Indexed: " + _indexedFlag); + } + + protected override void Read(byte[] buffer, int offset, out int length) + { + base.Read(buffer, offset, out length); + + uint dataLength = Utilities.ToUInt32LittleEndian(buffer, offset + 0x10); + ushort dataOffset = Utilities.ToUInt16LittleEndian(buffer, offset + 0x14); + _indexedFlag = buffer[offset + 0x16]; + + if (dataOffset + dataLength > length) + { + throw new IOException("Corrupt attribute, data outside of attribute"); + } + + _memoryBuffer = new SparseMemoryBuffer(1024); + _memoryBuffer.Write(0, buffer, offset + dataOffset, (int)dataLength); + } + } +} diff --git a/DiscUtils/Ntfs/SecurityDescriptor.cs b/DiscUtils/Ntfs/SecurityDescriptor.cs new file mode 100644 index 0000000..bdc7de2 --- /dev/null +++ b/DiscUtils/Ntfs/SecurityDescriptor.cs @@ -0,0 +1,177 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + using System.Security.AccessControl; + using System.Security.Principal; + + internal sealed class SecurityDescriptor : IByteArraySerializable, IDiagnosticTraceable + { + private RawSecurityDescriptor _securityDescriptor; + + public SecurityDescriptor() + { + } + + public SecurityDescriptor(RawSecurityDescriptor secDesc) + { + _securityDescriptor = secDesc; + } + + public RawSecurityDescriptor Descriptor + { + get { return _securityDescriptor; } + set { _securityDescriptor = value; } + } + + public int Size + { + get + { + return _securityDescriptor.BinaryLength; + } + } + + public uint CalcHash() + { + byte[] buffer = new byte[Size]; + WriteTo(buffer, 0); + uint hash = 0; + for (int i = 0; i < buffer.Length / 4; ++i) + { + hash = Utilities.ToUInt32LittleEndian(buffer, i * 4) + ((hash << 3) | (hash >> 29)); + } + + return hash; + } + + public int ReadFrom(byte[] buffer, int offset) + { + _securityDescriptor = new RawSecurityDescriptor(buffer, offset); + return _securityDescriptor.BinaryLength; + } + + public void WriteTo(byte[] buffer, int offset) + { + // Write out the security descriptor manually because on NTFS the DACL is written + // before the Owner & Group. Writing the components in the same order means the + // hashes will match for identical Security Descriptors. + ControlFlags controlFlags = _securityDescriptor.ControlFlags; + buffer[offset + 0x00] = 1; + buffer[offset + 0x01] = _securityDescriptor.ResourceManagerControl; + Utilities.WriteBytesLittleEndian((ushort)controlFlags, buffer, offset + 0x02); + + // Blank out offsets, will fill later + for (int i = 0x04; i < 0x14; ++i) + { + buffer[offset + i] = 0; + } + + int pos = 0x14; + + RawAcl discAcl = _securityDescriptor.DiscretionaryAcl; + if ((controlFlags & ControlFlags.DiscretionaryAclPresent) != 0 && discAcl != null) + { + Utilities.WriteBytesLittleEndian(pos, buffer, offset + 0x10); + discAcl.GetBinaryForm(buffer, offset + pos); + pos += _securityDescriptor.DiscretionaryAcl.BinaryLength; + } + else + { + Utilities.WriteBytesLittleEndian((int)0, buffer, offset + 0x10); + } + + RawAcl sysAcl = _securityDescriptor.SystemAcl; + if ((controlFlags & ControlFlags.SystemAclPresent) != 0 && sysAcl != null) + { + Utilities.WriteBytesLittleEndian(pos, buffer, offset + 0x0C); + sysAcl.GetBinaryForm(buffer, offset + pos); + pos += _securityDescriptor.SystemAcl.BinaryLength; + } + else + { + Utilities.WriteBytesLittleEndian((int)0, buffer, offset + 0x0C); + } + + Utilities.WriteBytesLittleEndian(pos, buffer, offset + 0x04); + _securityDescriptor.Owner.GetBinaryForm(buffer, offset + pos); + pos += _securityDescriptor.Owner.BinaryLength; + + Utilities.WriteBytesLittleEndian(pos, buffer, offset + 0x08); + _securityDescriptor.Group.GetBinaryForm(buffer, offset + pos); + pos += _securityDescriptor.Group.BinaryLength; + + if (pos != _securityDescriptor.BinaryLength) + { + throw new IOException("Failed to write Security Descriptor correctly"); + } + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "Descriptor: " + _securityDescriptor.GetSddlForm(AccessControlSections.All)); + } + + internal static RawSecurityDescriptor CalcNewObjectDescriptor(RawSecurityDescriptor parent, bool isContainer) + { + RawAcl sacl = InheritAcl(parent.SystemAcl, isContainer); + RawAcl dacl = InheritAcl(parent.DiscretionaryAcl, isContainer); + + return new RawSecurityDescriptor(parent.ControlFlags, parent.Owner, parent.Group, sacl, dacl); + } + + private static RawAcl InheritAcl(RawAcl parentAcl, bool isContainer) + { + AceFlags inheritTest = isContainer ? AceFlags.ContainerInherit : AceFlags.ObjectInherit; + + RawAcl newAcl = null; + if (parentAcl != null) + { + newAcl = new RawAcl(parentAcl.Revision, parentAcl.Count); + foreach (GenericAce ace in parentAcl) + { + if ((ace.AceFlags & inheritTest) != 0) + { + GenericAce newAce = ace.Copy(); + + AceFlags newFlags = ace.AceFlags; + if ((newFlags & AceFlags.NoPropagateInherit) != 0) + { + newFlags &= ~(AceFlags.ContainerInherit | AceFlags.ObjectInherit | AceFlags.NoPropagateInherit); + } + + newFlags &= ~AceFlags.InheritOnly; + newFlags |= AceFlags.Inherited; + + newAce.AceFlags = newFlags; + newAcl.InsertAce(newAcl.Count, newAce); + } + } + } + + return newAcl; + } + } +} diff --git a/DiscUtils/Ntfs/SecurityDescriptorRecord.cs b/DiscUtils/Ntfs/SecurityDescriptorRecord.cs new file mode 100644 index 0000000..d7bf66b --- /dev/null +++ b/DiscUtils/Ntfs/SecurityDescriptorRecord.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + + internal sealed class SecurityDescriptorRecord : IByteArraySerializable + { + public uint Hash; + public uint Id; + public long OffsetInFile; + public uint EntrySize; + public byte[] SecurityDescriptor; + + public int Size + { + get { return SecurityDescriptor.Length + 0x14; } + } + + public bool Read(byte[] buffer, int offset) + { + Hash = Utilities.ToUInt32LittleEndian(buffer, offset + 0x00); + Id = Utilities.ToUInt32LittleEndian(buffer, offset + 0x04); + OffsetInFile = Utilities.ToInt64LittleEndian(buffer, offset + 0x08); + EntrySize = Utilities.ToUInt32LittleEndian(buffer, offset + 0x10); + + if (EntrySize > 0) + { + SecurityDescriptor = new byte[EntrySize - 0x14]; + Array.Copy(buffer, offset + 0x14, SecurityDescriptor, 0, SecurityDescriptor.Length); + return true; + } + else + { + return false; + } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Read(buffer, offset); + return SecurityDescriptor.Length + 0x14; + } + + public void WriteTo(byte[] buffer, int offset) + { + EntrySize = (uint)Size; + + Utilities.WriteBytesLittleEndian(Hash, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian(Id, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(OffsetInFile, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(EntrySize, buffer, offset + 0x10); + + Array.Copy(SecurityDescriptor, 0, buffer, offset + 0x14, SecurityDescriptor.Length); + } + } +} diff --git a/DiscUtils/Ntfs/SecurityDescriptors.cs b/DiscUtils/Ntfs/SecurityDescriptors.cs new file mode 100644 index 0000000..13e796c --- /dev/null +++ b/DiscUtils/Ntfs/SecurityDescriptors.cs @@ -0,0 +1,383 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Globalization; + using System.IO; + using System.Security.AccessControl; + + internal sealed class SecurityDescriptors : IDiagnosticTraceable + { + // File consists of pairs of duplicate blocks (one after the other), providing + // redundancy. When a pair is full, the next pair is used. + private const int BlockSize = 0x40000; + + private File _file; + private IndexView _hashIndex; + private IndexView _idIndex; + private uint _nextId; + private long _nextSpace; + + public SecurityDescriptors(File file) + { + _file = file; + _hashIndex = new IndexView(file.GetIndex("$SDH")); + _idIndex = new IndexView(file.GetIndex("$SII")); + + foreach (var entry in _idIndex.Entries) + { + if (entry.Key.Id > _nextId) + { + _nextId = entry.Key.Id; + } + + long end = entry.Value.SdsOffset + entry.Value.SdsLength; + if (end > _nextSpace) + { + _nextSpace = end; + } + } + + if (_nextId == 0) + { + _nextId = 256; + } + else + { + _nextId++; + } + + _nextSpace = Utilities.RoundUp(_nextSpace, 16); + } + + public static SecurityDescriptors Initialize(File file) + { + file.CreateIndex("$SDH", (AttributeType)0, AttributeCollationRule.SecurityHash); + file.CreateIndex("$SII", (AttributeType)0, AttributeCollationRule.UnsignedLong); + file.CreateStream(AttributeType.Data, "$SDS"); + + return new SecurityDescriptors(file); + } + + public RawSecurityDescriptor GetDescriptorById(uint id) + { + IdIndexData data; + if (_idIndex.TryGetValue(new IdIndexKey(id), out data)) + { + return ReadDescriptor(data).Descriptor; + } + + return null; + } + + public uint AddDescriptor(RawSecurityDescriptor newDescriptor) + { + // Search to see if this is a known descriptor + SecurityDescriptor newDescObj = new SecurityDescriptor(newDescriptor); + uint newHash = newDescObj.CalcHash(); + byte[] newByteForm = new byte[newDescObj.Size]; + newDescObj.WriteTo(newByteForm, 0); + + foreach (var entry in _hashIndex.FindAll(new HashFinder(newHash))) + { + SecurityDescriptor stored = ReadDescriptor(entry.Value); + + byte[] storedByteForm = new byte[stored.Size]; + stored.WriteTo(storedByteForm, 0); + + if (Utilities.AreEqual(newByteForm, storedByteForm)) + { + return entry.Value.Id; + } + } + + long offset = _nextSpace; + + // Write the new descriptor to the end of the existing descriptors + SecurityDescriptorRecord record = new SecurityDescriptorRecord(); + record.SecurityDescriptor = newByteForm; + record.Hash = newHash; + record.Id = _nextId; + + // If we'd overflow into our duplicate block, skip over it to the + // start of the next block + if (((offset + record.Size) / BlockSize) % 2 == 1) + { + _nextSpace = Utilities.RoundUp(offset, BlockSize * 2); + offset = _nextSpace; + } + + record.OffsetInFile = offset; + + byte[] buffer = new byte[record.Size]; + record.WriteTo(buffer, 0); + + using (Stream s = _file.OpenStream(AttributeType.Data, "$SDS", FileAccess.ReadWrite)) + { + s.Position = _nextSpace; + s.Write(buffer, 0, buffer.Length); + s.Position = BlockSize + _nextSpace; + s.Write(buffer, 0, buffer.Length); + } + + // Make the next descriptor land at the end of this one + _nextSpace = Utilities.RoundUp(_nextSpace + buffer.Length, 16); + _nextId++; + + // Update the indexes + HashIndexData hashIndexData = new HashIndexData(); + hashIndexData.Hash = record.Hash; + hashIndexData.Id = record.Id; + hashIndexData.SdsOffset = record.OffsetInFile; + hashIndexData.SdsLength = (int)record.EntrySize; + + HashIndexKey hashIndexKey = new HashIndexKey(); + hashIndexKey.Hash = record.Hash; + hashIndexKey.Id = record.Id; + + _hashIndex[hashIndexKey] = hashIndexData; + + IdIndexData idIndexData = new IdIndexData(); + idIndexData.Hash = record.Hash; + idIndexData.Id = record.Id; + idIndexData.SdsOffset = record.OffsetInFile; + idIndexData.SdsLength = (int)record.EntrySize; + + IdIndexKey idIndexKey = new IdIndexKey(); + idIndexKey.Id = record.Id; + + _idIndex[idIndexKey] = idIndexData; + + _file.UpdateRecordInMft(); + + return record.Id; + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + "SECURITY DESCRIPTORS"); + + using (Stream s = _file.OpenStream(AttributeType.Data, "$SDS", FileAccess.Read)) + { + byte[] buffer = Utilities.ReadFully(s, (int)s.Length); + + foreach (var entry in _idIndex.Entries) + { + int pos = (int)entry.Value.SdsOffset; + + SecurityDescriptorRecord rec = new SecurityDescriptorRecord(); + if (!rec.Read(buffer, pos)) + { + break; + } + + string secDescStr = "--unknown--"; + if (rec.SecurityDescriptor[0] != 0) + { + RawSecurityDescriptor sd = new RawSecurityDescriptor(rec.SecurityDescriptor, 0); + secDescStr = sd.GetSddlForm(AccessControlSections.All); + } + + writer.WriteLine(indent + " SECURITY DESCRIPTOR RECORD"); + writer.WriteLine(indent + " Hash: " + rec.Hash); + writer.WriteLine(indent + " Id: " + rec.Id); + writer.WriteLine(indent + " File Offset: " + rec.OffsetInFile); + writer.WriteLine(indent + " Size: " + rec.EntrySize); + writer.WriteLine(indent + " Value: " + secDescStr); + } + } + } + + private SecurityDescriptor ReadDescriptor(IndexData data) + { + using (Stream s = _file.OpenStream(AttributeType.Data, "$SDS", FileAccess.Read)) + { + s.Position = data.SdsOffset; + byte[] buffer = Utilities.ReadFully(s, data.SdsLength); + + SecurityDescriptorRecord record = new SecurityDescriptorRecord(); + record.Read(buffer, 0); + + return new SecurityDescriptor(new RawSecurityDescriptor(record.SecurityDescriptor, 0)); + } + } + + internal abstract class IndexData + { + public uint Hash; + public uint Id; + public long SdsOffset; + public int SdsLength; + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[Data-Hash:{0},Id:{1},SdsOffset:{2},SdsLength:{3}]", Hash, Id, SdsOffset, SdsLength); + } + } + + internal sealed class HashIndexKey : IByteArraySerializable + { + public uint Hash; + public uint Id; + + public int Size + { + get { return 8; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Hash = Utilities.ToUInt32LittleEndian(buffer, offset + 0); + Id = Utilities.ToUInt32LittleEndian(buffer, offset + 4); + return 8; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Hash, buffer, offset + 0); + Utilities.WriteBytesLittleEndian(Id, buffer, offset + 4); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[Key-Hash:{0},Id:{1}]", Hash, Id); + } + } + + internal sealed class HashIndexData : IndexData, IByteArraySerializable + { + public int Size + { + get { return 0x14; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Hash = Utilities.ToUInt32LittleEndian(buffer, offset + 0x00); + Id = Utilities.ToUInt32LittleEndian(buffer, offset + 0x04); + SdsOffset = Utilities.ToInt64LittleEndian(buffer, offset + 0x08); + SdsLength = Utilities.ToInt32LittleEndian(buffer, offset + 0x10); + return 0x14; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Hash, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian(Id, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(SdsOffset, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(SdsLength, buffer, offset + 0x10); + ////Array.Copy(new byte[] { (byte)'I', 0, (byte)'I', 0 }, 0, buffer, offset + 0x14, 4); + } + } + + internal sealed class IdIndexKey : IByteArraySerializable + { + public uint Id; + + public IdIndexKey() + { + } + + public IdIndexKey(uint id) + { + Id = id; + } + + public int Size + { + get { return 4; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Id = Utilities.ToUInt32LittleEndian(buffer, offset + 0); + return 4; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Id, buffer, offset + 0); + } + + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "[Key-Id:{0}]", Id); + } + } + + internal sealed class IdIndexData : IndexData, IByteArraySerializable + { + public int Size + { + get { return 0x14; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Hash = Utilities.ToUInt32LittleEndian(buffer, offset + 0x00); + Id = Utilities.ToUInt32LittleEndian(buffer, offset + 0x04); + SdsOffset = Utilities.ToInt64LittleEndian(buffer, offset + 0x08); + SdsLength = Utilities.ToInt32LittleEndian(buffer, offset + 0x10); + return 0x14; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Hash, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian(Id, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(SdsOffset, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(SdsLength, buffer, offset + 0x10); + } + } + + private class HashFinder : IComparable + { + private uint _toMatch; + + public HashFinder(uint toMatch) + { + _toMatch = toMatch; + } + + public int CompareTo(uint otherHash) + { + if (_toMatch < otherHash) + { + return -1; + } + else if (_toMatch > otherHash) + { + return 1; + } + + return 0; + } + + public int CompareTo(HashIndexKey other) + { + return CompareTo(other.Hash); + } + } + } +} diff --git a/DiscUtils/Ntfs/ShortFileNameOption.cs b/DiscUtils/Ntfs/ShortFileNameOption.cs new file mode 100644 index 0000000..38d00e4 --- /dev/null +++ b/DiscUtils/Ntfs/ShortFileNameOption.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// Controls whether short file names are created automatically. + /// + public enum ShortFileNameOption + { + /// + /// Creates short file names, unless they've been disabled in NTFS. + /// + UseVolumeFlag, + + /// + /// Does not create short names, ignoring the NTFS setting. + /// + Disabled, + + /// + /// Always creates short names, ignoring the NTFS setting. + /// + Enabled + } +} diff --git a/DiscUtils/Ntfs/SparseClusterStream.cs b/DiscUtils/Ntfs/SparseClusterStream.cs new file mode 100644 index 0000000..a3c994c --- /dev/null +++ b/DiscUtils/Ntfs/SparseClusterStream.cs @@ -0,0 +1,91 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.Collections.Generic; + + internal sealed class SparseClusterStream : ClusterStream + { + private NtfsAttribute _attr; + private RawClusterStream _rawStream; + + public SparseClusterStream(NtfsAttribute attr, RawClusterStream rawStream) + { + _attr = attr; + _rawStream = rawStream; + } + + public override long AllocatedClusterCount + { + get { return _rawStream.AllocatedClusterCount; } + } + + public override IEnumerable> StoredClusters + { + get { return _rawStream.StoredClusters; } + } + + public override bool IsClusterStored(long vcn) + { + return _rawStream.IsClusterStored(vcn); + } + + public override void ExpandToClusters(long numVirtualClusters, NonResidentAttributeRecord extent, bool allocate) + { + _rawStream.ExpandToClusters(CompressionStart(numVirtualClusters), extent, false); + } + + public override void TruncateToClusters(long numVirtualClusters) + { + long alignedNum = CompressionStart(numVirtualClusters); + _rawStream.TruncateToClusters(alignedNum); + if (alignedNum != numVirtualClusters) + { + _rawStream.ReleaseClusters(numVirtualClusters, (int)(alignedNum - numVirtualClusters)); + } + } + + public override void ReadClusters(long startVcn, int count, byte[] buffer, int offset) + { + _rawStream.ReadClusters(startVcn, count, buffer, offset); + } + + public override int WriteClusters(long startVcn, int count, byte[] buffer, int offset) + { + int clustersAllocated = 0; + clustersAllocated += _rawStream.AllocateClusters(startVcn, count); + clustersAllocated += _rawStream.WriteClusters(startVcn, count, buffer, offset); + return clustersAllocated; + } + + public override int ClearClusters(long startVcn, int count) + { + return _rawStream.ReleaseClusters(startVcn, count); + } + + private long CompressionStart(long vcn) + { + return Utilities.RoundUp(vcn, _attr.CompressionUnitSize); + } + } +} diff --git a/DiscUtils/Ntfs/StandardInformation.cs b/DiscUtils/Ntfs/StandardInformation.cs new file mode 100644 index 0000000..82237bb --- /dev/null +++ b/DiscUtils/Ntfs/StandardInformation.cs @@ -0,0 +1,157 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + + internal sealed class StandardInformation : IByteArraySerializable, IDiagnosticTraceable + { + public DateTime CreationTime; + public DateTime ModificationTime; + public DateTime MftChangedTime; + public DateTime LastAccessTime; + public FileAttributeFlags FileAttributes; + public uint MaxVersions; + public uint Version; + public uint ClassId; + public uint OwnerId; + public uint SecurityId; + public ulong QuotaCharged; + public ulong UpdateSequenceNumber; + + private bool _haveExtraFields = true; + + public int Size + { + get { return _haveExtraFields ? 0x48 : 0x30; } + } + + public static StandardInformation InitializeNewFile(File file, FileAttributeFlags flags) + { + DateTime now = DateTime.UtcNow; + + NtfsStream siStream = file.CreateStream(AttributeType.StandardInformation, null); + StandardInformation si = new StandardInformation(); + si.CreationTime = now; + si.ModificationTime = now; + si.MftChangedTime = now; + si.LastAccessTime = now; + si.FileAttributes = flags; + siStream.SetContent(si); + + return si; + } + + public int ReadFrom(byte[] buffer, int offset) + { + CreationTime = ReadDateTime(buffer, 0x00); + ModificationTime = ReadDateTime(buffer, 0x08); + MftChangedTime = ReadDateTime(buffer, 0x10); + LastAccessTime = ReadDateTime(buffer, 0x18); + FileAttributes = (FileAttributeFlags)Utilities.ToUInt32LittleEndian(buffer, 0x20); + MaxVersions = Utilities.ToUInt32LittleEndian(buffer, 0x24); + Version = Utilities.ToUInt32LittleEndian(buffer, 0x28); + ClassId = Utilities.ToUInt32LittleEndian(buffer, 0x2C); + + if (buffer.Length > 0x30) + { + OwnerId = Utilities.ToUInt32LittleEndian(buffer, 0x30); + SecurityId = Utilities.ToUInt32LittleEndian(buffer, 0x34); + QuotaCharged = Utilities.ToUInt64LittleEndian(buffer, 0x38); + UpdateSequenceNumber = Utilities.ToUInt64LittleEndian(buffer, 0x40); + _haveExtraFields = true; + return 0x48; + } + else + { + _haveExtraFields = false; + return 0x30; + } + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(CreationTime.ToFileTimeUtc(), buffer, 0x00); + Utilities.WriteBytesLittleEndian(ModificationTime.ToFileTimeUtc(), buffer, 0x08); + Utilities.WriteBytesLittleEndian(MftChangedTime.ToFileTimeUtc(), buffer, 0x10); + Utilities.WriteBytesLittleEndian(LastAccessTime.ToFileTimeUtc(), buffer, 0x18); + Utilities.WriteBytesLittleEndian((uint)FileAttributes, buffer, 0x20); + Utilities.WriteBytesLittleEndian(MaxVersions, buffer, 0x24); + Utilities.WriteBytesLittleEndian(Version, buffer, 0x28); + Utilities.WriteBytesLittleEndian(ClassId, buffer, 0x2C); + + if (_haveExtraFields) + { + Utilities.WriteBytesLittleEndian(OwnerId, buffer, 0x30); + Utilities.WriteBytesLittleEndian(SecurityId, buffer, 0x34); + Utilities.WriteBytesLittleEndian(QuotaCharged, buffer, 0x38); + Utilities.WriteBytesLittleEndian(UpdateSequenceNumber, buffer, 0x38); + } + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + " Creation Time: " + CreationTime); + writer.WriteLine(indent + " Modification Time: " + ModificationTime); + writer.WriteLine(indent + " MFT Changed Time: " + MftChangedTime); + writer.WriteLine(indent + " Last Access Time: " + LastAccessTime); + writer.WriteLine(indent + " File Permissions: " + FileAttributes); + writer.WriteLine(indent + " Max Versions: " + MaxVersions); + writer.WriteLine(indent + " Version: " + Version); + writer.WriteLine(indent + " Class Id: " + ClassId); + writer.WriteLine(indent + " Security Id: " + SecurityId); + writer.WriteLine(indent + " Quota Charged: " + QuotaCharged); + writer.WriteLine(indent + " Update Seq Num: " + UpdateSequenceNumber); + } + + internal static FileAttributes ConvertFlags(FileAttributeFlags flags, bool isDirectory) + { + FileAttributes result = (FileAttributes)(((uint)flags) & 0xFFFF); + + if (isDirectory) + { + result |= System.IO.FileAttributes.Directory; + } + + return result; + } + + internal static FileAttributeFlags SetFileAttributes(FileAttributes newAttributes, FileAttributeFlags existing) + { + return (FileAttributeFlags)(((uint)existing & 0xFFFF0000) | ((uint)newAttributes & 0xFFFF)); + } + + private static DateTime ReadDateTime(byte[] buffer, int offset) + { + try + { + return DateTime.FromFileTimeUtc(Utilities.ToInt64LittleEndian(buffer, offset)); + } + catch (ArgumentException) + { + return DateTime.FromFileTimeUtc(0); + } + } + } +} diff --git a/DiscUtils/Ntfs/StructuredNtfsAttribute.cs b/DiscUtils/Ntfs/StructuredNtfsAttribute.cs new file mode 100644 index 0000000..b481d00 --- /dev/null +++ b/DiscUtils/Ntfs/StructuredNtfsAttribute.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System.IO; + + internal class StructuredNtfsAttribute : NtfsAttribute + where T : IByteArraySerializable, IDiagnosticTraceable, new() + { + private T _structure; + private bool _initialized; + private bool _hasContent; + + public StructuredNtfsAttribute(File file, FileRecordReference containingFile, AttributeRecord record) + : base(file, containingFile, record) + { + _structure = new T(); + } + + public T Content + { + get + { + Initialize(); + return _structure; + } + + set + { + _structure = value; + _hasContent = true; + } + } + + public bool HasContent + { + get + { + Initialize(); + return _hasContent; + } + } + + public void Save() + { + byte[] buffer = new byte[_structure.Size]; + _structure.WriteTo(buffer, 0); + using (Stream s = Open(FileAccess.Write)) + { + s.Write(buffer, 0, buffer.Length); + s.SetLength(buffer.Length); + } + } + + public override string ToString() + { + Initialize(); + return _structure.ToString(); + } + + public override void Dump(TextWriter writer, string indent) + { + Initialize(); + writer.WriteLine(indent + AttributeTypeName + " ATTRIBUTE (" + (Name == null ? "No Name" : Name) + ")"); + _structure.Dump(writer, indent + " "); + + _primaryRecord.Dump(writer, indent + " "); + } + + private void Initialize() + { + if (!_initialized) + { + using (Stream s = Open(FileAccess.Read)) + { + byte[] buffer = Utilities.ReadFully(s, (int)Length); + _structure.ReadFrom(buffer, 0); + _hasContent = s.Length != 0; + } + + _initialized = true; + } + } + } +} diff --git a/DiscUtils/Ntfs/UpperCase.cs b/DiscUtils/Ntfs/UpperCase.cs new file mode 100644 index 0000000..7788c14 --- /dev/null +++ b/DiscUtils/Ntfs/UpperCase.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal sealed class UpperCase : IComparer + { + private char[] _table; + + public UpperCase(File file) + { + using (Stream s = file.OpenStream(AttributeType.Data, null, FileAccess.Read)) + { + _table = new char[s.Length / 2]; + + byte[] buffer = Utilities.ReadFully(s, (int)s.Length); + + for (int i = 0; i < _table.Length; ++i) + { + _table[i] = (char)Utilities.ToUInt16LittleEndian(buffer, i * 2); + } + } + } + + public int Compare(string x, string y) + { + int compLen = Math.Min(x.Length, y.Length); + for (int i = 0; i < compLen; ++i) + { + int result = _table[x[i]] - _table[y[i]]; + if (result != 0) + { + return result; + } + } + + // Identical out to the shortest string, so length is now the + // determining factor. + return x.Length - y.Length; + } + + public int Compare(byte[] x, int xOffset, int xLength, byte[] y, int yOffset, int yLength) + { + int compLen = Math.Min(xLength, yLength) / 2; + for (int i = 0; i < compLen; ++i) + { + char xCh = (char)(x[xOffset + (i * 2)] | (x[xOffset + ((i * 2) + 1)] << 8)); + char yCh = (char)(y[yOffset + (i * 2)] | (y[yOffset + ((i * 2) + 1)] << 8)); + + int result = _table[xCh] - _table[yCh]; + if (result != 0) + { + return result; + } + } + + // Identical out to the shortest string, so length is now the + // determining factor. + return xLength - yLength; + } + + internal static UpperCase Initialize(File file) + { + byte[] buffer = new byte[(char.MaxValue + 1) * 2]; + for (int i = Char.MinValue; i <= char.MaxValue; ++i) + { + Utilities.WriteBytesLittleEndian((ushort)char.ToUpperInvariant((char)i), buffer, i * 2); + } + + using (Stream s = file.OpenStream(AttributeType.Data, null, FileAccess.ReadWrite)) + { + s.Write(buffer, 0, buffer.Length); + } + + return new UpperCase(file); + } + } +} diff --git a/DiscUtils/Ntfs/VolumeInformation.cs b/DiscUtils/Ntfs/VolumeInformation.cs new file mode 100644 index 0000000..cc8513f --- /dev/null +++ b/DiscUtils/Ntfs/VolumeInformation.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + + internal sealed class VolumeInformation : IByteArraySerializable, IDiagnosticTraceable + { + public const int VersionNt4 = 0x0102; + public const int VersionW2k = 0x0300; + public const int VersionXp = 0x0301; + + private byte _majorVersion; + private byte _minorVersion; + private VolumeInformationFlags _flags; + + public VolumeInformation() + { + } + + public VolumeInformation(byte major, byte minor, VolumeInformationFlags flags) + { + _majorVersion = major; + _minorVersion = minor; + _flags = flags; + } + + public VolumeInformationFlags Flags + { + get { return _flags; } + } + + public int Version + { + get { return ((int)_majorVersion) << 8 | _minorVersion; } + } + + public int Size + { + get { return 0x0C; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + _majorVersion = buffer[offset + 0x08]; + _minorVersion = buffer[offset + 0x09]; + _flags = (VolumeInformationFlags)Utilities.ToUInt16LittleEndian(buffer, offset + 0x0A); + return 0x0C; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian((ulong)0, buffer, offset + 0x00); + buffer[offset + 0x08] = _majorVersion; + buffer[offset + 0x09] = _minorVersion; + Utilities.WriteBytesLittleEndian((ushort)_flags, buffer, offset + 0x0A); + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + " Version: " + _majorVersion + "." + _minorVersion); + writer.WriteLine(indent + " Flags: " + _flags); + } + } +} diff --git a/DiscUtils/Ntfs/VolumeInformationFlags.cs b/DiscUtils/Ntfs/VolumeInformationFlags.cs new file mode 100644 index 0000000..ee23294 --- /dev/null +++ b/DiscUtils/Ntfs/VolumeInformationFlags.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + + [Flags] + internal enum VolumeInformationFlags : ushort + { + None = 0x00, + Dirty = 0x01, + ResizeLogFile = 0x02, + UpgradeOnMount = 0x04, + MountedOnNT4 = 0x08, + DeleteUSNUnderway = 0x10, + RepairObjectIds = 0x20, + DisableShortNameCreation = 0x80, + ModifiedByChkDsk = 0x8000 + } +} diff --git a/DiscUtils/Ntfs/VolumeName.cs b/DiscUtils/Ntfs/VolumeName.cs new file mode 100644 index 0000000..fbd843b --- /dev/null +++ b/DiscUtils/Ntfs/VolumeName.cs @@ -0,0 +1,68 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Ntfs +{ + using System; + using System.IO; + using System.Text; + + internal sealed class VolumeName : IByteArraySerializable, IDiagnosticTraceable + { + private string _name; + + public VolumeName() + { + } + + public VolumeName(string name) + { + _name = name; + } + + public string Name + { + get { return _name; } + } + + public int Size + { + get { return Encoding.Unicode.GetByteCount(_name); } + } + + public int ReadFrom(byte[] buffer, int offset) + { + _name = Encoding.Unicode.GetString(buffer, offset, buffer.Length - offset); + return buffer.Length - offset; + } + + public void WriteTo(byte[] buffer, int offset) + { + Encoding.Unicode.GetBytes(_name, 0, _name.Length, buffer, offset); + } + + public void Dump(TextWriter writer, string indent) + { + writer.WriteLine(indent + " Volume Name: " + _name); + } + } +} diff --git a/DiscUtils/Numbers.cs b/DiscUtils/Numbers.cs new file mode 100644 index 0000000..6bc2a10 --- /dev/null +++ b/DiscUtils/Numbers.cs @@ -0,0 +1,258 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + internal static class Numbers + where T : struct, IComparable, IEquatable + { + public static readonly T Zero = default(T); + public static readonly T One = GetOne(); + public static readonly DualParamFn Add = GetAdd(); + public static readonly DualParamFn Subtract = GetSubtract(); + public static readonly DualParamFn Multiply = GetMultiply(); + public static readonly DualParamFn Divide = GetDivide(); + public static readonly DualParamFn RoundUp = GetRoundUp(); + public static readonly DualParamFn RoundDown = GetRoundDown(); + public static readonly DualParamFn Ceil = GetCeil(); + public static readonly ConvertLongFn ConvertLong = GetConvertLong(); + public static readonly ConvertIntFn ConvertInt = GetConvertInt(); + + public delegate T NoParamFn(); + + public delegate T DualParamFn(T a, T b); + + public delegate bool ComparisonFn(T a, T b); + + public delegate T ConvertLongFn(long a); + + public delegate T ConvertIntFn(int a); + + private delegate long LongNoParamFn(); + + private delegate long LongDualParamFn(long a, long b); + + private delegate long LongConvertLongFn(long x); + + private delegate long LongConvertIntFn(int x); + + private delegate int IntNoParamFn(); + + private delegate int IntDualParamFn(int a, int b); + + private delegate int IntConvertLongFn(long x); + + private delegate int IntConvertIntFn(int x); + + public static bool GreaterThan(T a, T b) + { + return a.CompareTo(b) > 0; + } + + public static bool GreaterThanOrEqual(T a, T b) + { + return a.CompareTo(b) >= 0; + } + + public static bool LessThan(T a, T b) + { + return a.CompareTo(b) < 0; + } + + public static bool LessThanOrEqual(T a, T b) + { + return a.CompareTo(b) <= 0; + } + + public static bool Equal(T a, T b) + { + return a.CompareTo(b) == 0; + } + + public static bool NotEqual(T a, T b) + { + return a.CompareTo(b) != 0; + } + + private static T GetOne() + { + if (typeof(T) == typeof(long)) + { + return ((NoParamFn)(object)new LongNoParamFn(() => { return 1; }))(); + } + else if (typeof(T) == typeof(int)) + { + return ((NoParamFn)(object)new IntNoParamFn(() => { return 1; }))(); + } + else + { + throw new NotSupportedException(); + } + } + + private static ConvertLongFn GetConvertLong() + { + if (typeof(T) == typeof(long)) + { + return (ConvertLongFn)(object)new LongConvertLongFn((long x) => { return x; }); + } + else if (typeof(T) == typeof(int)) + { + return (ConvertLongFn)(object)new IntConvertLongFn((long x) => { return (int)x; }); + } + else + { + throw new NotSupportedException(); + } + } + + private static ConvertIntFn GetConvertInt() + { + if (typeof(T) == typeof(long)) + { + return (ConvertIntFn)(object)new LongConvertIntFn((int x) => { return x; }); + } + else if (typeof(T) == typeof(int)) + { + return (ConvertIntFn)(object)new IntConvertIntFn((int x) => { return x; }); + } + else + { + throw new NotSupportedException(); + } + } + + private static DualParamFn GetAdd() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((long a, long b) => { return a + b; }); + } + else if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((int a, int b) => { return a + b; }); + } + else + { + throw new NotSupportedException(); + } + } + + private static DualParamFn GetSubtract() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((long a, long b) => { return a - b; }); + } + else if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((int a, int b) => { return a - b; }); + } + else + { + throw new NotSupportedException(); + } + } + + private static DualParamFn GetMultiply() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((long a, long b) => { return a * b; }); + } + else if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((int a, int b) => { return a * b; }); + } + else + { + throw new NotSupportedException(); + } + } + + private static DualParamFn GetDivide() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((long a, long b) => { return a / b; }); + } + else if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((int a, int b) => { return a / b; }); + } + else + { + throw new NotSupportedException(); + } + } + + private static DualParamFn GetRoundUp() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((long a, long b) => { return ((a + b - 1) / b) * b; }); + } + else if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((int a, int b) => { return ((a + b - 1) / b) * b; }); + } + else + { + throw new NotSupportedException(); + } + } + + private static DualParamFn GetRoundDown() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((long a, long b) => { return (a / b) * b; }); + } + else if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((int a, int b) => { return (a / b) * b; }); + } + else + { + throw new NotSupportedException(); + } + } + + private static DualParamFn GetCeil() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((long a, long b) => { return (a + b - 1) / b; }); + } + else if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((int a, int b) => { return (a + b - 1) / b; }); + } + else + { + throw new NotSupportedException(); + } + } + } +} diff --git a/DiscUtils/ObjectCache.cs b/DiscUtils/ObjectCache.cs new file mode 100644 index 0000000..c86910e --- /dev/null +++ b/DiscUtils/ObjectCache.cs @@ -0,0 +1,150 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections; + using System.Collections.Generic; + + /// + /// Caches objects. + /// + /// The type of the object key. + /// The type of the objects to cache. + /// + /// Can be use for two purposes - to ensure there is only one instance of a given object, + /// and to prevent the need to recreate objects that are expensive to create. + /// + internal class ObjectCache + { + private const int MostRecentListSize = 20; + private const int PruneGap = 500; + + private Dictionary _entries; + private List> _recent; + private int _nextPruneCount; + + public ObjectCache() + { + _entries = new Dictionary(); + _recent = new List>(); + } + + public V this[K key] + { + get + { + for (int i = 0; i < _recent.Count; ++i) + { + KeyValuePair recentEntry = _recent[i]; + if (recentEntry.Key.Equals(key)) + { + MakeMostRecent(i); + return recentEntry.Value; + } + } + + WeakReference wRef; + if (_entries.TryGetValue(key, out wRef)) + { + V val = (V)wRef.Target; + if (val != null) + { + MakeMostRecent(key, val); + } + + return val; + } + + return default(V); + } + + set + { + _entries[key] = new WeakReference(value); + MakeMostRecent(key, value); + PruneEntries(); + } + } + + internal void Remove(K key) + { + for (int i = 0; i < _recent.Count; ++i) + { + if (_recent[i].Key.Equals(key)) + { + _recent.RemoveAt(i); + break; + } + } + + _entries.Remove(key); + } + + private void PruneEntries() + { + _nextPruneCount++; + + if (_nextPruneCount > PruneGap) + { + List toPrune = new List(); + foreach (var entry in _entries) + { + if (!entry.Value.IsAlive) + { + toPrune.Add(entry.Key); + } + } + + foreach (var key in toPrune) + { + _entries.Remove(key); + } + + _nextPruneCount = 0; + } + } + + private void MakeMostRecent(int i) + { + if (i == 0) + { + return; + } + + KeyValuePair entry = _recent[i]; + _recent.RemoveAt(i); + _recent.Insert(0, entry); + } + + private void MakeMostRecent(K key, V val) + { + while (_recent.Count >= MostRecentListSize) + { + _recent.RemoveAt(_recent.Count - 1); + } + + _recent.Insert(0, new KeyValuePair(key, val)); + } + } +} diff --git a/DiscUtils/Ownership.cs b/DiscUtils/Ownership.cs new file mode 100644 index 0000000..ff42a1f --- /dev/null +++ b/DiscUtils/Ownership.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// Enumeration used to indicate transfer of disposable objects. + /// + public enum Ownership + { + /// + /// Indicates there is no transfer of ownership. + /// + None, + + /// + /// Indicates ownership of the stream is transfered, the owner should dispose of the stream when appropriate. + /// + Dispose + } +} diff --git a/DiscUtils/Partitions/BiosExtendedPartitionTable.cs b/DiscUtils/Partitions/BiosExtendedPartitionTable.cs new file mode 100644 index 0000000..ae6f44a --- /dev/null +++ b/DiscUtils/Partitions/BiosExtendedPartitionTable.cs @@ -0,0 +1,117 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System.Collections.Generic; + using System.IO; + + internal class BiosExtendedPartitionTable + { + private Stream _disk; + private uint _firstSector; + + public BiosExtendedPartitionTable(Stream disk, uint firstSector) + { + _disk = disk; + _firstSector = firstSector; + } + + public BiosPartitionRecord[] GetPartitions() + { + List result = new List(); + + uint partPos = _firstSector; + while (partPos != 0) + { + _disk.Position = ((long)partPos) * Utilities.SectorSize; + byte[] sector = Utilities.ReadFully(_disk, Utilities.SectorSize); + if (sector[510] != 0x55 || sector[511] != 0xAA) + { + throw new IOException("Invalid extended partition sector"); + } + + uint nextPartPos = 0; + for (int offset = 0x1BE; offset <= 0x1EE; offset += 0x10) + { + BiosPartitionRecord thisPart = new BiosPartitionRecord(sector, offset, partPos, -1); + + if (thisPart.StartCylinder != 0 || thisPart.StartHead != 0 || thisPart.StartSector != 0) + { + if (thisPart.PartitionType != 0x05 && thisPart.PartitionType != 0x0F) + { + result.Add(thisPart); + } + else + { + nextPartPos = _firstSector + thisPart.LBAStart; + } + } + } + + partPos = nextPartPos; + } + + return result.ToArray(); + } + + /// + /// Gets all of the disk ranges containing partition table data. + /// + /// Set of stream extents, indicated as byte offset from the start of the disk. + public IEnumerable GetMetadataDiskExtents() + { + List extents = new List(); + + uint partPos = _firstSector; + while (partPos != 0) + { + extents.Add(new StreamExtent(((long)partPos) * Utilities.SectorSize, Utilities.SectorSize)); + + _disk.Position = ((long)partPos) * Utilities.SectorSize; + byte[] sector = Utilities.ReadFully(_disk, Utilities.SectorSize); + if (sector[510] != 0x55 || sector[511] != 0xAA) + { + throw new IOException("Invalid extended partition sector"); + } + + uint nextPartPos = 0; + for (int offset = 0x1BE; offset <= 0x1EE; offset += 0x10) + { + BiosPartitionRecord thisPart = new BiosPartitionRecord(sector, offset, partPos, -1); + + if (thisPart.StartCylinder != 0 || thisPart.StartHead != 0 || thisPart.StartSector != 0) + { + if (thisPart.PartitionType == 0x05 || thisPart.PartitionType == 0x0F) + { + nextPartPos = _firstSector + thisPart.LBAStart; + } + } + } + + partPos = nextPartPos; + } + + return extents; + } + } +} diff --git a/DiscUtils/Partitions/BiosPartitionInfo.cs b/DiscUtils/Partitions/BiosPartitionInfo.cs new file mode 100644 index 0000000..3412e8b --- /dev/null +++ b/DiscUtils/Partitions/BiosPartitionInfo.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + using System.IO; + + /// + /// Provides access to partition records in a BIOS (MBR) partition table. + /// + public sealed class BiosPartitionInfo : PartitionInfo + { + private BiosPartitionRecord _record; + private BiosPartitionTable _table; + + internal BiosPartitionInfo(BiosPartitionTable table, BiosPartitionRecord record) + { + _table = table; + _record = record; + } + + /// + /// Gets the first sector of the partion (relative to start of disk) as a Logical Block Address. + /// + public override long FirstSector + { + get { return _record.LBAStartAbsolute; } + } + + /// + /// Gets the last sector of the partion (relative to start of disk) as a Logical Block Address (inclusive). + /// + public override long LastSector + { + get { return _record.LBAStartAbsolute + _record.LBALength - 1; } + } + + /// + /// Always returns .Empty. + /// + public override Guid GuidType + { + get { return Guid.Empty; } + } + + /// + /// Gets the type of the partition. + /// + public override byte BiosType + { + get { return _record.PartitionType; } + } + + /// + /// Gets the type of the partition as a string. + /// + public override string TypeAsString + { + get { return _record.FriendlyPartitionType; } + } + + /// + /// Gets a value indicating whether this partition is active (bootable). + /// + public bool IsActive + { + get { return _record.Status != 0; } + } + + /// + /// Gets the start of the partition as a CHS address. + /// + public ChsAddress Start + { + get { return new ChsAddress(_record.StartCylinder, _record.StartHead, _record.StartSector); } + } + + /// + /// Gets the end (inclusive) of the partition as a CHS address. + /// + public ChsAddress End + { + get { return new ChsAddress(_record.EndCylinder, _record.EndHead, _record.EndSector); } + } + + /// + /// Gets the index of the partition in the primary partition table, or -1 if not a primary partition. + /// + public int PrimaryIndex + { + get { return _record.Index; } + } + + /// + /// Gets a value indicating whether the partition is a primary (rather than extended) partition. + /// + public bool IsPrimary + { + get { return PrimaryIndex >= 0; } + } + + internal override PhysicalVolumeType VolumeType + { + get { return PhysicalVolumeType.BiosPartition; } + } + + /// + /// Opens a stream to access the content of the partition. + /// + /// The new stream. + public override SparseStream Open() + { + return _table.Open(_record); + } + } +} diff --git a/DiscUtils/Partitions/BiosPartitionRecord.cs b/DiscUtils/Partitions/BiosPartitionRecord.cs new file mode 100644 index 0000000..c8aa8f6 --- /dev/null +++ b/DiscUtils/Partitions/BiosPartitionRecord.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + + internal class BiosPartitionRecord : IComparable + { + private uint _lbaOffset; + + private byte _status; + private ushort _startCylinder; + private byte _startHead; + private byte _startSector; + private byte _type; + private ushort _endCylinder; + private byte _endHead; + private byte _endSector; + private uint _lbaStart; + private uint _lbaLength; + private int _index; + + public BiosPartitionRecord() + { + } + + public BiosPartitionRecord(byte[] data, int offset, uint lbaOffset, int index) + { + _lbaOffset = lbaOffset; + + _status = data[offset]; + _startHead = data[offset + 1]; + _startSector = (byte)(data[offset + 2] & 0x3F); + _startCylinder = (ushort)(data[offset + 3] | ((data[offset + 2] & 0xC0) << 2)); + _type = data[offset + 4]; + _endHead = data[offset + 5]; + _endSector = (byte)(data[offset + 6] & 0x3F); + _endCylinder = (ushort)(data[offset + 7] | ((data[offset + 6] & 0xC0) << 2)); + _lbaStart = Utilities.ToUInt32LittleEndian(data, offset + 8); + _lbaLength = Utilities.ToUInt32LittleEndian(data, offset + 12); + _index = index; + } + + public bool IsValid + { + get + { + return _endHead != 0 || _endSector != 0 || _endCylinder != 0 || _lbaLength != 0; + } + } + + public byte Status + { + get { return _status; } + set { _status = value; } + } + + public ushort StartCylinder + { + get { return _startCylinder; } + set { _startCylinder = value; } + } + + public byte StartHead + { + get { return _startHead; } + set { _startHead = value; } + } + + public byte StartSector + { + get { return _startSector; } + set { _startSector = value; } + } + + public byte PartitionType + { + get { return _type; } + set { _type = value; } + } + + public string FriendlyPartitionType + { + get { return BiosPartitionTypes.ToString(_type); } + } + + public ushort EndCylinder + { + get { return _endCylinder; } + set { _endCylinder = value; } + } + + public byte EndHead + { + get { return _endHead; } + set { _endHead = value; } + } + + public byte EndSector + { + get { return _endSector; } + set { _endSector = value; } + } + + public uint LBAStart + { + get { return _lbaStart; } + set { _lbaStart = value; } + } + + public uint LBALength + { + get { return _lbaLength; } + set { _lbaLength = value; } + } + + public uint LBAStartAbsolute + { + get { return _lbaStart + _lbaOffset; } + } + + public int Index + { + get { return _index; } + } + + public int CompareTo(BiosPartitionRecord other) + { + return LBAStartAbsolute.CompareTo(other.LBAStartAbsolute); + } + + internal void WriteTo(byte[] buffer, int offset) + { + buffer[offset] = _status; + buffer[offset + 1] = _startHead; + buffer[offset + 2] = (byte)((_startSector & 0x3F) | ((_startCylinder >> 2) & 0xC0)); + buffer[offset + 3] = (byte)_startCylinder; + buffer[offset + 4] = _type; + buffer[offset + 5] = _endHead; + buffer[offset + 6] = (byte)((_endSector & 0x3F) | ((_endCylinder >> 2) & 0xC0)); + buffer[offset + 7] = (byte)_endCylinder; + Utilities.WriteBytesLittleEndian((uint)_lbaStart, buffer, offset + 8); + Utilities.WriteBytesLittleEndian((uint)_lbaLength, buffer, offset + 12); + } + } +} diff --git a/DiscUtils/Partitions/BiosPartitionTable.cs b/DiscUtils/Partitions/BiosPartitionTable.cs new file mode 100644 index 0000000..fd6cca3 --- /dev/null +++ b/DiscUtils/Partitions/BiosPartitionTable.cs @@ -0,0 +1,712 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Globalization; + using System.IO; + + /// + /// Represents a BIOS (MBR) Partition Table. + /// + public sealed class BiosPartitionTable : PartitionTable + { + private Stream _diskData; + private Geometry _diskGeometry; + + /// + /// Initializes a new instance of the BiosPartitionTable class. + /// + /// The disk containing the partition table. + public BiosPartitionTable(VirtualDisk disk) + { + Init(disk.Content, disk.BiosGeometry); + } + + /// + /// Initializes a new instance of the BiosPartitionTable class. + /// + /// The stream containing the disk data. + /// The geometry of the disk. + public BiosPartitionTable(Stream disk, Geometry diskGeometry) + { + Init(disk, diskGeometry); + } + + /// + /// Gets the GUID that uniquely identifies this disk, if supported (else returns null). + /// + public override Guid DiskGuid + { + get { return Guid.Empty; } + } + + /// + /// Gets a collection of the partitions for storing Operating System file-systems. + /// + public ReadOnlyCollection BiosUserPartitions + { + get + { + List result = new List(); + foreach (BiosPartitionRecord r in GetAllRecords()) + { + if (r.IsValid) + { + result.Add(new BiosPartitionInfo(this, r)); + } + } + + return new ReadOnlyCollection(result); + } + } + + /// + /// Gets a collection of the partitions for storing Operating System file-systems. + /// + public override ReadOnlyCollection Partitions + { + get + { + List result = new List(); + foreach (BiosPartitionRecord r in GetAllRecords()) + { + if (r.IsValid) + { + result.Add(new BiosPartitionInfo(this, r)); + } + } + + return new ReadOnlyCollection(result); + } + } + + /// + /// Makes a best guess at the geometry of a disk. + /// + /// String containing the disk image to detect the geometry from. + /// The detected geometry. + public static Geometry DetectGeometry(Stream disk) + { + if (disk.Length >= Utilities.SectorSize) + { + disk.Position = 0; + byte[] bootSector = Utilities.ReadFully(disk, Utilities.SectorSize); + if (bootSector[510] == 0x55 && bootSector[511] == 0xAA) + { + byte maxHead = 0; + byte maxSector = 0; + foreach (var record in ReadPrimaryRecords(bootSector)) + { + maxHead = Math.Max(maxHead, record.EndHead); + maxSector = Math.Max(maxSector, record.EndSector); + } + + if (maxHead > 0 && maxSector > 0) + { + int cylSize = (maxHead + 1) * maxSector * 512; + return new Geometry((int)Utilities.Ceil(disk.Length, cylSize), maxHead + 1, maxSector); + } + } + } + + return Geometry.FromCapacity(disk.Length); + } + + /// + /// Indicates if a stream contains a valid partition table. + /// + /// The stream to inspect. + /// true if the partition table is valid, else false. + public static bool IsValid(Stream disk) + { + if (disk.Length < Utilities.SectorSize) + { + return false; + } + + disk.Position = 0; + byte[] bootSector = Utilities.ReadFully(disk, Utilities.SectorSize); + + // Check for the 'bootable sector' marker + if (bootSector[510] != 0x55 || bootSector[511] != 0xAA) + { + return false; + } + + List knownPartitions = new List(); + foreach (var record in ReadPrimaryRecords(bootSector)) + { + // If the partition extends beyond the end of the disk, this is probably an invalid partition table + if (record.LBALength != 0xFFFFFFFF && (record.LBAStart + (long)record.LBALength) * Sizes.Sector > disk.Length) + { + return false; + } + + if (record.LBALength > 0) + { + StreamExtent[] thisPartitionExtents = new StreamExtent[] { new StreamExtent(record.LBAStart, record.LBALength) }; + + // If the partition intersects another partition, this is probably an invalid partition table + foreach (var overlap in StreamExtent.Intersect(knownPartitions, thisPartitionExtents)) + { + return false; + } + + knownPartitions = new List(StreamExtent.Union(knownPartitions, thisPartitionExtents)); + } + } + + return true; + } + + /// + /// Creates a new partition table on a disk. + /// + /// The disk to initialize. + /// An object to access the newly created partition table. + public static BiosPartitionTable Initialize(VirtualDisk disk) + { + return Initialize(disk.Content, disk.BiosGeometry); + } + + /// + /// Creates a new partition table on a disk containing a single partition. + /// + /// The disk to initialize. + /// The partition type for the single partition. + /// An object to access the newly created partition table. + public static BiosPartitionTable Initialize(VirtualDisk disk, WellKnownPartitionType type) + { + BiosPartitionTable table = Initialize(disk.Content, disk.BiosGeometry); + table.Create(type, true); + return table; + } + + /// + /// Creates a new partition table on a disk. + /// + /// The stream containing the disk data. + /// The geometry of the disk. + /// An object to access the newly created partition table. + public static BiosPartitionTable Initialize(Stream disk, Geometry diskGeometry) + { + Stream data = disk; + + byte[] bootSector; + if (data.Length >= Utilities.SectorSize) + { + data.Position = 0; + bootSector = Utilities.ReadFully(data, Utilities.SectorSize); + } + else + { + bootSector = new byte[Utilities.SectorSize]; + } + + // Wipe all four 16-byte partition table entries + Array.Clear(bootSector, 0x01BE, 16 * 4); + + // Marker bytes + bootSector[510] = 0x55; + bootSector[511] = 0xAA; + + data.Position = 0; + data.Write(bootSector, 0, bootSector.Length); + + return new BiosPartitionTable(disk, diskGeometry); + } + + /// + /// Creates a new partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + public override int Create(WellKnownPartitionType type, bool active) + { + Geometry allocationGeometry = new Geometry(_diskData.Length, _diskGeometry.HeadsPerCylinder, _diskGeometry.SectorsPerTrack, _diskGeometry.BytesPerSector); + + ChsAddress start = new ChsAddress(0, 1, 1); + ChsAddress last = allocationGeometry.LastSector; + + long startLba = allocationGeometry.ToLogicalBlockAddress(start); + long lastLba = allocationGeometry.ToLogicalBlockAddress(last); + + return CreatePrimaryByCylinder(0, allocationGeometry.Cylinders - 1, ConvertType(type, (lastLba - startLba) * Utilities.SectorSize), active); + } + + /// + /// Creates a new primary partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the new partition. + public override int Create(long size, WellKnownPartitionType type, bool active) + { + int cylinderCapacity = _diskGeometry.SectorsPerTrack * _diskGeometry.HeadsPerCylinder * _diskGeometry.BytesPerSector; + int numCylinders = (int)(size / cylinderCapacity); + + int startCylinder = FindCylinderGap(numCylinders); + + return CreatePrimaryByCylinder(startCylinder, startCylinder + numCylinders - 1, ConvertType(type, size), active); + } + + /// + /// Creates a new aligned partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in bytes). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is acheived by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(WellKnownPartitionType type, bool active, int alignment) + { + Geometry allocationGeometry = new Geometry(_diskData.Length, _diskGeometry.HeadsPerCylinder, _diskGeometry.SectorsPerTrack, _diskGeometry.BytesPerSector); + + ChsAddress start = new ChsAddress(0, 1, 1); + + long startLba = Utilities.RoundUp(allocationGeometry.ToLogicalBlockAddress(start), alignment / _diskGeometry.BytesPerSector); + long lastLba = Utilities.RoundDown(_diskData.Length / _diskGeometry.BytesPerSector, alignment / _diskGeometry.BytesPerSector); + + return CreatePrimaryBySector(startLba, lastLba - 1, ConvertType(type, (lastLba - startLba) * _diskGeometry.BytesPerSector), active); + } + + /// + /// Creates a new aligned partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in bytes). + /// The index of the new partition. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is achieved by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(long size, WellKnownPartitionType type, bool active, int alignment) + { + if (size < _diskGeometry.BytesPerSector) + { + throw new ArgumentOutOfRangeException("size", size, "size must be at least one sector"); + } + + if (alignment % _diskGeometry.BytesPerSector != 0) + { + throw new ArgumentException("Alignment is not a multiple of the sector size"); + } + + if (size % alignment != 0) + { + throw new ArgumentException("Size is not a multiple of the alignment"); + } + + long sectorLength = size / _diskGeometry.BytesPerSector; + long start = FindGap(size / _diskGeometry.BytesPerSector, alignment / _diskGeometry.BytesPerSector); + + return CreatePrimaryBySector(start, start + sectorLength - 1, ConvertType(type, sectorLength * Utilities.SectorSize), active); + } + + /// + /// Deletes a partition at a given index. + /// + /// The index of the partition. + public override void Delete(int index) + { + WriteRecord(index, new BiosPartitionRecord()); + } + + /// + /// Creates a new Primary Partition that occupies whole cylinders, for best compatibility. + /// + /// The first cylinder to include in the partition (inclusive). + /// The last cylinder to include in the partition (inclusive). + /// The BIOS (MBR) type of the new partition. + /// Whether to mark the partition active (bootable). + /// The index of the new partition. + /// If the cylinder 0 is given, the first track will not be used, to reserve space + /// for the meta-data at the start of the disk. + public int CreatePrimaryByCylinder(int first, int last, byte type, bool markActive) + { + if (first < 0) + { + throw new ArgumentOutOfRangeException("first", first, "First cylinder must be Zero or greater"); + } + + if (last <= first) + { + throw new ArgumentException("Last cylinder must be greater than first"); + } + + long lbaStart = (first == 0) ? _diskGeometry.ToLogicalBlockAddress(0, 1, 1) : _diskGeometry.ToLogicalBlockAddress(first, 0, 1); + long lbaLast = _diskGeometry.ToLogicalBlockAddress(last, _diskGeometry.HeadsPerCylinder - 1, _diskGeometry.SectorsPerTrack); + + return CreatePrimaryBySector(lbaStart, lbaLast, type, markActive); + } + + /// + /// Creates a new Primary Partition, specified by Logical Block Addresses. + /// + /// The LBA address of the first sector (inclusive). + /// The LBA address of the last sector (inclusive). + /// The BIOS (MBR) type of the new partition. + /// Whether to mark the partition active (bootable). + /// The index of the new partition. + public int CreatePrimaryBySector(long first, long last, byte type, bool markActive) + { + if (first >= last) + { + throw new ArgumentException("The first sector in a partition must be before the last"); + } + + if ((last + 1) * _diskGeometry.BytesPerSector > _diskData.Length) + { + throw new ArgumentOutOfRangeException("last", last, "The last sector extends beyond the end of the disk"); + } + + BiosPartitionRecord[] existing = GetPrimaryRecords(); + + BiosPartitionRecord newRecord = new BiosPartitionRecord(); + ChsAddress startAddr = _diskGeometry.ToChsAddress(first); + ChsAddress endAddr = _diskGeometry.ToChsAddress(last); + + // Because C/H/S addresses can max out at lower values than the LBA values, + // the special tuple (1023, 254, 63) is used. + if (startAddr.Cylinder > 1023) + { + startAddr = new ChsAddress(1023, 254, 63); + } + + if (endAddr.Cylinder > 1023) + { + endAddr = new ChsAddress(1023, 254, 63); + } + + newRecord.StartCylinder = (ushort)startAddr.Cylinder; + newRecord.StartHead = (byte)startAddr.Head; + newRecord.StartSector = (byte)startAddr.Sector; + newRecord.EndCylinder = (ushort)endAddr.Cylinder; + newRecord.EndHead = (byte)endAddr.Head; + newRecord.EndSector = (byte)endAddr.Sector; + newRecord.LBAStart = (uint)first; + newRecord.LBALength = (uint)(last - first + 1); + newRecord.PartitionType = type; + newRecord.Status = (byte)(markActive ? 0x80 : 0x00); + + // First check for overlap with existing partition... + foreach (var r in existing) + { + if (Utilities.RangesOverlap((uint)first, (uint)last + 1, r.LBAStartAbsolute, r.LBAStartAbsolute + r.LBALength)) + { + throw new IOException("New partition overlaps with existing partition"); + } + } + + // Now look for empty partition + for (int i = 0; i < 4; ++i) + { + if (!existing[i].IsValid) + { + WriteRecord(i, newRecord); + return i; + } + } + + throw new IOException("No primary partition slots available"); + } + + /// + /// Sets the active partition. + /// + /// The index of the primary partition to mark bootable, or -1 for none. + /// The supplied index is the index within the primary partition, see PrimaryIndex on BiosPartitionInfo. + public void SetActivePartition(int index) + { + List records = new List(GetPrimaryRecords()); + + for (int i = 0; i < records.Count; ++i) + { + records[i].Status = (i == index) ? (byte)0x80 : (byte)0x00; + WriteRecord(i, records[i]); + } + } + + /// + /// Gets all of the disk ranges containing partition table metadata. + /// + /// Set of stream extents, indicated as byte offset from the start of the disk. + public IEnumerable GetMetadataDiskExtents() + { + List extents = new List(); + + extents.Add(new StreamExtent(0, Sizes.Sector)); + + foreach (BiosPartitionRecord primaryRecord in GetPrimaryRecords()) + { + if (primaryRecord.IsValid) + { + if (IsExtendedPartition(primaryRecord)) + { + extents.AddRange(new BiosExtendedPartitionTable(_diskData, primaryRecord.LBAStart).GetMetadataDiskExtents()); + } + } + } + + return extents; + } + + /// + /// Updates the CHS fields in partition records to reflect a new BIOS geometry. + /// + /// The disk's new BIOS geometry. + /// The partitions are not relocated to a cylinder boundary, just the CHS fields are updated on the + /// assumption the LBA fields are definitive. + public void UpdateBiosGeometry(Geometry geometry) + { + _diskData.Position = 0; + byte[] bootSector = Utilities.ReadFully(_diskData, Utilities.SectorSize); + + BiosPartitionRecord[] records = ReadPrimaryRecords(bootSector); + for (int i = 0; i < records.Length; ++i) + { + BiosPartitionRecord record = records[i]; + if (record.IsValid) + { + ChsAddress newStartAddress = geometry.ToChsAddress(record.LBAStartAbsolute); + if (newStartAddress.Cylinder > 1023) + { + newStartAddress = new ChsAddress(1023, geometry.HeadsPerCylinder - 1, geometry.SectorsPerTrack); + } + + ChsAddress newEndAddress = geometry.ToChsAddress(record.LBAStartAbsolute + record.LBALength - 1); + if (newEndAddress.Cylinder > 1023) + { + newEndAddress = new ChsAddress(1023, geometry.HeadsPerCylinder - 1, geometry.SectorsPerTrack); + } + + record.StartCylinder = (ushort)newStartAddress.Cylinder; + record.StartHead = (byte)newStartAddress.Head; + record.StartSector = (byte)newStartAddress.Sector; + record.EndCylinder = (ushort)newEndAddress.Cylinder; + record.EndHead = (byte)newEndAddress.Head; + record.EndSector = (byte)newEndAddress.Sector; + + WriteRecord(i, record); + } + } + + _diskGeometry = geometry; + } + + internal SparseStream Open(BiosPartitionRecord record) + { + return new SubStream(_diskData, Ownership.None, ((long)record.LBAStartAbsolute) * _diskGeometry.BytesPerSector, ((long)record.LBALength) * _diskGeometry.BytesPerSector); + } + + private static BiosPartitionRecord[] ReadPrimaryRecords(byte[] bootSector) + { + BiosPartitionRecord[] records = new BiosPartitionRecord[4]; + for (int i = 0; i < 4; ++i) + { + records[i] = new BiosPartitionRecord(bootSector, 0x01BE + (i * 0x10), 0, i); + } + + return records; + } + + private static bool IsExtendedPartition(BiosPartitionRecord r) + { + return r.PartitionType == BiosPartitionTypes.Extended || r.PartitionType == BiosPartitionTypes.ExtendedLba; + } + + private static byte ConvertType(WellKnownPartitionType type, long size) + { + switch (type) + { + case WellKnownPartitionType.WindowsFat: + if (size < 512 * Sizes.OneMiB) + { + return BiosPartitionTypes.Fat16; + } + else if (size < 1023 * (long)254 * 63 * 512) + { + // Max BIOS size + return BiosPartitionTypes.Fat32; + } + else + { + return BiosPartitionTypes.Fat32Lba; + } + + case WellKnownPartitionType.WindowsNtfs: + return BiosPartitionTypes.Ntfs; + case WellKnownPartitionType.Linux: + return BiosPartitionTypes.LinuxNative; + case WellKnownPartitionType.LinuxSwap: + return BiosPartitionTypes.LinuxSwap; + case WellKnownPartitionType.LinuxLvm: + return BiosPartitionTypes.LinuxLvm; + default: + throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, "Unrecognized partition type: '{0}'", type), "type"); + } + } + + private BiosPartitionRecord[] GetAllRecords() + { + List newList = new List(); + + foreach (BiosPartitionRecord primaryRecord in GetPrimaryRecords()) + { + if (primaryRecord.IsValid) + { + if (IsExtendedPartition(primaryRecord)) + { + newList.AddRange(GetExtendedRecords(primaryRecord)); + } + else + { + newList.Add(primaryRecord); + } + } + } + + return newList.ToArray(); + } + + private BiosPartitionRecord[] GetPrimaryRecords() + { + _diskData.Position = 0; + byte[] bootSector = Utilities.ReadFully(_diskData, Utilities.SectorSize); + + return ReadPrimaryRecords(bootSector); + } + + private BiosPartitionRecord[] GetExtendedRecords(BiosPartitionRecord r) + { + return new BiosExtendedPartitionTable(_diskData, r.LBAStart).GetPartitions(); + } + + private void WriteRecord(int i, BiosPartitionRecord newRecord) + { + _diskData.Position = 0; + byte[] bootSector = Utilities.ReadFully(_diskData, Utilities.SectorSize); + newRecord.WriteTo(bootSector, 0x01BE + (i * 16)); + _diskData.Position = 0; + _diskData.Write(bootSector, 0, bootSector.Length); + } + + private int FindCylinderGap(int numCylinders) + { + var list = Utilities.Filter, BiosPartitionRecord>(GetPrimaryRecords(), (r) => r.IsValid); + list.Sort(); + + int startCylinder = 0; + foreach (var r in list) + { + int existingStart = r.StartCylinder; + int existingEnd = r.EndCylinder; + + // LBA can represent bigger disk locations than CHS, so assume the LBA to be definitive in the case where it + // appears the CHS address has been truncated. + if (r.LBAStart > _diskGeometry.ToLogicalBlockAddress(r.StartCylinder, r.StartHead, r.StartSector)) + { + existingStart = _diskGeometry.ToChsAddress((int)r.LBAStart).Cylinder; + } + + if (r.LBAStart + r.LBALength > _diskGeometry.ToLogicalBlockAddress(r.EndCylinder, r.EndHead, r.EndSector)) + { + existingEnd = _diskGeometry.ToChsAddress((int)(r.LBAStart + r.LBALength)).Cylinder; + } + + if (!Utilities.RangesOverlap(startCylinder, startCylinder + numCylinders - 1, existingStart, existingEnd)) + { + break; + } + else + { + startCylinder = existingEnd + 1; + } + } + + return startCylinder; + } + + private long FindGap(long numSectors, long alignmentSectors) + { + var list = Utilities.Filter, BiosPartitionRecord>(GetPrimaryRecords(), (r) => r.IsValid); + list.Sort(); + + long startSector = Utilities.RoundUp(_diskGeometry.ToLogicalBlockAddress(0, 1, 1), alignmentSectors); + + int idx = 0; + while (idx < list.Count) + { + var entry = list[idx]; + while (idx < list.Count && startSector >= entry.LBAStartAbsolute + entry.LBALength) + { + idx++; + entry = list[idx]; + } + + if (Utilities.RangesOverlap(startSector, startSector + numSectors, entry.LBAStartAbsolute, entry.LBAStartAbsolute + entry.LBALength)) + { + startSector = Utilities.RoundUp(entry.LBAStartAbsolute + entry.LBALength, alignmentSectors); + } + + idx++; + } + + if (_diskGeometry.TotalSectorsLong - startSector < numSectors) + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, "Unable to find free space of {0} sectors", numSectors)); + } + + return startSector; + } + + private void Init(Stream disk, Geometry diskGeometry) + { + _diskData = disk; + _diskGeometry = diskGeometry; + + _diskData.Position = 0; + byte[] bootSector = Utilities.ReadFully(_diskData, Utilities.SectorSize); + if (bootSector[510] != 0x55 || bootSector[511] != 0xAA) + { + throw new IOException("Invalid boot sector - no magic number 0xAA55"); + } + } + } +} diff --git a/DiscUtils/Partitions/BiosPartitionTypes.cs b/DiscUtils/Partitions/BiosPartitionTypes.cs new file mode 100644 index 0000000..96da403 --- /dev/null +++ b/DiscUtils/Partitions/BiosPartitionTypes.cs @@ -0,0 +1,157 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + /// + /// Convenient access to well-known BIOS (MBR) Partition Types. + /// + public static class BiosPartitionTypes + { + /// + /// Microsoft FAT12 (fewer than 32,680 sectors in the volume). + /// + public const byte Fat12 = 0x01; + + /// + /// Microsoft FAT16 (32,680–65,535 sectors or 16 MB–33 MB). + /// + public const byte Fat16Small = 0x04; + + /// + /// Extended Partition (contains other partitions). + /// + public const byte Extended = 0x05; + + /// + /// Microsoft BIGDOS FAT16 (33 MB–4 GB). + /// + public const byte Fat16 = 0x06; + + /// + /// Installable File System (NTFS). + /// + public const byte Ntfs = 0x07; + + /// + /// Microsoft FAT32. + /// + public const byte Fat32 = 0x0B; + + /// + /// Microsoft FAT32, accessed using Int13h BIOS LBA extensions. + /// + public const byte Fat32Lba = 0x0C; + + /// + /// Microsoft BIGDOS FAT16, accessed using Int13h BIOS LBA extensions. + /// + public const byte Fat16Lba = 0x0E; + + /// + /// Extended Partition (contains other partitions), accessed using Int13h BIOS LBA extensions. + /// + public const byte ExtendedLba = 0x0F; + + /// + /// Windows Logical Disk Manager dynamic volume. + /// + public const byte WindowsDynamicVolume = 0x42; + + /// + /// Linux Swap. + /// + public const byte LinuxSwap = 0x82; + + /// + /// Linux Native (ext2 and friends). + /// + public const byte LinuxNative = 0x83; + + /// + /// Linux Logical Volume Manager (LVM). + /// + public const byte LinuxLvm = 0x8E; + + /// + /// GUID Partition Table (GPT) protective partition, fills entire disk. + /// + public const byte GptProtective = 0xEE; + + /// + /// EFI System partition on an MBR disk. + /// + public const byte EfiSystem = 0xEF; + + /// + /// Provides a string representation of some known BIOS partition types. + /// + /// The partition type to represent as a string. + /// The string representation. + public static string ToString(byte type) + { + switch (type) + { + case 0x00: return "Unused"; + case 0x01: return "FAT12"; + case 0x02: return "XENIX root"; + case 0x03: return "XENIX /usr"; + case 0x04: return "FAT16 (<32M)"; + case 0x05: return "Extended (non-LBA)"; + case 0x06: return "FAT16 (>32M)"; + case 0x07: return "IFS (NTFS or HPFS)"; + case 0x0B: return "FAT32 (non-LBA)"; + case 0x0C: return "FAT32 (LBA)"; + case 0x0E: return "FAT16 (LBA)"; + case 0x0F: return "Extended (LBA)"; + case 0x11: return "Hidden FAT12"; + case 0x12: return "Vendor Config/Recovery/Diagnostics"; + case 0x14: return "Hidden FAT16 (<32M)"; + case 0x16: return "Hidden FAT16 (>32M)"; + case 0x17: return "Hidden IFS (NTFS or HPFS)"; + case 0x1B: return "Hidden FAT32 (non-LBA)"; + case 0x1C: return "Hidden FAT32 (LBA)"; + case 0x1E: return "Hidden FAT16 (LBA)"; + case 0x27: return "Windows Recovery Environment"; + case 0x42: return "Windows Dynamic Volume"; + case 0x80: return "Minix v1.1 - v1.4a"; + case 0x81: return "Minix / Early Linux"; + case 0x82: return "Linux Swap"; + case 0x83: return "Linux Native"; + case 0x84: return "Hibernation"; + case 0x8E: return "Linux LVM"; + case 0xA0: return "Laptop Hibernation"; + case 0xA8: return "Mac OS-X"; + case 0xAB: return "Mac OS-X Boot"; + case 0xAF: return "Mac OS-X HFS"; + case 0xC0: return "NTFT"; + case 0xDE: return "Dell OEM"; + case 0xEE: return "GPT Protective"; + case 0xEF: return "EFI"; + case 0xFB: return "VMware File System"; + case 0xFC: return "VMware Swap"; + case 0xFE: return "IBM OEM"; + default: return "Unknown"; + } + } + } +} diff --git a/DiscUtils/Partitions/BiosPartitionedDiskBuilder.cs b/DiscUtils/Partitions/BiosPartitionedDiskBuilder.cs new file mode 100644 index 0000000..62e614a --- /dev/null +++ b/DiscUtils/Partitions/BiosPartitionedDiskBuilder.cs @@ -0,0 +1,169 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Builds a stream with the contents of a BIOS partitioned disk. + /// + /// + /// This class assembles a disk image dynamically in memory. The + /// constructed stream will read data from the partition content + /// streams only when a client of this class tries to read from + /// that partition. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "SparseMemoryStream holds no resources")] + public class BiosPartitionedDiskBuilder : StreamBuilder + { + private long _capacity; + private BiosPartitionTable _partitionTable; + private Geometry _biosGeometry; + private SparseMemoryStream _bootSectors; + + private Dictionary _partitionContents; + + /// + /// Initializes a new instance of the BiosPartitionedDiskBuilder class. + /// + /// The capacity of the disk (in bytes). + /// The BIOS geometry of the disk. + public BiosPartitionedDiskBuilder(long capacity, Geometry biosGeometry) + { + _capacity = capacity; + _biosGeometry = biosGeometry; + + _bootSectors = new SparseMemoryStream(); + _bootSectors.SetLength(capacity); + _partitionTable = BiosPartitionTable.Initialize(_bootSectors, _biosGeometry); + + _partitionContents = new Dictionary(); + } + + /// + /// Initializes a new instance of the BiosPartitionedDiskBuilder class. + /// + /// The capacity of the disk (in bytes). + /// The boot sector(s) of the disk. + /// The BIOS geometry of the disk. + [Obsolete("Use the variant that takes VirtualDisk, this method breaks for disks with extended partitions", false)] + public BiosPartitionedDiskBuilder(long capacity, byte[] bootSectors, Geometry biosGeometry) + { + if (bootSectors == null) + { + throw new ArgumentNullException("bootSectors"); + } + + _capacity = capacity; + _biosGeometry = biosGeometry; + + _bootSectors = new SparseMemoryStream(); + _bootSectors.SetLength(capacity); + _bootSectors.Write(bootSectors, 0, bootSectors.Length); + _partitionTable = new BiosPartitionTable(_bootSectors, biosGeometry); + + _partitionContents = new Dictionary(); + } + + /// + /// Initializes a new instance of the BiosPartitionedDiskBuilder class by + /// cloning the partition structure of a source disk. + /// + /// The disk to clone. + public BiosPartitionedDiskBuilder(VirtualDisk sourceDisk) + { + if (sourceDisk == null) + { + throw new ArgumentNullException("sourceDisk"); + } + + _capacity = sourceDisk.Capacity; + _biosGeometry = sourceDisk.BiosGeometry; + + _bootSectors = new SparseMemoryStream(); + _bootSectors.SetLength(_capacity); + + foreach (var extent in new BiosPartitionTable(sourceDisk).GetMetadataDiskExtents()) + { + sourceDisk.Content.Position = extent.Start; + byte[] buffer = Utilities.ReadFully(sourceDisk.Content, (int)extent.Length); + _bootSectors.Position = extent.Start; + _bootSectors.Write(buffer, 0, buffer.Length); + } + + _partitionTable = new BiosPartitionTable(_bootSectors, _biosGeometry); + + _partitionContents = new Dictionary(); + } + + /// + /// Gets the partition table in the disk. + /// + public BiosPartitionTable PartitionTable + { + get { return _partitionTable; } + } + + /// + /// Sets a stream representing the content of a partition in the partition table. + /// + /// The index of the partition. + /// The stream with the contents of the partition. + public void SetPartitionContent(int index, SparseStream stream) + { + _partitionContents[index] = new BuilderSparseStreamExtent(_partitionTable[index].FirstSector * Sizes.Sector, stream); + } + + /// + /// Updates the CHS fields in partition records to reflect a new BIOS geometry. + /// + /// The disk's new BIOS geometry. + /// The partitions are not relocated to a cylinder boundary, just the CHS fields are updated on the + /// assumption the LBA fields are definitive. + public void UpdateBiosGeometry(Geometry geometry) + { + _partitionTable.UpdateBiosGeometry(geometry); + _biosGeometry = geometry; + } + + internal override List FixExtents(out long totalLength) + { + totalLength = _capacity; + + List extents = new List(); + + foreach (var extent in _partitionTable.GetMetadataDiskExtents()) + { + _bootSectors.Position = extent.Start; + byte[] buffer = Utilities.ReadFully(_bootSectors, (int)extent.Length); + + extents.Add(new BuilderBufferExtent(extent.Start, buffer)); + } + + extents.AddRange(_partitionContents.Values); + return extents; + } + } +} diff --git a/DiscUtils/Partitions/DefaultPartitionTableFactory.cs b/DiscUtils/Partitions/DefaultPartitionTableFactory.cs new file mode 100644 index 0000000..049f1b3 --- /dev/null +++ b/DiscUtils/Partitions/DefaultPartitionTableFactory.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System.IO; + + [PartitionTableFactory] + internal sealed class DefaultPartitionTableFactory : PartitionTableFactory + { + public override bool DetectIsPartitioned(Stream s) + { + return BiosPartitionTable.IsValid(s); + } + + public override PartitionTable DetectPartitionTable(VirtualDisk disk) + { + if (BiosPartitionTable.IsValid(disk.Content)) + { + BiosPartitionTable table = new BiosPartitionTable(disk); + if (table.Count == 1 && table[0].BiosType == BiosPartitionTypes.GptProtective) + { + return new GuidPartitionTable(disk); + } + else + { + return table; + } + } + else + { + return null; + } + } + } +} diff --git a/DiscUtils/Partitions/GptEntry.cs b/DiscUtils/Partitions/GptEntry.cs new file mode 100644 index 0000000..281922b --- /dev/null +++ b/DiscUtils/Partitions/GptEntry.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + using System.Text; + + internal class GptEntry : IComparable + { + public Guid PartitionType; + public Guid Identity; + public long FirstUsedLogicalBlock; + public long LastUsedLogicalBlock; + public ulong Attributes; + public string Name; + + public GptEntry() + { + PartitionType = Guid.Empty; + Identity = Guid.Empty; + Name = string.Empty; + } + + public string FriendlyPartitionType + { + get + { + switch (PartitionType.ToString().ToUpperInvariant()) + { + case "00000000-0000-0000-0000-000000000000": return "Unused"; + case "024DEE41-33E7-11D3-9D69-0008C781F39F": return "MBR Partition Scheme"; + case "C12A7328-F81F-11D2-BA4B-00A0C93EC93B": return "EFI System"; + case "21686148-6449-6E6F-744E-656564454649": return "BIOS Boot"; + case "E3C9E316-0B5C-4DB8-817D-F92DF00215AE": return "Microsoft Reserved"; + case "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7": return "Windows Basic Data"; + case "5808C8AA-7E8F-42E0-85D2-E1E90434CFB3": return "Windows Logical Disk Manager Metadata"; + case "AF9B60A0-1431-4F62-BC68-3311714A69AD": return "Windows Logical Disk Manager Data"; + case "75894C1E-3AEB-11D3-B7C1-7B03A0000000": return "HP-UX Data"; + case "E2A1E728-32E3-11D6-A682-7B03A0000000": return "HP-UX Service"; + case "A19D880F-05FC-4D3B-A006-743F0F84911E": return "Linux RAID"; + case "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F": return "Linux Swap"; + case "E6D6D379-F507-44C2-A23C-238F2A3DF928": return "Linux Logical Volume Manager"; + case "83BD6B9D-7F41-11DC-BE0B-001560B84F0F": return "FreeBSD Boot"; + case "516E7CB4-6ECF-11D6-8FF8-00022D09712B": return "FreeBSD Data"; + case "516E7CB5-6ECF-11D6-8FF8-00022D09712B": return "FreeBSD Swap"; + case "516E7CB6-6ECF-11D6-8FF8-00022D09712B": return "FreeBSD Unix File System"; + case "516E7CB8-6ECF-11D6-8FF8-00022D09712B": return "FreeBSD Vinum volume manager"; + case "516E7CBA-6ECF-11D6-8FF8-00022D09712B": return "FreeBSD ZFS"; + case "48465300-0000-11AA-AA11-00306543ECAC": return "Mac OS X HFS+"; + case "55465300-0000-11AA-AA11-00306543ECAC": return "Mac OS X UFS"; + case "6A898CC3-1DD2-11B2-99A6-080020736631": return "Mac OS X ZFS"; + case "52414944-0000-11AA-AA11-00306543ECAC": return "Mac OS X RAID"; + case "52414944-5F4F-11AA-AA11-00306543ECAC": return "Mac OS X RAID, Offline"; + case "426F6F74-0000-11AA-AA11-00306543ECAC": return "Mac OS X Boot"; + case "4C616265-6C00-11AA-AA11-00306543ECAC": return "Mac OS X Label"; + case "49F48D32-B10E-11DC-B99B-0019D1879648": return "NetBSD Swap"; + case "49F48D5A-B10E-11DC-B99B-0019D1879648": return "NetBSD Fast File System"; + case "49F48D82-B10E-11DC-B99B-0019D1879648": return "NetBSD Log-Structed File System"; + case "49F48DAA-B10E-11DC-B99B-0019D1879648": return "NetBSD RAID"; + case "2DB519C4-B10F-11DC-B99B-0019D1879648": return "NetBSD Concatenated"; + case "2DB519EC-B10F-11DC-B99B-0019D1879648": return "NetBSD Encrypted"; + default: return "Unknown"; + } + } + } + + public void ReadFrom(byte[] buffer, int offset) + { + PartitionType = Utilities.ToGuidLittleEndian(buffer, offset + 0); + Identity = Utilities.ToGuidLittleEndian(buffer, offset + 16); + FirstUsedLogicalBlock = Utilities.ToInt64LittleEndian(buffer, offset + 32); + LastUsedLogicalBlock = Utilities.ToInt64LittleEndian(buffer, offset + 40); + Attributes = Utilities.ToUInt64LittleEndian(buffer, offset + 48); + Name = Encoding.Unicode.GetString(buffer, offset + 56, 72).TrimEnd(new char[] { '\0' }); + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(PartitionType, buffer, offset + 0); + Utilities.WriteBytesLittleEndian(Identity, buffer, offset + 16); + Utilities.WriteBytesLittleEndian(FirstUsedLogicalBlock, buffer, offset + 32); + Utilities.WriteBytesLittleEndian(LastUsedLogicalBlock, buffer, offset + 40); + Utilities.WriteBytesLittleEndian(Attributes, buffer, offset + 48); + Encoding.Unicode.GetBytes(Name + new string('\0', 36), 0, 36, buffer, offset + 56); + } + + public int CompareTo(GptEntry other) + { + return FirstUsedLogicalBlock.CompareTo(other.FirstUsedLogicalBlock); + } + } +} diff --git a/DiscUtils/Partitions/GptHeader.cs b/DiscUtils/Partitions/GptHeader.cs new file mode 100644 index 0000000..eef965a --- /dev/null +++ b/DiscUtils/Partitions/GptHeader.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + + internal class GptHeader + { + public const string GptSignature = "EFI PART"; + + public string Signature; + public uint Version; + public int HeaderSize; + public uint Crc; + public long HeaderLba; + public long AlternateHeaderLba; + public long FirstUsable; + public long LastUsable; + public Guid DiskGuid; + public long PartitionEntriesLba; + public uint PartitionEntryCount; + public int PartitionEntrySize; + public uint EntriesCrc; + + public byte[] Buffer; + + public GptHeader(int sectorSize) + { + Signature = GptSignature; + Version = 0x00010000; + HeaderSize = 92; + Buffer = new byte[sectorSize]; + } + + public GptHeader(GptHeader toCopy) + { + Signature = toCopy.Signature; + Version = toCopy.Version; + HeaderSize = toCopy.HeaderSize; + Crc = toCopy.Crc; + HeaderLba = toCopy.HeaderLba; + AlternateHeaderLba = toCopy.AlternateHeaderLba; + FirstUsable = toCopy.FirstUsable; + LastUsable = toCopy.LastUsable; + DiskGuid = toCopy.DiskGuid; + PartitionEntriesLba = toCopy.PartitionEntriesLba; + PartitionEntryCount = toCopy.PartitionEntryCount; + PartitionEntrySize = toCopy.PartitionEntrySize; + EntriesCrc = toCopy.EntriesCrc; + + Buffer = new byte[toCopy.Buffer.Length]; + Array.Copy(toCopy.Buffer, Buffer, Buffer.Length); + } + + public bool ReadFrom(byte[] buffer, int offset) + { + Signature = Utilities.BytesToString(buffer, offset + 0, 8); + Version = Utilities.ToUInt32LittleEndian(buffer, offset + 8); + HeaderSize = Utilities.ToInt32LittleEndian(buffer, offset + 12); + Crc = Utilities.ToUInt32LittleEndian(buffer, offset + 16); + HeaderLba = Utilities.ToInt64LittleEndian(buffer, offset + 24); + AlternateHeaderLba = Utilities.ToInt64LittleEndian(buffer, offset + 32); + FirstUsable = Utilities.ToInt64LittleEndian(buffer, offset + 40); + LastUsable = Utilities.ToInt64LittleEndian(buffer, offset + 48); + DiskGuid = Utilities.ToGuidLittleEndian(buffer, offset + 56); + PartitionEntriesLba = Utilities.ToInt64LittleEndian(buffer, offset + 72); + PartitionEntryCount = Utilities.ToUInt32LittleEndian(buffer, offset + 80); + PartitionEntrySize = Utilities.ToInt32LittleEndian(buffer, offset + 84); + EntriesCrc = Utilities.ToUInt32LittleEndian(buffer, offset + 88); + + // In case the header has new fields unknown to us, store the entire header + // as a byte array + Buffer = new byte[HeaderSize]; + Array.Copy(buffer, offset, Buffer, 0, HeaderSize); + + // Reject obviously invalid data + if (Signature != GptSignature || HeaderSize == 0) + { + return false; + } + + return Crc == CalcCrc(buffer, offset, HeaderSize); + } + + public void WriteTo(byte[] buffer, int offset) + { + // First, copy the cached header to allow for unknown fields + Array.Copy(Buffer, 0, buffer, offset, Buffer.Length); + + // Next, write the fields + Utilities.StringToBytes(Signature, buffer, offset + 0, 8); + Utilities.WriteBytesLittleEndian(Version, buffer, offset + 8); + Utilities.WriteBytesLittleEndian(HeaderSize, buffer, offset + 12); + Utilities.WriteBytesLittleEndian((uint)0, buffer, offset + 16); + Utilities.WriteBytesLittleEndian(HeaderLba, buffer, offset + 24); + Utilities.WriteBytesLittleEndian(AlternateHeaderLba, buffer, offset + 32); + Utilities.WriteBytesLittleEndian(FirstUsable, buffer, offset + 40); + Utilities.WriteBytesLittleEndian(LastUsable, buffer, offset + 48); + Utilities.WriteBytesLittleEndian(DiskGuid, buffer, offset + 56); + Utilities.WriteBytesLittleEndian(PartitionEntriesLba, buffer, offset + 72); + Utilities.WriteBytesLittleEndian(PartitionEntryCount, buffer, offset + 80); + Utilities.WriteBytesLittleEndian(PartitionEntrySize, buffer, offset + 84); + Utilities.WriteBytesLittleEndian(EntriesCrc, buffer, offset + 88); + + // Calculate & write the CRC + Utilities.WriteBytesLittleEndian(CalcCrc(buffer, offset, HeaderSize), buffer, offset + 16); + + // Update the cached copy - re-allocate the buffer to allow for HeaderSize potentially having changed + Buffer = new byte[HeaderSize]; + Array.Copy(buffer, offset, Buffer, 0, HeaderSize); + } + + internal static uint CalcCrc(byte[] buffer, int offset, int count) + { + byte[] temp = new byte[count]; + Array.Copy(buffer, offset, temp, 0, count); + + // Reset CRC field + Utilities.WriteBytesLittleEndian((uint)0, temp, 16); + + return Crc32LittleEndian.Compute(Crc32Algorithm.Common, temp, 0, count); + } + } +} diff --git a/DiscUtils/Partitions/GuidPartitionInfo.cs b/DiscUtils/Partitions/GuidPartitionInfo.cs new file mode 100644 index 0000000..4778c0b --- /dev/null +++ b/DiscUtils/Partitions/GuidPartitionInfo.cs @@ -0,0 +1,120 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + using System.IO; + + /// + /// Provides access to partition records in a GUID partition table. + /// + public sealed class GuidPartitionInfo : PartitionInfo + { + private GuidPartitionTable _table; + private GptEntry _entry; + + internal GuidPartitionInfo(GuidPartitionTable table, GptEntry entry) + { + _table = table; + _entry = entry; + } + + /// + /// Gets the first sector of the partion (relative to start of disk) as a Logical Block Address. + /// + public override long FirstSector + { + get { return _entry.FirstUsedLogicalBlock; } + } + + /// + /// Gets the last sector of the partion (relative to start of disk) as a Logical Block Address (inclusive). + /// + public override long LastSector + { + get { return _entry.LastUsedLogicalBlock; } + } + + /// + /// Gets the type of the partition, as a GUID. + /// + public override Guid GuidType + { + get { return _entry.PartitionType; } + } + + /// + /// Always returns Zero. + /// + public override byte BiosType + { + get { return 0; } + } + + /// + /// Gets the type of the partition as a string. + /// + public override string TypeAsString + { + get { return _entry.FriendlyPartitionType; } + } + + /// + /// Gets the name of the partition. + /// + public string Name + { + get { return _entry.Name; } + } + + /// + /// Gets the attributes of the partition. + /// + public long Attributes + { + get { return (long)_entry.Attributes; } + } + + /// + /// Gets the unique identity of this specific partition. + /// + public Guid Identity + { + get { return _entry.Identity; } + } + + internal override PhysicalVolumeType VolumeType + { + get { return PhysicalVolumeType.GptPartition; } + } + + /// + /// Opens a stream to access the content of the partition. + /// + /// The new stream. + public override SparseStream Open() + { + return _table.Open(_entry); + } + } +} diff --git a/DiscUtils/Partitions/GuidPartitionTable.cs b/DiscUtils/Partitions/GuidPartitionTable.cs new file mode 100644 index 0000000..5f41346 --- /dev/null +++ b/DiscUtils/Partitions/GuidPartitionTable.cs @@ -0,0 +1,651 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Globalization; + using System.IO; + + /// + /// Represents a GUID Partition Table. + /// + public sealed class GuidPartitionTable : PartitionTable + { + private Stream _diskData; + private Geometry _diskGeometry; + private GptHeader _primaryHeader; + private GptHeader _secondaryHeader; + private byte[] _entryBuffer; + + /// + /// Initializes a new instance of the GuidPartitionTable class. + /// + /// The disk containing the partition table. + public GuidPartitionTable(VirtualDisk disk) + { + Init(disk.Content, disk.Geometry); + } + + /// + /// Initializes a new instance of the GuidPartitionTable class. + /// + /// The stream containing the disk data. + /// The geometry of the disk. + public GuidPartitionTable(Stream disk, Geometry diskGeometry) + { + Init(disk, diskGeometry); + } + + /// + /// Gets the first sector of the disk available to hold partitions. + /// + public long FirstUsableSector + { + get { return _primaryHeader.FirstUsable; } + } + + /// + /// Gets the last sector of the disk available to hold partitions. + /// + public long LastUsableSector + { + get { return _primaryHeader.LastUsable; } + } + + /// + /// Gets the unique GPT identifier for this disk. + /// + public override Guid DiskGuid + { + get { return _primaryHeader.DiskGuid; } + } + + /// + /// Gets a collection of the partitions for storing Operating System file-systems. + /// + public override ReadOnlyCollection Partitions + { + get + { + return new ReadOnlyCollection(Utilities.Map(GetAllEntries(), (e) => new GuidPartitionInfo(this, e))); + } + } + + /// + /// Creates a new partition table on a disk. + /// + /// The disk to initialize. + /// An object to access the newly created partition table. + public static GuidPartitionTable Initialize(VirtualDisk disk) + { + return Initialize(disk.Content, disk.Geometry); + } + + /// + /// Creates a new partition table on a disk. + /// + /// The stream containing the disk data. + /// The geometry of the disk. + /// An object to access the newly created partition table. + public static GuidPartitionTable Initialize(Stream disk, Geometry diskGeometry) + { + // Create the protective MBR partition record. + BiosPartitionTable pt = BiosPartitionTable.Initialize(disk, diskGeometry); + pt.CreatePrimaryByCylinder(0, diskGeometry.Cylinders - 1, BiosPartitionTypes.GptProtective, false); + + // Create the GPT headers, and blank-out the entry areas + const int EntryCount = 128; + const int EntrySize = 128; + + int entrySectors = ((EntryCount * EntrySize) + diskGeometry.BytesPerSector - 1) / diskGeometry.BytesPerSector; + + byte[] entriesBuffer = new byte[EntryCount * EntrySize]; + + // Prepare primary header + GptHeader header = new GptHeader(diskGeometry.BytesPerSector); + header.HeaderLba = 1; + header.AlternateHeaderLba = (disk.Length / diskGeometry.BytesPerSector) - 1; + header.FirstUsable = header.HeaderLba + entrySectors + 1; + header.LastUsable = header.AlternateHeaderLba - entrySectors - 1; + header.DiskGuid = Guid.NewGuid(); + header.PartitionEntriesLba = 2; + header.PartitionEntryCount = EntryCount; + header.PartitionEntrySize = EntrySize; + header.EntriesCrc = CalcEntriesCrc(entriesBuffer); + + // Write the primary header + byte[] headerBuffer = new byte[diskGeometry.BytesPerSector]; + header.WriteTo(headerBuffer, 0); + disk.Position = header.HeaderLba * diskGeometry.BytesPerSector; + disk.Write(headerBuffer, 0, headerBuffer.Length); + + // Calc alternate header + header.HeaderLba = header.AlternateHeaderLba; + header.AlternateHeaderLba = 1; + header.PartitionEntriesLba = header.HeaderLba - entrySectors; + + // Write the alternate header + header.WriteTo(headerBuffer, 0); + disk.Position = header.HeaderLba * diskGeometry.BytesPerSector; + disk.Write(headerBuffer, 0, headerBuffer.Length); + + return new GuidPartitionTable(disk, diskGeometry); + } + + /// + /// Creates a new partition table on a disk containing a single partition. + /// + /// The disk to initialize. + /// The partition type for the single partition. + /// An object to access the newly created partition table. + public static GuidPartitionTable Initialize(VirtualDisk disk, WellKnownPartitionType type) + { + GuidPartitionTable pt = Initialize(disk); + pt.Create(type, true); + return pt; + } + + /// + /// Creates a new partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + public override int Create(WellKnownPartitionType type, bool active) + { + List allEntries = new List(GetAllEntries()); + + EstablishReservedPartition(allEntries); + + // Fill the rest of the disk with the requested partition + long start = FirstAvailableSector(allEntries); + long end = FindLastFreeSector(start, allEntries); + + return Create(start, end, GuidPartitionTypes.Convert(type), 0, "Data Partition"); + } + + /// + /// Creates a new primary partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the new partition. + public override int Create(long size, WellKnownPartitionType type, bool active) + { + if (size < _diskGeometry.BytesPerSector) + { + throw new ArgumentOutOfRangeException("size", size, "size must be at least one sector"); + } + + long sectorLength = size / _diskGeometry.BytesPerSector; + long start = FindGap(size / _diskGeometry.BytesPerSector, 1); + + return Create(start, start + sectorLength - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); + } + + /// + /// Creates a new aligned partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in bytes). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is acheived by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(WellKnownPartitionType type, bool active, int alignment) + { + if (alignment % _diskGeometry.BytesPerSector != 0) + { + throw new ArgumentException("Alignment is not a multiple of the sector size"); + } + + List allEntries = new List(GetAllEntries()); + + EstablishReservedPartition(allEntries); + + // Fill the rest of the disk with the requested partition + long start = Utilities.RoundUp(FirstAvailableSector(allEntries), alignment / _diskGeometry.BytesPerSector); + long end = Utilities.RoundDown(FindLastFreeSector(start, allEntries) + 1, alignment / _diskGeometry.BytesPerSector); + + if (end <= start) + { + throw new IOException("No available space"); + } + + return Create(start, end - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); + } + + /// + /// Creates a new aligned partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in bytes). + /// The index of the new partition. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is achieved by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(long size, WellKnownPartitionType type, bool active, int alignment) + { + if (size < _diskGeometry.BytesPerSector) + { + throw new ArgumentOutOfRangeException("size", size, "size must be at least one sector"); + } + + if (alignment % _diskGeometry.BytesPerSector != 0) + { + throw new ArgumentException("Alignment is not a multiple of the sector size"); + } + + if (size % alignment != 0) + { + throw new ArgumentException("Size is not a multiple of the alignment"); + } + + long sectorLength = size / _diskGeometry.BytesPerSector; + long start = FindGap(size / _diskGeometry.BytesPerSector, alignment / _diskGeometry.BytesPerSector); + + return Create(start, start + sectorLength - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); + } + + /// + /// Creates a new GUID partition on the disk. + /// + /// The first sector of the partition. + /// The last sector of the partition. + /// The partition type. + /// The partition attributes. + /// The name of the partition. + /// The index of the new partition. + /// No checking is performed on the parameters, the caller is + /// responsible for ensuring that the partition does not overlap other partitions. + public int Create(long startSector, long endSector, Guid type, long attributes, string name) + { + GptEntry newEntry = CreateEntry(startSector, endSector, type, attributes, name); + return GetEntryIndex(newEntry.Identity); + } + + /// + /// Deletes a partition at a given index. + /// + /// The index of the partition. + public override void Delete(int index) + { + int offset = GetPartitionOffset(index); + Array.Clear(_entryBuffer, offset, _primaryHeader.PartitionEntrySize); + Write(); + } + + internal SparseStream Open(GptEntry entry) + { + long start = entry.FirstUsedLogicalBlock * _diskGeometry.BytesPerSector; + long end = (entry.LastUsedLogicalBlock + 1) * _diskGeometry.BytesPerSector; + return new SubStream(_diskData, start, end - start); + } + + private static uint CalcEntriesCrc(byte[] buffer) + { + return Crc32LittleEndian.Compute(Crc32Algorithm.Common, buffer, 0, buffer.Length); + } + + private static int CountEntries(ICollection values, Func pred) + { + int count = 0; + + foreach (var val in values) + { + if (pred(val)) + { + ++count; + } + } + + return count; + } + + private void Init(Stream disk, Geometry diskGeometry) + { + BiosPartitionTable bpt; + try + { + bpt = new BiosPartitionTable(disk, diskGeometry); + } + catch (IOException ioe) + { + throw new IOException("Invalid GPT disk, protective MBR table not present or invalid", ioe); + } + + if (bpt.Count != 1 || bpt[0].BiosType != BiosPartitionTypes.GptProtective) + { + throw new IOException("Invalid GPT disk, protective MBR table is not valid"); + } + + _diskData = disk; + _diskGeometry = diskGeometry; + + disk.Position = diskGeometry.BytesPerSector; + byte[] sector = Utilities.ReadFully(disk, diskGeometry.BytesPerSector); + + _primaryHeader = new GptHeader(diskGeometry.BytesPerSector); + if (!_primaryHeader.ReadFrom(sector, 0) || !ReadEntries(_primaryHeader)) + { + disk.Position = disk.Length - diskGeometry.BytesPerSector; + disk.Read(sector, 0, sector.Length); + _secondaryHeader = new GptHeader(diskGeometry.BytesPerSector); + if (!_secondaryHeader.ReadFrom(sector, 0) || !ReadEntries(_secondaryHeader)) + { + throw new IOException("No valid GUID Partition Table found"); + } + + // Generate from the primary table from the secondary one + _primaryHeader = new GptHeader(_secondaryHeader); + _primaryHeader.HeaderLba = _secondaryHeader.AlternateHeaderLba; + _primaryHeader.AlternateHeaderLba = _secondaryHeader.HeaderLba; + _primaryHeader.PartitionEntriesLba = 2; + + // If the disk is writeable, fix up the primary partition table based on the + // (valid) secondary table. + if (disk.CanWrite) + { + WritePrimaryHeader(); + } + } + + if (_secondaryHeader == null) + { + _secondaryHeader = new GptHeader(diskGeometry.BytesPerSector); + disk.Position = disk.Length - diskGeometry.BytesPerSector; + disk.Read(sector, 0, sector.Length); + if (!_secondaryHeader.ReadFrom(sector, 0) || !ReadEntries(_secondaryHeader)) + { + // Generate from the secondary table from the primary one + _secondaryHeader = new GptHeader(_primaryHeader); + _secondaryHeader.HeaderLba = _secondaryHeader.AlternateHeaderLba; + _secondaryHeader.AlternateHeaderLba = _secondaryHeader.HeaderLba; + _secondaryHeader.PartitionEntriesLba = _secondaryHeader.HeaderLba - Utilities.RoundUp(_secondaryHeader.PartitionEntryCount * _secondaryHeader.PartitionEntrySize, diskGeometry.BytesPerSector); + + // If the disk is writeable, fix up the secondary partition table based on the + // (valid) primary table. + if (disk.CanWrite) + { + WriteSecondaryHeader(); + } + } + } + } + + private void EstablishReservedPartition(List allEntries) + { + // If no MicrosoftReserved partition, and no Microsoft Data partitions, and the disk + // has a 'reasonable' size free, create a Microsoft Reserved partition. + if (CountEntries(allEntries, e => e.PartitionType == GuidPartitionTypes.MicrosoftReserved) == 0 + && CountEntries(allEntries, e => e.PartitionType == GuidPartitionTypes.WindowsBasicData) == 0 + && _diskGeometry.Capacity > 512 * 1024 * 1024) + { + long reservedStart = FirstAvailableSector(allEntries); + long reservedEnd = FindLastFreeSector(reservedStart, allEntries); + + if ((reservedEnd - reservedStart + 1) * _diskGeometry.BytesPerSector > 512 * 1024 * 1024) + { + long size = ((_diskGeometry.Capacity < (16 * 1024L * 1024 * 1024)) ? 32 : 128) * 1024 * 1024; + reservedEnd = reservedStart + (size / _diskGeometry.BytesPerSector) - 1; + + int reservedOffset = GetFreeEntryOffset(); + GptEntry newReservedEntry = new GptEntry(); + newReservedEntry.PartitionType = GuidPartitionTypes.MicrosoftReserved; + newReservedEntry.Identity = Guid.NewGuid(); + newReservedEntry.FirstUsedLogicalBlock = reservedStart; + newReservedEntry.LastUsedLogicalBlock = reservedEnd; + newReservedEntry.Attributes = 0; + newReservedEntry.Name = "Microsoft reserved partition"; + newReservedEntry.WriteTo(_entryBuffer, reservedOffset); + allEntries.Add(newReservedEntry); + } + } + } + + private GptEntry CreateEntry(long startSector, long endSector, Guid type, long attributes, string name) + { + if (endSector < startSector) + { + throw new ArgumentException("The end sector is before the start sector"); + } + + int offset = GetFreeEntryOffset(); + GptEntry newEntry = new GptEntry(); + newEntry.PartitionType = type; + newEntry.Identity = Guid.NewGuid(); + newEntry.FirstUsedLogicalBlock = startSector; + newEntry.LastUsedLogicalBlock = endSector; + newEntry.Attributes = (ulong)attributes; + newEntry.Name = name; + newEntry.WriteTo(_entryBuffer, offset); + + // Commit changes to disk + Write(); + + return newEntry; + } + + private long FindGap(long numSectors, long alignmentSectors) + { + List list = new List(GetAllEntries()); + list.Sort(); + + long startSector = Utilities.RoundUp(_primaryHeader.FirstUsable, alignmentSectors); + foreach (var entry in list) + { + if (!Utilities.RangesOverlap(startSector, startSector + numSectors - 1, entry.FirstUsedLogicalBlock, entry.LastUsedLogicalBlock)) + { + break; + } + else + { + startSector = Utilities.RoundUp(entry.LastUsedLogicalBlock + 1, alignmentSectors); + } + } + + if (_diskGeometry.TotalSectorsLong - startSector < numSectors) + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, "Unable to find free space of {0} sectors", numSectors)); + } + + return startSector; + } + + private long FirstAvailableSector(List allEntries) + { + long start = _primaryHeader.FirstUsable; + + foreach (GptEntry entry in allEntries) + { + if (entry.LastUsedLogicalBlock >= start) + { + start = entry.LastUsedLogicalBlock + 1; + } + } + + return start; + } + + private long FindLastFreeSector(long start, List allEntries) + { + long end = _primaryHeader.LastUsable; + + foreach (GptEntry entry in allEntries) + { + if (entry.LastUsedLogicalBlock > start && entry.FirstUsedLogicalBlock <= end) + { + end = entry.FirstUsedLogicalBlock - 1; + } + } + + return end; + } + + private void Write() + { + WritePrimaryHeader(); + WriteSecondaryHeader(); + } + + private void WritePrimaryHeader() + { + byte[] buffer = new byte[_diskGeometry.BytesPerSector]; + _primaryHeader.EntriesCrc = CalcEntriesCrc(); + _primaryHeader.WriteTo(buffer, 0); + _diskData.Position = _diskGeometry.BytesPerSector; + _diskData.Write(buffer, 0, buffer.Length); + + _diskData.Position = 2 * _diskGeometry.BytesPerSector; + _diskData.Write(_entryBuffer, 0, _entryBuffer.Length); + } + + private void WriteSecondaryHeader() + { + byte[] buffer = new byte[_diskGeometry.BytesPerSector]; + _secondaryHeader.EntriesCrc = CalcEntriesCrc(); + _secondaryHeader.WriteTo(buffer, 0); + _diskData.Position = _diskData.Length - _diskGeometry.BytesPerSector; + _diskData.Write(buffer, 0, buffer.Length); + + _diskData.Position = _secondaryHeader.PartitionEntriesLba * _diskGeometry.BytesPerSector; + _diskData.Write(_entryBuffer, 0, _entryBuffer.Length); + } + + private bool ReadEntries(GptHeader header) + { + _diskData.Position = header.PartitionEntriesLba * _diskGeometry.BytesPerSector; + _entryBuffer = Utilities.ReadFully(_diskData, (int)(header.PartitionEntrySize * header.PartitionEntryCount)); + if (header.EntriesCrc != CalcEntriesCrc()) + { + return false; + } + + return true; + } + + private uint CalcEntriesCrc() + { + return Crc32LittleEndian.Compute(Crc32Algorithm.Common, _entryBuffer, 0, _entryBuffer.Length); + } + + private IEnumerable GetAllEntries() + { + for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) + { + GptEntry entry = new GptEntry(); + entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); + if (entry.PartitionType != Guid.Empty) + { + yield return entry; + } + } + } + + private int GetPartitionOffset(int index) + { + bool found = false; + int entriesSoFar = 0; + int position = 0; + + while (!found && position < _primaryHeader.PartitionEntryCount) + { + GptEntry entry = new GptEntry(); + entry.ReadFrom(_entryBuffer, position * _primaryHeader.PartitionEntrySize); + if (entry.PartitionType != Guid.Empty) + { + if (index == entriesSoFar) + { + found = true; + break; + } + + entriesSoFar++; + } + + position++; + } + + if (found) + { + return position * _primaryHeader.PartitionEntrySize; + } + else + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, "No such partition: {0}", index)); + } + } + + private int GetEntryIndex(Guid identity) + { + int index = 0; + + for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) + { + GptEntry entry = new GptEntry(); + entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); + + if (entry.Identity == identity) + { + return index; + } + else if (entry.PartitionType != Guid.Empty) + { + index++; + } + } + + throw new IOException("No such partition"); + } + + private int GetFreeEntryOffset() + { + for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) + { + GptEntry entry = new GptEntry(); + entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); + + if (entry.PartitionType == Guid.Empty) + { + return i * _primaryHeader.PartitionEntrySize; + } + } + + throw new IOException("No free partition entries available"); + } + } +} diff --git a/DiscUtils/Partitions/GuidPartitionTypes.cs b/DiscUtils/Partitions/GuidPartitionTypes.cs new file mode 100644 index 0000000..7fce76d --- /dev/null +++ b/DiscUtils/Partitions/GuidPartitionTypes.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + + /// + /// Convenient access to well known GPT partition types. + /// + public static class GuidPartitionTypes + { + /// + /// EFI system partition. + /// + public static readonly Guid EfiSystem = new Guid("C12A7328-F81F-11D2-BA4B-00A0C93EC93B"); + + /// + /// BIOS boot partition. + /// + public static readonly Guid BiosBoot = new Guid("21686148-6449-6E6F-744E-656564454649"); + + /// + /// Microsoft reserved partition. + /// + public static readonly Guid MicrosoftReserved = new Guid("E3C9E316-0B5C-4DB8-817D-F92DF00215AE"); + + /// + /// Windows basic data partition. + /// + public static readonly Guid WindowsBasicData = new Guid("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7"); + + /// + /// Linux LVM partition. + /// + public static readonly Guid LinuxLvm = new Guid("E6D6D379-F507-44C2-A23C-238F2A3DF928"); + + /// + /// Linux swap partition. + /// + public static readonly Guid LinuxSwap = new Guid("0657FD6D-A4AB-43C4-84E5-0933C84B4F4F"); + + /// + /// Windows Logical Disk Manager metadata. + /// + public static readonly Guid WindowsLdmMetadata = new Guid("5808C8AA-7E8F-42E0-85D2-E1E90434CFB3"); + + /// + /// Windows Logical Disk Manager data. + /// + public static readonly Guid WindowsLdmData = new Guid("AF9B60A0-1431-4F62-BC68-3311714A69AD"); + + /// + /// Converts a well known partition type to a Guid. + /// + /// The value to convert. + /// The GUID value. + internal static Guid Convert(WellKnownPartitionType wellKnown) + { + switch (wellKnown) + { + case WellKnownPartitionType.Linux: + case WellKnownPartitionType.WindowsFat: + case WellKnownPartitionType.WindowsNtfs: + return WindowsBasicData; + case WellKnownPartitionType.LinuxLvm: + return LinuxLvm; + case WellKnownPartitionType.LinuxSwap: + return LinuxSwap; + default: + throw new ArgumentException("Unknown partition type"); + } + } + } +} diff --git a/DiscUtils/Partitions/PartitionInfo.cs b/DiscUtils/Partitions/PartitionInfo.cs new file mode 100644 index 0000000..19a5f67 --- /dev/null +++ b/DiscUtils/Partitions/PartitionInfo.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + using System.Globalization; + using System.IO; + + /// + /// Base class representing a disk partition. + /// + /// The purpose of this class is to provide a minimal view of a partition, + /// such that callers can access existing partitions without specific knowledge of + /// the partitioning system. + public abstract class PartitionInfo + { + /// + /// Gets the first sector of the partion (relative to start of disk) as a Logical Block Address. + /// + public abstract long FirstSector { get; } + + /// + /// Gets the last sector of the partion (relative to start of disk) as a Logical Block Address (inclusive). + /// + public abstract long LastSector { get; } + + /// + /// Gets the length of the partition in sectors. + /// + public virtual long SectorCount + { + get { return 1 + LastSector - FirstSector; } + } + + /// + /// Gets the type of the partition, as a GUID, when available. + /// + /// .Empty for MBR-style partitions. + public abstract Guid GuidType { get; } + + /// + /// Gets the type of the partition, in legacy BIOS form, when available. + /// + /// Zero for GUID-style partitions. + public abstract byte BiosType { get; } + + /// + /// Gets the partition type as a 'friendly' string. + /// + public abstract string TypeAsString { get; } + + /// + /// Gets the physical volume type for this type of partition. + /// + internal abstract PhysicalVolumeType VolumeType + { + get; + } + + /// + /// Opens a stream that accesses the partition's contents. + /// + /// The new stream. + public abstract SparseStream Open(); + + /// + /// Gets a summary of the partition information as 'first - last (type)'. + /// + /// A string representation of the partition information. + public override string ToString() + { + return String.Format(CultureInfo.InvariantCulture, "0x{0:X} - 0x{1:X} ({2})", FirstSector, LastSector, TypeAsString); + } + } +} diff --git a/DiscUtils/Partitions/PartitionTable.cs b/DiscUtils/Partitions/PartitionTable.cs new file mode 100644 index 0000000..971c163 --- /dev/null +++ b/DiscUtils/Partitions/PartitionTable.cs @@ -0,0 +1,208 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + + /// + /// Base class for classes which represent a disk partitioning scheme. + /// + /// After modifying the table, by creating or deleting a partition assume that any + /// previously stored partition indexes of higher value are no longer valid. Re-enumerate + /// the partitions to discover the next index-to-partition mapping. + public abstract class PartitionTable + { + private static List s_factories; + + /// + /// Gets the GUID that uniquely identifies this disk, if supported (else returns null). + /// + public abstract Guid DiskGuid { get; } + + /// + /// Gets the list of partitions that contain user data (i.e. non-system / empty). + /// + public abstract ReadOnlyCollection Partitions { get; } + + /// + /// Gets the number of User partitions on the disk. + /// + public int Count + { + get { return Partitions.Count; } + } + + private static List Factories + { + get + { + if (s_factories == null) + { + List factories = new List(); + + foreach (var type in typeof(VolumeManager).Assembly.GetTypes()) + { + foreach (PartitionTableFactoryAttribute attr in Attribute.GetCustomAttributes(type, typeof(PartitionTableFactoryAttribute), false)) + { + factories.Add((PartitionTableFactory)Activator.CreateInstance(type)); + } + } + + s_factories = factories; + } + + return s_factories; + } + } + + /// + /// Gets information about a particular User partition. + /// + /// The index of the partition. + /// Information about the partition. + public PartitionInfo this[int index] + { + get { return Partitions[index]; } + } + + /// + /// Determines if a disk is partitioned with a known partitioning scheme. + /// + /// The content of the disk to check. + /// true if the disk is partitioned, else false. + public static bool IsPartitioned(Stream content) + { + foreach (var partTableFactory in Factories) + { + if (partTableFactory.DetectIsPartitioned(content)) + { + return true; + } + } + + return false; + } + + /// + /// Determines if a disk is partitioned with a known partitioning scheme. + /// + /// The disk to check. + /// true if the disk is partitioned, else false. + public static bool IsPartitioned(VirtualDisk disk) + { + return IsPartitioned(disk.Content); + } + + /// + /// Gets all of the partition tables found on a disk. + /// + /// The disk to inspect. + /// It is rare for a disk to have multiple partition tables, but theoretically + /// possible. + public static IList GetPartitionTables(VirtualDisk disk) + { + List tables = new List(); + + foreach (var factory in Factories) + { + PartitionTable table = factory.DetectPartitionTable(disk); + if (table != null) + { + tables.Add(table); + } + } + + return tables; + } + + /// + /// Gets all of the partition tables found on a disk. + /// + /// The content of the disk to inspect. + /// It is rare for a disk to have multiple partition tables, but theoretically + /// possible. + public static IList GetPartitionTables(Stream contentStream) + { + return GetPartitionTables(new Raw.Disk(contentStream, Ownership.None)); + } + + /// + /// Creates a new partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + public abstract int Create(WellKnownPartitionType type, bool active); + + /// + /// Creates a new partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the new partition. + public abstract int Create(long size, WellKnownPartitionType type, bool active); + + /// + /// Creates a new aligned partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in byte). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is acheived by aligning partitions on + /// large values that are a power of two. + /// + public abstract int CreateAligned(WellKnownPartitionType type, bool active, int alignment); + + /// + /// Creates a new aligned partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in byte). + /// The index of the new partition. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is achieved by aligning partitions on + /// large values that are a power of two. + /// + public abstract int CreateAligned(long size, WellKnownPartitionType type, bool active, int alignment); + + /// + /// Deletes a partition at a given index. + /// + /// The index of the partition. + public abstract void Delete(int index); + } +} diff --git a/DiscUtils/Partitions/PartitionTableFactory.cs b/DiscUtils/Partitions/PartitionTableFactory.cs new file mode 100644 index 0000000..19ec3da --- /dev/null +++ b/DiscUtils/Partitions/PartitionTableFactory.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System.IO; + + internal abstract class PartitionTableFactory + { + public abstract bool DetectIsPartitioned(Stream s); + + public abstract PartitionTable DetectPartitionTable(VirtualDisk disk); + } +} diff --git a/DiscUtils/Partitions/PartitionTableFactoryAttribute.cs b/DiscUtils/Partitions/PartitionTableFactoryAttribute.cs new file mode 100644 index 0000000..5cb4cbb --- /dev/null +++ b/DiscUtils/Partitions/PartitionTableFactoryAttribute.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + using System; + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + internal sealed class PartitionTableFactoryAttribute : Attribute + { + } +} diff --git a/DiscUtils/Partitions/WellKnownPartitionType.cs b/DiscUtils/Partitions/WellKnownPartitionType.cs new file mode 100644 index 0000000..c91e573 --- /dev/null +++ b/DiscUtils/Partitions/WellKnownPartitionType.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Partitions +{ + /// + /// Enumeration of partition-table technology neutral partition types. + /// + public enum WellKnownPartitionType + { + /// + /// Windows FAT-based partition. + /// + WindowsFat = 0, + + /// + /// Windows NTFS-based partition. + /// + WindowsNtfs = 1, + + /// + /// Linux native file system. + /// + Linux = 2, + + /// + /// Linux swap. + /// + LinuxSwap = 3, + + /// + /// Linux Logical Volume Manager (LVM). + /// + LinuxLvm = 4, + } +} diff --git a/DiscUtils/PhysicalVolumeInfo.cs b/DiscUtils/PhysicalVolumeInfo.cs new file mode 100644 index 0000000..701d4d6 --- /dev/null +++ b/DiscUtils/PhysicalVolumeInfo.cs @@ -0,0 +1,241 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Globalization; + using DiscUtils.Partitions; + + /// + /// Enumeration of possible types of physical volume. + /// + public enum PhysicalVolumeType + { + /// + /// Unknown type. + /// + None, + + /// + /// Physical volume encompasses the entire disk. + /// + EntireDisk, + + /// + /// Physical volume is defined by a BIOS-style partition table. + /// + BiosPartition, + + /// + /// Physical volume is defined by a GUID partition table. + /// + GptPartition, + + /// + /// Physical volume is defined by an Apple partition map. + /// + ApplePartition + } + + /// + /// Information about a physical disk volume, which may be a partition or an entire disk. + /// + public sealed class PhysicalVolumeInfo : VolumeInfo + { + private string _diskId; + private VirtualDisk _disk; + private SparseStreamOpenDelegate _streamOpener; + private PhysicalVolumeType _type; + private PartitionInfo _partitionInfo; + + /// + /// Initializes a new instance of the PhysicalVolumeInfo class. + /// + /// The containing disk's identity. + /// The disk containing the partition. + /// Information about the partition. + /// Use this constructor to represent a (BIOS or GPT) partition. + internal PhysicalVolumeInfo( + string diskId, + VirtualDisk disk, + PartitionInfo partitionInfo) + { + _diskId = diskId; + _disk = disk; + _streamOpener = partitionInfo.Open; + _type = partitionInfo.VolumeType; + _partitionInfo = partitionInfo; + } + + /// + /// Initializes a new instance of the PhysicalVolumeInfo class. + /// + /// The identity of the disk. + /// The disk itself. + /// Use this constructor to represent an entire disk as a single volume. + internal PhysicalVolumeInfo( + string diskId, + VirtualDisk disk) + { + _diskId = diskId; + _disk = disk; + _streamOpener = delegate { return new SubStream(disk.Content, Ownership.None, 0, disk.Capacity); }; + _type = PhysicalVolumeType.EntireDisk; + } + + /// + /// Gets the type of the volume. + /// + public PhysicalVolumeType VolumeType + { + get { return _type; } + } + + /// + /// Gets the signature of the disk containing the volume (only valid for partition-type volumes). + /// + public int DiskSignature + { + get { return (_type != PhysicalVolumeType.EntireDisk) ? _disk.Signature : 0; } + } + + /// + /// Gets the unique identity of the disk containing the volume, if known. + /// + public Guid DiskIdentity + { + get { return (_type != PhysicalVolumeType.EntireDisk) ? _disk.Partitions.DiskGuid : Guid.Empty; } + } + + /// + /// Gets the one-byte BIOS type for this volume, which indicates the content. + /// + public override byte BiosType + { + get { return (_partitionInfo == null) ? (byte)0 : _partitionInfo.BiosType; } + } + + /// + /// Gets the size of the volume, in bytes. + /// + public override long Length + { + get { return (_partitionInfo == null) ? _disk.Capacity : _partitionInfo.SectorCount * _disk.SectorSize; } + } + + /// + /// Gets the stable identity for this physical volume. + /// + /// The stability of the identity depends the disk structure. + /// In some cases the identity may include a simple index, when no other information + /// is available. Best practice is to add disks to the Volume Manager in a stable + /// order, if the stability of this identity is paramount. + public override string Identity + { + get + { + if (_type == PhysicalVolumeType.GptPartition) + { + return "VPG" + PartitionIdentity.ToString("B"); + } + else + { + string partId; + switch (_type) + { + case PhysicalVolumeType.EntireDisk: + partId = "PD"; + break; + case PhysicalVolumeType.BiosPartition: + case PhysicalVolumeType.ApplePartition: + partId = "PO" + (_partitionInfo.FirstSector * _disk.SectorSize).ToString("X", CultureInfo.InvariantCulture); + break; + default: + partId = "P*"; + break; + } + + return "VPD:" + _diskId + ":" + partId; + } + } + } + + /// + /// Gets the disk geometry of the underlying storage medium, if any (may be null). + /// + public override Geometry PhysicalGeometry + { + get { return _disk.Geometry; } + } + + /// + /// Gets the disk geometry of the underlying storage medium (as used in BIOS calls), may be null. + /// + public override Geometry BiosGeometry + { + get { return _disk.BiosGeometry; } + } + + /// + /// Gets the offset of this volume in the underlying storage medium, if any (may be Zero). + /// + public override long PhysicalStartSector + { + get { return _type == PhysicalVolumeType.EntireDisk ? 0 : _partitionInfo.FirstSector; } + } + + /// + /// Gets the unique identity of the physical partition, if known. + /// + public Guid PartitionIdentity + { + get + { + GuidPartitionInfo gpi = _partitionInfo as GuidPartitionInfo; + if (gpi != null) + { + return gpi.Identity; + } + + return Guid.Empty; + } + } + + /// + /// Gets the underlying partition (if any). + /// + internal PartitionInfo Partition + { + get { return _partitionInfo; } + } + + /// + /// Opens the volume, providing access to its contents. + /// + /// A stream that can be used to access the volume. + public override SparseStream Open() + { + return _streamOpener(); + } + } +} diff --git a/DiscUtils/PumpProgressEventArgs.cs b/DiscUtils/PumpProgressEventArgs.cs new file mode 100644 index 0000000..382ae6c --- /dev/null +++ b/DiscUtils/PumpProgressEventArgs.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + /// + /// Event arguments indicating progress on pumping a stream. + /// + public class PumpProgressEventArgs : EventArgs + { + /// + /// Gets or sets the number of bytes read from InputStream. + /// + public long BytesRead { get; set; } + + /// + /// Gets or sets the number of bytes written to OutputStream. + /// + public long BytesWritten { get; set; } + + /// + /// Gets or sets the absolute position in InputStream. + /// + public long SourcePosition { get; set; } + + /// + /// Gets or sets the absolute position in OutputStream. + /// + public long DestinationPosition { get; set; } + } +} diff --git a/DiscUtils/Range.cs b/DiscUtils/Range.cs new file mode 100644 index 0000000..8488a8b --- /dev/null +++ b/DiscUtils/Range.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// Represents a range of values. + /// + /// The type of the offset element. + /// The type of the size element. + public class Range : IEquatable> + where TOffset : IEquatable + where TCount : IEquatable + { + private TOffset _offset; + private TCount _count; + + /// + /// Initializes a new instance of the Range class. + /// + /// The offset (i.e. start) of the range. + /// The size of the range. + public Range(TOffset offset, TCount count) + { + _offset = offset; + _count = count; + } + + /// + /// Gets the offset (i.e. start) of the range. + /// + public TOffset Offset + { + get { return _offset; } + } + + /// + /// Gets the size of the range. + /// + public TCount Count + { + get { return _count; } + } + + /// + /// Merges sets of ranges into chunks. + /// + /// The ranges to merge. + /// The size of each chunk. + /// Ranges combined into larger chunks. + /// The type of the offset and count in the ranges. + public static IEnumerable> Chunked(IEnumerable> ranges, T chunkSize) + where T : struct, IEquatable, IComparable + { + T? chunkStart = Numbers.Zero; + T chunkLength = Numbers.Zero; + + foreach (var range in ranges) + { + if (Numbers.NotEqual(range.Count, Numbers.Zero)) + { + T rangeStart = Numbers.RoundDown(range.Offset, chunkSize); + T rangeNext = Numbers.RoundUp(Numbers.Add(range.Offset, range.Count), chunkSize); + + if (chunkStart.HasValue && Numbers.GreaterThan(rangeStart, Numbers.Add(chunkStart.Value, chunkLength))) + { + // This extent is non-contiguous (in terms of blocks), so write out the last range and start new + yield return new Range(chunkStart.Value, chunkLength); + chunkStart = rangeStart; + } + else if (!chunkStart.HasValue) + { + // First extent, so start first range + chunkStart = rangeStart; + } + + // Set the length of the current range, based on the end of this extent + chunkLength = Numbers.Subtract(rangeNext, chunkStart.Value); + } + } + + // Final range (if any ranges at all) hasn't been returned yet, so do that now + if (chunkStart.HasValue) + { + yield return new Range(chunkStart.Value, chunkLength); + } + } + + /// + /// Returns a string representation of the extent as [start:+length]. + /// + /// The string representation. + public override string ToString() + { + return "[" + _offset + ":+" + _count + "]"; + } + + #region IEquatable> Members + + /// + /// Compares this range to another. + /// + /// The range to compare. + /// true if the ranges are equivalent, else false. + public bool Equals(Range other) + { + if (other == null) + { + return false; + } + + return _offset.Equals(other.Offset) && _count.Equals(other._count); + } + + #endregion + } +} diff --git a/DiscUtils/Raw/Disk.cs b/DiscUtils/Raw/Disk.cs new file mode 100644 index 0000000..64142fb --- /dev/null +++ b/DiscUtils/Raw/Disk.cs @@ -0,0 +1,223 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Raw +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Represents a raw disk image. + /// + /// This disk format is simply an uncompressed capture of all blocks on a disk. + public sealed class Disk : VirtualDisk + { + private DiskImageFile _file; + + /// + /// Initializes a new instance of the Disk class. + /// + /// The stream to read. + /// Indicates if the new instance should control the lifetime of the stream. + public Disk(Stream stream, Ownership ownsStream) + : this(stream, ownsStream, null) + { + } + + /// + /// Initializes a new instance of the Disk class. + /// + /// The stream to read. + /// Indicates if the new instance should control the lifetime of the stream. + /// The emulated geometry of the disk. + public Disk(Stream stream, Ownership ownsStream, Geometry geometry) + { + _file = new DiskImageFile(stream, ownsStream, geometry); + } + + /// + /// Initializes a new instance of the Disk class. + /// + /// The path to the disk image. + public Disk(string path) + { + _file = new DiskImageFile(new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None), Ownership.Dispose, null); + } + + /// + /// Initializes a new instance of the Disk class. + /// + /// The path to the disk image. + /// The access requested to the disk. + public Disk(string path, FileAccess access) + { + FileShare share = (access == FileAccess.Read) ? FileShare.Read : FileShare.None; + _file = new DiskImageFile(new FileStream(path, FileMode.Open, access, share), Ownership.Dispose, null); + } + + /// + /// Initializes a new instance of the Disk class. + /// + /// The contents of the disk. + private Disk(DiskImageFile file) + { + _file = file; + } + + /// + /// Gets the geometry of the disk. + /// + public override Geometry Geometry + { + get { return _file.Geometry; } + } + + /// + /// Gets the type of disk represented by this object. + /// + public override VirtualDiskClass DiskClass + { + get { return _file.DiskType; } + } + + /// + /// Gets the capacity of the disk (in bytes). + /// + public override long Capacity + { + get { return _file.Capacity; } + } + + /// + /// Gets the content of the disk as a stream. + /// + /// Note the returned stream is not guaranteed to be at any particular position. The actual position + /// will depend on the last partition table/file system activity, since all access to the disk contents pass + /// through a single stream instance. Set the stream position before accessing the stream. + public override SparseStream Content + { + get { return _file.Content; } + } + + /// + /// Gets the layers that make up the disk. + /// + public override IEnumerable Layers + { + get { yield return _file; } + } + + /// + /// Gets information about the type of disk. + /// + /// This property provides access to meta-data about the disk format, for example whether the + /// BIOS geometry is preserved in the disk file. + public override VirtualDiskTypeInfo DiskTypeInfo + { + get { return DiskFactory.MakeDiskTypeInfo(); } + } + + /// + /// Initializes a stream as an unformatted disk. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The desired capacity of the new disk. + /// An object that accesses the stream as a disk. + public static Disk Initialize(Stream stream, Ownership ownsStream, long capacity) + { + return Initialize(stream, ownsStream, capacity, null); + } + + /// + /// Initializes a stream as an unformatted disk. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The desired capacity of the new disk. + /// The desired geometry of the new disk, or null for default. + /// An object that accesses the stream as a disk. + public static Disk Initialize(Stream stream, Ownership ownsStream, long capacity, Geometry geometry) + { + return new Disk(DiskImageFile.Initialize(stream, ownsStream, capacity, geometry)); + } + + /// + /// Initializes a stream as an unformatted floppy disk. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The type of floppy disk image to create. + /// An object that accesses the stream as a disk. + public static Disk Initialize(Stream stream, Ownership ownsStream, FloppyDiskType type) + { + return new Disk(DiskImageFile.Initialize(stream, ownsStream, type)); + } + + /// + /// Create a new differencing disk, possibly within an existing disk. + /// + /// The file system to create the disk on. + /// The path (or URI) for the disk to create. + /// The newly created disk. + public override VirtualDisk CreateDifferencingDisk(DiscFileSystem fileSystem, string path) + { + throw new NotSupportedException("Differencing disks not supported for raw disks"); + } + + /// + /// Create a new differencing disk. + /// + /// The path (or URI) for the disk to create. + /// The newly created disk. + public override VirtualDisk CreateDifferencingDisk(string path) + { + throw new NotSupportedException("Differencing disks not supported for raw disks"); + } + + /// + /// Disposes of underlying resources. + /// + /// Set to true if called within Dispose(), + /// else false. + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_file != null) + { + _file.Dispose(); + } + + _file = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} diff --git a/DiscUtils/Raw/DiskFactory.cs b/DiscUtils/Raw/DiskFactory.cs new file mode 100644 index 0000000..8fa40d1 --- /dev/null +++ b/DiscUtils/Raw/DiskFactory.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Raw +{ + using System; + using System.Collections.Generic; + using System.IO; + + [VirtualDiskFactory("RAW", ".img,.ima,.vfd,.flp,.bif")] + internal sealed class DiskFactory : VirtualDiskFactory + { + public override string[] Variants + { + get { return new string[] { }; } + } + + public override VirtualDiskTypeInfo GetDiskTypeInformation(string variant) + { + return MakeDiskTypeInfo(); + } + + public override DiskImageBuilder GetImageBuilder(string variant) + { + throw new NotSupportedException(); + } + + public override VirtualDisk CreateDisk(FileLocator locator, string variant, string path, VirtualDiskParameters diskParameters) + { + return Disk.Initialize(locator.Open(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None), Ownership.Dispose, diskParameters.Capacity, diskParameters.Geometry); + } + + public override VirtualDisk OpenDisk(string path, FileAccess access) + { + return new Disk(path, access); + } + + public override VirtualDisk OpenDisk(FileLocator locator, string path, FileAccess access) + { + FileShare share = access == FileAccess.Read ? FileShare.Read : FileShare.None; + return new Disk(locator.Open(path, FileMode.Open, access, share), Ownership.Dispose); + } + + public override VirtualDiskLayer OpenDiskLayer(FileLocator locator, string path, FileAccess access) + { + return null; + } + + internal static VirtualDiskTypeInfo MakeDiskTypeInfo() + { + return new VirtualDiskTypeInfo() + { + Name = "RAW", + Variant = string.Empty, + CanBeHardDisk = true, + DeterministicGeometry = true, + PreservesBiosGeometry = false, + CalcGeometry = c => Geometry.FromCapacity(c), + }; + } + } +} diff --git a/DiscUtils/Raw/DiskImageFile.cs b/DiscUtils/Raw/DiskImageFile.cs new file mode 100644 index 0000000..2f1b6f5 --- /dev/null +++ b/DiscUtils/Raw/DiskImageFile.cs @@ -0,0 +1,258 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Raw +{ + using System; + using System.IO; + using DiscUtils.Partitions; + + /// + /// Represents a single raw disk image file. + /// + public sealed class DiskImageFile : VirtualDiskLayer + { + private SparseStream _content; + private Ownership _ownsContent; + private Geometry _geometry; + + /// + /// Initializes a new instance of the DiskImageFile class. + /// + /// The stream to interpret. + public DiskImageFile(Stream stream) + : this(stream, Ownership.None, null) + { + } + + /// + /// Initializes a new instance of the DiskImageFile class. + /// + /// The stream to interpret. + /// Indicates if the new instance should control the lifetime of the stream. + /// The emulated geometry of the disk. + public DiskImageFile(Stream stream, Ownership ownsStream, Geometry geometry) + { + _content = stream as SparseStream; + _ownsContent = ownsStream; + + if (_content == null) + { + _content = SparseStream.FromStream(stream, ownsStream); + _ownsContent = Ownership.Dispose; + } + + _geometry = geometry ?? DetectGeometry(_content); + } + + /// + /// Gets the geometry of the file. + /// + public override Geometry Geometry + { + get { return _geometry; } + } + + /// + /// Gets a value indicating if the layer only stores meaningful sectors. + /// + public override bool IsSparse + { + get { return false; } + } + + /// + /// Gets a value indicating whether the file is a differencing disk. + /// + public override bool NeedsParent + { + get { return false; } + } + + /// + /// Gets the type of disk represented by this object. + /// + public VirtualDiskClass DiskType + { + get { return DetectDiskType(Capacity); } + } + + internal override long Capacity + { + get { return _content.Length; } + } + + internal override FileLocator RelativeFileLocator + { + get { return null; } + } + + internal SparseStream Content + { + get { return _content; } + } + + /// + /// Initializes a stream as a raw disk image. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The desired capacity of the new disk. + /// The geometry of the new disk. + /// An object that accesses the stream as a raw disk image. + public static DiskImageFile Initialize(Stream stream, Ownership ownsStream, long capacity, Geometry geometry) + { + stream.SetLength(Utilities.RoundUp(capacity, Sizes.Sector)); + + // Wipe any pre-existing master boot record / BPB + stream.Position = 0; + stream.Write(new byte[Sizes.Sector], 0, Sizes.Sector); + stream.Position = 0; + + return new DiskImageFile(stream, ownsStream, geometry); + } + + /// + /// Initializes a stream as an unformatted floppy disk. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The type of floppy disk image to create. + /// An object that accesses the stream as a disk. + public static DiskImageFile Initialize(Stream stream, Ownership ownsStream, FloppyDiskType type) + { + return Initialize(stream, ownsStream, FloppyCapacity(type), null); + } + + /// + /// Gets the content of this layer. + /// + /// The parent stream (if any). + /// Controls ownership of the parent stream. + /// The content as a stream. + public override SparseStream OpenContent(SparseStream parent, Ownership ownsParent) + { + if (ownsParent == Ownership.Dispose && parent != null) + { + parent.Dispose(); + } + + return SparseStream.FromStream(Content, Ownership.None); + } + + /// + /// Gets the possible locations of the parent file (if any). + /// + /// Array of strings, empty if no parent. + public override string[] GetParentLocations() + { + return new string[0]; + } + + /// + /// Disposes of underlying resources. + /// + /// Set to true if called within Dispose(), + /// else false. + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_ownsContent == Ownership.Dispose && _content != null) + { + _content.Dispose(); + } + + _content = null; + } + } + finally + { + base.Dispose(disposing); + } + } + + /// + /// Calculates the best guess geometry of a disk. + /// + /// The disk to detect the geometry of. + /// The geometry of the disk. + private static Geometry DetectGeometry(Stream disk) + { + long capacity = disk.Length; + + // First, check for floppy disk capacities - these have well-defined geometries + if (capacity == Sizes.Sector * 1440) + { + return new Geometry(80, 2, 9); + } + else if (capacity == Sizes.Sector * 2880) + { + return new Geometry(80, 2, 18); + } + else if (capacity == Sizes.Sector * 5760) + { + return new Geometry(80, 2, 36); + } + + // Failing that, try to detect the geometry from any partition table. + // Note: this call falls back to guessing the geometry from the capacity + return BiosPartitionTable.DetectGeometry(disk); + } + + /// + /// Calculates the best guess disk type (i.e. floppy or hard disk). + /// + /// The capacity of the disk. + /// The disk type. + private static VirtualDiskClass DetectDiskType(long capacity) + { + if (capacity == Sizes.Sector * 1440 + || capacity == Sizes.Sector * 2880 + || capacity == Sizes.Sector * 5760) + { + return VirtualDiskClass.FloppyDisk; + } + else + { + return VirtualDiskClass.HardDisk; + } + } + + private static long FloppyCapacity(FloppyDiskType type) + { + switch (type) + { + case FloppyDiskType.DoubleDensity: + return Sizes.Sector * 1440; + case FloppyDiskType.HighDensity: + return Sizes.Sector * 2880; + case FloppyDiskType.Extended: + return Sizes.Sector * 5760; + default: + throw new ArgumentException("Invalid floppy disk type", "type"); + } + } + } +} diff --git a/DiscUtils/Registry/Bin.cs b/DiscUtils/Registry/Bin.cs new file mode 100644 index 0000000..78564a5 --- /dev/null +++ b/DiscUtils/Registry/Bin.cs @@ -0,0 +1,217 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// An internal structure within registry files, bins are the major unit of allocation in a registry hive. + /// + /// Bins are divided into multiple cells, that contain actual registry data. + internal sealed class Bin + { + private RegistryHive _hive; + private Stream _fileStream; + private long _streamPos; + + private BinHeader _header; + private byte[] _buffer; + + private List> _freeCells; + + public Bin(RegistryHive hive, Stream stream) + { + _hive = hive; + _fileStream = stream; + _streamPos = stream.Position; + + stream.Position = _streamPos; + byte[] buffer = Utilities.ReadFully(stream, 0x20); + _header = new BinHeader(); + _header.ReadFrom(buffer, 0); + + _fileStream.Position = _streamPos; + _buffer = Utilities.ReadFully(_fileStream, _header.BinSize); + + // Gather list of all free cells. + _freeCells = new List>(); + int pos = 0x20; + while (pos < _buffer.Length) + { + int size = Utilities.ToInt32LittleEndian(_buffer, pos); + if (size > 0) + { + _freeCells.Add(new Range(pos, size)); + } + + pos += Math.Abs(size); + } + } + + public Cell TryGetCell(int index) + { + int size = Utilities.ToInt32LittleEndian(_buffer, index - _header.FileOffset); + if (size >= 0) + { + return null; + } + + return Cell.Parse(_hive, index, _buffer, index + 4 - _header.FileOffset); + } + + public void FreeCell(int index) + { + int freeIndex = index - _header.FileOffset; + + int len = Utilities.ToInt32LittleEndian(_buffer, freeIndex); + if (len >= 0) + { + throw new ArgumentException("Attempt to free non-allocated cell"); + } + + len = Math.Abs(len); + + // If there's a free cell before this one, combine + int i = 0; + while (i < _freeCells.Count && _freeCells[i].Offset < freeIndex) + { + if (_freeCells[i].Offset + _freeCells[i].Count == freeIndex) + { + freeIndex = _freeCells[i].Offset; + len += _freeCells[i].Count; + _freeCells.RemoveAt(i); + } + else + { + ++i; + } + } + + // If there's a free cell after this one, combine + if (i < _freeCells.Count && _freeCells[i].Offset == freeIndex + len) + { + len += _freeCells[i].Count; + _freeCells.RemoveAt(i); + } + + // Record the new free cell + _freeCells.Insert(i, new Range(freeIndex, len)); + + // Free cells are indicated by length > 0 + Utilities.WriteBytesLittleEndian(len, _buffer, freeIndex); + + _fileStream.Position = _streamPos + freeIndex; + _fileStream.Write(_buffer, freeIndex, 4); + } + + public bool UpdateCell(Cell cell) + { + int index = cell.Index - _header.FileOffset; + int allocSize = Math.Abs(Utilities.ToInt32LittleEndian(_buffer, index)); + + int newSize = cell.Size + 4; + if (newSize > allocSize) + { + return false; + } + + cell.WriteTo(_buffer, index + 4); + + _fileStream.Position = _streamPos + index; + _fileStream.Write(_buffer, index, newSize); + + return true; + } + + public byte[] ReadRawCellData(int cellIndex, int maxBytes) + { + int index = cellIndex - _header.FileOffset; + int len = Math.Abs(Utilities.ToInt32LittleEndian(_buffer, index)); + byte[] result = new byte[Math.Min(len - 4, maxBytes)]; + Array.Copy(_buffer, index + 4, result, 0, result.Length); + return result; + } + + internal bool WriteRawCellData(int cellIndex, byte[] data, int offset, int count) + { + int index = cellIndex - _header.FileOffset; + int allocSize = Math.Abs(Utilities.ToInt32LittleEndian(_buffer, index)); + + int newSize = count + 4; + if (newSize > allocSize) + { + return false; + } + + Array.Copy(data, offset, _buffer, index + 4, count); + + _fileStream.Position = _streamPos + index; + _fileStream.Write(_buffer, index, newSize); + + return true; + } + + internal int AllocateCell(int size) + { + if (size < 8 || size % 8 != 0) + { + throw new ArgumentException("Invalid cell size"); + } + + // Very inefficient algorithm - will lead to fragmentation + for (int i = 0; i < _freeCells.Count; ++i) + { + int result = _freeCells[i].Offset + _header.FileOffset; + if (_freeCells[i].Count > size) + { + // Record the newly allocated cell + Utilities.WriteBytesLittleEndian(-size, _buffer, _freeCells[i].Offset); + _fileStream.Position = _streamPos + _freeCells[i].Offset; + _fileStream.Write(_buffer, _freeCells[i].Offset, 4); + + // Keep the remainder of the free buffer as unallocated + _freeCells[i] = new Range(_freeCells[i].Offset + size, _freeCells[i].Count - size); + Utilities.WriteBytesLittleEndian(_freeCells[i].Count, _buffer, _freeCells[i].Offset); + _fileStream.Position = _streamPos + _freeCells[i].Offset; + _fileStream.Write(_buffer, _freeCells[i].Offset, 4); + + return result; + } + else if (_freeCells[i].Count == size) + { + // Record the whole of the free buffer as a newly allocated cell + Utilities.WriteBytesLittleEndian(-size, _buffer, _freeCells[i].Offset); + _fileStream.Position = _streamPos + _freeCells[i].Offset; + _fileStream.Write(_buffer, _freeCells[i].Offset, 4); + + _freeCells.RemoveAt(i); + return result; + } + } + + return -1; + } + } +} diff --git a/DiscUtils/Registry/BinHeader.cs b/DiscUtils/Registry/BinHeader.cs new file mode 100644 index 0000000..a071739 --- /dev/null +++ b/DiscUtils/Registry/BinHeader.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.IO; + + internal sealed class BinHeader : IByteArraySerializable + { + public const int HeaderSize = 0x20; + + public int FileOffset; + public int BinSize; + + private const uint Signature = 0x6E696268; + + public BinHeader() + { + } + + public int Size + { + get { return HeaderSize; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + uint sig = Utilities.ToUInt32LittleEndian(buffer, offset + 0); + if (sig != Signature) + { + throw new IOException("Invalid signature for registry bin"); + } + + FileOffset = Utilities.ToInt32LittleEndian(buffer, offset + 0x04); + BinSize = Utilities.ToInt32LittleEndian(buffer, offset + 0x08); + long unknown = Utilities.ToInt64LittleEndian(buffer, offset + 0x0C); + long unknown1 = Utilities.ToInt64LittleEndian(buffer, offset + 0x14); + int unknown2 = Utilities.ToInt32LittleEndian(buffer, offset + 0x1C); + return HeaderSize; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Signature, buffer, offset + 0x00); + Utilities.WriteBytesLittleEndian(FileOffset, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(BinSize, buffer, offset + 0x08); + } + } +} diff --git a/DiscUtils/Registry/Cell.cs b/DiscUtils/Registry/Cell.cs new file mode 100644 index 0000000..07e7f64 --- /dev/null +++ b/DiscUtils/Registry/Cell.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + /// + /// Base class for the different kinds of cell present in a hive. + /// + internal abstract class Cell : IByteArraySerializable + { + private int _index; + + public Cell(int index) + { + _index = index; + } + + public int Index + { + get { return _index; } + set { _index = value; } + } + + public abstract int Size + { + get; + } + + public abstract int ReadFrom(byte[] buffer, int offset); + + public abstract void WriteTo(byte[] buffer, int offset); + + internal static Cell Parse(RegistryHive hive, int index, byte[] buffer, int pos) + { + string type = Utilities.BytesToString(buffer, pos, 2); + + Cell result = null; + + switch (type) + { + case "nk": + result = new KeyNodeCell(index); + break; + + case "sk": + result = new SecurityCell(index); + break; + + case "vk": + result = new ValueCell(index); + break; + + case "lh": + case "lf": + result = new SubKeyHashedListCell(hive, index); + break; + + case "li": + case "ri": + result = new SubKeyIndirectListCell(hive, index); + break; + + default: + throw new RegistryCorruptException("Unknown cell type '" + type + "'"); + } + + result.ReadFrom(buffer, pos); + return result; + } + } +} diff --git a/DiscUtils/Registry/HiveHeader.cs b/DiscUtils/Registry/HiveHeader.cs new file mode 100644 index 0000000..61012a8 --- /dev/null +++ b/DiscUtils/Registry/HiveHeader.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.IO; + using System.Text; + + internal sealed class HiveHeader : IByteArraySerializable + { + public const int HeaderSize = 512; + + public int Sequence1; + public int Sequence2; + public DateTime Timestamp; + public int MajorVersion; + public int MinorVersion; + public int RootCell; + public int Length; + public uint Checksum; + public string Path; + public Guid Guid1; + public Guid Guid2; + + private const uint Signature = 0x66676572; + + public HiveHeader() + { + Sequence1 = 1; + Sequence2 = 1; + Timestamp = DateTime.UtcNow; + MajorVersion = 1; + MinorVersion = 3; + RootCell = -1; + Path = string.Empty; + Guid1 = Guid.NewGuid(); + Guid2 = Guid.NewGuid(); + } + + public int Size + { + get { return HeaderSize; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + uint sig = Utilities.ToUInt32LittleEndian(buffer, offset + 0); + if (sig != Signature) + { + throw new IOException("Invalid signature for registry hive"); + } + + Sequence1 = Utilities.ToInt32LittleEndian(buffer, offset + 0x0004); + Sequence2 = Utilities.ToInt32LittleEndian(buffer, offset + 0x0008); + + Timestamp = DateTime.FromFileTimeUtc(Utilities.ToInt64LittleEndian(buffer, offset + 0x000C)); + + MajorVersion = Utilities.ToInt32LittleEndian(buffer, 0x0014); + MinorVersion = Utilities.ToInt32LittleEndian(buffer, 0x0018); + + int isLog = Utilities.ToInt32LittleEndian(buffer, 0x001C); + + RootCell = Utilities.ToInt32LittleEndian(buffer, 0x0024); + Length = Utilities.ToInt32LittleEndian(buffer, 0x0028); + + Path = Encoding.Unicode.GetString(buffer, 0x0030, 0x0040).Trim('\0'); + + Guid1 = Utilities.ToGuidLittleEndian(buffer, 0x0070); + Guid2 = Utilities.ToGuidLittleEndian(buffer, 0x0094); + + Checksum = Utilities.ToUInt32LittleEndian(buffer, 0x01FC); + + if (Sequence1 != Sequence2) + { + throw new NotImplementedException("Support for replaying registry log file"); + } + + if (Checksum != CalcChecksum(buffer, offset)) + { + throw new IOException("Invalid checksum on registry file"); + } + + return HeaderSize; + } + + public void WriteTo(byte[] buffer, int offset) + { + Utilities.WriteBytesLittleEndian(Signature, buffer, offset); + Utilities.WriteBytesLittleEndian(Sequence1, buffer, offset + 0x0004); + Utilities.WriteBytesLittleEndian(Sequence2, buffer, offset + 0x0008); + Utilities.WriteBytesLittleEndian(Timestamp.ToFileTimeUtc(), buffer, offset + 0x000C); + Utilities.WriteBytesLittleEndian(MajorVersion, buffer, offset + 0x0014); + Utilities.WriteBytesLittleEndian(MinorVersion, buffer, offset + 0x0018); + + Utilities.WriteBytesLittleEndian((uint)1, buffer, offset + 0x0020); // Unknown - seems to be '1' + + Utilities.WriteBytesLittleEndian(RootCell, buffer, offset + 0x0024); + Utilities.WriteBytesLittleEndian(Length, buffer, offset + 0x0028); + + Encoding.Unicode.GetBytes(Path, 0, Path.Length, buffer, offset + 0x0030); + Utilities.WriteBytesLittleEndian((ushort)0, buffer, offset + 0x0030 + (Path.Length * 2)); + + Utilities.WriteBytesLittleEndian(Guid1, buffer, offset + 0x0070); + Utilities.WriteBytesLittleEndian(Guid2, buffer, offset + 0x0094); + + Utilities.WriteBytesLittleEndian(CalcChecksum(buffer, offset), buffer, offset + 0x01FC); + } + + private static uint CalcChecksum(byte[] buffer, int offset) + { + uint sum = 0; + + for (int i = 0; i < 0x01FC; i += 4) + { + sum = sum ^ Utilities.ToUInt32LittleEndian(buffer, offset + i); + } + + return sum; + } + } +} diff --git a/DiscUtils/Registry/KeyNodeCell.cs b/DiscUtils/Registry/KeyNodeCell.cs new file mode 100644 index 0000000..99cf06c --- /dev/null +++ b/DiscUtils/Registry/KeyNodeCell.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + + internal sealed class KeyNodeCell : Cell + { + public RegistryKeyFlags Flags; + public DateTime Timestamp; + public int ParentIndex; + public int NumSubKeys; + public int SubKeysIndex; + public int NumValues; + public int ValueListIndex; + public int SecurityIndex; + public int ClassNameIndex; + + /// + /// Number of bytes to represent largest subkey name in Unicode - no null terminator. + /// + public int MaxSubKeyNameBytes; + + /// + /// Number of bytes to represent largest value name in Unicode - no null terminator. + /// + public int MaxValNameBytes; + + /// + /// Number of bytes to represent largest value content (strings in Unicode, with null terminator - if stored). + /// + public int MaxValDataBytes; + + public int IndexInParent; + public int ClassNameLength; + public string Name; + + public KeyNodeCell(string name, int parentCellIndex) + : this(-1) + { + Flags = RegistryKeyFlags.Normal; + Timestamp = DateTime.UtcNow; + ParentIndex = parentCellIndex; + SubKeysIndex = -1; + ValueListIndex = -1; + SecurityIndex = -1; + ClassNameIndex = -1; + Name = name; + } + + public KeyNodeCell(int index) + : base(index) + { + } + + public override int Size + { + get { return 0x4C + Name.Length; } + } + + public override int ReadFrom(byte[] buffer, int offset) + { + Flags = (RegistryKeyFlags)Utilities.ToUInt16LittleEndian(buffer, offset + 0x02); + Timestamp = DateTime.FromFileTimeUtc(Utilities.ToInt64LittleEndian(buffer, offset + 0x04)); + ParentIndex = Utilities.ToInt32LittleEndian(buffer, offset + 0x10); + NumSubKeys = Utilities.ToInt32LittleEndian(buffer, offset + 0x14); + SubKeysIndex = Utilities.ToInt32LittleEndian(buffer, offset + 0x1C); + NumValues = Utilities.ToInt32LittleEndian(buffer, offset + 0x24); + ValueListIndex = Utilities.ToInt32LittleEndian(buffer, offset + 0x28); + SecurityIndex = Utilities.ToInt32LittleEndian(buffer, offset + 0x2C); + ClassNameIndex = Utilities.ToInt32LittleEndian(buffer, offset + 0x30); + MaxSubKeyNameBytes = Utilities.ToInt32LittleEndian(buffer, offset + 0x34); + MaxValNameBytes = Utilities.ToInt32LittleEndian(buffer, offset + 0x3C); + MaxValDataBytes = Utilities.ToInt32LittleEndian(buffer, offset + 0x40); + IndexInParent = Utilities.ToInt32LittleEndian(buffer, offset + 0x44); + int nameLength = Utilities.ToInt16LittleEndian(buffer, offset + 0x48); + ClassNameLength = Utilities.ToInt16LittleEndian(buffer, offset + 0x4A); + Name = Utilities.BytesToString(buffer, offset + 0x4C, nameLength); + + return 0x4C + nameLength; + } + + public override void WriteTo(byte[] buffer, int offset) + { + Utilities.StringToBytes("nk", buffer, offset, 2); + Utilities.WriteBytesLittleEndian((ushort)Flags, buffer, offset + 0x02); + Utilities.WriteBytesLittleEndian(Timestamp.ToFileTimeUtc(), buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(ParentIndex, buffer, offset + 0x10); + Utilities.WriteBytesLittleEndian(NumSubKeys, buffer, offset + 0x14); + Utilities.WriteBytesLittleEndian(SubKeysIndex, buffer, offset + 0x1C); + Utilities.WriteBytesLittleEndian(NumValues, buffer, offset + 0x24); + Utilities.WriteBytesLittleEndian(ValueListIndex, buffer, offset + 0x28); + Utilities.WriteBytesLittleEndian(SecurityIndex, buffer, offset + 0x2C); + Utilities.WriteBytesLittleEndian(ClassNameIndex, buffer, offset + 0x30); + Utilities.WriteBytesLittleEndian(IndexInParent, buffer, offset + 0x44); + Utilities.WriteBytesLittleEndian((ushort)Name.Length, buffer, offset + 0x48); + Utilities.WriteBytesLittleEndian(ClassNameLength, buffer, offset + 0x4A); + Utilities.StringToBytes(Name, buffer, offset + 0x4C, Name.Length); + } + + public override string ToString() + { + return "Key:" + Name + "[" + Flags + "] <" + Timestamp + ">"; + } + } +} diff --git a/DiscUtils/Registry/ListCell.cs b/DiscUtils/Registry/ListCell.cs new file mode 100644 index 0000000..e69c0c7 --- /dev/null +++ b/DiscUtils/Registry/ListCell.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System.Collections.Generic; + + internal abstract class ListCell : Cell + { + public ListCell(int index) + : base(index) + { + } + + /// + /// Gets the number of subkeys in this list. + /// + internal abstract int Count { get; } + + /// + /// Searches for a key with a given name. + /// + /// The name to search for. + /// The index of the cell, if found. + /// The search result. + internal abstract int FindKey(string name, out int cellIndex); + + /// + /// Enumerates all of the keys in the list. + /// + /// The list to populate. + internal abstract void EnumerateKeys(List names); + + /// + /// Enumerates all of the keys in the list. + /// + /// Enumeration of key cells. + internal abstract IEnumerable EnumerateKeys(); + + /// + /// Adds a subkey to this list. + /// + /// The name of the subkey. + /// The cell index of the subkey. + /// The new cell index of the list, which may have changed. + internal abstract int LinkSubKey(string name, int cellIndex); + + /// + /// Removes a subkey from this list. + /// + /// The name of the subkey. + /// The new cell index of the list, which may have changed. + internal abstract int UnlinkSubKey(string name); + } +} diff --git a/DiscUtils/Registry/RegistryCorruptException.cs b/DiscUtils/Registry/RegistryCorruptException.cs new file mode 100644 index 0000000..d80dd39 --- /dev/null +++ b/DiscUtils/Registry/RegistryCorruptException.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.Runtime.Serialization; + + /// + /// Exception thrown when some corruption is found in the registry hive. + /// + [Serializable] + public class RegistryCorruptException : Exception + { + /// + /// Initializes a new instance of the RegistryCorruptException class. + /// + public RegistryCorruptException() + { + } + + /// + /// Initializes a new instance of the RegistryCorruptException class. + /// + /// The exception message. + public RegistryCorruptException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the RegistryCorruptException class. + /// + /// The exception message. + /// The inner exception. + public RegistryCorruptException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the RegistryCorruptException class. + /// + /// The serialization info. + /// The streaming context. + protected RegistryCorruptException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/DiscUtils/Registry/RegistryHive.cs b/DiscUtils/Registry/RegistryHive.cs new file mode 100644 index 0000000..d61b33b --- /dev/null +++ b/DiscUtils/Registry/RegistryHive.cs @@ -0,0 +1,404 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Security.AccessControl; + + /// + /// A registry hive. + /// + public sealed class RegistryHive : IDisposable + { + private const long BinStart = 4 * Sizes.OneKiB; + + private Stream _fileStream; + private Ownership _ownsStream; + private HiveHeader _header; + private List _bins; + + /// + /// Initializes a new instance of the RegistryHive class. + /// + /// The stream containing the registry hive. + /// + /// The created object does not assume ownership of the stream. + /// + public RegistryHive(Stream hive) + : this(hive, Ownership.None) + { + } + + /// + /// Initializes a new instance of the RegistryHive class. + /// + /// The stream containing the registry hive. + /// Whether the new object assumes object of the stream. + public RegistryHive(Stream hive, Ownership ownership) + { + _fileStream = hive; + _fileStream.Position = 0; + _ownsStream = ownership; + + byte[] buffer = Utilities.ReadFully(_fileStream, HiveHeader.HeaderSize); + + _header = new HiveHeader(); + _header.ReadFrom(buffer, 0); + + _bins = new List(); + int pos = 0; + while (pos < _header.Length) + { + _fileStream.Position = BinStart + pos; + byte[] headerBuffer = Utilities.ReadFully(_fileStream, BinHeader.HeaderSize); + BinHeader header = new BinHeader(); + header.ReadFrom(headerBuffer, 0); + _bins.Add(header); + + pos += header.BinSize; + } + } + + /// + /// Gets the root key in the registry hive. + /// + public RegistryKey Root + { + get { return new RegistryKey(this, GetCell(_header.RootCell)); } + } + + /// + /// Creates a new (empty) registry hive. + /// + /// The stream to contain the new hive. + /// The new hive. + /// + /// The returned object does not assume ownership of the stream. + /// + public static RegistryHive Create(Stream stream) + { + return Create(stream, Ownership.None); + } + + /// + /// Creates a new (empty) registry hive. + /// + /// The stream to contain the new hive. + /// Whether the returned object owns the stream. + /// The new hive. + public static RegistryHive Create(Stream stream, Ownership ownership) + { + if (stream == null) + { + throw new ArgumentNullException("stream", "Attempt to create registry hive in null stream"); + } + + // Construct a file with minimal structure - hive header, plus one (empty) bin + BinHeader binHeader = new BinHeader(); + binHeader.FileOffset = 0; + binHeader.BinSize = (int)(4 * Sizes.OneKiB); + + HiveHeader hiveHeader = new HiveHeader(); + hiveHeader.Length = binHeader.BinSize; + + stream.Position = 0; + + byte[] buffer = new byte[hiveHeader.Size]; + hiveHeader.WriteTo(buffer, 0); + stream.Write(buffer, 0, buffer.Length); + + buffer = new byte[binHeader.Size]; + binHeader.WriteTo(buffer, 0); + stream.Position = BinStart; + stream.Write(buffer, 0, buffer.Length); + + buffer = new byte[4]; + Utilities.WriteBytesLittleEndian(binHeader.BinSize - binHeader.Size, buffer, 0); + stream.Write(buffer, 0, buffer.Length); + + // Make sure the file is initialized out to the end of the firs bin + stream.Position = BinStart + binHeader.BinSize - 1; + stream.WriteByte(0); + + // Temporary hive to perform construction of higher-level structures + RegistryHive newHive = new RegistryHive(stream); + KeyNodeCell rootCell = new KeyNodeCell("root", -1); + rootCell.Flags = RegistryKeyFlags.Normal | RegistryKeyFlags.Root; + newHive.UpdateCell(rootCell, true); + + RegistrySecurity sd = new RegistrySecurity(); + sd.SetSecurityDescriptorSddlForm("O:BAG:BAD:PAI(A;;KA;;;SY)(A;CI;KA;;;BA)", AccessControlSections.All); + SecurityCell secCell = new SecurityCell(sd); + newHive.UpdateCell(secCell, true); + secCell.NextIndex = secCell.Index; + secCell.PreviousIndex = secCell.Index; + newHive.UpdateCell(secCell, false); + + rootCell.SecurityIndex = secCell.Index; + newHive.UpdateCell(rootCell, false); + + // Ref the root cell from the hive header + hiveHeader.RootCell = rootCell.Index; + buffer = new byte[hiveHeader.Size]; + hiveHeader.WriteTo(buffer, 0); + stream.Position = 0; + stream.Write(buffer, 0, buffer.Length); + + // Finally, return the new hive + return new RegistryHive(stream, ownership); + } + + /// + /// Creates a new (empty) registry hive. + /// + /// The file to create the new hive in. + /// The new hive. + public static RegistryHive Create(string path) + { + return Create(new FileStream(path, FileMode.Create, FileAccess.ReadWrite), Ownership.Dispose); + } + + /// + /// Disposes of this instance, freeing any underlying stream (if any). + /// + public void Dispose() + { + if (_fileStream != null && _ownsStream == Ownership.Dispose) + { + _fileStream.Dispose(); + _fileStream = null; + } + } + + internal K GetCell(int index) + where K : Cell + { + Bin bin = GetBin(index); + + if (bin != null) + { + return (K)bin.TryGetCell(index); + } + else + { + return null; + } + } + + internal void FreeCell(int index) + { + Bin bin = GetBin(index); + + if (bin != null) + { + bin.FreeCell(index); + } + } + + internal int UpdateCell(Cell cell, bool canRelocate) + { + if (cell.Index == -1 && canRelocate) + { + cell.Index = AllocateRawCell(cell.Size); + } + + Bin bin = GetBin(cell.Index); + + if (bin != null) + { + if (bin.UpdateCell(cell)) + { + return cell.Index; + } + else if (canRelocate) + { + int oldCell = cell.Index; + cell.Index = AllocateRawCell(cell.Size); + bin = GetBin(cell.Index); + if (!bin.UpdateCell(cell)) + { + cell.Index = oldCell; + throw new RegistryCorruptException("Failed to migrate cell to new location"); + } + + FreeCell(oldCell); + return cell.Index; + } + else + { + throw new ArgumentException("Can't update cell, needs relocation but relocation disabled", "canRelocate"); + } + } + else + { + throw new RegistryCorruptException("No bin found containing index: " + cell.Index); + } + } + + internal byte[] RawCellData(int index, int maxBytes) + { + Bin bin = GetBin(index); + + if (bin != null) + { + return bin.ReadRawCellData(index, maxBytes); + } + else + { + return null; + } + } + + internal bool WriteRawCellData(int index, byte[] data, int offset, int count) + { + Bin bin = GetBin(index); + + if (bin != null) + { + return bin.WriteRawCellData(index, data, offset, count); + } + else + { + throw new RegistryCorruptException("No bin found containing index: " + index); + } + } + + internal int AllocateRawCell(int capacity) + { + int minSize = Utilities.RoundUp(capacity + 4, 8); // Allow for size header and ensure multiple of 8 + + // Incredibly inefficient algorithm... + foreach (var binHeader in _bins) + { + Bin bin = LoadBin(binHeader); + int cellIndex = bin.AllocateCell(minSize); + + if (cellIndex >= 0) + { + return cellIndex; + } + } + + BinHeader newBinHeader = AllocateBin(minSize); + Bin newBin = LoadBin(newBinHeader); + return newBin.AllocateCell(minSize); + } + + private BinHeader FindBin(int index) + { + int binsIdx = _bins.BinarySearch(null, new BinFinder(index)); + if (binsIdx >= 0) + { + return _bins[binsIdx]; + } + + return null; + } + + private Bin GetBin(int cellIndex) + { + BinHeader binHeader = FindBin(cellIndex); + + if (binHeader != null) + { + return LoadBin(binHeader); + } + + return null; + } + + private Bin LoadBin(BinHeader binHeader) + { + _fileStream.Position = BinStart + binHeader.FileOffset; + return new Bin(this, _fileStream); + } + + private BinHeader AllocateBin(int minSize) + { + BinHeader lastBin = _bins[_bins.Count - 1]; + + BinHeader newBinHeader = new BinHeader(); + newBinHeader.FileOffset = lastBin.FileOffset + lastBin.BinSize; + newBinHeader.BinSize = Utilities.RoundUp(minSize + newBinHeader.Size, 4 * (int)Sizes.OneKiB); + + byte[] buffer = new byte[newBinHeader.Size]; + newBinHeader.WriteTo(buffer, 0); + _fileStream.Position = BinStart + newBinHeader.FileOffset; + _fileStream.Write(buffer, 0, buffer.Length); + + byte[] cellHeader = new byte[4]; + Utilities.WriteBytesLittleEndian(newBinHeader.BinSize - newBinHeader.Size, cellHeader, 0); + _fileStream.Write(cellHeader, 0, 4); + + // Update hive with new length + _header.Length = newBinHeader.FileOffset + newBinHeader.BinSize; + _header.Timestamp = DateTime.UtcNow; + _header.Sequence1++; + _header.Sequence2++; + _fileStream.Position = 0; + byte[] hiveHeader = Utilities.ReadFully(_fileStream, _header.Size); + _header.WriteTo(hiveHeader, 0); + _fileStream.Position = 0; + _fileStream.Write(hiveHeader, 0, hiveHeader.Length); + + // Make sure the file is initialized to desired position + _fileStream.Position = BinStart + _header.Length - 1; + _fileStream.WriteByte(0); + + _bins.Add(newBinHeader); + return newBinHeader; + } + + private class BinFinder : IComparer + { + private int _index; + + public BinFinder(int index) + { + _index = index; + } + + #region IComparer Members + + public int Compare(BinHeader x, BinHeader y) + { + if (x.FileOffset + x.BinSize < _index) + { + return -1; + } + else if (x.FileOffset > _index) + { + return 1; + } + else + { + return 0; + } + } + + #endregion + } + } +} diff --git a/DiscUtils/Registry/RegistryKey.cs b/DiscUtils/Registry/RegistryKey.cs new file mode 100644 index 0000000..ef31e1d --- /dev/null +++ b/DiscUtils/Registry/RegistryKey.cs @@ -0,0 +1,853 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.Collections.Generic; + using System.Security.AccessControl; + using System.Text; + + /// + /// A key within a registry hive. + /// + public sealed class RegistryKey + { + private RegistryHive _hive; + private KeyNodeCell _cell; + + internal RegistryKey(RegistryHive hive, KeyNodeCell cell) + { + _hive = hive; + _cell = cell; + } + + /// + /// Gets the name of this key. + /// + public string Name + { + get + { + RegistryKey parent = Parent; + if (parent != null && ((parent.Flags & RegistryKeyFlags.Root) == 0)) + { + return parent.Name + @"\" + _cell.Name; + } + else + { + return _cell.Name; + } + } + } + + /// + /// Gets the number of child keys. + /// + public int SubKeyCount + { + get { return _cell.NumSubKeys; } + } + + /// + /// Gets the number of values in this key. + /// + public int ValueCount + { + get { return _cell.NumValues; } + } + + /// + /// Gets the time the key was last modified. + /// + public DateTime Timestamp + { + get { return _cell.Timestamp; } + } + + /// + /// Gets the parent key, or null if this is the root key. + /// + public RegistryKey Parent + { + get + { + if ((_cell.Flags & RegistryKeyFlags.Root) == 0) + { + return new RegistryKey(_hive, _hive.GetCell(_cell.ParentIndex)); + } + else + { + return null; + } + } + } + + /// + /// Gets the flags of this registry key. + /// + public RegistryKeyFlags Flags + { + get { return _cell.Flags; } + } + + /// + /// Gets the class name of this registry key. + /// + /// Class name is rarely used. + public string ClassName + { + get + { + if (_cell.ClassNameIndex > 0) + { + return Encoding.Unicode.GetString(_hive.RawCellData(_cell.ClassNameIndex, _cell.ClassNameLength)); + } + + return null; + } + } + + /// + /// Gets an enumerator over all sub child keys. + /// + public IEnumerable SubKeys + { + get + { + if (_cell.NumSubKeys != 0) + { + ListCell list = _hive.GetCell(_cell.SubKeysIndex); + foreach (var key in list.EnumerateKeys()) + { + yield return new RegistryKey(_hive, key); + } + } + } + } + + /// + /// Gets an enumerator over all values in this key. + /// + private IEnumerable Values + { + get + { + if (_cell.NumValues != 0) + { + byte[] valueList = _hive.RawCellData(_cell.ValueListIndex, _cell.NumValues * 4); + + for (int i = 0; i < _cell.NumValues; ++i) + { + int valueIndex = Utilities.ToInt32LittleEndian(valueList, i * 4); + yield return new RegistryValue(_hive, _hive.GetCell(valueIndex)); + } + } + } + } + + /// + /// Gets the Security Descriptor applied to the registry key. + /// + /// The security descriptor as a RegistrySecurity instance. + public RegistrySecurity GetAccessControl() + { + if (_cell.SecurityIndex > 0) + { + SecurityCell secCell = _hive.GetCell(_cell.SecurityIndex); + return secCell.SecurityDescriptor; + } + + return null; + } + + /// + /// Gets the names of all child sub keys. + /// + /// The names of the sub keys. + public string[] GetSubKeyNames() + { + List names = new List(); + + if (_cell.NumSubKeys != 0) + { + _hive.GetCell(_cell.SubKeysIndex).EnumerateKeys(names); + } + + return names.ToArray(); + } + + /// + /// Gets a named value stored within this key. + /// + /// The name of the value to retrieve. + /// The value as a .NET object. + /// The mapping from registry type of .NET type is as follows: + /// + /// + /// Value Type + /// .NET type + /// + /// + /// String + /// string + /// + /// + /// ExpandString + /// string + /// + /// + /// Link + /// string + /// + /// + /// DWord + /// uint + /// + /// + /// DWordBigEndian + /// uint + /// + /// + /// MultiString + /// string[] + /// + /// + /// QWord + /// ulong + /// + /// + /// + public object GetValue(string name) + { + return GetValue(name, null, Microsoft.Win32.RegistryValueOptions.None); + } + + /// + /// Gets a named value stored within this key. + /// + /// The name of the value to retrieve. + /// The default value to return, if no existing value is stored. + /// The value as a .NET object. + /// The mapping from registry type of .NET type is as follows: + /// + /// + /// Value Type + /// .NET type + /// + /// + /// String + /// string + /// + /// + /// ExpandString + /// string + /// + /// + /// Link + /// string + /// + /// + /// DWord + /// uint + /// + /// + /// DWordBigEndian + /// uint + /// + /// + /// MultiString + /// string[] + /// + /// + /// QWord + /// ulong + /// + /// + /// + public object GetValue(string name, object defaultValue) + { + return GetValue(name, defaultValue, Microsoft.Win32.RegistryValueOptions.None); + } + + /// + /// Gets a named value stored within this key. + /// + /// The name of the value to retrieve. + /// The default value to return, if no existing value is stored. + /// Flags controlling how the value is processed before it's returned. + /// The value as a .NET object. + /// The mapping from registry type of .NET type is as follows: + /// + /// + /// Value Type + /// .NET type + /// + /// + /// String + /// string + /// + /// + /// ExpandString + /// string + /// + /// + /// Link + /// string + /// + /// + /// DWord + /// uint + /// + /// + /// DWordBigEndian + /// uint + /// + /// + /// MultiString + /// string[] + /// + /// + /// QWord + /// ulong + /// + /// + /// + public object GetValue(string name, object defaultValue, Microsoft.Win32.RegistryValueOptions options) + { + RegistryValue regVal = GetRegistryValue(name); + if (regVal != null) + { + if (regVal.DataType == RegistryValueType.ExpandString && (options & Microsoft.Win32.RegistryValueOptions.DoNotExpandEnvironmentNames) == 0) + { + return Environment.ExpandEnvironmentVariables((string)regVal.Value); + } + else + { + return regVal.Value; + } + } + + return defaultValue; + } + + /// + /// Sets a named value stored within this key. + /// + /// The name of the value to store. + /// The value to store. + public void SetValue(string name, object value) + { + SetValue(name, value, RegistryValueType.None); + } + + /// + /// Sets a named value stored within this key. + /// + /// The name of the value to store. + /// The value to store. + /// The registry type of the data. + public void SetValue(string name, object value, RegistryValueType valueType) + { + RegistryValue valObj = GetRegistryValue(name); + if (valObj == null) + { + valObj = AddRegistryValue(name); + } + + valObj.SetValue(value, valueType); + } + + /// + /// Deletes a named value stored within this key. + /// + /// The name of the value to delete. + public void DeleteValue(string name) + { + DeleteValue(name, true); + } + + /// + /// Deletes a named value stored within this key. + /// + /// The name of the value to delete. + /// Throws ArgumentException if name doesn't exist. + public void DeleteValue(string name, bool throwOnMissingValue) + { + bool foundValue = false; + + if (_cell.NumValues != 0) + { + byte[] valueList = _hive.RawCellData(_cell.ValueListIndex, _cell.NumValues * 4); + + int i = 0; + while (i < _cell.NumValues) + { + int valueIndex = Utilities.ToInt32LittleEndian(valueList, i * 4); + ValueCell valueCell = _hive.GetCell(valueIndex); + if (string.Compare(valueCell.Name, name, StringComparison.OrdinalIgnoreCase) == 0) + { + foundValue = true; + _hive.FreeCell(valueIndex); + _cell.NumValues--; + _hive.UpdateCell(_cell, false); + break; + } + + ++i; + } + + // Move following value's to fill gap + if (i < _cell.NumValues) + { + while (i < _cell.NumValues) + { + int valueIndex = Utilities.ToInt32LittleEndian(valueList, (i + 1) * 4); + Utilities.WriteBytesLittleEndian(valueIndex, valueList, i * 4); + + ++i; + } + + _hive.WriteRawCellData(_cell.ValueListIndex, valueList, 0, _cell.NumValues * 4); + } + + // TODO: Update maxbytes for value name and value content if this was the largest value for either. + // Windows seems to repair this info, if not accurate, though. + } + + if (throwOnMissingValue && !foundValue) + { + throw new ArgumentException("No such value: " + name, "name"); + } + } + + /// + /// Gets the type of a named value. + /// + /// The name of the value to inspect. + /// The value's type. + public RegistryValueType GetValueType(string name) + { + RegistryValue regVal = GetRegistryValue(name); + if (regVal != null) + { + return regVal.DataType; + } + + return RegistryValueType.None; + } + + /// + /// Gets the names of all values in this key. + /// + /// An array of strings containing the value names. + public string[] GetValueNames() + { + List names = new List(); + foreach (var value in Values) + { + names.Add(value.Name); + } + + return names.ToArray(); + } + + /// + /// Creates or opens a subkey. + /// + /// The relative path the the subkey. + /// The subkey. + public RegistryKey CreateSubKey(string subkey) + { + if (string.IsNullOrEmpty(subkey)) + { + return this; + } + + string[] split = subkey.Split(new char[] { '\\' }, 2); + int cellIndex = FindSubKeyCell(split[0]); + + if (cellIndex < 0) + { + KeyNodeCell newKeyCell = new KeyNodeCell(split[0], _cell.Index); + newKeyCell.SecurityIndex = _cell.SecurityIndex; + ReferenceSecurityCell(newKeyCell.SecurityIndex); + _hive.UpdateCell(newKeyCell, true); + + LinkSubKey(split[0], newKeyCell.Index); + + if (split.Length == 1) + { + return new RegistryKey(_hive, newKeyCell); + } + else + { + return new RegistryKey(_hive, newKeyCell).CreateSubKey(split[1]); + } + } + else + { + KeyNodeCell cell = _hive.GetCell(cellIndex); + if (split.Length == 1) + { + return new RegistryKey(_hive, cell); + } + else + { + return new RegistryKey(_hive, cell).CreateSubKey(split[1]); + } + } + } + + /// + /// Opens a sub key. + /// + /// The relative path to the sub key. + /// The sub key, or null if not found. + public RegistryKey OpenSubKey(string path) + { + if (string.IsNullOrEmpty(path)) + { + return this; + } + + string[] split = path.Split(new char[] { '\\' }, 2); + int cellIndex = FindSubKeyCell(split[0]); + + if (cellIndex < 0) + { + return null; + } + else + { + KeyNodeCell cell = _hive.GetCell(cellIndex); + if (split.Length == 1) + { + return new RegistryKey(_hive, cell); + } + else + { + return new RegistryKey(_hive, cell).OpenSubKey(split[1]); + } + } + } + + /// + /// Deletes a subkey and any child subkeys recursively. The string subkey is not case-sensitive. + /// + /// The subkey to delete. + public void DeleteSubKeyTree(string subkey) + { + RegistryKey subKeyObj = OpenSubKey(subkey); + if (subKeyObj == null) + { + return; + } + + if ((subKeyObj.Flags & RegistryKeyFlags.Root) != 0) + { + throw new ArgumentException("Attempt to delete root key"); + } + + foreach (var child in subKeyObj.GetSubKeyNames()) + { + subKeyObj.DeleteSubKeyTree(child); + } + + DeleteSubKey(subkey); + } + + /// + /// Deletes the specified subkey. The string subkey is not case-sensitive. + /// + /// The subkey to delete. + public void DeleteSubKey(string subkey) + { + DeleteSubKey(subkey, true); + } + + /// + /// Deletes the specified subkey. The string subkey is not case-sensitive. + /// + /// The subkey to delete. + /// true to throw an argument exception if subkey doesn't exist. + public void DeleteSubKey(string subkey, bool throwOnMissingSubKey) + { + if (string.IsNullOrEmpty(subkey)) + { + throw new ArgumentException("Invalid SubKey", "subkey"); + } + + string[] split = subkey.Split(new char[] { '\\' }, 2); + + int subkeyCellIndex = FindSubKeyCell(split[0]); + if (subkeyCellIndex < 0) + { + if (throwOnMissingSubKey) + { + throw new ArgumentException("No such SubKey", "subkey"); + } + else + { + return; + } + } + + KeyNodeCell subkeyCell = _hive.GetCell(subkeyCellIndex); + + if (split.Length == 1) + { + if (subkeyCell.NumSubKeys != 0) + { + throw new InvalidOperationException("The registry key has subkeys"); + } + + if (subkeyCell.ClassNameIndex != -1) + { + _hive.FreeCell(subkeyCell.ClassNameIndex); + subkeyCell.ClassNameIndex = -1; + subkeyCell.ClassNameLength = 0; + } + + if (subkeyCell.SecurityIndex != -1) + { + DereferenceSecurityCell(subkeyCell.SecurityIndex); + subkeyCell.SecurityIndex = -1; + } + + if (subkeyCell.SubKeysIndex != -1) + { + FreeSubKeys(subkeyCell); + } + + if (subkeyCell.ValueListIndex != -1) + { + FreeValues(subkeyCell); + } + + UnlinkSubKey(subkey); + _hive.FreeCell(subkeyCellIndex); + _hive.UpdateCell(_cell, false); + } + else + { + new RegistryKey(_hive, subkeyCell).DeleteSubKey(split[1], throwOnMissingSubKey); + } + } + + private RegistryValue GetRegistryValue(string name) + { + if (name != null && name.Length == 0) + { + name = null; + } + + if (_cell.NumValues != 0) + { + byte[] valueList = _hive.RawCellData(_cell.ValueListIndex, _cell.NumValues * 4); + + for (int i = 0; i < _cell.NumValues; ++i) + { + int valueIndex = Utilities.ToInt32LittleEndian(valueList, i * 4); + ValueCell cell = _hive.GetCell(valueIndex); + if (string.Compare(cell.Name, name, StringComparison.OrdinalIgnoreCase) == 0) + { + return new RegistryValue(_hive, cell); + } + } + } + + return null; + } + + private RegistryValue AddRegistryValue(string name) + { + byte[] valueList = _hive.RawCellData(_cell.ValueListIndex, _cell.NumValues * 4); + if (valueList == null) + { + valueList = new byte[0]; + } + + int insertIdx = 0; + while (insertIdx < _cell.NumValues) + { + int valueCellIndex = Utilities.ToInt32LittleEndian(valueList, insertIdx * 4); + ValueCell cell = _hive.GetCell(valueCellIndex); + if (string.Compare(name, cell.Name, StringComparison.OrdinalIgnoreCase) < 0) + { + break; + } + + ++insertIdx; + } + + // Allocate a new value cell (note _hive.UpdateCell does actual allocation). + ValueCell valueCell = new ValueCell(name); + _hive.UpdateCell(valueCell, true); + + // Update the value list, re-allocating if necessary + byte[] newValueList = new byte[(_cell.NumValues * 4) + 4]; + Array.Copy(valueList, 0, newValueList, 0, insertIdx * 4); + Utilities.WriteBytesLittleEndian(valueCell.Index, newValueList, insertIdx * 4); + Array.Copy(valueList, insertIdx * 4, newValueList, (insertIdx * 4) + 4, (_cell.NumValues - insertIdx) * 4); + if (_cell.ValueListIndex == -1 || !_hive.WriteRawCellData(_cell.ValueListIndex, newValueList, 0, newValueList.Length)) + { + int newListCellIndex = _hive.AllocateRawCell(Utilities.RoundUp(newValueList.Length, 8)); + _hive.WriteRawCellData(newListCellIndex, newValueList, 0, newValueList.Length); + + if (_cell.ValueListIndex != -1) + { + _hive.FreeCell(_cell.ValueListIndex); + } + + _cell.ValueListIndex = newListCellIndex; + } + + // Record the new value and save this cell + _cell.NumValues++; + _hive.UpdateCell(_cell, false); + + // Finally, set the data in the value cell + return new RegistryValue(_hive, valueCell); + } + + private int FindSubKeyCell(string name) + { + if (_cell.NumSubKeys != 0) + { + ListCell listCell = _hive.GetCell(_cell.SubKeysIndex); + + int cellIndex; + if (listCell.FindKey(name, out cellIndex) == 0) + { + return cellIndex; + } + } + + return -1; + } + + private void LinkSubKey(string name, int cellIndex) + { + if (_cell.SubKeysIndex == -1) + { + SubKeyHashedListCell newListCell = new SubKeyHashedListCell(_hive, "lf"); + newListCell.Add(name, cellIndex); + _hive.UpdateCell(newListCell, true); + _cell.NumSubKeys = 1; + _cell.SubKeysIndex = newListCell.Index; + } + else + { + ListCell list = _hive.GetCell(_cell.SubKeysIndex); + _cell.SubKeysIndex = list.LinkSubKey(name, cellIndex); + _cell.NumSubKeys++; + } + + _hive.UpdateCell(_cell, false); + } + + private void UnlinkSubKey(string name) + { + if (_cell.SubKeysIndex == -1 || _cell.NumSubKeys == 0) + { + throw new InvalidOperationException("No subkey list"); + } + + ListCell list = _hive.GetCell(_cell.SubKeysIndex); + _cell.SubKeysIndex = list.UnlinkSubKey(name); + _cell.NumSubKeys--; + } + + private void ReferenceSecurityCell(int cellIndex) + { + SecurityCell sc = _hive.GetCell(cellIndex); + sc.UsageCount++; + _hive.UpdateCell(sc, false); + } + + private void DereferenceSecurityCell(int cellIndex) + { + SecurityCell sc = _hive.GetCell(cellIndex); + sc.UsageCount--; + if (sc.UsageCount == 0) + { + SecurityCell prev = _hive.GetCell(sc.PreviousIndex); + prev.NextIndex = sc.NextIndex; + _hive.UpdateCell(prev, false); + + SecurityCell next = _hive.GetCell(sc.NextIndex); + next.PreviousIndex = sc.PreviousIndex; + _hive.UpdateCell(next, false); + + _hive.FreeCell(cellIndex); + } + else + { + _hive.UpdateCell(sc, false); + } + } + + private void FreeValues(KeyNodeCell cell) + { + if (cell.NumValues != 0 && cell.ValueListIndex != -1) + { + byte[] valueList = _hive.RawCellData(cell.ValueListIndex, cell.NumValues * 4); + + for (int i = 0; i < cell.NumValues; ++i) + { + int valueIndex = Utilities.ToInt32LittleEndian(valueList, i * 4); + _hive.FreeCell(valueIndex); + } + + _hive.FreeCell(cell.ValueListIndex); + cell.ValueListIndex = -1; + cell.NumValues = 0; + cell.MaxValDataBytes = 0; + cell.MaxValNameBytes = 0; + } + } + + private void FreeSubKeys(KeyNodeCell subkeyCell) + { + if (subkeyCell.SubKeysIndex == -1) + { + throw new InvalidOperationException("No subkey list"); + } + + Cell list = _hive.GetCell(subkeyCell.SubKeysIndex); + + SubKeyIndirectListCell indirectList = list as SubKeyIndirectListCell; + if (indirectList != null) + { + ////foreach (int listIndex in indirectList.CellIndexes) + for (int i = 0; i < indirectList.CellIndexes.Count; ++i) + { + int listIndex = indirectList.CellIndexes[i]; + _hive.FreeCell(listIndex); + } + } + + _hive.FreeCell(list.Index); + } + } +} diff --git a/DiscUtils/Registry/RegistryKeyFlags.cs b/DiscUtils/Registry/RegistryKeyFlags.cs new file mode 100644 index 0000000..a17bc11 --- /dev/null +++ b/DiscUtils/Registry/RegistryKeyFlags.cs @@ -0,0 +1,113 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + + /// + /// The per-key flags present on registry keys. + /// + [Flags] + public enum RegistryKeyFlags : int + { + /// + /// Unknown purpose. + /// + Unknown0001 = 0x0001, + + /// + /// Unknown purpose. + /// + Unknown0002 = 0x0002, + + /// + /// The key is the root key in the registry hive. + /// + Root = 0x0004, + + /// + /// Unknown purpose. + /// + Unknown0008 = 0x0008, + + /// + /// The key is a link to another key. + /// + Link = 0x0010, + + /// + /// This is a normal key. + /// + Normal = 0x0020, + + /// + /// Unknown purpose. + /// + Unknown0040 = 0x0040, + + /// + /// Unknown purpose. + /// + Unknown0080 = 0x0080, + + /// + /// Unknown purpose. + /// + Unknown0100 = 0x0100, + + /// + /// Unknown purpose. + /// + Unknown0200 = 0x0200, + + /// + /// Unknown purpose. + /// + Unknown0400 = 0x0400, + + /// + /// Unknown purpose. + /// + Unknown0800 = 0x0800, + + /// + /// Unknown purpose. + /// + Unknown1000 = 0x1000, + + /// + /// Unknown purpose. + /// + Unknown2000 = 0x2000, + + /// + /// Unknown purpose. + /// + Unknown4000 = 0x4000, + + /// + /// Unknown purpose. + /// + Unknown8000 = 0x8000 + } +} diff --git a/DiscUtils/Registry/RegistryValue.cs b/DiscUtils/Registry/RegistryValue.cs new file mode 100644 index 0000000..925b980 --- /dev/null +++ b/DiscUtils/Registry/RegistryValue.cs @@ -0,0 +1,304 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.Globalization; + using System.Text; + + /// + /// A registry value. + /// + internal sealed class RegistryValue + { + private RegistryHive _hive; + private ValueCell _cell; + + internal RegistryValue(RegistryHive hive, ValueCell cell) + { + _hive = hive; + _cell = cell; + } + + /// + /// Gets the name of the value, or empty string if unnamed. + /// + public string Name + { + get { return _cell.Name ?? string.Empty; } + } + + /// + /// Gets the type of the value. + /// + public RegistryValueType DataType + { + get { return _cell.DataType; } + } + + /// + /// Gets the value data mapped to a .net object. + /// + /// The mapping from registry type of .NET type is as follows: + /// + /// + /// Value Type + /// .NET type + /// + /// + /// String + /// string + /// + /// + /// ExpandString + /// string + /// + /// + /// Link + /// string + /// + /// + /// DWord + /// uint + /// + /// + /// DWordBigEndian + /// uint + /// + /// + /// MultiString + /// string[] + /// + /// + /// QWord + /// ulong + /// + /// + /// + public object Value + { + get + { + return ConvertToObject(GetData(), DataType); + } + } + + /// + /// The raw value data as a byte array. + /// + /// The value as a raw byte array. + public byte[] GetData() + { + if (_cell.DataLength < 0) + { + int len = _cell.DataLength & 0x7FFFFFFF; + byte[] buffer = new byte[4]; + Utilities.WriteBytesLittleEndian(_cell.DataIndex, buffer, 0); + + byte[] result = new byte[len]; + Array.Copy(buffer, result, len); + return result; + } + + return _hive.RawCellData(_cell.DataIndex, _cell.DataLength); + } + + /// + /// Sets the value as raw bytes, with no validation that enough data is specified for the given value type. + /// + /// The data to store. + /// The offset within data of the first byte to store. + /// The number of bytes to store. + /// The type of the data. + public void SetData(byte[] data, int offset, int count, RegistryValueType valueType) + { + // If we can place the data in the DataIndex field, do that to save space / allocation + if ((valueType == RegistryValueType.Dword || valueType == RegistryValueType.DwordBigEndian) && count <= 4) + { + if (_cell.DataLength >= 0) + { + _hive.FreeCell(_cell.DataIndex); + } + + _cell.DataLength = (int)((uint)count | 0x80000000); + _cell.DataIndex = Utilities.ToInt32LittleEndian(data, offset); + _cell.DataType = valueType; + } + else + { + if (_cell.DataIndex == -1 || _cell.DataLength < 0) + { + _cell.DataIndex = _hive.AllocateRawCell(count); + } + + if (!_hive.WriteRawCellData(_cell.DataIndex, data, offset, count)) + { + int newDataIndex = _hive.AllocateRawCell(count); + _hive.WriteRawCellData(newDataIndex, data, offset, count); + _hive.FreeCell(_cell.DataIndex); + _cell.DataIndex = newDataIndex; + } + + _cell.DataLength = count; + _cell.DataType = valueType; + } + + _hive.UpdateCell(_cell, false); + } + + /// + /// Sets the value stored. + /// + /// The value to store. + /// The registry type of the data. + public void SetValue(object value, RegistryValueType valueType) + { + if (valueType == RegistryValueType.None) + { + if (value is int) + { + valueType = RegistryValueType.Dword; + } + else if (value is byte[]) + { + valueType = RegistryValueType.Binary; + } + else if (value is string[]) + { + valueType = RegistryValueType.MultiString; + } + else + { + valueType = RegistryValueType.String; + } + } + + byte[] data = ConvertToData(value, valueType); + SetData(data, 0, data.Length, valueType); + } + + /// + /// Gets a string representation of the registry value. + /// + /// The registry value as a string. + public override string ToString() + { + return Name + ":" + DataType + ":" + DataAsString(); + } + + private static object ConvertToObject(byte[] data, RegistryValueType type) + { + switch (type) + { + case RegistryValueType.String: + case RegistryValueType.ExpandString: + case RegistryValueType.Link: + return Encoding.Unicode.GetString(data).Trim('\0'); + + case RegistryValueType.Dword: + return Utilities.ToInt32LittleEndian(data, 0); + + case RegistryValueType.DwordBigEndian: + return Utilities.ToInt32BigEndian(data, 0); + + case RegistryValueType.MultiString: + string multiString = Encoding.Unicode.GetString(data).Trim('\0'); + return multiString.Split('\0'); + + case RegistryValueType.QWord: + return string.Empty + Utilities.ToUInt64LittleEndian(data, 0); + + default: + return data; + } + } + + private static byte[] ConvertToData(object value, RegistryValueType valueType) + { + if (valueType == RegistryValueType.None) + { + throw new ArgumentException("Specific registry value type must be specified", "valueType"); + } + + byte[] data; + switch (valueType) + { + case RegistryValueType.String: + case RegistryValueType.ExpandString: + string strValue = value.ToString(); + data = new byte[(strValue.Length * 2) + 2]; + Encoding.Unicode.GetBytes(strValue, 0, strValue.Length, data, 0); + break; + + case RegistryValueType.Dword: + data = new byte[4]; + Utilities.WriteBytesLittleEndian((int)value, data, 0); + break; + + case RegistryValueType.DwordBigEndian: + data = new byte[4]; + Utilities.WriteBytesBigEndian((int)value, data, 0); + break; + + case RegistryValueType.MultiString: + string multiStrValue = string.Join("\0", (string[])value) + "\0"; + data = new byte[(multiStrValue.Length * 2) + 2]; + Encoding.Unicode.GetBytes(multiStrValue, 0, multiStrValue.Length, data, 0); + break; + + default: + data = (byte[])value; + break; + } + + return data; + } + + private string DataAsString() + { + switch (DataType) + { + case RegistryValueType.String: + case RegistryValueType.ExpandString: + case RegistryValueType.Link: + case RegistryValueType.Dword: + case RegistryValueType.DwordBigEndian: + case RegistryValueType.QWord: + return ConvertToObject(GetData(), DataType).ToString(); + + case RegistryValueType.MultiString: + return string.Join(",", (string[])ConvertToObject(GetData(), DataType)); + + default: + byte[] data = GetData(); + string result = string.Empty; + for (int i = 0; i < Math.Min(data.Length, 8); ++i) + { + result += string.Format(CultureInfo.InvariantCulture, "{0:X2} ", (int)data[i]); + } + + return result + string.Format(CultureInfo.InvariantCulture, " ({0} bytes)", data.Length); + } + } + } +} diff --git a/DiscUtils/Registry/RegistryValueType.cs b/DiscUtils/Registry/RegistryValueType.cs new file mode 100644 index 0000000..d86cb44 --- /dev/null +++ b/DiscUtils/Registry/RegistryValueType.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + /// + /// The types of registry values. + /// + public enum RegistryValueType : int + { + /// + /// Unknown type. + /// + None = 0x00, + + /// + /// A unicode string. + /// + String = 0x01, + + /// + /// A string containing environment variables. + /// + ExpandString = 0x02, + + /// + /// Binary data. + /// + Binary = 0x03, + + /// + /// A 32-bit integer. + /// + Dword = 0x04, + + /// + /// A 32-bit integer. + /// + DwordBigEndian = 0x05, + + /// + /// A registry link. + /// + Link = 0x06, + + /// + /// A multistring. + /// + MultiString = 0x07, + + /// + /// An unknown binary format. + /// + ResourceList = 0x08, + + /// + /// An unknown binary format. + /// + FullResourceDescriptor = 0x09, + + /// + /// An unknown binary format. + /// + ResourceRequirementsList = 0x0A, + + /// + /// A 64-bit integer. + /// + QWord = 0x0B, + } +} diff --git a/DiscUtils/Registry/SecurityCell.cs b/DiscUtils/Registry/SecurityCell.cs new file mode 100644 index 0000000..ee8e96c --- /dev/null +++ b/DiscUtils/Registry/SecurityCell.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.Security.AccessControl; + + internal sealed class SecurityCell : Cell + { + private int _prevIndex; + private int _nextIndex; + private int _usageCount; + private RegistrySecurity _secDesc; + + public SecurityCell(RegistrySecurity secDesc) + : this(-1) + { + _secDesc = secDesc; + } + + public SecurityCell(int index) + : base(index) + { + _prevIndex = -1; + _nextIndex = -1; + } + + public int PreviousIndex + { + get { return _prevIndex; } + set { _prevIndex = value; } + } + + public int NextIndex + { + get { return _nextIndex; } + set { _nextIndex = value; } + } + + public int UsageCount + { + get { return _usageCount; } + set { _usageCount = value; } + } + + public RegistrySecurity SecurityDescriptor + { + get { return _secDesc; } + } + + public override int Size + { + get + { + int sdLen = _secDesc.GetSecurityDescriptorBinaryForm().Length; + return 0x14 + sdLen; + } + } + + public override int ReadFrom(byte[] buffer, int offset) + { + _prevIndex = Utilities.ToInt32LittleEndian(buffer, offset + 0x04); + _nextIndex = Utilities.ToInt32LittleEndian(buffer, offset + 0x08); + _usageCount = Utilities.ToInt32LittleEndian(buffer, offset + 0x0C); + int secDescSize = Utilities.ToInt32LittleEndian(buffer, offset + 0x10); + + byte[] secDesc = new byte[secDescSize]; + Array.Copy(buffer, offset + 0x14, secDesc, 0, secDescSize); + _secDesc = new RegistrySecurity(); + _secDesc.SetSecurityDescriptorBinaryForm(secDesc); + + return 0x14 + secDescSize; + } + + public override void WriteTo(byte[] buffer, int offset) + { + byte[] sd = _secDesc.GetSecurityDescriptorBinaryForm(); + + Utilities.StringToBytes("sk", buffer, offset, 2); + Utilities.WriteBytesLittleEndian(_prevIndex, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(_nextIndex, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian(_usageCount, buffer, offset + 0x0C); + Utilities.WriteBytesLittleEndian(sd.Length, buffer, offset + 0x10); + Array.Copy(sd, 0, buffer, offset + 0x14, sd.Length); + } + + public override string ToString() + { + return "SecDesc:" + _secDesc.GetSecurityDescriptorSddlForm(AccessControlSections.All) + " (refCount:" + _usageCount + ")"; + } + } +} diff --git a/DiscUtils/Registry/SubKeyHashedListCell.cs b/DiscUtils/Registry/SubKeyHashedListCell.cs new file mode 100644 index 0000000..78ded1c --- /dev/null +++ b/DiscUtils/Registry/SubKeyHashedListCell.cs @@ -0,0 +1,320 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.Collections.Generic; + using System.Globalization; + + internal sealed class SubKeyHashedListCell : ListCell + { + private string _hashType; + private short _numElements; + private List _subKeyIndexes; + private List _nameHashes; + private RegistryHive _hive; + + public SubKeyHashedListCell(RegistryHive hive, string hashType) + : base(-1) + { + _hive = hive; + _hashType = hashType; + _subKeyIndexes = new List(); + _nameHashes = new List(); + } + + public SubKeyHashedListCell(RegistryHive hive, int index) + : base(index) + { + _hive = hive; + } + + public override int Size + { + get { return 0x4 + (_numElements * 0x8); } + } + + internal override int Count + { + get { return _subKeyIndexes.Count; } + } + + public override int ReadFrom(byte[] buffer, int offset) + { + _hashType = Utilities.BytesToString(buffer, offset, 2); + _numElements = Utilities.ToInt16LittleEndian(buffer, offset + 2); + + _subKeyIndexes = new List(_numElements); + _nameHashes = new List(_numElements); + for (int i = 0; i < _numElements; ++i) + { + _subKeyIndexes.Add(Utilities.ToInt32LittleEndian(buffer, offset + 0x4 + (i * 0x8))); + _nameHashes.Add(Utilities.ToUInt32LittleEndian(buffer, offset + 0x4 + (i * 0x8) + 0x4)); + } + + return 0x4 + (_numElements * 0x8); + } + + public override void WriteTo(byte[] buffer, int offset) + { + Utilities.StringToBytes(_hashType, buffer, offset, 2); + Utilities.WriteBytesLittleEndian(_numElements, buffer, offset + 0x2); + for (int i = 0; i < _numElements; ++i) + { + Utilities.WriteBytesLittleEndian(_subKeyIndexes[i], buffer, offset + 0x4 + (i * 0x8)); + Utilities.WriteBytesLittleEndian(_nameHashes[i], buffer, offset + 0x4 + (i * 0x8) + 0x4); + } + } + + /// + /// Adds a new entry. + /// + /// The name of the subkey. + /// The cell index of the subkey. + /// The index of the new entry. + internal int Add(string name, int cellIndex) + { + for (int i = 0; i < _numElements; ++i) + { + KeyNodeCell cell = _hive.GetCell(_subKeyIndexes[i]); + if (string.Compare(cell.Name, name, StringComparison.OrdinalIgnoreCase) > 0) + { + _subKeyIndexes.Insert(i, cellIndex); + _nameHashes.Insert(i, CalcHash(name)); + _numElements++; + return i; + } + } + + _subKeyIndexes.Add(cellIndex); + _nameHashes.Add(CalcHash(name)); + return _numElements++; + } + + internal override int FindKey(string name, out int cellIndex) + { + // Check first and last, to early abort if the name is outside the range of this list + int result = FindKeyAt(name, 0, out cellIndex); + if (result <= 0) + { + return result; + } + + result = FindKeyAt(name, _subKeyIndexes.Count - 1, out cellIndex); + if (result >= 0) + { + return result; + } + + KeyFinder finder = new KeyFinder(_hive, name); + int idx = _subKeyIndexes.BinarySearch(-1, finder); + cellIndex = finder.CellIndex; + return (idx < 0) ? -1 : 0; + } + + internal override void EnumerateKeys(List names) + { + for (int i = 0; i < _subKeyIndexes.Count; ++i) + { + names.Add(_hive.GetCell(_subKeyIndexes[i]).Name); + } + } + + internal override IEnumerable EnumerateKeys() + { + for (int i = 0; i < _subKeyIndexes.Count; ++i) + { + yield return _hive.GetCell(_subKeyIndexes[i]); + } + } + + internal override int LinkSubKey(string name, int cellIndex) + { + Add(name, cellIndex); + return _hive.UpdateCell(this, true); + } + + internal override int UnlinkSubKey(string name) + { + int index = IndexOf(name); + if (index >= 0) + { + RemoveAt(index); + return _hive.UpdateCell(this, true); + } + + return Index; + } + + /// + /// Finds a subkey cell, returning it's index in this list. + /// + /// The name of the key to find. + /// The index of the found key, or -1. + internal int IndexOf(string name) + { + foreach (var index in Find(name, 0)) + { + KeyNodeCell cell = _hive.GetCell(_subKeyIndexes[index]); + if (cell.Name.ToUpperInvariant() == name.ToUpperInvariant()) + { + return index; + } + } + + return -1; + } + + internal void RemoveAt(int index) + { + _nameHashes.RemoveAt(index); + _subKeyIndexes.RemoveAt(index); + _numElements--; + } + + private uint CalcHash(string name) + { + uint hash = 0; + if (_hashType == "lh") + { + for (int i = 0; i < name.Length; ++i) + { + hash *= 37; + hash += char.ToUpper(name[i], CultureInfo.InvariantCulture); + } + } + else + { + string hashStr = name + "\0\0\0\0"; + for (int i = 0; i < 4; ++i) + { + hash |= (uint)((hashStr[i] & 0xFF) << (i * 8)); + } + } + + return hash; + } + + private int FindKeyAt(string name, int listIndex, out int cellIndex) + { + Cell cell = _hive.GetCell(_subKeyIndexes[listIndex]); + if (cell == null) + { + cellIndex = 0; + return -1; + } + + ListCell listCell = cell as ListCell; + if (listCell != null) + { + return listCell.FindKey(name, out cellIndex); + } + + cellIndex = _subKeyIndexes[listIndex]; + return string.Compare(name, ((KeyNodeCell)cell).Name, StringComparison.OrdinalIgnoreCase); + } + + private IEnumerable Find(string name, int start) + { + if (_hashType == "lh") + { + return FindByHash(name, start); + } + else + { + return FindByPrefix(name, start); + } + } + + private IEnumerable FindByHash(string name, int start) + { + uint hash = CalcHash(name); + + for (int i = start; i < _nameHashes.Count; ++i) + { + if (_nameHashes[i] == hash) + { + yield return i; + } + } + } + + private IEnumerable FindByPrefix(string name, int start) + { + int compChars = Math.Min(name.Length, 4); + string compStr = name.Substring(0, compChars).ToUpperInvariant() + "\0\0\0\0"; + + for (int i = start; i < _nameHashes.Count; ++i) + { + bool match = true; + uint hash = _nameHashes[i]; + + for (int j = 0; j < 4; ++j) + { + char ch = (char)((hash >> (j * 8)) & 0xFF); + if (char.ToUpperInvariant(ch) != compStr[j]) + { + match = false; + break; + } + } + + if (match) + { + yield return i; + } + } + } + + private class KeyFinder : IComparer + { + private RegistryHive _hive; + private string _searchName; + + public KeyFinder(RegistryHive hive, string searchName) + { + _hive = hive; + _searchName = searchName; + } + + public int CellIndex { get; set; } + + #region IComparer Members + + public int Compare(int x, int y) + { + // TODO: Be more efficient at ruling out no-hopes by using the hash values + KeyNodeCell cell = _hive.GetCell(x); + int result = string.Compare(((KeyNodeCell)cell).Name, _searchName, StringComparison.OrdinalIgnoreCase); + if (result == 0) + { + CellIndex = x; + } + + return result; + } + + #endregion + } + } +} diff --git a/DiscUtils/Registry/SubKeyIndirectListCell.cs b/DiscUtils/Registry/SubKeyIndirectListCell.cs new file mode 100644 index 0000000..4581e95 --- /dev/null +++ b/DiscUtils/Registry/SubKeyIndirectListCell.cs @@ -0,0 +1,310 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + using System.Collections.Generic; + + internal sealed class SubKeyIndirectListCell : ListCell + { + private RegistryHive _hive; + private string _listType; + private List _listIndexes; + + public SubKeyIndirectListCell(RegistryHive hive, int index) + : base(index) + { + _hive = hive; + } + + public string ListType + { + get { return _listType; } + } + + public List CellIndexes + { + get { return _listIndexes; } + } + + public override int Size + { + get { return 4 + (_listIndexes.Count * 4); } + } + + internal override int Count + { + get + { + int total = 0; + foreach (var cellIndex in _listIndexes) + { + Cell cell = _hive.GetCell(cellIndex); + ListCell listCell = cell as ListCell; + if (listCell != null) + { + total += listCell.Count; + } + else + { + total++; + } + } + + return total; + } + } + + public override int ReadFrom(byte[] buffer, int offset) + { + _listType = Utilities.BytesToString(buffer, offset, 2); + int numElements = Utilities.ToInt16LittleEndian(buffer, offset + 2); + _listIndexes = new List(numElements); + + for (int i = 0; i < numElements; ++i) + { + _listIndexes.Add(Utilities.ToInt32LittleEndian(buffer, offset + 0x4 + (i * 0x4))); + } + + return 4 + (_listIndexes.Count * 4); + } + + public override void WriteTo(byte[] buffer, int offset) + { + Utilities.StringToBytes(_listType, buffer, offset, 2); + Utilities.WriteBytesLittleEndian((ushort)_listIndexes.Count, buffer, offset + 2); + for (int i = 0; i < _listIndexes.Count; ++i) + { + Utilities.WriteBytesLittleEndian(_listIndexes[i], buffer, offset + 4 + (i * 4)); + } + } + + internal override int FindKey(string name, out int cellIndex) + { + if (_listIndexes.Count <= 0) + { + cellIndex = 0; + return -1; + } + + // Check first and last, to early abort if the name is outside the range of this list + int result = DoFindKey(name, 0, out cellIndex); + if (result <= 0) + { + return result; + } + + result = DoFindKey(name, _listIndexes.Count - 1, out cellIndex); + if (result >= 0) + { + return result; + } + + KeyFinder finder = new KeyFinder(_hive, name); + int idx = _listIndexes.BinarySearch(-1, finder); + cellIndex = finder.CellIndex; + return (idx < 0) ? -1 : 0; + } + + internal override void EnumerateKeys(List names) + { + for (int i = 0; i < _listIndexes.Count; ++i) + { + Cell cell = _hive.GetCell(_listIndexes[i]); + ListCell listCell = cell as ListCell; + if (listCell != null) + { + listCell.EnumerateKeys(names); + } + else + { + names.Add(((KeyNodeCell)cell).Name); + } + } + } + + internal override IEnumerable EnumerateKeys() + { + for (int i = 0; i < _listIndexes.Count; ++i) + { + Cell cell = _hive.GetCell(_listIndexes[i]); + ListCell listCell = cell as ListCell; + if (listCell != null) + { + foreach (var keyNodeCell in listCell.EnumerateKeys()) + { + yield return keyNodeCell; + } + } + else + { + yield return (KeyNodeCell)cell; + } + } + } + + internal override int LinkSubKey(string name, int cellIndex) + { + // Look for the first sublist that has a subkey name greater than name + if (ListType == "ri") + { + if (_listIndexes.Count == 0) + { + throw new NotImplementedException("Empty indirect list"); + } + + for (int i = 0; i < _listIndexes.Count - 1; ++i) + { + int tempIndex; + ListCell cell = _hive.GetCell(_listIndexes[i]); + if (cell.FindKey(name, out tempIndex) <= 0) + { + _listIndexes[i] = cell.LinkSubKey(name, cellIndex); + return _hive.UpdateCell(this, false); + } + } + + ListCell lastCell = _hive.GetCell(_listIndexes[_listIndexes.Count - 1]); + _listIndexes[_listIndexes.Count - 1] = lastCell.LinkSubKey(name, cellIndex); + return _hive.UpdateCell(this, false); + } + else + { + for (int i = 0; i < _listIndexes.Count; ++i) + { + KeyNodeCell cell = _hive.GetCell(_listIndexes[i]); + if (string.Compare(name, cell.Name, StringComparison.OrdinalIgnoreCase) < 0) + { + _listIndexes.Insert(i, cellIndex); + return _hive.UpdateCell(this, true); + } + } + + _listIndexes.Add(cellIndex); + return _hive.UpdateCell(this, true); + } + } + + internal override int UnlinkSubKey(string name) + { + if (ListType == "ri") + { + if (_listIndexes.Count == 0) + { + throw new NotImplementedException("Empty indirect list"); + } + + for (int i = 0; i < _listIndexes.Count; ++i) + { + int tempIndex; + ListCell cell = _hive.GetCell(_listIndexes[i]); + if (cell.FindKey(name, out tempIndex) <= 0) + { + _listIndexes[i] = cell.UnlinkSubKey(name); + if (cell.Count == 0) + { + _hive.FreeCell(_listIndexes[i]); + _listIndexes.RemoveAt(i); + } + + return _hive.UpdateCell(this, false); + } + } + } + else + { + for (int i = 0; i < _listIndexes.Count; ++i) + { + KeyNodeCell cell = _hive.GetCell(_listIndexes[i]); + if (string.Compare(name, cell.Name, StringComparison.OrdinalIgnoreCase) == 0) + { + _listIndexes.RemoveAt(i); + return _hive.UpdateCell(this, true); + } + } + } + + return Index; + } + + private int DoFindKey(string name, int listIndex, out int cellIndex) + { + Cell cell = _hive.GetCell(_listIndexes[listIndex]); + ListCell listCell = cell as ListCell; + if (listCell != null) + { + return listCell.FindKey(name, out cellIndex); + } + + cellIndex = _listIndexes[listIndex]; + return string.Compare(name, ((KeyNodeCell)cell).Name, StringComparison.OrdinalIgnoreCase); + } + + private class KeyFinder : IComparer + { + private RegistryHive _hive; + private string _searchName; + + public KeyFinder(RegistryHive hive, string searchName) + { + _hive = hive; + _searchName = searchName; + } + + public int CellIndex { get; set; } + + #region IComparer Members + + public int Compare(int x, int y) + { + Cell cell = _hive.GetCell(x); + ListCell listCell = cell as ListCell; + + int result; + if (listCell != null) + { + int cellIndex; + result = listCell.FindKey(_searchName, out cellIndex); + if (result == 0) + { + CellIndex = cellIndex; + } + + return -result; + } + else + { + result = string.Compare(((KeyNodeCell)cell).Name, _searchName, StringComparison.OrdinalIgnoreCase); + if (result == 0) + { + CellIndex = x; + } + } + + return result; + } + + #endregion + } + } +} diff --git a/DiscUtils/Registry/ValueCell.cs b/DiscUtils/Registry/ValueCell.cs new file mode 100644 index 0000000..0a59927 --- /dev/null +++ b/DiscUtils/Registry/ValueCell.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + internal sealed class ValueCell : Cell + { + private int _dataLength; + private int _dataIndex; + private RegistryValueType _type; + private ValueFlags _flags; + private string _name; + + public ValueCell(string name) + : this(-1) + { + _name = name; + } + + public ValueCell(int index) + : base(index) + { + _dataIndex = -1; + } + + public int DataLength + { + get { return _dataLength; } + set { _dataLength = value; } + } + + public int DataIndex + { + get { return _dataIndex; } + set { _dataIndex = value; } + } + + public RegistryValueType DataType + { + get { return _type; } + set { _type = value; } + } + + public string Name + { + get { return _name; } + } + + public override int Size + { + get { return 0x14 + (string.IsNullOrEmpty(_name) ? 0 : _name.Length); } + } + + public override int ReadFrom(byte[] buffer, int offset) + { + int nameLen = Utilities.ToUInt16LittleEndian(buffer, offset + 0x02); + _dataLength = Utilities.ToInt32LittleEndian(buffer, offset + 0x04); + _dataIndex = Utilities.ToInt32LittleEndian(buffer, offset + 0x08); + _type = (RegistryValueType)Utilities.ToInt32LittleEndian(buffer, offset + 0x0C); + _flags = (ValueFlags)Utilities.ToUInt16LittleEndian(buffer, offset + 0x10); + + if ((_flags & ValueFlags.Named) != 0) + { + _name = Utilities.BytesToString(buffer, offset + 0x14, nameLen).Trim('\0'); + } + + return 0x14 + nameLen; + } + + public override void WriteTo(byte[] buffer, int offset) + { + int nameLen; + + if (string.IsNullOrEmpty(_name)) + { + _flags &= ~ValueFlags.Named; + nameLen = 0; + } + else + { + _flags |= ValueFlags.Named; + nameLen = _name.Length; + } + + Utilities.StringToBytes("vk", buffer, offset, 2); + Utilities.WriteBytesLittleEndian(nameLen, buffer, offset + 0x02); + Utilities.WriteBytesLittleEndian(_dataLength, buffer, offset + 0x04); + Utilities.WriteBytesLittleEndian(_dataIndex, buffer, offset + 0x08); + Utilities.WriteBytesLittleEndian((int)_type, buffer, offset + 0x0C); + Utilities.WriteBytesLittleEndian((ushort)_flags, buffer, offset + 0x10); + if (nameLen != 0) + { + Utilities.StringToBytes(_name, buffer, offset + 0x14, nameLen); + } + } + } +} diff --git a/DiscUtils/Registry/ValueFlags.cs b/DiscUtils/Registry/ValueFlags.cs new file mode 100644 index 0000000..c910d35 --- /dev/null +++ b/DiscUtils/Registry/ValueFlags.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Registry +{ + using System; + + [Flags] + internal enum ValueFlags : ushort + { + Named = 0x0001, + Unknown0002 = 0x0002, + Unknown0004 = 0x0004, + Unknown0008 = 0x0008, + Unknown0010 = 0x0010, + Unknown0020 = 0x0020, + Unknown0040 = 0x0040, + Unknown0080 = 0x0080, + Unknown0100 = 0x0100, + Unknown0200 = 0x0200, + Unknown0400 = 0x0400, + Unknown0800 = 0x0800, + Unknown1000 = 0x1000, + Unknown2000 = 0x2000, + Unknown4000 = 0x4000, + Unknown8000 = 0x8000 + } +} diff --git a/DiscUtils/ReparsePoint.cs b/DiscUtils/ReparsePoint.cs new file mode 100644 index 0000000..7998b45 --- /dev/null +++ b/DiscUtils/ReparsePoint.cs @@ -0,0 +1,62 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Represents a Reparse Point, which can be associated with a file or directory. + /// + public sealed class ReparsePoint + { + private int _tag; + private byte[] _content; + + /// + /// Initializes a new instance of the ReparsePoint class. + /// + /// The defined reparse point tag. + /// The reparse point's content. + public ReparsePoint(int tag, byte[] content) + { + _tag = tag; + _content = content; + } + + /// + /// Gets or sets the defined reparse point tag. + /// + public int Tag + { + get { return _tag; } + set { _tag = value; } + } + + /// + /// Gets or sets the reparse point's content. + /// + public byte[] Content + { + get { return _content; } + set { _content = value; } + } + } +} diff --git a/DiscUtils/Sizes.cs b/DiscUtils/Sizes.cs new file mode 100644 index 0000000..d640b8b --- /dev/null +++ b/DiscUtils/Sizes.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + internal static class Sizes + { + public const long OneKiB = 1024; + public const long OneMiB = 1024 * OneKiB; + public const long OneGiB = 1024 * OneMiB; + + public const int Sector = 512; + } +} diff --git a/DiscUtils/SnapshotStream.cs b/DiscUtils/SnapshotStream.cs new file mode 100644 index 0000000..934909e --- /dev/null +++ b/DiscUtils/SnapshotStream.cs @@ -0,0 +1,423 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// A wrapper stream that enables you to take a snapshot, pushing changes into a side buffer. + /// + /// Once a snapshot is taken, you can discard subsequent changes or merge them back + /// into the wrapped stream. + public sealed class SnapshotStream : SparseStream + { + private Stream _baseStream; + + private Ownership _baseStreamOwnership; + + /// + /// Captures changes to the base stream (when enabled). + /// + private SparseMemoryStream _diffStream; + + /// + /// Records which byte ranges in diffStream hold changes. + /// + /// Can't use _diffStream's own tracking because that's based on it's + /// internal block size, not on the _actual_ bytes stored. + private List _diffExtents; + + /// + /// The saved stream position (if the diffStream is active). + /// + private long _savedPosition; + + /// + /// Indicates that no writes should be permitted. + /// + private bool _frozen; + + private long _position; + + /// + /// Initializes a new instance of the SnapshotStream class. + /// + /// The stream to wrap. + /// Indicates if this stream should control the lifetime of baseStream. + public SnapshotStream(Stream baseStream, Ownership owns) + { + _baseStream = baseStream; + _baseStreamOwnership = owns; + _diffExtents = new List(); + } + + /// + /// Gets an indication as to whether the stream can be read. + /// + public override bool CanRead + { + get { return _baseStream.CanRead; } + } + + /// + /// Gets an indication as to whether the stream position can be changed. + /// + public override bool CanSeek + { + get { return _baseStream.CanSeek; } + } + + /// + /// Gets an indication as to whether the stream can be written to. + /// + /// This property is orthogonal to Freezing/Thawing, it's + /// perfectly possible for a stream to be frozen and this method + /// return true. + public override bool CanWrite + { + get { return (_diffStream != null) ? true : _baseStream.CanWrite; } + } + + /// + /// Gets the length of the stream. + /// + public override long Length + { + get + { + if (_diffStream != null) + { + return _diffStream.Length; + } + else + { + return _baseStream.Length; + } + } + } + + /// + /// Gets and sets the current stream position. + /// + public override long Position + { + get + { + return _position; + } + + set + { + _position = value; + } + } + + /// + /// Returns an enumeration over the parts of the stream that contain real data. + /// + public override IEnumerable Extents + { + get + { + SparseStream sparseBase = _baseStream as SparseStream; + if (sparseBase == null) + { + return new StreamExtent[] { new StreamExtent(0, Length) }; + } + else + { + return StreamExtent.Union(sparseBase.Extents, _diffExtents); + } + } + } + + /// + /// Prevents any write operations to the stream. + /// + /// Useful to prevent changes whilst inspecting the stream. + public void Freeze() + { + _frozen = true; + } + + /// + /// Re-permits write operations to the stream. + /// + public void Thaw() + { + _frozen = false; + } + + /// + /// Takes a snapshot of the current stream contents. + /// + public void Snapshot() + { + if (_diffStream != null) + { + throw new InvalidOperationException("Already have a snapshot"); + } + + _savedPosition = _position; + + _diffExtents = new List(); + _diffStream = new SparseMemoryStream(); + _diffStream.SetLength(_baseStream.Length); + } + + /// + /// Reverts to a previous snapshot, discarding any changes made to the stream. + /// + public void RevertToSnapshot() + { + if (_diffStream == null) + { + throw new InvalidOperationException("No snapshot"); + } + + _diffStream = null; + _diffExtents = null; + + _position = _savedPosition; + } + + /// + /// Discards the snapshot any changes made after the snapshot was taken are kept. + /// + public void ForgetSnapshot() + { + if (_diffStream == null) + { + throw new InvalidOperationException("No snapshot"); + } + + byte[] buffer = new byte[8192]; + + foreach (var extent in _diffExtents) + { + _diffStream.Position = extent.Start; + _baseStream.Position = extent.Start; + + int totalRead = 0; + while (totalRead < extent.Length) + { + int toRead = (int)Math.Min(extent.Length - totalRead, buffer.Length); + + int read = _diffStream.Read(buffer, 0, toRead); + _baseStream.Write(buffer, 0, read); + + totalRead += read; + } + } + + _diffStream = null; + _diffExtents = null; + } + + /// + /// Flushes the stream. + /// + public override void Flush() + { + CheckFrozen(); + + _baseStream.Flush(); + } + + /// + /// Reads data from the stream. + /// + /// The buffer to fill. + /// The buffer offset to start from. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + int numRead; + + if (_diffStream == null) + { + _baseStream.Position = _position; + numRead = _baseStream.Read(buffer, offset, count); + } + else + { + if (_position > _diffStream.Length) + { + throw new IOException("Attempt to read beyond end of file"); + } + + int toRead = (int)Math.Min(count, _diffStream.Length - _position); + + // If the read is within the base stream's range, then touch it first to get the + // (potentially) stale data. + if (_position < _baseStream.Length) + { + int baseToRead = (int)Math.Min(toRead, _baseStream.Length - _position); + _baseStream.Position = _position; + + int totalBaseRead = 0; + while (totalBaseRead < baseToRead) + { + totalBaseRead += _baseStream.Read(buffer, offset + totalBaseRead, baseToRead - totalBaseRead); + } + } + + // Now overlay any data from the overlay stream (if any) + IEnumerable overlayExtents = StreamExtent.Intersect(_diffExtents, new StreamExtent(_position, toRead)); + foreach (var extent in overlayExtents) + { + _diffStream.Position = extent.Start; + int overlayNumRead = 0; + while (overlayNumRead < extent.Length) + { + overlayNumRead += _diffStream.Read( + buffer, + (int)(offset + (extent.Start - _position) + overlayNumRead), + (int)(extent.Length - overlayNumRead)); + } + } + + numRead = toRead; + } + + _position += numRead; + + return numRead; + } + + /// + /// Moves the stream position. + /// + /// The origin-relative location. + /// The base location. + /// The new absolute stream position. + public override long Seek(long offset, SeekOrigin origin) + { + CheckFrozen(); + + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += Length; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + else + { + _position = effectiveOffset; + return _position; + } + } + + /// + /// Sets the length of the stream. + /// + /// The new length. + public override void SetLength(long value) + { + CheckFrozen(); + + if (_diffStream != null) + { + _diffStream.SetLength(value); + } + else + { + _baseStream.SetLength(value); + } + } + + /// + /// Writes data to the stream at the current location. + /// + /// The data to write. + /// The first byte to write from buffer. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + CheckFrozen(); + + if (_diffStream != null) + { + _diffStream.Position = _position; + _diffStream.Write(buffer, offset, count); + + // Beware of Linq's delayed model - force execution now by placing into a list. + // Without this, large execution chains can build up (v. slow) and potential for stack overflow. + _diffExtents = new List(StreamExtent.Union(_diffExtents, new StreamExtent(_position, count))); + + _position += count; + } + else + { + _baseStream.Position = _position; + _baseStream.Write(buffer, offset, count); + _position += count; + } + } + + /// + /// Disposes of this instance. + /// + /// true if called from Dispose(), else false. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_baseStreamOwnership == Ownership.Dispose && _baseStream != null) + { + _baseStream.Dispose(); + } + + _baseStream = null; + + if (_diffStream != null) + { + _diffStream.Dispose(); + } + + _diffStream = null; + } + + base.Dispose(disposing); + } + + private void CheckFrozen() + { + if (_frozen) + { + throw new InvalidOperationException("The stream is frozen"); + } + } + } +} diff --git a/DiscUtils/SparseMemoryBuffer.cs b/DiscUtils/SparseMemoryBuffer.cs new file mode 100644 index 0000000..e4f6c9a --- /dev/null +++ b/DiscUtils/SparseMemoryBuffer.cs @@ -0,0 +1,228 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + + /// + /// A sparse in-memory buffer. + /// + /// This class is useful for storing large sparse buffers in memory, unused + /// chunks of the buffer are not stored (assumed to be zero). + public sealed class SparseMemoryBuffer : Buffer + { + private Dictionary _buffers; + private int _chunkSize; + + private long _capacity; + + /// + /// Initializes a new instance of the SparseMemoryBuffer class. + /// + /// The size of each allocation chunk. + public SparseMemoryBuffer(int chunkSize) + { + _chunkSize = chunkSize; + _buffers = new Dictionary(); + } + + /// + /// Indicates this stream can be read (always true). + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Indicates this stream can be written (always true). + /// + public override bool CanWrite + { + get { return true; } + } + + /// + /// Gets the current capacity of the sparse buffer (number of logical bytes stored). + /// + public override long Capacity + { + get { return _capacity; } + } + + /// + /// Gets the size of each allocation chunk. + /// + public int ChunkSize + { + get { return _chunkSize; } + } + + /// + /// Gets the (sorted) list of allocated chunks, as chunk indexes. + /// + /// An enumeration of chunk indexes. + /// This method returns chunks as an index rather than absolute stream position. + /// For example, if ChunkSize is 16KB, and the first 32KB of the buffer is actually stored, + /// this method will return 0 and 1. This indicates the first and second chunks are stored. + public IEnumerable AllocatedChunks + { + get + { + List keys = new List(_buffers.Keys); + keys.Sort(); + return keys; + } + } + + /// + /// Accesses this memory buffer as an infinite byte array. + /// + /// The buffer position to read. + /// The byte stored at this position (or Zero if not explicitly stored). + public byte this[long pos] + { + get + { + byte[] buffer = new byte[1]; + if (Read(pos, buffer, 0, 1) != 0) + { + return buffer[0]; + } + else + { + return 0; + } + } + + set + { + byte[] buffer = new byte[1]; + buffer[0] = value; + Write(pos, buffer, 0, 1); + } + } + + /// + /// Reads a section of the sparse buffer into a byte array. + /// + /// The offset within the sparse buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + public override int Read(long pos, byte[] buffer, int offset, int count) + { + int totalRead = 0; + + while (totalRead < count && pos < _capacity) + { + int chunk = (int)(pos / _chunkSize); + int chunkOffset = (int)(pos % _chunkSize); + int numToRead = (int)Math.Min(Math.Min(_chunkSize - chunkOffset, _capacity - pos), count - totalRead); + + byte[] chunkBuffer; + if (!_buffers.TryGetValue(chunk, out chunkBuffer)) + { + Array.Clear(buffer, offset + totalRead, numToRead); + } + else + { + Array.Copy(chunkBuffer, chunkOffset, buffer, offset + totalRead, numToRead); + } + + totalRead += numToRead; + pos += numToRead; + } + + return totalRead; + } + + /// + /// Writes a byte array into the sparse buffer. + /// + /// The start offset within the sparse buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + public override void Write(long pos, byte[] buffer, int offset, int count) + { + int totalWritten = 0; + + while (totalWritten < count) + { + int chunk = (int)(pos / _chunkSize); + int chunkOffset = (int)(pos % _chunkSize); + int numToWrite = (int)Math.Min(_chunkSize - chunkOffset, count - totalWritten); + + byte[] chunkBuffer; + if (!_buffers.TryGetValue(chunk, out chunkBuffer)) + { + chunkBuffer = new byte[_chunkSize]; + _buffers[chunk] = chunkBuffer; + } + + Array.Copy(buffer, offset + totalWritten, chunkBuffer, chunkOffset, numToWrite); + + totalWritten += numToWrite; + pos += numToWrite; + } + + _capacity = Math.Max(_capacity, pos); + } + + /// + /// Sets the capacity of the sparse buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + /// This method does not allocate any chunks, it merely records the logical + /// capacity of the sparse buffer. Writes beyond the specified capacity will increase + /// the capacity. + public override void SetCapacity(long value) + { + _capacity = value; + } + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + long end = start + count; + foreach (var chunk in AllocatedChunks) + { + long chunkStart = chunk * (long)_chunkSize; + long chunkEnd = chunkStart + _chunkSize; + if (chunkEnd > start && chunkStart < end) + { + long extentStart = Math.Max(start, chunkStart); + yield return new StreamExtent(extentStart, Math.Min(chunkEnd, end) - extentStart); + } + } + } + } +} diff --git a/DiscUtils/SparseMemoryStream.cs b/DiscUtils/SparseMemoryStream.cs new file mode 100644 index 0000000..5c22bf0 --- /dev/null +++ b/DiscUtils/SparseMemoryStream.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Collections.Generic; + using System.IO; + + /// + /// Provides a sparse equivalent to MemoryStream. + /// + public sealed class SparseMemoryStream : BufferStream + { + /// + /// Initializes a new instance of the SparseMemoryStream class. + /// + /// The created instance permits read and write access. + public SparseMemoryStream() + : base(new SparseMemoryBuffer(16 * 1024), FileAccess.ReadWrite) + { + } + + /// + /// Initializes a new instance of the SparseMemoryStream class. + /// + /// The buffer to use. + /// The access permitted to clients. + public SparseMemoryStream(SparseMemoryBuffer buffer, FileAccess access) + : base(buffer, access) + { + } + } +} diff --git a/DiscUtils/SparseStream.cs b/DiscUtils/SparseStream.cs new file mode 100644 index 0000000..8b314ad --- /dev/null +++ b/DiscUtils/SparseStream.cs @@ -0,0 +1,349 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal delegate SparseStream SparseStreamOpenDelegate(); + + /// + /// Represents a sparse stream. + /// + /// A sparse stream is a logically contiguous stream where some parts of the stream + /// aren't stored. The unstored parts are implicitly zero-byte ranges. + public abstract class SparseStream : Stream + { + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public abstract IEnumerable Extents + { + get; + } + + /// + /// Converts any stream into a sparse stream. + /// + /// The stream to convert. + /// true to have the new stream dispose the wrapped + /// stream when it is disposed. + /// A sparse stream. + /// The returned stream has the entire wrapped stream as a + /// single extent. + public static SparseStream FromStream(Stream stream, Ownership takeOwnership) + { + return new SparseWrapperStream(stream, takeOwnership, null); + } + + /// + /// Converts any stream into a sparse stream. + /// + /// The stream to convert. + /// true to have the new stream dispose the wrapped + /// stream when it is disposed. + /// The set of extents actually stored in stream. + /// A sparse stream. + /// The returned stream has the entire wrapped stream as a + /// single extent. + public static SparseStream FromStream(Stream stream, Ownership takeOwnership, IEnumerable extents) + { + return new SparseWrapperStream(stream, takeOwnership, extents); + } + + /// + /// Efficiently pumps data from a sparse stream to another stream. + /// + /// The sparse stream to pump from. + /// The stream to pump to. + /// must support seeking. + public static void Pump(Stream inStream, Stream outStream) + { + Pump(inStream, outStream, Sizes.Sector); + } + + /// + /// Efficiently pumps data from a sparse stream to another stream. + /// + /// The stream to pump from. + /// The stream to pump to. + /// The smallest sequence of zero bytes that will be skipped when writing to . + /// must support seeking. + public static void Pump(Stream inStream, Stream outStream, int chunkSize) + { + StreamPump pump = new StreamPump(inStream, outStream, chunkSize); + pump.Run(); + } + + /// + /// Wraps a sparse stream in a read-only wrapper, preventing modification. + /// + /// The stream to make read-only. + /// Whether to transfer responsibility for calling Dispose on toWrap. + /// The read-only stream. + public static SparseStream ReadOnly(SparseStream toWrap, Ownership ownership) + { + return new SparseReadOnlyWrapperStream(toWrap, ownership); + } + + /// + /// Clears bytes from the stream. + /// + /// The number of bytes (from the current position) to clear. + /// + /// Logically equivalent to writing count null/zero bytes to the stream, some + /// implementations determine that some (or all) of the range indicated is not actually + /// stored. There is no direct, automatic, correspondence to clearing bytes and them + /// not being represented as an 'extent' - for example, the implementation of the underlying + /// stream may not permit fine-grained extent storage. + /// It is always safe to call this method to 'zero-out' a section of a stream, regardless of + /// the underlying stream implementation. + /// + public virtual void Clear(int count) + { + Write(new byte[count], 0, count); + } + + /// + /// Gets the parts of a stream that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public virtual IEnumerable GetExtentsInRange(long start, long count) + { + return StreamExtent.Intersect(Extents, new StreamExtent[] { new StreamExtent(start, count) }); + } + + private class SparseReadOnlyWrapperStream : SparseStream + { + private SparseStream _wrapped; + private Ownership _ownsWrapped; + + public SparseReadOnlyWrapperStream(SparseStream wrapped, Ownership ownsWrapped) + { + _wrapped = wrapped; + _ownsWrapped = ownsWrapped; + } + + public override bool CanRead + { + get { return _wrapped.CanRead; } + } + + public override bool CanSeek + { + get { return _wrapped.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _wrapped.Length; } + } + + public override long Position + { + get + { + return _wrapped.Position; + } + + set + { + _wrapped.Position = value; + } + } + + public override IEnumerable Extents + { + get { return _wrapped.Extents; } + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped.Seek(offset, origin); + } + + public override void SetLength(long value) + { + throw new InvalidOperationException("Attempt to change length of read-only stream"); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new InvalidOperationException("Attempt to write to read-only stream"); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsWrapped == Ownership.Dispose && _wrapped != null) + { + _wrapped.Dispose(); + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } + + private class SparseWrapperStream : SparseStream + { + private Stream _wrapped; + private Ownership _ownsWrapped; + private List _extents; + + public SparseWrapperStream(Stream wrapped, Ownership ownsWrapped, IEnumerable extents) + { + _wrapped = wrapped; + _ownsWrapped = ownsWrapped; + if (extents != null) + { + _extents = new List(extents); + } + } + + public override bool CanRead + { + get { return _wrapped.CanRead; } + } + + public override bool CanSeek + { + get { return _wrapped.CanSeek; } + } + + public override bool CanWrite + { + get { return _wrapped.CanWrite; } + } + + public override long Length + { + get { return _wrapped.Length; } + } + + public override long Position + { + get + { + return _wrapped.Position; + } + + set + { + _wrapped.Position = value; + } + } + + public override IEnumerable Extents + { + get + { + if (_extents != null) + { + return _extents; + } + else + { + SparseStream wrappedAsSparse = _wrapped as SparseStream; + if (wrappedAsSparse != null) + { + return wrappedAsSparse.Extents; + } + else + { + return new StreamExtent[] { new StreamExtent(0, _wrapped.Length) }; + } + } + } + } + + public override void Flush() + { + _wrapped.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _wrapped.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_extents != null) + { + throw new InvalidOperationException("Attempt to write to stream with explicit extents"); + } + + _wrapped.Write(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsWrapped == Ownership.Dispose && _wrapped != null) + { + _wrapped.Dispose(); + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } + } +} diff --git a/DiscUtils/StreamBuilder.cs b/DiscUtils/StreamBuilder.cs new file mode 100644 index 0000000..c485868 --- /dev/null +++ b/DiscUtils/StreamBuilder.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Collections.Generic; + using System.IO; + + /// + /// Base class for objects that can dynamically construct a stream. + /// + public abstract class StreamBuilder + { + /// + /// Builds a new stream. + /// + /// The stream created by the StreamBuilder instance. + public virtual SparseStream Build() + { + long totalLength; + List extents = FixExtents(out totalLength); + return new BuiltStream(totalLength, extents); + } + + /// + /// Writes the stream contents to an existing stream. + /// + /// The stream to write to. + public void Build(Stream output) + { + using (Stream src = Build()) + { + byte[] buffer = new byte[64 * 1024]; + int numRead = src.Read(buffer, 0, buffer.Length); + while (numRead != 0) + { + output.Write(buffer, 0, numRead); + numRead = src.Read(buffer, 0, buffer.Length); + } + } + } + + /// + /// Writes the stream contents to a file. + /// + /// The file to write to. + public void Build(string outputFile) + { + using (FileStream destStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write)) + { + Build(destStream); + } + } + + internal abstract List FixExtents(out long totalLength); + } +} diff --git a/DiscUtils/StreamExtent.cs b/DiscUtils/StreamExtent.cs new file mode 100644 index 0000000..daf15e3 --- /dev/null +++ b/DiscUtils/StreamExtent.cs @@ -0,0 +1,512 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + + /// + /// Represents a range of bytes in a stream. + /// + /// This is normally used to represent regions of a SparseStream that + /// are actually stored in the underlying storage medium (rather than implied + /// zero bytes). Extents are stored as a zero-based byte offset (from the + /// beginning of the stream), and a byte length. + public sealed class StreamExtent : IEquatable, IComparable + { + private long _start; + private long _length; + + /// + /// Initializes a new instance of the StreamExtent class. + /// + /// The start of the extent. + /// The length of the extent. + public StreamExtent(long start, long length) + { + _start = start; + _length = length; + } + + /// + /// Gets the start of the extent (in bytes). + /// + public long Start + { + get { return _start; } + } + + /// + /// Gets the start of the extent (in bytes). + /// + public long Length + { + get { return _length; } + } + + /// + /// Calculates the union of a list of extents with another extent. + /// + /// The list of extents. + /// The other extent. + /// The union of the extents. + public static IEnumerable Union(IEnumerable extents, StreamExtent other) + { + List otherList = new List(); + otherList.Add(other); + return Union(extents, otherList); + } + + /// + /// Calculates the union of the extents of multiple streams. + /// + /// The stream extents. + /// The union of the extents from multiple streams. + /// A typical use of this method is to calculate the combined set of + /// stored extents from a number of overlayed sparse streams. + public static IEnumerable Union(params IEnumerable[] streams) + { + long extentStart = long.MaxValue; + long extentEnd = 0; + + // Initialize enumerations and find first stored byte position + IEnumerator[] enums = new IEnumerator[streams.Length]; + bool[] streamsValid = new bool[streams.Length]; + int validStreamsRemaining = 0; + for (int i = 0; i < streams.Length; ++i) + { + enums[i] = streams[i].GetEnumerator(); + streamsValid[i] = enums[i].MoveNext(); + if (streamsValid[i]) + { + ++validStreamsRemaining; + if (enums[i].Current.Start < extentStart) + { + extentStart = enums[i].Current.Start; + extentEnd = enums[i].Current.Start + enums[i].Current.Length; + } + } + } + + while (validStreamsRemaining > 0) + { + // Find the end of this extent + bool foundIntersection; + do + { + foundIntersection = false; + validStreamsRemaining = 0; + for (int i = 0; i < streams.Length; ++i) + { + while (streamsValid[i] && enums[i].Current.Start + enums[i].Current.Length <= extentEnd) + { + streamsValid[i] = enums[i].MoveNext(); + } + + if (streamsValid[i]) + { + ++validStreamsRemaining; + } + + if (streamsValid[i] && enums[i].Current.Start <= extentEnd) + { + extentEnd = enums[i].Current.Start + enums[i].Current.Length; + foundIntersection = true; + streamsValid[i] = enums[i].MoveNext(); + } + } + } + while (foundIntersection && validStreamsRemaining > 0); + + // Return the discovered extent + yield return new StreamExtent(extentStart, extentEnd - extentStart); + + // Find the next extent start point + extentStart = long.MaxValue; + validStreamsRemaining = 0; + for (int i = 0; i < streams.Length; ++i) + { + if (streamsValid[i]) + { + ++validStreamsRemaining; + if (enums[i].Current.Start < extentStart) + { + extentStart = enums[i].Current.Start; + extentEnd = enums[i].Current.Start + enums[i].Current.Length; + } + } + } + } + } + + /// + /// Calculates the intersection of the extents of a stream with another extent. + /// + /// The stream extents. + /// The extent to intersect. + /// The intersection of the extents. + public static IEnumerable Intersect(IEnumerable extents, StreamExtent other) + { + List otherList = new List(1); + otherList.Add(other); + return Intersect(extents, otherList); + } + + /// + /// Calculates the intersection of the extents of multiple streams. + /// + /// The stream extents. + /// The intersection of the extents from multiple streams. + /// A typical use of this method is to calculate the extents in a + /// region of a stream.. + public static IEnumerable Intersect(params IEnumerable[] streams) + { + long extentStart = long.MinValue; + long extentEnd = long.MaxValue; + + IEnumerator[] enums = new IEnumerator[streams.Length]; + for (int i = 0; i < streams.Length; ++i) + { + enums[i] = streams[i].GetEnumerator(); + if (!enums[i].MoveNext()) + { + // Gone past end of one stream (in practice was empty), so no intersections + yield break; + } + } + + int overlapsFound = 0; + while (true) + { + // We keep cycling round the streams, until we get streams.Length continuous overlaps + for (int i = 0; i < streams.Length; ++i) + { + // Move stream on past all extents that are earlier than our candidate start point + while (enums[i].Current.Length == 0 + || enums[i].Current.Start + enums[i].Current.Length <= extentStart) + { + if (!enums[i].MoveNext()) + { + // Gone past end of this stream, no more intersections possible + yield break; + } + } + + // If this stream has an extent that spans over the candidate start point + if (enums[i].Current.Start <= extentStart) + { + extentEnd = Math.Min(extentEnd, enums[i].Current.Start + enums[i].Current.Length); + overlapsFound++; + } + else + { + extentStart = enums[i].Current.Start; + extentEnd = extentStart + enums[i].Current.Length; + overlapsFound = 1; + } + + // We've just done a complete loop of all streams, they overlapped this start position + // and we've cut the extent's end down to the shortest run. + if (overlapsFound == streams.Length) + { + yield return new StreamExtent(extentStart, extentEnd - extentStart); + extentStart = extentEnd; + extentEnd = long.MaxValue; + overlapsFound = 0; + } + } + } + } + + /// + /// Calculates the subtraction of the extents of a stream by another extent. + /// + /// The stream extents. + /// The extent to subtract. + /// The subtraction of other from extents. + public static IEnumerable Subtract(IEnumerable extents, StreamExtent other) + { + return Subtract(extents, new StreamExtent[] { other }); + } + + /// + /// Calculates the subtraction of the extents of a stream by another stream. + /// + /// The stream extents to subtract from. + /// The stream extents to subtract. + /// The subtraction of the extents of b from a. + public static IEnumerable Subtract(IEnumerable a, IEnumerable b) + { + return Intersect(a, Invert(b)); + } + + /// + /// Calculates the inverse of the extents of a stream. + /// + /// The stream extents to inverse. + /// The inverted extents. + /// + /// This method assumes a logical stream addressable from 0 to long.MaxValue, and is undefined + /// should any stream extent start at less than 0. To constrain the extents to a specific range, use the + /// Intersect method. + /// + public static IEnumerable Invert(IEnumerable extents) + { + StreamExtent last = new StreamExtent(0, 0); + foreach (StreamExtent extent in extents) + { + // Skip over any 'noise' + if (extent.Length == 0) + { + continue; + } + + long lastEnd = last.Start + last.Length; + if (lastEnd < extent.Start) + { + yield return new StreamExtent(lastEnd, extent.Start - lastEnd); + } + + last = extent; + } + + long finalEnd = last.Start + last.Length; + if (finalEnd < long.MaxValue) + { + yield return new StreamExtent(finalEnd, long.MaxValue - finalEnd); + } + } + + /// + /// Offsets the extents of a stream. + /// + /// The stream extents. + /// The amount to offset the extents by. + /// The stream extents, offset by delta. + public static IEnumerable Offset(IEnumerable stream, long delta) + { + foreach (StreamExtent extent in stream) + { + yield return new StreamExtent(extent.Start + delta, extent.Length); + } + } + + /// + /// Returns the number of blocks containing stream data. + /// + /// The stream extents. + /// The size of each block. + /// The number of blocks containing stream data. + /// This method logically divides the stream into blocks of a specified + /// size, then indicates how many of those blocks contain actual stream data. + public static long BlockCount(IEnumerable stream, long blockSize) + { + long totalBlocks = 0; + long lastBlock = -1; + + foreach (var extent in stream) + { + if (extent.Length > 0) + { + long extentStartBlock = extent.Start / blockSize; + long extentNextBlock = Utilities.Ceil(extent.Start + extent.Length, blockSize); + + long extentNumBlocks = extentNextBlock - extentStartBlock; + if (extentStartBlock == lastBlock) + { + extentNumBlocks--; + } + + lastBlock = extentNextBlock - 1; + + totalBlocks += extentNumBlocks; + } + } + + return totalBlocks; + } + + /// + /// Returns all of the blocks containing stream data. + /// + /// The stream extents. + /// The size of each block. + /// Ranges of blocks, as block indexes. + /// This method logically divides the stream into blocks of a specified + /// size, then indicates ranges of blocks that contain stream data. + public static IEnumerable> Blocks(IEnumerable stream, long blockSize) + { + long? rangeStart = null; + long rangeLength = 0; + + foreach (var extent in stream) + { + if (extent.Length > 0) + { + long extentStartBlock = extent.Start / blockSize; + long extentNextBlock = Utilities.Ceil(extent.Start + extent.Length, blockSize); + + if (rangeStart != null && extentStartBlock > rangeStart + rangeLength) + { + // This extent is non-contiguous (in terms of blocks), so write out the last range and start new + yield return new Range((long)rangeStart, rangeLength); + rangeStart = extentStartBlock; + } + else if (rangeStart == null) + { + // First extent, so start first range + rangeStart = extentStartBlock; + } + + // Set the length of the current range, based on the end of this extent + rangeLength = extentNextBlock - (long)rangeStart; + } + } + + // Final range (if any ranges at all) hasn't been returned yet, so do that now + if (rangeStart != null) + { + yield return new Range((long)rangeStart, rangeLength); + } + } + + /// + /// The equality operator. + /// + /// The first extent to compare. + /// The second extent to compare. + /// Whether the two extents are equal. + public static bool operator ==(StreamExtent a, StreamExtent b) + { + if (Object.ReferenceEquals(a, null)) + { + return Object.ReferenceEquals(b, null); + } + else + { + return a.Equals(b); + } + } + + /// + /// The inequality operator. + /// + /// The first extent to compare. + /// The second extent to compare. + /// Whether the two extents are different. + public static bool operator !=(StreamExtent a, StreamExtent b) + { + return !(a == b); + } + + /// + /// The less-than operator. + /// + /// The first extent to compare. + /// The second extent to compare. + /// Whether a is less than b. + public static bool operator <(StreamExtent a, StreamExtent b) + { + return a.CompareTo(b) < 0; + } + + /// + /// The greater-than operator. + /// + /// The first extent to compare. + /// The second extent to compare. + /// Whether a is greater than b. + public static bool operator >(StreamExtent a, StreamExtent b) + { + return a.CompareTo(b) > 0; + } + + /// + /// Indicates if this StreamExtent is equal to another. + /// + /// The extent to compare. + /// true if the extents are equal, else false. + public bool Equals(StreamExtent other) + { + if (other == null) + { + return false; + } + else + { + return _start == other._start && _length == other._length; + } + } + + /// + /// Returns a string representation of the extent as [start:+length]. + /// + /// The string representation. + public override string ToString() + { + return "[" + _start + ":+" + _length + "]"; + } + + /// + /// Indicates if this stream extent is equal to another object. + /// + /// The object to test. + /// true if obj is equivalent, else false. + public override bool Equals(object obj) + { + return Equals(obj as StreamExtent); + } + + /// + /// Gets a hash code for this extent. + /// + /// The extent's hash code. + public override int GetHashCode() + { + return _start.GetHashCode() ^ _length.GetHashCode(); + } + + /// + /// Compares this stream extent to another. + /// + /// The extent to compare. + /// Value greater than zero if this extent starts after + /// other, zero if they start at the same position, else + /// a value less than zero. + public int CompareTo(StreamExtent other) + { + if (_start > other._start) + { + return 1; + } + else if (_start == other._start) + { + return 0; + } + else + { + return -1; + } + } + } +} diff --git a/DiscUtils/StreamPump.cs b/DiscUtils/StreamPump.cs new file mode 100644 index 0000000..03424a9 --- /dev/null +++ b/DiscUtils/StreamPump.cs @@ -0,0 +1,270 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + /// + /// Utility class for pumping the contents of one stream into another. + /// + /// + /// This class is aware of sparse streams, and will avoid copying data that is not + /// valid in the source stream. This functionality should normally only be used + /// when the destination stream is known not to contain any existing data. + /// + public sealed class StreamPump + { + /// + /// Initializes a new instance of the StreamPump class. + /// + public StreamPump() + { + SparseChunkSize = 512; + BufferSize = (int)(512 * Sizes.OneKiB); + SparseCopy = true; + } + + /// + /// Initializes a new instance of the StreamPump class. + /// + /// The stream to read from. + /// The stream to write to. + /// The size of each sparse chunk. + public StreamPump(Stream inStream, Stream outStream, int sparseChunkSize) + { + InputStream = inStream; + OutputStream = outStream; + SparseChunkSize = sparseChunkSize; + BufferSize = (int)(512 * Sizes.OneKiB); + SparseCopy = true; + } + + /// + /// Event raised periodically through the pump operation. + /// + /// + /// This event is signalled synchronously, so to avoid slowing the pumping activity + /// implementations should return quickly. + /// + public event EventHandler ProgressEvent; + + /// + /// Gets or sets the stream that will be read from. + /// + public Stream InputStream { get; set; } + + /// + /// Gets or sets the stream that will be written to. + /// + public Stream OutputStream { get; set; } + + /// + /// Gets or sets, for sparse transfers, the size of each chunk. + /// + /// + /// A chunk is transfered if any byte in the chunk is valid, otherwise it is not. + /// This value should normally be set to reflect the underlying storage granularity + /// of OutputStream. + /// + public int SparseChunkSize { get; set; } + + /// + /// Gets or sets the amount of data to read at a time from InputStream. + /// + public int BufferSize { get; set; } + + /// + /// Gets or sets a value indicating whether to enable the sparse copy behaviour (default true). + /// + public bool SparseCopy { get; set; } + + /// + /// Gets the number of bytes read from InputStream. + /// + public long BytesRead { get; private set; } + + /// + /// Gets the number of bytes written to OutputStream. + /// + public long BytesWritten { get; private set; } + + /// + /// Performs the pump activity, blocking until complete. + /// + public void Run() + { + if (InputStream == null) + { + throw new InvalidOperationException("Input stream is null"); + } + + if (OutputStream == null) + { + throw new InvalidOperationException("Output stream is null"); + } + + if (!OutputStream.CanSeek) + { + throw new InvalidOperationException("Output stream does not support seek operations"); + } + + if (SparseChunkSize <= 1) + { + throw new InvalidOperationException("Chunk size is invalid"); + } + + if (SparseCopy) + { + RunSparse(); + } + else + { + RunNonSparse(); + } + } + + private static bool IsAllZeros(byte[] buffer, int offset, int count) + { + for (int j = 0; j < count; j++) + { + if (buffer[offset + j] != 0) + { + return false; + } + } + + return true; + } + + private void RunNonSparse() + { + byte[] copyBuffer = new byte[BufferSize]; + + InputStream.Position = 0; + OutputStream.Position = 0; + + int numRead = InputStream.Read(copyBuffer, 0, copyBuffer.Length); + while (numRead > 0) + { + BytesRead += numRead; + + OutputStream.Write(copyBuffer, 0, numRead); + BytesWritten += numRead; + + RaiseProgressEvent(); + + numRead = InputStream.Read(copyBuffer, 0, copyBuffer.Length); + } + } + + private void RunSparse() + { + SparseStream inStream = InputStream as SparseStream; + if (inStream == null) + { + inStream = SparseStream.FromStream(InputStream, Ownership.None); + } + + if (BufferSize > SparseChunkSize && (BufferSize % SparseChunkSize) != 0) + { + throw new InvalidOperationException("Buffer size is not a multiple of the sparse chunk size"); + } + + byte[] copyBuffer = new byte[Math.Max(BufferSize, SparseChunkSize)]; + + BytesRead = 0; + BytesWritten = 0; + + foreach (var extent in inStream.Extents) + { + inStream.Position = extent.Start; + + long extentOffset = 0; + while (extentOffset < extent.Length) + { + int toRead = (int)Math.Min(copyBuffer.Length, extent.Length - extentOffset); + int numRead = Utilities.ReadFully(inStream, copyBuffer, 0, toRead); + BytesRead += numRead; + + int copyBufferOffset = 0; + for (int i = 0; i < numRead; i += SparseChunkSize) + { + if (IsAllZeros(copyBuffer, i, Math.Min(SparseChunkSize, numRead - i))) + { + if (copyBufferOffset < i) + { + OutputStream.Position = extent.Start + extentOffset + copyBufferOffset; + OutputStream.Write(copyBuffer, copyBufferOffset, i - copyBufferOffset); + BytesWritten += i - copyBufferOffset; + } + + copyBufferOffset = i + SparseChunkSize; + } + } + + if (copyBufferOffset < numRead) + { + OutputStream.Position = extent.Start + extentOffset + copyBufferOffset; + OutputStream.Write(copyBuffer, copyBufferOffset, numRead - copyBufferOffset); + BytesWritten += numRead - copyBufferOffset; + } + + extentOffset += numRead; + + RaiseProgressEvent(); + } + } + + // Ensure the output stream is at least as long as the input stream. This uses + // read/write, rather than SetLength, to avoid failing on streams that can't be + // explicitly resized. Side-effect of this, is that if outStream is an NTFS + // file stream, then actual clusters will be allocated out to at least the + // length of the input stream. + if (OutputStream.Length < inStream.Length) + { + inStream.Position = inStream.Length - 1; + int b = inStream.ReadByte(); + if (b >= 0) + { + OutputStream.Position = inStream.Length - 1; + OutputStream.WriteByte((byte)b); + } + } + } + + private void RaiseProgressEvent() + { + // Raise the event by using the () operator. + if (ProgressEvent != null) + { + PumpProgressEventArgs args = new PumpProgressEventArgs(); + args.BytesRead = BytesRead; + args.BytesWritten = BytesWritten; + args.SourcePosition = InputStream.Position; + args.DestinationPosition = OutputStream.Position; + ProgressEvent(this, args); + } + } + } +} diff --git a/DiscUtils/StripedStream.cs b/DiscUtils/StripedStream.cs new file mode 100644 index 0000000..32aa0a7 --- /dev/null +++ b/DiscUtils/StripedStream.cs @@ -0,0 +1,229 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class StripedStream : SparseStream + { + private List _wrapped; + private long _stripeSize; + private Ownership _ownsWrapped; + private bool _canRead; + private bool _canWrite; + private long _length; + + private long _position; + + public StripedStream(long stripeSize, Ownership ownsWrapped, params SparseStream[] wrapped) + { + _wrapped = new List(wrapped); + _stripeSize = stripeSize; + _ownsWrapped = ownsWrapped; + + _canRead = _wrapped[0].CanRead; + _canWrite = _wrapped[0].CanWrite; + long subStreamLength = _wrapped[0].Length; + + foreach (var stream in _wrapped) + { + if (stream.CanRead != _canRead || stream.CanWrite != _canWrite) + { + throw new ArgumentException("All striped streams must have the same read/write permissions", "wrapped"); + } + + if (stream.Length != subStreamLength) + { + throw new ArgumentException("All striped streams must have the same length", "wrapped"); + } + } + + _length = subStreamLength * wrapped.Length; + } + + public override bool CanRead + { + get { return _canRead; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return _canWrite; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get + { + return _position; + } + + set + { + _position = value; + } + } + + public override IEnumerable Extents + { + get + { + // Temporary, indicate there are no 'unstored' extents. + // Consider combining extent information from all wrapped streams in future. + yield return new StreamExtent(0, _length); + } + } + + public override void Flush() + { + foreach (var stream in _wrapped) + { + stream.Flush(); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!CanRead) + { + throw new InvalidOperationException("Attempt to read to non-readable stream"); + } + + int maxToRead = (int)Math.Min(_length - _position, count); + + int totalRead = 0; + while (totalRead < maxToRead) + { + long stripe = _position / _stripeSize; + long stripeOffset = _position % _stripeSize; + int stripeToRead = (int)Math.Min(maxToRead - totalRead, _stripeSize - stripeOffset); + + int streamIdx = (int)(stripe % _wrapped.Count); + long streamStripe = stripe / _wrapped.Count; + + Stream targetStream = _wrapped[streamIdx]; + targetStream.Position = (streamStripe * _stripeSize) + stripeOffset; + + int numRead = targetStream.Read(buffer, offset + totalRead, stripeToRead); + _position += numRead; + totalRead += numRead; + } + + return totalRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += _length; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of stream"); + } + else + { + _position = effectiveOffset; + return _position; + } + } + + public override void SetLength(long value) + { + if (value != _length) + { + throw new InvalidOperationException("Changing the stream length is not permitted for striped streams"); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!CanWrite) + { + throw new InvalidOperationException("Attempt to write to read-only stream"); + } + + if (_position + count > _length) + { + throw new IOException("Attempt to write beyond end of stream"); + } + + int totalWritten = 0; + while (totalWritten < count) + { + long stripe = _position / _stripeSize; + long stripeOffset = _position % _stripeSize; + int stripeToWrite = (int)Math.Min(count - totalWritten, _stripeSize - stripeOffset); + + int streamIdx = (int)(stripe % _wrapped.Count); + long streamStripe = stripe / _wrapped.Count; + + Stream targetStream = _wrapped[streamIdx]; + targetStream.Position = (streamStripe * _stripeSize) + stripeOffset; + targetStream.Write(buffer, offset + totalWritten, stripeToWrite); + + _position += stripeToWrite; + totalWritten += stripeToWrite; + } + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsWrapped == Ownership.Dispose && _wrapped != null) + { + foreach (var stream in _wrapped) + { + stream.Dispose(); + } + + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} diff --git a/DiscUtils/SubBuffer.cs b/DiscUtils/SubBuffer.cs new file mode 100644 index 0000000..1a0230e --- /dev/null +++ b/DiscUtils/SubBuffer.cs @@ -0,0 +1,175 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + + /// + /// Class representing a portion of an existing buffer. + /// + internal class SubBuffer : Buffer + { + private long _first; + private long _length; + + private IBuffer _parent; + + /// + /// Initializes a new instance of the SubBuffer class. + /// + /// The parent buffer. + /// The first byte in represented by this sub-buffer. + /// The number of bytes of represented by this sub-buffer. + public SubBuffer(IBuffer parent, long first, long length) + { + _parent = parent; + _first = first; + _length = length; + + if (_first + _length > _parent.Capacity) + { + throw new ArgumentException("Substream extends beyond end of parent stream"); + } + } + + /// + /// Can this buffer be read. + /// + public override bool CanRead + { + get { return _parent.CanRead; } + } + + /// + /// Can this buffer be modified. + /// + public override bool CanWrite + { + get { return _parent.CanWrite; } + } + + /// + /// Gets the current capacity of the buffer, in bytes. + /// + public override long Capacity + { + get { return _length; } + } + + /// + /// Gets the parts of the buffer that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public override IEnumerable Extents + { + get + { + return OffsetExtents(_parent.GetExtentsInRange(_first, _length)); + } + } + + /// + /// Flushes all data to the underlying storage. + /// + public override void Flush() + { + _parent.Flush(); + } + + /// + /// Reads from the buffer into a byte array. + /// + /// The offset within the buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + public override int Read(long pos, byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException("count", "Attempt to read negative bytes"); + } + + if (pos >= _length) + { + return 0; + } + + return _parent.Read(pos + _first, buffer, offset, (int)Math.Min(count, Math.Min(_length - pos, int.MaxValue))); + } + + /// + /// Writes a byte array into the buffer. + /// + /// The start offset within the buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + public override void Write(long pos, byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException("count", "Attempt to write negative bytes"); + } + + if (pos + count > _length) + { + throw new ArgumentOutOfRangeException("count", "Attempt to write beyond end of substream"); + } + + _parent.Write(pos + _first, buffer, offset, count); + } + + /// + /// Sets the capacity of the buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + public override void SetCapacity(long value) + { + throw new NotSupportedException("Attempt to change length of a subbuffer"); + } + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + long absStart = _first + start; + long absEnd = Math.Min(absStart + count, _first + _length); + return OffsetExtents(_parent.GetExtentsInRange(absStart, absEnd - absStart)); + } + + private IEnumerable OffsetExtents(IEnumerable src) + { + foreach (StreamExtent e in src) + { + yield return new StreamExtent(e.Start - _first, e.Length); + } + } + } +} diff --git a/DiscUtils/SubStream.cs b/DiscUtils/SubStream.cs new file mode 100644 index 0000000..0d3d399 --- /dev/null +++ b/DiscUtils/SubStream.cs @@ -0,0 +1,217 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class SubStream : MappedStream + { + private long _position; + private long _first; + private long _length; + + private Stream _parent; + private Ownership _ownsParent; + + public SubStream(Stream parent, long first, long length) + { + _parent = parent; + _first = first; + _length = length; + _ownsParent = Ownership.None; + + if (_first + _length > _parent.Length) + { + throw new ArgumentException("Substream extends beyond end of parent stream"); + } + } + + public SubStream(Stream parent, Ownership ownsParent, long first, long length) + { + _parent = parent; + _ownsParent = ownsParent; + _first = first; + _length = length; + + if (_first + _length > _parent.Length) + { + throw new ArgumentException("Substream extends beyond end of parent stream"); + } + } + + public override bool CanRead + { + get { return _parent.CanRead; } + } + + public override bool CanSeek + { + get { return _parent.CanSeek; } + } + + public override bool CanWrite + { + get { return _parent.CanWrite; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get + { + return _position; + } + + set + { + if (value <= _length) + { + _position = value; + } + else + { + throw new ArgumentOutOfRangeException("value", "Attempt to move beyond end of stream"); + } + } + } + + public override IEnumerable Extents + { + get + { + SparseStream parentAsSparse = _parent as SparseStream; + if (parentAsSparse != null) + { + return OffsetExtents(parentAsSparse.GetExtentsInRange(_first, _length)); + } + else + { + return new StreamExtent[] { new StreamExtent(0, _length) }; + } + } + } + + public override IEnumerable MapContent(long start, long length) + { + return new StreamExtent[] { new StreamExtent(start + _first, length) }; + } + + public override void Flush() + { + _parent.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException("count", "Attempt to read negative bytes"); + } + + if (_position > _length) + { + return 0; + } + + _parent.Position = _first + _position; + int numRead = _parent.Read(buffer, offset, (int)Math.Min(count, Math.Min(_length - _position, int.MaxValue))); + _position += numRead; + return numRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long absNewPos = offset; + if (origin == SeekOrigin.Current) + { + absNewPos += _position; + } + else if (origin == SeekOrigin.End) + { + absNewPos += _length; + } + + if (absNewPos < 0) + { + throw new ArgumentOutOfRangeException("offset", "Attempt to move before start of stream"); + } + + _position = absNewPos; + return _position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException("Attempt to change length of a substream"); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException("count", "Attempt to write negative bytes"); + } + + if (_position + count > _length) + { + throw new ArgumentOutOfRangeException("count", "Attempt to write beyond end of substream"); + } + + _parent.Position = _first + _position; + _parent.Write(buffer, offset, count); + _position += count; + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_ownsParent == Ownership.Dispose) + { + _parent.Dispose(); + } + } + } + finally + { + base.Dispose(disposing); + } + } + + private IEnumerable OffsetExtents(IEnumerable src) + { + foreach (StreamExtent e in src) + { + yield return new StreamExtent(e.Start - _first, e.Length); + } + } + } +} diff --git a/DiscUtils/Tuple.cs b/DiscUtils/Tuple.cs new file mode 100644 index 0000000..ba30e61 --- /dev/null +++ b/DiscUtils/Tuple.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + internal abstract class Tuple + { + public abstract object this[int i] + { + get; + } + + protected static bool Equals(V a, V b) + { + if (a == null && b == null) + { + return true; + } + else if (a == null) + { + return false; + } + else + { + return a.Equals(b); + } + } + } +} diff --git a/DiscUtils/Tuple_2.cs b/DiscUtils/Tuple_2.cs new file mode 100644 index 0000000..4d58e6c --- /dev/null +++ b/DiscUtils/Tuple_2.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + internal class Tuple : Tuple + { + private A _a; + private B _b; + + public Tuple(A a, B b) + { + _a = a; + _b = b; + } + + public A First + { + get { return _a; } + } + + public B Second + { + get { return _b; } + } + + public override object this[int i] + { + get + { + switch (i) + { + case 0: return _a; + case 1: return _b; + default: throw new ArgumentOutOfRangeException("i", i, "Invalid index"); + } + } + } + + public override bool Equals(object obj) + { + Tuple asType = obj as Tuple; + if (asType == null) + { + return false; + } + + return Equals(_a, asType._a) && Equals(_b, asType._b); + } + + public override int GetHashCode() + { + return ((_a == null) ? 0x14AB32BC : _a.GetHashCode()) ^ ((_b == null) ? 0x65BC32DE : _b.GetHashCode()); + } + } +} diff --git a/DiscUtils/Tuple_3.cs b/DiscUtils/Tuple_3.cs new file mode 100644 index 0000000..05db4aa --- /dev/null +++ b/DiscUtils/Tuple_3.cs @@ -0,0 +1,87 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + internal class Tuple : Tuple + { + private A _a; + private B _b; + private C _c; + + public Tuple(A a, B b, C c) + { + _a = a; + _b = b; + _c = c; + } + + public A First + { + get { return _a; } + } + + public B Second + { + get { return _b; } + } + + public C Third + { + get { return _c; } + } + + public override object this[int i] + { + get + { + switch (i) + { + case 0: return _a; + case 1: return _b; + case 2: return _c; + default: throw new ArgumentOutOfRangeException("i", i, "Invalid index"); + } + } + } + + public override bool Equals(object obj) + { + Tuple asType = obj as Tuple; + if (asType == null) + { + return false; + } + + return Equals(_a, asType._a) && Equals(_b, asType._b) && Equals(_c, asType._c); + } + + public override int GetHashCode() + { + return ((_a == null) ? 0x14AB32BC : _a.GetHashCode()) + ^ ((_b == null) ? 0x65BC32DE : _b.GetHashCode()) + ^ ((_c == null) ? 0x2D4C25CF : _b.GetHashCode()); + } + } +} diff --git a/DiscUtils/UnixFileType.cs b/DiscUtils/UnixFileType.cs new file mode 100644 index 0000000..3b6289a --- /dev/null +++ b/DiscUtils/UnixFileType.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + /// + /// Standard Unix-style file type. + /// + public enum UnixFileType + { + /// + /// No type specified. + /// + None = 0, + + /// + /// A FIFO / Named Pipe. + /// + Fifo = 0x1, + + /// + /// A character device. + /// + Character = 0x2, + + /// + /// A normal directory. + /// + Directory = 0x4, + + /// + /// A block device. + /// + Block = 0x6, + + /// + /// A regular file. + /// + Regular = 0x8, + + /// + /// A soft link. + /// + Link = 0xA, + + /// + /// A unix socket. + /// + Socket = 0xC + } +} diff --git a/DiscUtils/Utilities.cs b/DiscUtils/Utilities.cs new file mode 100644 index 0000000..460941c --- /dev/null +++ b/DiscUtils/Utilities.cs @@ -0,0 +1,1106 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + + internal delegate TResult Func(T arg); + + internal static class Utilities + { + /// + /// The number of bytes in a standard disk sector (512). + /// + internal const int SectorSize = Sizes.Sector; + + /// + /// The Epoch common to most (all?) Unix systems. + /// + internal static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1); + + /// + /// Round up a value to a multiple of a unit size. + /// + /// The value to round up. + /// The unit (the returned value will be a multiple of this number). + /// The rounded-up value. + public static long RoundUp(long value, long unit) + { + return ((value + (unit - 1)) / unit) * unit; + } + + /// + /// Round up a value to a multiple of a unit size. + /// + /// The value to round up. + /// The unit (the returned value will be a multiple of this number). + /// The rounded-up value. + public static int RoundUp(int value, int unit) + { + return ((value + (unit - 1)) / unit) * unit; + } + + /// + /// Round down a value to a multiple of a unit size. + /// + /// The value to round down. + /// The unit (the returned value will be a multiple of this number). + /// The rounded-down value. + public static long RoundDown(long value, long unit) + { + return (value / unit) * unit; + } + + /// + /// Calculates the CEIL function. + /// + /// The value to divide. + /// The value to divide by. + /// The value of CEIL(numerator/denominator). + public static int Ceil(int numerator, int denominator) + { + return (numerator + (denominator - 1)) / denominator; + } + + /// + /// Calculates the CEIL function. + /// + /// The value to divide. + /// The value to divide by. + /// The value of CEIL(numerator/denominator). + public static uint Ceil(uint numerator, uint denominator) + { + return (numerator + (denominator - 1)) / denominator; + } + + /// + /// Calculates the CEIL function. + /// + /// The value to divide. + /// The value to divide by. + /// The value of CEIL(numerator/denominator). + public static long Ceil(long numerator, long denominator) + { + return (numerator + (denominator - 1)) / denominator; + } + + /// + /// Converts between two arrays. + /// + /// The type of the elements of the source array. + /// The type of the elements of the destination array. + /// The source array. + /// The function to map from source type to destination type. + /// The resultant array. + public static U[] Map(ICollection source, Func func) + { + U[] result = new U[source.Count]; + int i = 0; + + foreach (T sVal in source) + { + result[i++] = func(sVal); + } + + return result; + } + + /// + /// Converts between two arrays. + /// + /// The type of the elements of the source array. + /// The type of the elements of the destination array. + /// The source array. + /// The function to map from source type to destination type. + /// The resultant array. + public static U[] Map(IEnumerable source, Func func) + { + List result = new List(); + + foreach (T sVal in source) + { + result.Add(func(sVal)); + } + + return result.ToArray(); + } + + /// + /// Filters a collection into a new collection. + /// + /// The type of the new collection. + /// The type of the collection entries. + /// The collection to filter. + /// The predicate to select which entries are carried over. + /// The new collection, containing all entries where the predicate returns true. + public static C Filter(ICollection source, Func predicate) where C : ICollection, new() + { + C result = new C(); + foreach (T val in source) + { + if (predicate(val)) + { + result.Add(val); + } + } + + return result; + } + + /// + /// Indicates if two ranges overlap. + /// + /// The type of the ordinals. + /// The lowest ordinal of the first range (inclusive). + /// The highest ordinal of the first range (exclusive). + /// The lowest ordinal of the second range (inclusive). + /// The highest ordinal of the second range (exclusive). + /// true if the ranges overlap, else false. + public static bool RangesOverlap(T xFirst, T xLast, T yFirst, T yLast) where T : IComparable + { + return !((xLast.CompareTo(yFirst) <= 0) || (xFirst.CompareTo(yLast) >= 0)); + } + + /// + /// Validates standard buffer, offset, count parameters to a method. + /// + /// The byte array to read from / write to. + /// The starting offset in buffer. + /// The number of bytes to read / write. + public static void AssertBufferParameters(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException("offset", offset, "Offset is negative"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException("count", count, "Count is negative"); + } + + if (buffer.Length < offset + count) + { + throw new ArgumentException("buffer is too small", "buffer"); + } + } + + #region Bit Twiddling + public static bool IsAllZeros(byte[] buffer, int offset, int count) + { + int end = offset + count; + for (int i = offset; i < end; ++i) + { + if (buffer[i] != 0) + { + return false; + } + } + + return true; + } + + public static bool IsPowerOfTwo(uint val) + { + if (val == 0) + { + return false; + } + + while ((val & 1) != 1) + { + val >>= 1; + } + + return val == 1; + } + + public static bool IsPowerOfTwo(long val) + { + if (val == 0) + { + return false; + } + + while ((val & 1) != 1) + { + val >>= 1; + } + + return val == 1; + } + + public static int Log2(uint val) + { + if (val == 0) + { + throw new ArgumentException("Cannot calculate log of Zero", "val"); + } + + int result = 0; + while ((val & 1) != 1) + { + val >>= 1; + ++result; + } + + if (val == 1) + { + return result; + } + else + { + throw new ArgumentException("Input is not a power of Two", "val"); + } + } + + public static int Log2(int val) + { + if (val == 0) + { + throw new ArgumentException("Cannot calculate log of Zero", "val"); + } + + int result = 0; + while ((val & 1) != 1) + { + val >>= 1; + ++result; + } + + if (val == 1) + { + return result; + } + else + { + throw new ArgumentException("Input is not a power of Two", "val"); + } + } + + public static bool AreEqual(byte[] a, byte[] b) + { + if (a.Length != b.Length) + { + return false; + } + + for (int i = 0; i < a.Length; ++i) + { + if (a[i] != b[i]) + { + return false; + } + } + + return true; + } + + public static ushort BitSwap(ushort value) + { + return (ushort)(((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)); + } + + public static uint BitSwap(uint value) + { + return ((value & 0xFF) << 24) | ((value & 0xFF00) << 8) | ((value & 0x00FF0000) >> 8) | ((value & 0xFF000000) >> 24); + } + + public static ulong BitSwap(ulong value) + { + return (((ulong)BitSwap((uint)(value & 0xFFFFFFFF))) << 32) | BitSwap((uint)(value >> 32)); + } + + public static short BitSwap(short value) + { + return (short)BitSwap((ushort)value); + } + + public static int BitSwap(int value) + { + return (int)BitSwap((uint)value); + } + + public static long BitSwap(long value) + { + return (long)BitSwap((ulong)value); + } + + public static void WriteBytesLittleEndian(ushort val, byte[] buffer, int offset) + { + buffer[offset] = (byte)(val & 0xFF); + buffer[offset + 1] = (byte)((val >> 8) & 0xFF); + } + + public static void WriteBytesLittleEndian(uint val, byte[] buffer, int offset) + { + buffer[offset] = (byte)(val & 0xFF); + buffer[offset + 1] = (byte)((val >> 8) & 0xFF); + buffer[offset + 2] = (byte)((val >> 16) & 0xFF); + buffer[offset + 3] = (byte)((val >> 24) & 0xFF); + } + + public static void WriteBytesLittleEndian(ulong val, byte[] buffer, int offset) + { + buffer[offset] = (byte)(val & 0xFF); + buffer[offset + 1] = (byte)((val >> 8) & 0xFF); + buffer[offset + 2] = (byte)((val >> 16) & 0xFF); + buffer[offset + 3] = (byte)((val >> 24) & 0xFF); + buffer[offset + 4] = (byte)((val >> 32) & 0xFF); + buffer[offset + 5] = (byte)((val >> 40) & 0xFF); + buffer[offset + 6] = (byte)((val >> 48) & 0xFF); + buffer[offset + 7] = (byte)((val >> 56) & 0xFF); + } + + public static void WriteBytesLittleEndian(short val, byte[] buffer, int offset) + { + WriteBytesLittleEndian((ushort)val, buffer, offset); + } + + public static void WriteBytesLittleEndian(int val, byte[] buffer, int offset) + { + WriteBytesLittleEndian((uint)val, buffer, offset); + } + + public static void WriteBytesLittleEndian(long val, byte[] buffer, int offset) + { + WriteBytesLittleEndian((ulong)val, buffer, offset); + } + + public static void WriteBytesLittleEndian(Guid val, byte[] buffer, int offset) + { + byte[] le = val.ToByteArray(); + Array.Copy(le, 0, buffer, offset, 16); + } + + public static void WriteBytesBigEndian(ushort val, byte[] buffer, int offset) + { + buffer[offset] = (byte)(val >> 8); + buffer[offset + 1] = (byte)(val & 0xFF); + } + + public static void WriteBytesBigEndian(uint val, byte[] buffer, int offset) + { + buffer[offset] = (byte)((val >> 24) & 0xFF); + buffer[offset + 1] = (byte)((val >> 16) & 0xFF); + buffer[offset + 2] = (byte)((val >> 8) & 0xFF); + buffer[offset + 3] = (byte)(val & 0xFF); + } + + public static void WriteBytesBigEndian(ulong val, byte[] buffer, int offset) + { + buffer[offset] = (byte)((val >> 56) & 0xFF); + buffer[offset + 1] = (byte)((val >> 48) & 0xFF); + buffer[offset + 2] = (byte)((val >> 40) & 0xFF); + buffer[offset + 3] = (byte)((val >> 32) & 0xFF); + buffer[offset + 4] = (byte)((val >> 24) & 0xFF); + buffer[offset + 5] = (byte)((val >> 16) & 0xFF); + buffer[offset + 6] = (byte)((val >> 8) & 0xFF); + buffer[offset + 7] = (byte)(val & 0xFF); + } + + public static void WriteBytesBigEndian(short val, byte[] buffer, int offset) + { + WriteBytesBigEndian((ushort)val, buffer, offset); + } + + public static void WriteBytesBigEndian(int val, byte[] buffer, int offset) + { + WriteBytesBigEndian((uint)val, buffer, offset); + } + + public static void WriteBytesBigEndian(long val, byte[] buffer, int offset) + { + WriteBytesBigEndian((ulong)val, buffer, offset); + } + + public static void WriteBytesBigEndian(Guid val, byte[] buffer, int offset) + { + byte[] le = val.ToByteArray(); + WriteBytesBigEndian(ToUInt32LittleEndian(le, 0), buffer, offset + 0); + WriteBytesBigEndian(ToUInt16LittleEndian(le, 4), buffer, offset + 4); + WriteBytesBigEndian(ToUInt16LittleEndian(le, 6), buffer, offset + 6); + Array.Copy(le, 8, buffer, offset + 8, 8); + } + + public static ushort ToUInt16LittleEndian(byte[] buffer, int offset) + { + return (ushort)(((buffer[offset + 1] << 8) & 0xFF00) | ((buffer[offset + 0] << 0) & 0x00FF)); + } + + public static uint ToUInt32LittleEndian(byte[] buffer, int offset) + { + return (uint)(((buffer[offset + 3] << 24) & 0xFF000000U) | ((buffer[offset + 2] << 16) & 0x00FF0000U) + | ((buffer[offset + 1] << 8) & 0x0000FF00U) | ((buffer[offset + 0] << 0) & 0x000000FFU)); + } + + public static ulong ToUInt64LittleEndian(byte[] buffer, int offset) + { + return (((ulong)ToUInt32LittleEndian(buffer, offset + 4)) << 32) | ToUInt32LittleEndian(buffer, offset + 0); + } + + public static short ToInt16LittleEndian(byte[] buffer, int offset) + { + return (short)ToUInt16LittleEndian(buffer, offset); + } + + public static int ToInt32LittleEndian(byte[] buffer, int offset) + { + return (int)ToUInt32LittleEndian(buffer, offset); + } + + public static long ToInt64LittleEndian(byte[] buffer, int offset) + { + return (long)ToUInt64LittleEndian(buffer, offset); + } + + public static ushort ToUInt16BigEndian(byte[] buffer, int offset) + { + return (ushort)(((buffer[offset] << 8) & 0xFF00) | ((buffer[offset + 1] << 0) & 0x00FF)); + } + + public static uint ToUInt32BigEndian(byte[] buffer, int offset) + { + uint val = (uint)(((buffer[offset + 0] << 24) & 0xFF000000U) | ((buffer[offset + 1] << 16) & 0x00FF0000U) + | ((buffer[offset + 2] << 8) & 0x0000FF00U) | ((buffer[offset + 3] << 0) & 0x000000FFU)); + return val; + } + + public static ulong ToUInt64BigEndian(byte[] buffer, int offset) + { + return (((ulong)ToUInt32BigEndian(buffer, offset + 0)) << 32) | ToUInt32BigEndian(buffer, offset + 4); + } + + public static short ToInt16BigEndian(byte[] buffer, int offset) + { + return (short)ToUInt16BigEndian(buffer, offset); + } + + public static int ToInt32BigEndian(byte[] buffer, int offset) + { + return (int)ToUInt32BigEndian(buffer, offset); + } + + public static long ToInt64BigEndian(byte[] buffer, int offset) + { + return (long)ToUInt64BigEndian(buffer, offset); + } + + public static Guid ToGuidLittleEndian(byte[] buffer, int offset) + { + byte[] temp = new byte[16]; + Array.Copy(buffer, offset, temp, 0, 16); + return new Guid(temp); + } + + public static Guid ToGuidBigEndian(byte[] buffer, int offset) + { + return new Guid( + ToUInt32BigEndian(buffer, offset + 0), + ToUInt16BigEndian(buffer, offset + 4), + ToUInt16BigEndian(buffer, offset + 6), + buffer[offset + 8], + buffer[offset + 9], + buffer[offset + 10], + buffer[offset + 11], + buffer[offset + 12], + buffer[offset + 13], + buffer[offset + 14], + buffer[offset + 15]); + } + + public static byte[] ToByteArray(byte[] buffer, int offset, int length) + { + byte[] result = new byte[length]; + Array.Copy(buffer, offset, result, 0, length); + return result; + } + + public static T ToStruct(byte[] buffer, int offset) + where T : IByteArraySerializable, new() + { + T result = new T(); + result.ReadFrom(buffer, offset); + return result; + } + + /// + /// Primitive conversion from Unicode to ASCII that preserves special characters. + /// + /// The string to convert. + /// The buffer to fill. + /// The start of the string in the buffer. + /// The number of characters to convert. + /// The built-in ASCIIEncoding converts characters of codepoint > 127 to ?, + /// this preserves those code points by removing the top 16 bits of each character. + public static void StringToBytes(string value, byte[] dest, int offset, int count) + { + char[] chars = value.ToCharArray(); + + int i = 0; + while (i < chars.Length) + { + dest[i + offset] = (byte)chars[i]; + ++i; + } + + while (i < count) + { + dest[i + offset] = 0; + ++i; + } + } + + /// + /// Primitive conversion from ASCII to Unicode that preserves special characters. + /// + /// The data to convert. + /// The first byte to convert. + /// The number of bytes to convert. + /// The string. + /// The built-in ASCIIEncoding converts characters of codepoint > 127 to ?, + /// this preserves those code points. + public static string BytesToString(byte[] data, int offset, int count) + { + char[] result = new char[count]; + + for (int i = 0; i < count; ++i) + { + result[i] = (char)data[i + offset]; + } + + return new string(result); + } + + /// + /// Primitive conversion from ASCII to Unicode that stops at a null-terminator. + /// + /// The data to convert. + /// The first byte to convert. + /// The number of bytes to convert. + /// The string. + /// The built-in ASCIIEncoding converts characters of codepoint > 127 to ?, + /// this preserves those code points. + public static string BytesToZString(byte[] data, int offset, int count) + { + char[] result = new char[count]; + + for (int i = 0; i < count; ++i) + { + byte ch = data[i + offset]; + if (ch == 0) + { + return new string(result, 0, i); + } + + result[i] = (char)ch; + } + + return new string(result); + } + + #endregion + + #region Path Manipulation + /// + /// Extracts the directory part of a path. + /// + /// The path to process. + /// The directory part. + public static string GetDirectoryFromPath(string path) + { + string trimmed = path.TrimEnd('\\'); + + int index = trimmed.LastIndexOf('\\'); + if (index < 0) + { + return string.Empty; // No directory, just a file name + } + + return trimmed.Substring(0, index); + } + + /// + /// Extracts the file part of a path. + /// + /// The path to process. + /// The file part of the path. + public static string GetFileFromPath(string path) + { + string trimmed = path.Trim('\\'); + + int index = trimmed.LastIndexOf('\\'); + if (index < 0) + { + return trimmed; // No directory, just a file name + } + + return trimmed.Substring(index + 1); + } + + /// + /// Combines two paths. + /// + /// The first part of the path. + /// The second part of the path. + /// The combined path. + public static string CombinePaths(string a, string b) + { + if (string.IsNullOrEmpty(a) || (b.Length > 0 && b[0] == '\\')) + { + return b; + } + else if (string.IsNullOrEmpty(b)) + { + return a; + } + else + { + return a.TrimEnd('\\') + '\\' + b.TrimStart('\\'); + } + } + + /// + /// Resolves a relative path into an absolute one. + /// + /// The base path to resolve from. + /// The relative path. + /// The absolute path, so far as it can be resolved. If the + /// contains more '..' characters than the + /// base path contains levels of directory, the resultant string will be relative. + /// For example: (TEMP\Foo.txt, ..\..\Bar.txt) gives (..\Bar.txt). + public static string ResolveRelativePath(string basePath, string relativePath) + { + List pathElements = new List(basePath.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)); + if (!basePath.EndsWith(@"\", StringComparison.Ordinal) && pathElements.Count > 0) + { + pathElements.RemoveAt(pathElements.Count - 1); + } + + pathElements.AddRange(relativePath.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)); + + int pos = 1; + while (pos < pathElements.Count) + { + if (pathElements[pos] == ".") + { + pathElements.RemoveAt(pos); + } + else if (pathElements[pos] == ".." && pos > 0 && pathElements[pos - 1][0] != '.') + { + pathElements.RemoveAt(pos); + pathElements.RemoveAt(pos - 1); + pos--; + } + else + { + pos++; + } + } + + string merged = string.Join(@"\", pathElements.ToArray()); + if (relativePath.EndsWith(@"\", StringComparison.Ordinal)) + { + merged += @"\"; + } + + if (basePath.StartsWith(@"\\", StringComparison.Ordinal)) + { + merged = @"\\" + merged; + } + else if (basePath.StartsWith(@"\", StringComparison.Ordinal)) + { + merged = @"\" + merged; + } + + return merged; + } + + public static string ResolvePath(string basePath, string path) + { + if (!path.StartsWith("\\", StringComparison.OrdinalIgnoreCase)) + { + return ResolveRelativePath(basePath, path); + } + else + { + return path; + } + } + + public static string MakeRelativePath(string path, string basePath) + { + List pathElements = new List(path.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)); + List basePathElements = new List(basePath.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)); + + if (!basePath.EndsWith("\\", StringComparison.Ordinal) && basePathElements.Count > 0) + { + basePathElements.RemoveAt(basePathElements.Count - 1); + } + + // Find first part of paths that don't match + int i = 0; + while (i < Math.Min(pathElements.Count - 1, basePathElements.Count)) + { + if (pathElements[i].ToUpperInvariant() != basePathElements[i].ToUpperInvariant()) + { + break; + } + + ++i; + } + + // For each remaining part of the base path, insert '..' + StringBuilder result = new StringBuilder(); + if (i == basePathElements.Count) + { + result.Append(@".\"); + } + else if (i < basePathElements.Count) + { + for (int j = 0; j < basePathElements.Count - i; ++j) + { + result.Append(@"..\"); + } + } + + // For each remaining part of the path, add the path element + for (int j = i; j < pathElements.Count - 1; ++j) + { + result.Append(pathElements[j]); + result.Append(@"\"); + } + + result.Append(pathElements[pathElements.Count - 1]); + + // If the target was a directory, put the terminator back + if (path.EndsWith(@"\", StringComparison.Ordinal)) + { + result.Append(@"\"); + } + + return result.ToString(); + } + #endregion + + #region Stream Manipulation + /// + /// Read bytes until buffer filled or EOF. + /// + /// The stream to read. + /// The buffer to populate. + /// Offset in the buffer to start. + /// The number of bytes to read. + /// The number of bytes actually read. + public static int ReadFully(Stream stream, byte[] buffer, int offset, int length) + { + int totalRead = 0; + int numRead = stream.Read(buffer, offset, length); + while (numRead > 0) + { + totalRead += numRead; + if (totalRead == length) + { + break; + } + + numRead = stream.Read(buffer, offset + totalRead, length - totalRead); + } + + return totalRead; + } + + /// + /// Read bytes until buffer filled or throw IOException. + /// + /// The stream to read. + /// The number of bytes to read. + /// The data read from the stream. + public static byte[] ReadFully(Stream stream, int count) + { + byte[] buffer = new byte[count]; + if (ReadFully(stream, buffer, 0, count) == count) + { + return buffer; + } + else + { + throw new IOException("Unable to complete read of " + count + " bytes"); + } + } + + /// + /// Read bytes until buffer filled or EOF. + /// + /// The stream to read. + /// The position in buffer to read from. + /// The buffer to populate. + /// Offset in the buffer to start. + /// The number of bytes to read. + /// The number of bytes actually read. + public static int ReadFully(IBuffer buffer, long pos, byte[] data, int offset, int length) + { + int totalRead = 0; + int numRead = buffer.Read(pos, data, offset, length); + while (numRead > 0) + { + totalRead += numRead; + if (totalRead == length) + { + break; + } + + numRead = buffer.Read(pos, data, offset + totalRead, length - totalRead); + } + + return totalRead; + } + + /// + /// Read bytes until buffer filled or throw IOException. + /// + /// The buffer to read. + /// The position in buffer to read from. + /// The number of bytes to read. + /// The data read from the stream. + public static byte[] ReadFully(IBuffer buffer, long pos, int count) + { + byte[] result = new byte[count]; + if (ReadFully(buffer, pos, result, 0, count) == count) + { + return result; + } + else + { + throw new IOException("Unable to complete read of " + count + " bytes"); + } + } + + /// + /// Read bytes until buffer filled or throw IOException. + /// + /// The buffer to read. + /// The data read from the stream. + public static byte[] ReadAll(IBuffer buffer) + { + return ReadFully(buffer, 0, (int)buffer.Capacity); + } + + /// + /// Reads a disk sector (512 bytes). + /// + /// The stream to read. + /// The sector data as a byte array. + public static byte[] ReadSector(Stream stream) + { + return ReadFully(stream, SectorSize); + } + + /// + /// Reads a structure from a stream. + /// + /// The type of the structure. + /// The stream to read. + /// The structure. + public static T ReadStruct(Stream stream) + where T : IByteArraySerializable, new() + { + T result = new T(); + byte[] buffer = Utilities.ReadFully(stream, result.Size); + result.ReadFrom(buffer, 0); + return result; + } + + /// + /// Reads a structure from a stream. + /// + /// The type of the structure. + /// The stream to read. + /// The number of bytes to read. + /// The structure. + public static T ReadStruct(Stream stream, int length) + where T : IByteArraySerializable, new() + { + T result = new T(); + byte[] buffer = Utilities.ReadFully(stream, length); + result.ReadFrom(buffer, 0); + return result; + } + + /// + /// Writes a structure to a stream. + /// + /// The type of the structure. + /// The stream to write to. + /// The structure to write. + public static void WriteStruct(Stream stream, T obj) + where T : IByteArraySerializable + { + byte[] buffer = new byte[obj.Size]; + obj.WriteTo(buffer, 0); + stream.Write(buffer, 0, buffer.Length); + } + + /// + /// Copies the contents of one stream to another. + /// + /// The stream to copy from. + /// The destination stream. + /// Copying starts at the current stream positions. + public static void PumpStreams(Stream source, Stream dest) + { + byte[] buffer = new byte[64 * 1024]; + + int numRead = source.Read(buffer, 0, buffer.Length); + while (numRead != 0) + { + dest.Write(buffer, 0, numRead); + numRead = source.Read(buffer, 0, buffer.Length); + } + } + + #endregion + + #region Filesystem Support + + /// + /// Indicates if a file name matches the 8.3 pattern. + /// + /// The name to test. + /// true if the name is 8.3, otherwise false. + public static bool Is8Dot3(string name) + { + if (name.Length > 12) + { + return false; + } + + string[] split = name.Split(new char[] { '.' }); + + if (split.Length > 2 || split.Length < 1) + { + return false; + } + + if (split[0].Length > 8) + { + return false; + } + + foreach (char ch in split[0]) + { + if (!Is8Dot3Char(ch)) + { + return false; + } + } + + if (split.Length > 1) + { + if (split[1].Length > 3) + { + return false; + } + + foreach (char ch in split[1]) + { + if (!Is8Dot3Char(ch)) + { + return false; + } + } + } + + return true; + } + + public static bool Is8Dot3Char(char ch) + { + return (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || "_^$~!#%£-{}()@'`&".IndexOf(ch) != -1; + } + + /// + /// Converts a 'standard' wildcard file/path specification into a regular expression. + /// + /// The wildcard pattern to convert. + /// The resultant regular expression. + /// + /// The wildcard * (star) matches zero or more characters (including '.'), and ? + /// (question mark) matches precisely one character (except '.'). + /// + public static Regex ConvertWildcardsToRegEx(string pattern) + { + if (!pattern.Contains(".")) + { + pattern += "."; + } + + string query = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", "[^.]") + "$"; + return new Regex(query, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + } + + public static DateTime DateTimeFromUnix(uint fileTime) + { + long ticks = fileTime * (long)10 * 1000 * 1000; + return new DateTime(ticks + UnixEpoch.Ticks); + } + + public static uint DateTimeToUnix(DateTime time) + { + return (uint)((time.Ticks - UnixEpoch.Ticks) / (10 * 1000 * 1000)); + } + + public static FileAttributes FileAttributesFromUnixFileType(UnixFileType fileType) + { + switch (fileType) + { + case UnixFileType.Fifo: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Character: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Directory: + return FileAttributes.Directory; + case UnixFileType.Block: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Regular: + return FileAttributes.Normal; + case UnixFileType.Link: + return FileAttributes.ReparsePoint; + case UnixFileType.Socket: + return FileAttributes.Device | FileAttributes.System; + default: + return (FileAttributes)0; + } + } + #endregion + } +} diff --git a/DiscUtils/Vfs/IVfsDirectory.cs b/DiscUtils/Vfs/IVfsDirectory.cs new file mode 100644 index 0000000..901766d --- /dev/null +++ b/DiscUtils/Vfs/IVfsDirectory.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System.Collections.Generic; + + /// + /// Interface implemented by classes representing a directory. + /// + /// Concrete type representing directory entries. + /// Concrete type representing files. + public interface IVfsDirectory : IVfsFile + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + { + /// + /// Gets all of the directory entries. + /// + ICollection AllEntries { get; } + + /// + /// Gets a self-reference, if available. + /// + TDirEntry Self + { + get; + } + + /// + /// Gets a specific directory entry, by name. + /// + /// The name of the directory entry. + /// The directory entry, or null if not found. + TDirEntry GetEntryByName(string name); + + /// + /// Creates a new file. + /// + /// The name of the file (relative to this directory). + /// The newly created file. + TDirEntry CreateNewFile(string name); + } +} diff --git a/DiscUtils/Vfs/IVfsFile.cs b/DiscUtils/Vfs/IVfsFile.cs new file mode 100644 index 0000000..89f5abf --- /dev/null +++ b/DiscUtils/Vfs/IVfsFile.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System; + using System.IO; + + /// + /// Interface implemented by a class representing a file. + /// + /// + /// File system implementations should have a class that implements this + /// interface. If the file system implementation is read-only, it is + /// acceptable to throw NotImplementedException from setters. + /// + public interface IVfsFile + { + /// + /// Gets or sets the last access time in UTC. + /// + DateTime LastAccessTimeUtc { get; set; } + + /// + /// Gets or sets the last write time in UTC. + /// + DateTime LastWriteTimeUtc { get; set; } + + /// + /// Gets or sets the last creation time in UTC. + /// + DateTime CreationTimeUtc { get; set; } + + /// + /// Gets or sets the file's attributes. + /// + FileAttributes FileAttributes { get; set; } + + /// + /// Gets the length of the file. + /// + long FileLength { get; } + + /// + /// Gets a buffer to access the file's contents. + /// + IBuffer FileContent { get; } + } + + /// + /// Interface implemented by classes representing files, in file systems that support multi-stream files. + /// + public interface IVfsFileWithStreams : IVfsFile + { + /// + /// Creates a new stream. + /// + /// The name of the stream. + /// An object representing the stream. + SparseStream CreateStream(string name); + + /// + /// Opens an existing stream. + /// + /// The name of the stream. + /// An object representing the stream. + /// The implementation must not implicitly create the stream if it doesn't already + /// exist. + SparseStream OpenExistingStream(string name); + } +} diff --git a/DiscUtils/Vfs/IVfsSymlink.cs b/DiscUtils/Vfs/IVfsSymlink.cs new file mode 100644 index 0000000..2fe23b8 --- /dev/null +++ b/DiscUtils/Vfs/IVfsSymlink.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + /// + /// Interface implemented by classes representing a directory. + /// + /// Concrete type representing directory entries. + /// Concrete type representing files. + public interface IVfsSymlink : IVfsFile + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + { + /// + /// Gets the target path for this symlink. + /// + string TargetPath { get; } + } +} diff --git a/DiscUtils/Vfs/VfsContext.cs b/DiscUtils/Vfs/VfsContext.cs new file mode 100644 index 0000000..22476c2 --- /dev/null +++ b/DiscUtils/Vfs/VfsContext.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + /// + /// Base class for a context object that holds global state for file system implementations. + /// + public abstract class VfsContext + { + } +} diff --git a/DiscUtils/Vfs/VfsDirEntry.cs b/DiscUtils/Vfs/VfsDirEntry.cs new file mode 100644 index 0000000..3ae4d52 --- /dev/null +++ b/DiscUtils/Vfs/VfsDirEntry.cs @@ -0,0 +1,131 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System; + using System.IO; + + /// + /// Base class for directory entries in a file system. + /// + /// + /// File system implementations should have a class that derives from + /// this abstract class. If the file system implementation is read-only, + /// it is acceptable to throw NotImplementedException from methods + /// that attempt to modify the file system. + /// + public abstract class VfsDirEntry + { + /// + /// Gets a value indicating whether this directory entry represents a directory (rather than a file). + /// + public abstract bool IsDirectory { get; } + + /// + /// Gets a value indicating whether this directory entry represents a symlink (rather than a file or directory). + /// + public abstract bool IsSymlink { get; } + + /// + /// Gets the name of this directory entry. + /// + public abstract string FileName { get; } + + /// + /// Gets a value indicating whether this directory entry contains time information. + /// + /// + /// Typically either always returns true or false. + /// + public abstract bool HasVfsTimeInfo { get; } + + /// + /// Gets the last access time of the file or directory. + /// + /// + /// May throw NotSupportedException if HasVfsTimeInfo is false. + /// + public abstract DateTime LastAccessTimeUtc { get; } + + /// + /// Gets the last write time of the file or directory. + /// + /// + /// May throw NotSupportedException if HasVfsTimeInfo is false. + /// + public abstract DateTime LastWriteTimeUtc { get; } + + /// + /// Gets the creation time of the file or directory. + /// + /// + /// May throw NotSupportedException if HasVfsTimeInfo is false. + /// + public abstract DateTime CreationTimeUtc { get; } + + /// + /// Gets a value indicating whether this directory entry contains file attribute information. + /// + /// + /// Typically either always returns true or false. + /// + public abstract bool HasVfsFileAttributes { get; } + + /// + /// Gets the file attributes from the directory entry. + /// + /// + /// May throw NotSupportedException if HasVfsFileAttributes is false. + /// + public abstract FileAttributes FileAttributes { get; } + + /// + /// Gets a unique id for the file or directory represented by this directory entry. + /// + public abstract long UniqueCacheId { get; } + + /// + /// Gets a version of FileName that can be used in wildcard matches. + /// + /// + /// The returned name, must have an extension separator '.', and not have any optional version + /// information found in some files. The returned name is matched against a wildcard patterns + /// such as "*.*". + /// + public virtual string SearchName + { + get + { + string fileName = FileName; + if (fileName.IndexOf('.') == -1) + { + return fileName + "."; + } + else + { + return fileName; + } + } + } + } +} diff --git a/DiscUtils/Vfs/VfsFileSystem.cs b/DiscUtils/Vfs/VfsFileSystem.cs new file mode 100644 index 0000000..f81c605 --- /dev/null +++ b/DiscUtils/Vfs/VfsFileSystem.cs @@ -0,0 +1,772 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Text.RegularExpressions; + + /// + /// Base class for VFS file systems. + /// + /// The concrete type representing directory entries. + /// The concrete type representing files. + /// The concrete type representing directories. + /// The concrete type holding global state. + public abstract class VfsFileSystem : DiscFileSystem + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + where TDirectory : class, IVfsDirectory, TFile + where TContext : VfsContext + { + private TContext _context; + private TDirectory _rootDir; + private ObjectCache _fileCache; + + /// + /// Initializes a new instance of the VfsFileSystem class. + /// + /// The default file system options. + protected VfsFileSystem(DiscFileSystemOptions defaultOptions) + : base(defaultOptions) + { + _fileCache = new ObjectCache(); + } + + /// + /// Delegate for processing directory entries. + /// + /// Full path to the directory entry. + /// The directory entry itself. + protected delegate void DirEntryHandler(string path, TDirEntry dirEntry); + + /// + /// Gets the volume label. + /// + public override abstract string VolumeLabel + { + get; + } + + /// + /// Gets or sets the global shared state. + /// + protected TContext Context + { + get { return _context; } + set { _context = value; } + } + + /// + /// Gets or sets the object representing the root directory. + /// + protected TDirectory RootDirectory + { + get { return _rootDir; } + set { _rootDir = value; } + } + + /// + /// Copies a file - not supported on read-only file systems. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + throw new NotImplementedException(); + } + + /// + /// Creates a directory - not supported on read-only file systems. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + throw new NotImplementedException(); + } + + /// + /// Deletes a directory - not supported on read-only file systems. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + throw new NotImplementedException(); + } + + /// + /// Deletes a file - not supported on read-only file systems. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + throw new NotImplementedException(); + } + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public override bool DirectoryExists(string path) + { + if (IsRoot(path)) + { + return true; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + + if (dirEntry != null) + { + return dirEntry.IsDirectory; + } + + return false; + } + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public override bool FileExists(string path) + { + TDirEntry dirEntry = GetDirectoryEntry(path); + + if (dirEntry != null) + { + return !dirEntry.IsDirectory; + } + + return false; + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + List dirs = new List(); + DoSearch(dirs, path, re, searchOption == SearchOption.AllDirectories, true, false); + return dirs.ToArray(); + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern, SearchOption searchOption) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + List results = new List(); + DoSearch(results, path, re, searchOption == SearchOption.AllDirectories, false, true); + return results.ToArray(); + } + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path) + { + string fullPath = path; + if (!fullPath.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + fullPath = @"\" + fullPath; + } + + TDirectory parentDir = GetDirectory(fullPath); + return Utilities.Map(parentDir.AllEntries, (m) => Utilities.CombinePaths(fullPath, FormatFileName(m.FileName))); + } + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path, string searchPattern) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + TDirectory parentDir = GetDirectory(path); + + List result = new List(); + foreach (TDirEntry dirEntry in parentDir.AllEntries) + { + if (re.IsMatch(dirEntry.SearchName)) + { + result.Add(Utilities.CombinePaths(path, dirEntry.FileName)); + } + } + + return result.ToArray(); + } + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + throw new NotImplementedException(); + } + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + /// Overwrite any existing file. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + throw new NotImplementedException(); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) + { + if (!CanWrite) + { + if (mode != FileMode.Open) + { + throw new NotSupportedException("Only existing files can be opened"); + } + + if (access != FileAccess.Read) + { + throw new NotSupportedException("Files cannot be opened for write"); + } + } + + string fileName = Utilities.GetFileFromPath(path); + string attributeName = null; + + int streamSepPos = fileName.IndexOf(':'); + if (streamSepPos >= 0) + { + attributeName = fileName.Substring(streamSepPos + 1); + } + + string dirName; + try + { + dirName = Utilities.GetDirectoryFromPath(path); + } + catch (ArgumentException) + { + throw new IOException("Invalid path: " + path); + } + + string entryPath = Utilities.CombinePaths(dirName, fileName); + TDirEntry entry = GetDirectoryEntry(entryPath); + if (entry == null) + { + if (mode == FileMode.Open) + { + throw new FileNotFoundException("No such file", path); + } + else + { + TDirectory parentDir = GetDirectory(Utilities.GetDirectoryFromPath(path)); + entry = parentDir.CreateNewFile(Utilities.GetFileFromPath(path)); + } + } + else if (mode == FileMode.CreateNew) + { + throw new IOException("File already exists"); + } + + if (entry.IsSymlink) + { + entry = ResolveSymlink(entry, entryPath); + } + + if (entry.IsDirectory) + { + throw new IOException("Attempt to open directory as a file"); + } + else + { + TFile file = GetFile(entry); + + SparseStream stream = null; + if (string.IsNullOrEmpty(attributeName)) + { + stream = new BufferStream(file.FileContent, access); + } + else + { + IVfsFileWithStreams fileStreams = file as IVfsFileWithStreams; + if (fileStreams != null) + { + stream = fileStreams.OpenExistingStream(attributeName); + if (stream == null) + { + if (mode == FileMode.Create || mode == FileMode.OpenOrCreate) + { + stream = fileStreams.CreateStream(attributeName); + } + else + { + throw new FileNotFoundException("No such attribute on file", path); + } + } + } + else + { + throw new NotSupportedException("Attempt to open a file stream on a file system that doesn't support them"); + } + } + + if (mode == FileMode.Create || mode == FileMode.Truncate) + { + stream.SetLength(0); + } + + return stream; + } + } + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public override FileAttributes GetAttributes(string path) + { + if (IsRoot(path)) + { + return _rootDir.FileAttributes; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + + if (dirEntry.HasVfsFileAttributes) + { + return dirEntry.FileAttributes; + } + else + { + return GetFile(dirEntry).FileAttributes; + } + } + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + throw new NotImplementedException(); + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTimeUtc(string path) + { + if (IsRoot(path)) + { + return _rootDir.CreationTimeUtc; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file or directory", path); + } + + if (dirEntry.HasVfsTimeInfo) + { + return dirEntry.CreationTimeUtc; + } + else + { + return GetFile(dirEntry).CreationTimeUtc; + } + } + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + throw new NotImplementedException(); + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTimeUtc(string path) + { + if (IsRoot(path)) + { + return _rootDir.LastAccessTimeUtc; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file or directory", path); + } + + if (dirEntry.HasVfsTimeInfo) + { + return dirEntry.LastAccessTimeUtc; + } + else + { + return GetFile(dirEntry).LastAccessTimeUtc; + } + } + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + throw new NotImplementedException(); + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTimeUtc(string path) + { + if (IsRoot(path)) + { + return _rootDir.LastWriteTimeUtc; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file or directory", path); + } + + if (dirEntry.HasVfsTimeInfo) + { + return dirEntry.LastWriteTimeUtc; + } + else + { + return GetFile(dirEntry).LastWriteTimeUtc; + } + } + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + throw new NotImplementedException(); + } + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public override long GetFileLength(string path) + { + TFile file = GetFile(path); + if (file == null || (file.FileAttributes & FileAttributes.Directory) != 0) + { + throw new FileNotFoundException("No such file", path); + } + + return file.FileLength; + } + + internal TFile GetFile(TDirEntry dirEntry) + { + long cacheKey = dirEntry.UniqueCacheId; + + TFile file = _fileCache[cacheKey]; + if (file == null) + { + file = ConvertDirEntryToFile(dirEntry); + _fileCache[cacheKey] = file; + } + + return file; + } + + internal TDirectory GetDirectory(string path) + { + if (IsRoot(path)) + { + return _rootDir; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + + if (dirEntry.IsSymlink) + { + dirEntry = ResolveSymlink(dirEntry, path); + } + + if (dirEntry == null || !dirEntry.IsDirectory) + { + throw new DirectoryNotFoundException("No such directory: " + path); + } + + return (TDirectory)GetFile(dirEntry); + } + + internal TDirEntry GetDirectoryEntry(string path) + { + return GetDirectoryEntry(_rootDir, path); + } + + /// + /// Gets all directory entries in the specified directory and sub-directories. + /// + /// The path to inspect. + /// Delegate invoked for each directory entry. + protected void ForAllDirEntries(string path, DirEntryHandler handler) + { + TDirectory dir = null; + TDirEntry self = GetDirectoryEntry(path); + + if (self != null) + { + handler(path, self); + if (self.IsDirectory) + { + dir = GetFile(self) as TDirectory; + } + } + else + { + dir = GetFile(path) as TDirectory; + } + + if (dir != null) + { + foreach (var subentry in dir.AllEntries) + { + ForAllDirEntries(Utilities.CombinePaths(path, subentry.FileName), handler); + } + } + } + + /// + /// Gets the file object for a given path. + /// + /// The path to query. + /// The file object corresponding to the path. + protected TFile GetFile(string path) + { + if (IsRoot(path)) + { + return _rootDir; + } + else if (path == null) + { + return default(TFile); + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file or directory", path); + } + + return GetFile(dirEntry); + } + + /// + /// Converts a directory entry to an object representing a file. + /// + /// The directory entry to convert. + /// The corresponding file object. + protected abstract TFile ConvertDirEntryToFile(TDirEntry dirEntry); + + /// + /// Converts an internal directory entry name into an external one. + /// + /// The name to convert. + /// The external name. + /// + /// This method is called on a single path element (i.e. name contains no path + /// separators). + /// + protected virtual string FormatFileName(string name) + { + return name; + } + + private static bool IsRoot(string path) + { + return string.IsNullOrEmpty(path) || path == @"\"; + } + + private TDirEntry GetDirectoryEntry(TDirectory dir, string path) + { + string[] pathElements = path.Split(new char[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + return GetDirectoryEntry(dir, pathElements, 0); + } + + private TDirEntry GetDirectoryEntry(TDirectory dir, string[] pathEntries, int pathOffset) + { + TDirEntry entry; + + if (pathEntries.Length == 0) + { + return dir.Self; + } + else + { + entry = dir.GetEntryByName(pathEntries[pathOffset]); + if (entry != null) + { + if (pathOffset == pathEntries.Length - 1) + { + return entry; + } + else if (entry.IsDirectory) + { + return GetDirectoryEntry((TDirectory)ConvertDirEntryToFile(entry), pathEntries, pathOffset + 1); + } + else + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, "{0} is a file, not a directory", pathEntries[pathOffset])); + } + } + else + { + return null; + } + } + } + + private void DoSearch(List results, string path, Regex regex, bool subFolders, bool dirs, bool files) + { + TDirectory parentDir = GetDirectory(path); + if (parentDir == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The directory '{0}' was not found", path)); + } + + string resultPrefixPath = path; + if (IsRoot(path)) + { + resultPrefixPath = @"\"; + } + + foreach (TDirEntry de in parentDir.AllEntries) + { + bool isDir = de.IsDirectory; + + if ((isDir && dirs) || (!isDir && files)) + { + if (regex.IsMatch(de.SearchName)) + { + results.Add(Utilities.CombinePaths(resultPrefixPath, FormatFileName(de.FileName))); + } + } + + if (subFolders && isDir) + { + DoSearch(results, Utilities.CombinePaths(resultPrefixPath, FormatFileName(de.FileName)), regex, subFolders, dirs, files); + } + } + } + + private TDirEntry ResolveSymlink(TDirEntry entry, string path) + { + TDirEntry currentEntry = entry; + string currentPath = path; + + int resolvesLeft = 20; + while (currentEntry.IsSymlink && resolvesLeft > 0) + { + IVfsSymlink symlink = GetFile(currentEntry) as IVfsSymlink; + if (symlink == null) + { + throw new FileNotFoundException("Unable to resolve symlink", path); + } + + currentPath = Utilities.ResolvePath(currentPath.TrimEnd('\\'), symlink.TargetPath); + currentEntry = GetDirectoryEntry(currentPath); + if (currentEntry == null) + { + throw new FileNotFoundException("Unable to resolve symlink", path); + } + + --resolvesLeft; + } + + if (currentEntry.IsSymlink) + { + throw new FileNotFoundException("Unable to resolve symlink - too many links", path); + } + + return currentEntry; + } + } +} diff --git a/DiscUtils/Vfs/VfsFileSystemFacade.cs b/DiscUtils/Vfs/VfsFileSystemFacade.cs new file mode 100644 index 0000000..95b028d --- /dev/null +++ b/DiscUtils/Vfs/VfsFileSystemFacade.cs @@ -0,0 +1,541 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System; + using System.IO; + + /// + /// Base class for the public facade on a file system. + /// + /// + /// The derived class can extend the functionality available from a file system + /// beyond that defined by DiscFileSystem. + /// + public abstract class VfsFileSystemFacade : DiscFileSystem + { + private DiscFileSystem _wrapped; + + /// + /// Initializes a new instance of the VfsFileSystemFacade class. + /// + /// The actual file system instance. + protected VfsFileSystemFacade(DiscFileSystem toWrap) + { + _wrapped = toWrap; + } + + /// + /// Gets the file system options, which can be modified. + /// + public override DiscFileSystemOptions Options + { + get { return _wrapped.Options; } + } + + /// + /// Gets a friendly name for the file system. + /// + public override string FriendlyName + { + get { return _wrapped.FriendlyName; } + } + + /// + /// Indicates whether the file system is read-only or read-write. + /// + /// true if the file system is read-write. + public override bool CanWrite + { + get { return _wrapped.CanWrite; } + } + + /// + /// Gets the root directory of the file system. + /// + public override DiscDirectoryInfo Root + { + get { return new DiscDirectoryInfo(this, string.Empty); } + } + + /// + /// Gets the volume label. + /// + public override string VolumeLabel + { + get { return _wrapped.VolumeLabel; } + } + + /// + /// Gets a value indicating whether the file system is thread-safe. + /// + public override bool IsThreadSafe + { + get { return _wrapped.IsThreadSafe; } + } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + public override void CopyFile(string sourceFile, string destinationFile) + { + _wrapped.CopyFile(sourceFile, destinationFile); + } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + /// Overwrite any existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + _wrapped.CopyFile(sourceFile, destinationFile, overwrite); + } + + /// + /// Creates a directory. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + _wrapped.CreateDirectory(path); + } + + /// + /// Deletes a directory. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + _wrapped.DeleteDirectory(path); + } + + /// + /// Deletes a directory, optionally with all descendants. + /// + /// The path of the directory to delete. + /// Determines if the all descendants should be deleted. + public override void DeleteDirectory(string path, bool recursive) + { + _wrapped.DeleteDirectory(path, recursive); + } + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + _wrapped.DeleteFile(path); + } + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public override bool DirectoryExists(string path) + { + return _wrapped.DirectoryExists(path); + } + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public override bool FileExists(string path) + { + return _wrapped.FileExists(path); + } + + /// + /// Indicates if a file or directory exists. + /// + /// The path to test. + /// true if the file or directory exists. + public override bool Exists(string path) + { + return _wrapped.Exists(path); + } + + /// + /// Gets the names of subdirectories in a specified directory. + /// + /// The path to search. + /// Array of directories. + public override string[] GetDirectories(string path) + { + return _wrapped.GetDirectories(path); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern) + { + return _wrapped.GetDirectories(path, searchPattern); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) + { + return _wrapped.GetDirectories(path, searchPattern, searchOption); + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// Array of files. + public override string[] GetFiles(string path) + { + return _wrapped.GetFiles(path); + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// The search string to match against. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern) + { + return _wrapped.GetFiles(path, searchPattern); + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern, SearchOption searchOption) + { + return _wrapped.GetFiles(path, searchPattern, searchOption); + } + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path) + { + return _wrapped.GetFileSystemEntries(path); + } + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path, string searchPattern) + { + return _wrapped.GetFileSystemEntries(path, searchPattern); + } + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + _wrapped.MoveDirectory(sourceDirectoryName, destinationDirectoryName); + } + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + public override void MoveFile(string sourceName, string destinationName) + { + _wrapped.MoveFile(sourceName, destinationName); + } + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + _wrapped.MoveFile(sourceName, destinationName, overwrite); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode) + { + return _wrapped.OpenFile(path, mode); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) + { + return _wrapped.OpenFile(path, mode, access); + } + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public override FileAttributes GetAttributes(string path) + { + return _wrapped.GetAttributes(path); + } + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + _wrapped.SetAttributes(path, newValue); + } + + /// + /// Gets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTime(string path) + { + return _wrapped.GetCreationTime(path); + } + + /// + /// Sets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTime(string path, DateTime newTime) + { + _wrapped.SetCreationTime(path, newTime); + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTimeUtc(string path) + { + return _wrapped.GetCreationTimeUtc(path); + } + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + _wrapped.SetCreationTimeUtc(path, newTime); + } + + /// + /// Gets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTime(string path) + { + return _wrapped.GetLastAccessTime(path); + } + + /// + /// Sets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTime(string path, DateTime newTime) + { + _wrapped.SetLastAccessTime(path, newTime); + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTimeUtc(string path) + { + return _wrapped.GetLastAccessTimeUtc(path); + } + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + _wrapped.SetLastAccessTimeUtc(path, newTime); + } + + /// + /// Gets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTime(string path) + { + return _wrapped.GetLastWriteTime(path); + } + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTime(string path, DateTime newTime) + { + _wrapped.SetLastWriteTime(path, newTime); + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTimeUtc(string path) + { + return _wrapped.GetLastWriteTimeUtc(path); + } + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + _wrapped.SetLastWriteTimeUtc(path, newTime); + } + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public override long GetFileLength(string path) + { + return _wrapped.GetFileLength(path); + } + + /// + /// Gets an object representing a possible file. + /// + /// The file path. + /// The representing object. + /// The file does not need to exist. + public override DiscFileInfo GetFileInfo(string path) + { + return new DiscFileInfo(this, path); + } + + /// + /// Gets an object representing a possible directory. + /// + /// The directory path. + /// The representing object. + /// The directory does not need to exist. + public override DiscDirectoryInfo GetDirectoryInfo(string path) + { + return new DiscDirectoryInfo(this, path); + } + + /// + /// Gets an object representing a possible file system object (file or directory). + /// + /// The file system path. + /// The representing object. + /// The file system object does not need to exist. + public override DiscFileSystemInfo GetFileSystemInfo(string path) + { + return new DiscFileSystemInfo(this, path); + } + + /// + /// Provides access to the actual file system implementation. + /// + /// The concrete type representing directory entries. + /// The concrete type representing files. + /// The concrete type representing directories. + /// The concrete type holding global state. + /// The actual file system instance. + protected VfsFileSystem GetRealFileSystem() + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + where TDirectory : class, IVfsDirectory, TFile + where TContext : VfsContext + { + return (VfsFileSystem)_wrapped; + } + + /// + /// Provides access to the actual file system implementation. + /// + /// The concrete type of the actual file system. + /// The actual file system instance. + protected T GetRealFileSystem() + where T : DiscFileSystem + { + return (T)_wrapped; + } + } +} diff --git a/DiscUtils/Vfs/VfsFileSystemFactory.cs b/DiscUtils/Vfs/VfsFileSystemFactory.cs new file mode 100644 index 0000000..dfb07d2 --- /dev/null +++ b/DiscUtils/Vfs/VfsFileSystemFactory.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System.IO; + + /// + /// Base class for logic to detect file systems. + /// + public abstract class VfsFileSystemFactory + { + /// + /// Detects if a stream contains any known file systems. + /// + /// The stream to inspect. + /// A list of file systems (may be empty). + public DiscUtils.FileSystemInfo[] Detect(Stream stream) + { + return Detect(stream, null); + } + + /// + /// Detects if a volume contains any known file systems. + /// + /// The volume to inspect. + /// A list of file systems (may be empty). + public DiscUtils.FileSystemInfo[] Detect(VolumeInfo volume) + { + using (Stream stream = volume.Open()) + { + return Detect(stream, volume); + } + } + + /// + /// The logic for detecting file systems. + /// + /// The stream to inspect. + /// Optionally, information about the volume. + /// A list of file systems detected (may be empty). + public abstract DiscUtils.FileSystemInfo[] Detect(Stream stream, VolumeInfo volumeInfo); + } +} diff --git a/DiscUtils/Vfs/VfsFileSystemFactoryAttribute.cs b/DiscUtils/Vfs/VfsFileSystemFactoryAttribute.cs new file mode 100644 index 0000000..3532ebf --- /dev/null +++ b/DiscUtils/Vfs/VfsFileSystemFactoryAttribute.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System; + using System.Collections.Generic; + using System.Text; + + /// + /// Attribute identifying file system factory classes. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class VfsFileSystemFactoryAttribute : Attribute + { + } +} diff --git a/DiscUtils/Vfs/VfsFileSystemInfo.cs b/DiscUtils/Vfs/VfsFileSystemInfo.cs new file mode 100644 index 0000000..cf23d57 --- /dev/null +++ b/DiscUtils/Vfs/VfsFileSystemInfo.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System.IO; + + /// + /// Delegate for instantiating a file system. + /// + /// The stream containing the file system. + /// Optional, information about the volume the file system is on. + /// Parameters for the file system. + /// A file system implementation. + public delegate DiscFileSystem VfsFileSystemOpener(Stream stream, VolumeInfo volumeInfo, FileSystemParameters parameters); + + /// + /// Class holding information about a file system. + /// + public sealed class VfsFileSystemInfo : DiscUtils.FileSystemInfo + { + private string _name; + private string _description; + private VfsFileSystemOpener _openDelegate; + + /// + /// Initializes a new instance of the VfsFileSystemInfo class. + /// + /// The name of the file system. + /// A one-line description of the file system. + /// A delegate that can open streams as the indicated file system. + public VfsFileSystemInfo(string name, string description, VfsFileSystemOpener openDelegate) + { + _name = name; + _description = description; + _openDelegate = openDelegate; + } + + /// + /// Gets the name of the file system. + /// + public override string Name + { + get { return _name; } + } + + /// + /// Gets a one-line description of the file system. + /// + public override string Description + { + get { return _description; } + } + + /// + /// Opens a volume using the file system. + /// + /// The volume to access. + /// Parameters for the file system. + /// A file system instance. + public override DiscFileSystem Open(VolumeInfo volume, FileSystemParameters parameters) + { + return _openDelegate(volume.Open(), volume, parameters); + } + + /// + /// Opens a stream using the file system. + /// + /// The stream to access. + /// Parameters for the file system. + /// A file system instance. + public override DiscFileSystem Open(Stream stream, FileSystemParameters parameters) + { + return _openDelegate(stream, null, parameters); + } + } +} diff --git a/DiscUtils/Vfs/VfsReadOnlyFileSystem.cs b/DiscUtils/Vfs/VfsReadOnlyFileSystem.cs new file mode 100644 index 0000000..69ff03a --- /dev/null +++ b/DiscUtils/Vfs/VfsReadOnlyFileSystem.cs @@ -0,0 +1,169 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils.Vfs +{ + using System; + using System.IO; + + /// + /// Base class for read-only file system implementations. + /// + /// The concrete type representing directory entries. + /// The concrete type representing files. + /// The concrete type representing directories. + /// The concrete type holding global state. + public abstract class VfsReadOnlyFileSystem : VfsFileSystem + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + where TDirectory : class, IVfsDirectory, TFile + where TContext : VfsContext + { + /// + /// Initializes a new instance of the VfsReadOnlyFileSystem class. + /// + /// The default file system options. + protected VfsReadOnlyFileSystem(DiscFileSystemOptions defaultOptions) + : base(defaultOptions) + { + } + + /// + /// Indicates whether the file system is read-only or read-write. + /// + /// Always false. + public override bool CanWrite + { + get { return false; } + } + + /// + /// Copies a file - not supported on read-only file systems. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + throw new NotSupportedException(); + } + + /// + /// Creates a directory - not supported on read-only file systems. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + throw new NotSupportedException(); + } + + /// + /// Deletes a directory - not supported on read-only file systems. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + throw new NotSupportedException(); + } + + /// + /// Deletes a file - not supported on read-only file systems. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + throw new NotSupportedException(); + } + + /// + /// Moves a directory - not supported on read-only file systems. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + throw new NotSupportedException(); + } + + /// + /// Moves a file - not supported on read-only file systems. + /// + /// The file to move. + /// The target file name. + /// Whether to allow an existing file to be overwritten. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + throw new NotSupportedException(); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode) + { + return OpenFile(path, mode, FileAccess.Read); + } + + /// + /// Sets the attributes of a file or directory - not supported on read-only file systems. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + throw new NotSupportedException(); + } + + /// + /// Sets the creation time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + + /// + /// Sets the last access time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + + /// + /// Sets the last modification time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + } +} diff --git a/DiscUtils/VirtualDisk.cs b/DiscUtils/VirtualDisk.cs new file mode 100644 index 0000000..7fc15cf --- /dev/null +++ b/DiscUtils/VirtualDisk.cs @@ -0,0 +1,736 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using DiscUtils.Partitions; + + /// + /// Base class representing virtual hard disks. + /// + public abstract class VirtualDisk : MarshalByRefObject, IDisposable + { + private static Dictionary s_extensionMap; + private static Dictionary s_typeMap; + private static Dictionary s_diskTransports; + + private VirtualDiskTransport _transport; + + /// + /// Finalizes an instance of the VirtualDisk class. + /// + ~VirtualDisk() + { + Dispose(false); + } + + /// + /// Gets the set of disk formats supported as an array of file extensions. + /// + public static ICollection SupportedDiskFormats + { + get { return ExtensionMap.Keys; } + } + + /// + /// Gets the set of disk types supported, as an array of identifiers. + /// + public static ICollection SupportedDiskTypes + { + get { return TypeMap.Keys; } + } + + /// + /// Gets the geometry of the disk. + /// + public abstract Geometry Geometry + { + get; + } + + /// + /// Gets the geometry of the disk as it is anticipated a hypervisor BIOS will represent it. + /// + public virtual Geometry BiosGeometry + { + get { return Geometry.MakeBiosSafe(Geometry, Capacity); } + } + + /// + /// Gets the type of disk represented by this object. + /// + public abstract VirtualDiskClass DiskClass + { + get; + } + + /// + /// Gets the capacity of the disk (in bytes). + /// + public abstract long Capacity + { + get; + } + + /// + /// Gets the size of the disk's logical blocks (aka sector size), in bytes. + /// + public virtual int BlockSize + { + get { return Sizes.Sector; } + } + + /// + /// Gets the logical sector size of the disk, in bytes. + /// + /// This is an alias for the BlockSize property. + public int SectorSize + { + get { return BlockSize; } + } + + /// + /// Gets the content of the disk as a stream. + /// + /// Note the returned stream is not guaranteed to be at any particular position. The actual position + /// will depend on the last partition table/file system activity, since all access to the disk contents pass + /// through a single stream instance. Set the stream position before accessing the stream. + public abstract SparseStream Content + { + get; + } + + /// + /// Gets the layers that make up the disk. + /// + public abstract IEnumerable Layers + { + get; + } + + /// + /// Gets or sets the Windows disk signature of the disk, which uniquely identifies the disk. + /// + public virtual int Signature + { + get + { + return Utilities.ToInt32LittleEndian(GetMasterBootRecord(), 0x01B8); + } + + set + { + byte[] mbr = GetMasterBootRecord(); + Utilities.WriteBytesLittleEndian(value, mbr, 0x01B8); + SetMasterBootRecord(mbr); + } + } + + /// + /// Gets a value indicating whether the disk appears to have a valid partition table. + /// + /// There is no reliable way to determine whether a disk has a valid partition + /// table. The 'guess' consists of checking for basic indicators and looking for obviously + /// invalid data, such as overlapping partitions. + public virtual bool IsPartitioned + { + get { return PartitionTable.IsPartitioned(Content); } + } + + /// + /// Gets the object that interprets the partition structure. + /// + /// It is theoretically possible for a disk to contain two independent partition structures - a + /// BIOS/GPT one and an Apple one, for example. This method will return in order of preference, + /// a GUID partition table, a BIOS partition table, then in undefined preference one of any other partition + /// tables found. See PartitionTable.GetPartitionTables to gain access to all the discovered partition + /// tables on a disk. + public virtual PartitionTable Partitions + { + get + { + IList tables = PartitionTable.GetPartitionTables(this); + if (tables == null || tables.Count == 0) + { + return null; + } + else if (tables.Count == 1) + { + return tables[0]; + } + else + { + PartitionTable best = null; + int bestScore = -1; + for (int i = 0; i < tables.Count; ++i) + { + int newScore = 0; + if (tables[i] is GuidPartitionTable) + { + newScore = 2; + } + else if (tables[i] is BiosPartitionTable) + { + newScore = 1; + } + + if (newScore > bestScore) + { + bestScore = newScore; + best = tables[i]; + } + } + + return best; + } + } + } + + /// + /// Gets the parameters of the disk. + /// + /// Most of the parameters are also available individually, such as DiskType and Capacity. + public virtual VirtualDiskParameters Parameters + { + get + { + return new VirtualDiskParameters() + { + DiskType = DiskClass, + Capacity = Capacity, + Geometry = Geometry, + BiosGeometry = BiosGeometry, + AdapterType = GenericDiskAdapterType.Ide + }; + } + } + + /// + /// Gets information about the type of disk. + /// + /// This property provides access to meta-data about the disk format, for example whether the + /// BIOS geometry is preserved in the disk file. + public abstract VirtualDiskTypeInfo DiskTypeInfo + { + get; + } + + private static Dictionary ExtensionMap + { + get + { + if (s_extensionMap == null) + { + InitializeMaps(); + } + + return s_extensionMap; + } + } + + private static Dictionary TypeMap + { + get + { + if (s_typeMap == null) + { + InitializeMaps(); + } + + return s_typeMap; + } + } + + private static Dictionary DiskTransports + { + get + { + if (s_diskTransports == null) + { + Dictionary transports = new Dictionary(); + + foreach (var type in typeof(VirtualDisk).Assembly.GetTypes()) + { + foreach (VirtualDiskTransportAttribute attr in Attribute.GetCustomAttributes(type, typeof(VirtualDiskTransportAttribute), false)) + { + transports.Add(attr.Scheme.ToUpperInvariant(), type); + } + } + + s_diskTransports = transports; + } + + return s_diskTransports; + } + } + + /// + /// Gets the set of supported variants of a type of virtual disk. + /// + /// A type, as returned by . + /// A collection of identifiers, or empty if there is no variant concept for this type of disk. + public static ICollection GetSupportedDiskVariants(string type) + { + return TypeMap[type].Variants; + } + + /// + /// Gets information about disk type. + /// + /// The disk type, as returned by . + /// The variant of the disk type. + /// Information about the disk type. + public static VirtualDiskTypeInfo GetDiskType(string type, string variant) + { + return TypeMap[type].GetDiskTypeInformation(variant); + } + + /// + /// Create a new virtual disk, possibly within an existing disk. + /// + /// The file system to create the disk on. + /// The type of disk to create (see ). + /// The variant of the type to create (see ). + /// The path (or URI) for the disk to create. + /// The capacity of the new disk. + /// The geometry of the new disk (or null). + /// Untyped parameters controlling the creation process (TBD). + /// The newly created disk. + public static VirtualDisk CreateDisk(DiscFileSystem fileSystem, string type, string variant, string path, long capacity, Geometry geometry, Dictionary parameters) + { + VirtualDiskFactory factory = TypeMap[type]; + + VirtualDiskParameters diskParams = new VirtualDiskParameters() + { + AdapterType = GenericDiskAdapterType.Scsi, + Capacity = capacity, + Geometry = geometry, + }; + + if (parameters != null) + { + foreach (var key in parameters.Keys) + { + diskParams.ExtendedParameters[key] = parameters[key]; + } + } + + return factory.CreateDisk(new DiscFileLocator(fileSystem, Utilities.GetDirectoryFromPath(path)), variant.ToLowerInvariant(), Utilities.GetFileFromPath(path), diskParams); + } + + /// + /// Create a new virtual disk. + /// + /// The type of disk to create (see ). + /// The variant of the type to create (see ). + /// The path (or URI) for the disk to create. + /// The capacity of the new disk. + /// The geometry of the new disk (or null). + /// Untyped parameters controlling the creation process (TBD). + /// The newly created disk. + public static VirtualDisk CreateDisk(string type, string variant, string path, long capacity, Geometry geometry, Dictionary parameters) + { + return CreateDisk(type, variant, path, capacity, geometry, null, null, parameters); + } + + /// + /// Create a new virtual disk. + /// + /// The type of disk to create (see ). + /// The variant of the type to create (see ). + /// The path (or URI) for the disk to create. + /// The capacity of the new disk. + /// The geometry of the new disk (or null). + /// The user identity to use when accessing the path (or null). + /// The password to use when accessing the path (or null). + /// Untyped parameters controlling the creation process (TBD). + /// The newly created disk. + public static VirtualDisk CreateDisk(string type, string variant, string path, long capacity, Geometry geometry, string user, string password, Dictionary parameters) + { + VirtualDiskParameters diskParams = new VirtualDiskParameters() + { + AdapterType = GenericDiskAdapterType.Scsi, + Capacity = capacity, + Geometry = geometry, + }; + + if (parameters != null) + { + foreach (var key in parameters.Keys) + { + diskParams.ExtendedParameters[key] = parameters[key]; + } + } + + return CreateDisk(type, variant, path, diskParams, user, password); + } + + /// + /// Create a new virtual disk. + /// + /// The type of disk to create (see ). + /// The variant of the type to create (see ). + /// The path (or URI) for the disk to create. + /// Parameters controlling the capacity, geometry, etc of the new disk. + /// The user identity to use when accessing the path (or null). + /// The password to use when accessing the path (or null). + /// The newly created disk. + public static VirtualDisk CreateDisk(string type, string variant, string path, VirtualDiskParameters diskParameters, string user, string password) + { + Uri uri = PathToUri(path); + VirtualDisk result = null; + + Type transportType; + if (!DiskTransports.TryGetValue(uri.Scheme.ToUpperInvariant(), out transportType)) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "Unable to parse path '{0}'", path), path); + } + + VirtualDiskTransport transport = (VirtualDiskTransport)Activator.CreateInstance(transportType); + + try + { + transport.Connect(uri, user, password); + + if (transport.IsRawDisk) + { + result = transport.OpenDisk(FileAccess.ReadWrite); + } + else + { + VirtualDiskFactory factory = TypeMap[type]; + + result = factory.CreateDisk(transport.GetFileLocator(), variant.ToLowerInvariant(), Utilities.GetFileFromPath(path), diskParameters); + } + + if (result != null) + { + result._transport = transport; + transport = null; + } + + return result; + } + finally + { + if (transport != null) + { + transport.Dispose(); + } + } + } + + /// + /// Opens an existing virtual disk. + /// + /// The path of the virtual disk to open, can be a URI. + /// The desired access to the disk. + /// The Virtual Disk, or null if an unknown disk format. + public static VirtualDisk OpenDisk(string path, FileAccess access) + { + return OpenDisk(path, null, access, null, null); + } + + /// + /// Opens an existing virtual disk. + /// + /// The path of the virtual disk to open, can be a URI. + /// The desired access to the disk. + /// The user name to use for authentication (if necessary). + /// The password to use for authentication (if necessary). + /// The Virtual Disk, or null if an unknown disk format. + public static VirtualDisk OpenDisk(string path, FileAccess access, string user, string password) + { + return OpenDisk(path, null, access, user, password); + } + + /// + /// Opens an existing virtual disk. + /// + /// The path of the virtual disk to open, can be a URI. + /// Force the detected disk type (null to detect). + /// The desired access to the disk. + /// The user name to use for authentication (if necessary). + /// The password to use for authentication (if necessary). + /// The Virtual Disk, or null if an unknown disk format. + /// + /// The detected disk type can be forced by specifying a known disk type: + /// RAW, VHD, VMDK, etc. + /// + public static VirtualDisk OpenDisk(string path, string forceType, FileAccess access, string user, string password) + { + Uri uri = PathToUri(path); + VirtualDisk result = null; + + Type transportType; + if (!DiskTransports.TryGetValue(uri.Scheme.ToUpperInvariant(), out transportType)) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "Unable to parse path '{0}'", path), path); + } + + VirtualDiskTransport transport = (VirtualDiskTransport)Activator.CreateInstance(transportType); + + try + { + transport.Connect(uri, user, password); + + if (transport.IsRawDisk) + { + result = transport.OpenDisk(access); + } + else + { + bool foundFactory; + VirtualDiskFactory factory; + + if (!string.IsNullOrEmpty(forceType)) + { + foundFactory = TypeMap.TryGetValue(forceType, out factory); + } + else + { + string extension = Path.GetExtension(uri.AbsolutePath).ToUpperInvariant(); + if (extension.StartsWith(".", StringComparison.Ordinal)) + { + extension = extension.Substring(1); + } + + foundFactory = ExtensionMap.TryGetValue(extension, out factory); + } + + if (foundFactory) + { + result = factory.OpenDisk(transport.GetFileLocator(), transport.GetFileName(), access); + } + } + + if (result != null) + { + result._transport = transport; + transport = null; + } + + return result; + } + finally + { + if (transport != null) + { + transport.Dispose(); + } + } + } + + /// + /// Opens an existing virtual disk, possibly from within an existing disk. + /// + /// The file system to open the disk on. + /// The path of the virtual disk to open. + /// The desired access to the disk. + /// The Virtual Disk, or null if an unknown disk format. + public static VirtualDisk OpenDisk(DiscFileSystem fs, string path, FileAccess access) + { + if (fs == null) + { + return OpenDisk(path, access); + } + + string extension = Path.GetExtension(path).ToUpperInvariant(); + if (extension.StartsWith(".", StringComparison.Ordinal)) + { + extension = extension.Substring(1); + } + + VirtualDiskFactory factory; + if (ExtensionMap.TryGetValue(extension, out factory)) + { + return factory.OpenDisk(fs, path, access); + } + + return null; + } + + /// + /// Disposes of this instance, freeing underlying resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Reads the first sector of the disk, known as the Master Boot Record. + /// + /// The MBR as a byte array. + public virtual byte[] GetMasterBootRecord() + { + byte[] sector = new byte[Sizes.Sector]; + + long oldPos = Content.Position; + Content.Position = 0; + Utilities.ReadFully(Content, sector, 0, Sizes.Sector); + Content.Position = oldPos; + + return sector; + } + + /// + /// Overwrites the first sector of the disk, known as the Master Boot Record. + /// + /// The master boot record, must be 512 bytes in length. + public virtual void SetMasterBootRecord(byte[] data) + { + if (data == null) + { + throw new ArgumentNullException("data"); + } + else if (data.Length != Sizes.Sector) + { + throw new ArgumentException("The Master Boot Record must be exactly 512 bytes in length", "data"); + } + + long oldPos = Content.Position; + Content.Position = 0; + Content.Write(data, 0, Sizes.Sector); + Content.Position = oldPos; + } + + /// + /// Create a new differencing disk, possibly within an existing disk. + /// + /// The file system to create the disk on. + /// The path (or URI) for the disk to create. + /// The newly created disk. + public abstract VirtualDisk CreateDifferencingDisk(DiscFileSystem fileSystem, string path); + + /// + /// Create a new differencing disk. + /// + /// The path (or URI) for the disk to create. + /// The newly created disk. + public abstract VirtualDisk CreateDifferencingDisk(string path); + + internal static VirtualDiskLayer OpenDiskLayer(FileLocator locator, string path, FileAccess access) + { + string extension = Path.GetExtension(path).ToUpperInvariant(); + if (extension.StartsWith(".", StringComparison.Ordinal)) + { + extension = extension.Substring(1); + } + + VirtualDiskFactory factory; + if (ExtensionMap.TryGetValue(extension, out factory)) + { + return factory.OpenDiskLayer(locator, path, access); + } + + return null; + } + + /// + /// Disposes of underlying resources. + /// + /// true if running inside Dispose(), indicating + /// graceful cleanup of all managed objects should be performed, or false + /// if running inside destructor. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_transport != null) + { + _transport.Dispose(); + } + + _transport = null; + } + } + + private static Uri PathToUri(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("Path must not be null or empty", "path"); + } + + if (path.Contains("://")) + { + return new Uri(path); + } + + if (!Path.IsPathRooted(path)) + { + path = Path.GetFullPath(path); + } + + // Built-in Uri class does cope well with query params on file Uris, so do some + // parsing ourselves... + if (path.Length >= 1 && path[0] == '\\') + { + UriBuilder builder = new UriBuilder("file:" + path.Replace('\\', '/')); + return builder.Uri; + } + else if (path.StartsWith("//", StringComparison.OrdinalIgnoreCase)) + { + UriBuilder builder = new UriBuilder("file:" + path); + return builder.Uri; + } + else if (path.Length >= 2 && path[1] == ':') + { + UriBuilder builder = new UriBuilder("file:///" + path.Replace('\\', '/')); + return builder.Uri; + } + else + { + return new Uri(path); + } + } + + private static void InitializeMaps() + { + Dictionary typeMap = new Dictionary(); + Dictionary extensionMap = new Dictionary(); + + foreach (var type in typeof(VirtualDisk).Assembly.GetTypes()) + { + VirtualDiskFactoryAttribute attr = (VirtualDiskFactoryAttribute)Attribute.GetCustomAttribute(type, typeof(VirtualDiskFactoryAttribute), false); + if (attr != null) + { + VirtualDiskFactory factory = (VirtualDiskFactory)Activator.CreateInstance(type); + typeMap.Add(attr.Type, factory); + foreach (var extension in attr.FileExtensions) + { + extensionMap.Add(extension.ToUpperInvariant(), factory); + } + } + } + + s_typeMap = typeMap; + s_extensionMap = extensionMap; + } + } +} diff --git a/DiscUtils/VirtualDiskClass.cs b/DiscUtils/VirtualDiskClass.cs new file mode 100644 index 0000000..81c6188 --- /dev/null +++ b/DiscUtils/VirtualDiskClass.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Enumeration of different classes of disk. + /// + public enum VirtualDiskClass + { + /// + /// Unknown (or unspecified) type. + /// + None = 0, + + /// + /// Hard disk. + /// + HardDisk = 1, + + /// + /// Optical disk, such as CD or DVD. + /// + OpticalDisk = 2, + + /// + /// Floppy disk. + /// + FloppyDisk = 3, + } +} diff --git a/DiscUtils/VirtualDiskExtent.cs b/DiscUtils/VirtualDiskExtent.cs new file mode 100644 index 0000000..816a2a1 --- /dev/null +++ b/DiscUtils/VirtualDiskExtent.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + /// + /// Base class represented a stored extent of a virtual disk. + /// + /// + /// Some file formats can divide a logical disk layer into multiple extents, stored in + /// different files. This class represents those extents. Normally, all virtual disks + /// have at least one extent. + /// + public abstract class VirtualDiskExtent : IDisposable + { + /// + /// Gets a value indicating whether the extent only stores meaningful sectors. + /// + public abstract bool IsSparse + { + get; + } + + /// + /// Gets the capacity of the extent (in bytes). + /// + public abstract long Capacity + { + get; + } + + /// + /// Gets the size of the extent (in bytes) on underlying storage. + /// + public abstract long StoredSize + { + get; + } + + /// + /// Gets the content of this extent. + /// + /// The parent stream (if any). + /// Controls ownership of the parent stream. + /// The content as a stream. + public abstract MappedStream OpenContent(SparseStream parent, Ownership ownsParent); + + /// + /// Disposes of this instance, freeing underlying resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of underlying resources. + /// + /// true if running inside Dispose(), indicating + /// graceful cleanup of all managed objects should be performed, or false + /// if running inside destructor. + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/DiscUtils/VirtualDiskFactory.cs b/DiscUtils/VirtualDiskFactory.cs new file mode 100644 index 0000000..5db867e --- /dev/null +++ b/DiscUtils/VirtualDiskFactory.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Collections.Generic; + using System.IO; + + internal abstract class VirtualDiskFactory + { + public abstract string[] Variants { get; } + + public abstract VirtualDiskTypeInfo GetDiskTypeInformation(string variant); + + public abstract DiskImageBuilder GetImageBuilder(string variant); + + public abstract VirtualDisk CreateDisk(FileLocator locator, string variant, string path, VirtualDiskParameters diskParameters); + + public abstract VirtualDisk OpenDisk(string path, FileAccess access); + + public abstract VirtualDisk OpenDisk(FileLocator locator, string path, FileAccess access); + + public virtual VirtualDisk OpenDisk(FileLocator locator, string path, string extraInfo, Dictionary parameters, FileAccess access) + { + return OpenDisk(locator, path, access); + } + + public VirtualDisk OpenDisk(DiscFileSystem fileSystem, string path, FileAccess access) + { + return OpenDisk(new DiscFileLocator(fileSystem, @"\"), path, access); + } + + public abstract VirtualDiskLayer OpenDiskLayer(FileLocator locator, string path, FileAccess access); + } +} diff --git a/DiscUtils/VirtualDiskFactoryAttribute.cs b/DiscUtils/VirtualDiskFactoryAttribute.cs new file mode 100644 index 0000000..c404685 --- /dev/null +++ b/DiscUtils/VirtualDiskFactoryAttribute.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + internal sealed class VirtualDiskFactoryAttribute : Attribute + { + private string _type; + private string[] _fileExtensions; + + public VirtualDiskFactoryAttribute(string type, string fileExtensions) + { + _type = type; + _fileExtensions = fileExtensions.Replace(".", string.Empty).Split(','); + } + + public string Type + { + get { return _type; } + } + + public string[] FileExtensions + { + get { return _fileExtensions; } + } + } +} diff --git a/DiscUtils/VirtualDiskLayer.cs b/DiscUtils/VirtualDiskLayer.cs new file mode 100644 index 0000000..e627279 --- /dev/null +++ b/DiscUtils/VirtualDiskLayer.cs @@ -0,0 +1,142 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + + /// + /// Represents the base layer, or a differencing layer of a VirtualDisk. + /// + /// + /// VirtualDisks are composed of one or more layers - a base layer + /// which represents the entire disk (even if not all bytes are actually stored), + /// and a number of differencing layers that store the disk sectors that are + /// logically different to the base layer. + /// Disk Layers may not store all sectors. Any sectors that are not stored + /// are logically zero's (for base layers), or holes through to the layer underneath + /// (all other layers). + /// + public abstract class VirtualDiskLayer : IDisposable + { + /// + /// Finalizes an instance of the VirtualDiskLayer class. + /// + ~VirtualDiskLayer() + { + Dispose(false); + } + + /// + /// Gets the geometry of the virtual disk layer. + /// + public abstract Geometry Geometry + { + get; + } + + /// + /// Gets and sets the logical extents that make up this layer. + /// + public virtual IList Extents + { + get { return new List(); } + } + + /// + /// Gets a value indicating whether the layer only stores meaningful sectors. + /// + public abstract bool IsSparse + { + get; + } + + /// + /// Gets a value indicating whether this is a differential disk. + /// + public abstract bool NeedsParent + { + get; + } + + /// + /// Gets the full path to this disk layer, or empty string. + /// + public virtual string FullPath + { + get { return string.Empty; } + } + + /// + /// Gets the capacity of the disk (in bytes). + /// + internal abstract long Capacity + { + get; + } + + /// + /// Gets a FileLocator that can resolve relative paths, or null. + /// + /// + /// Typically used to locate parent disks. + /// + internal abstract FileLocator RelativeFileLocator + { + get; + } + + /// + /// Gets the content of this layer. + /// + /// The parent stream (if any). + /// Controls ownership of the parent stream. + /// The content as a stream. + public abstract SparseStream OpenContent(SparseStream parent, Ownership ownsParent); + + /// + /// Gets the possible locations of the parent file (if any). + /// + /// Array of strings, empty if no parent. + public abstract string[] GetParentLocations(); + + /// + /// Disposes of this instance, freeing underlying resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of underlying resources. + /// + /// true if running inside Dispose(), indicating + /// graceful cleanup of all managed objects should be performed, or false + /// if running inside destructor. + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/DiscUtils/VirtualDiskParameters.cs b/DiscUtils/VirtualDiskParameters.cs new file mode 100644 index 0000000..8385f29 --- /dev/null +++ b/DiscUtils/VirtualDiskParameters.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Collections.Generic; + + /// + /// Common parameters for virtual disks. + /// + /// Not all attributes make sense for all kinds of disks, so some + /// may be null. Modifying instances of this class does not modify the + /// disk itself. + public sealed class VirtualDiskParameters + { + private Dictionary _extendedParameters = new Dictionary(); + + /// + /// Gets or sets the type of disk (optical, hard disk, etc). + /// + public VirtualDiskClass DiskType { get; set; } + + /// + /// Gets or sets the disk capacity. + /// + public long Capacity { get; set; } + + /// + /// Gets or sets the physical (aka IDE) geometry of the disk. + /// + public Geometry Geometry { get; set; } + + /// + /// Gets or sets the logical (aka BIOS) geometry of the disk. + /// + public Geometry BiosGeometry { get; set; } + + /// + /// Gets or sets the type of disk adapter. + /// + public GenericDiskAdapterType AdapterType { get; set; } + + /// + /// Gets a dictionary of extended parameters, that varies by disk type. + /// + public Dictionary ExtendedParameters + { + get { return _extendedParameters; } + } + } +} diff --git a/DiscUtils/VirtualDiskTransport.cs b/DiscUtils/VirtualDiskTransport.cs new file mode 100644 index 0000000..90e33e9 --- /dev/null +++ b/DiscUtils/VirtualDiskTransport.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + internal abstract class VirtualDiskTransport : IDisposable + { + public abstract bool IsRawDisk { get; } + + public abstract void Connect(Uri uri, string username, string password); + + public abstract VirtualDisk OpenDisk(FileAccess access); + + public abstract FileLocator GetFileLocator(); + + public abstract string GetFileName(); + + public abstract string GetExtraInfo(); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/DiscUtils/VirtualDiskTransportAttribute.cs b/DiscUtils/VirtualDiskTransportAttribute.cs new file mode 100644 index 0000000..e86e2f5 --- /dev/null +++ b/DiscUtils/VirtualDiskTransportAttribute.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + internal sealed class VirtualDiskTransportAttribute : Attribute + { + private string _scheme; + + public VirtualDiskTransportAttribute(string scheme) + { + _scheme = scheme; + } + + public string Scheme + { + get { return _scheme; } + } + } +} diff --git a/DiscUtils/VirtualDiskTypeInfo.cs b/DiscUtils/VirtualDiskTypeInfo.cs new file mode 100644 index 0000000..62d6248 --- /dev/null +++ b/DiscUtils/VirtualDiskTypeInfo.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + /// + /// Delegate for calculating a disk geometry from a capacity. + /// + /// The disk capacity to convert. + /// The appropriate geometry for the disk. + public delegate Geometry GeometryCalculation(long capacity); + + /// + /// Information about a type of virtual disk. + /// + public sealed class VirtualDiskTypeInfo + { + /// + /// Gets or sets the name of the virtual disk type. + /// + public string Name { get; set; } + + /// + /// Gets or sets the variant of the virtual disk type. + /// + public string Variant { get; set; } + + /// + /// Gets or sets a value indicating whether this disk type can represent hard disks. + /// + public bool CanBeHardDisk { get; set; } + + /// + /// Gets or sets a value indicating whether this disk type requires a specific geometry for any given disk capacity. + /// + public bool DeterministicGeometry { get; set; } + + /// + /// Gets or sets a value indicating whether this disk type persists the BIOS geometry. + /// + public bool PreservesBiosGeometry { get; set; } + + /// + /// Gets or sets the algorithm for determining the geometry for a given disk capacity. + /// + public GeometryCalculation CalcGeometry { get; set; } + } +} diff --git a/DiscUtils/VolumeInfo.cs b/DiscUtils/VolumeInfo.cs new file mode 100644 index 0000000..68cb714 --- /dev/null +++ b/DiscUtils/VolumeInfo.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + + /// + /// Base class that holds information about a disk volume. + /// + public abstract class VolumeInfo : MarshalByRefObject + { + internal VolumeInfo() + { + } + + /// + /// Gets the one-byte BIOS type for this volume, which indicates the content. + /// + public abstract byte BiosType { get; } + + /// + /// Gets the size of the volume, in bytes. + /// + public abstract long Length { get; } + + /// + /// Gets the stable volume identity. + /// + /// The stability of the identity depends the disk structure. + /// In some cases the identity may include a simple index, when no other information + /// is available. Best practice is to add disks to the Volume Manager in a stable + /// order, if the stability of this identity is paramount. + public abstract string Identity { get; } + + /// + /// Gets the disk geometry of the underlying storage medium, if any (may be null). + /// + public abstract Geometry PhysicalGeometry { get; } + + /// + /// Gets the disk geometry of the underlying storage medium (as used in BIOS calls), may be null. + /// + public abstract Geometry BiosGeometry { get; } + + /// + /// Gets the offset of this volume in the underlying storage medium, if any (may be Zero). + /// + public abstract long PhysicalStartSector { get; } + + /// + /// Opens the volume, providing access to it's contents. + /// + /// Stream that can access the volume's contents. + public abstract SparseStream Open(); + } +} diff --git a/DiscUtils/VolumeManager.cs b/DiscUtils/VolumeManager.cs new file mode 100644 index 0000000..e4a4397 --- /dev/null +++ b/DiscUtils/VolumeManager.cs @@ -0,0 +1,321 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using DiscUtils.Partitions; + + /// + /// VolumeManager interprets partitions and other on-disk structures (possibly combining multiple disks). + /// + /// + /// Although file systems commonly are placed directly within partitions on a disk, in some + /// cases a logical volume manager / logical disk manager may be used, to combine disk regions in multiple + /// ways for data redundancy or other purposes. + /// + public sealed class VolumeManager : MarshalByRefObject + { + private static List s_logicalVolumeFactories; + private List _disks; + private bool _needScan; + + private Dictionary _physicalVolumes; + private Dictionary _logicalVolumes; + + /// + /// Initializes a new instance of the VolumeManager class. + /// + public VolumeManager() + { + _disks = new List(); + _physicalVolumes = new Dictionary(); + _logicalVolumes = new Dictionary(); + } + + /// + /// Initializes a new instance of the VolumeManager class. + /// + /// The initial disk to add. + public VolumeManager(VirtualDisk initialDisk) + : this() + { + AddDisk(initialDisk); + } + + /// + /// Initializes a new instance of the VolumeManager class. + /// + /// Content of the initial disk to add. + public VolumeManager(Stream initialDiskContent) + : this() + { + AddDisk(initialDiskContent); + } + + private static List LogicalVolumeFactories + { + get + { + if (s_logicalVolumeFactories == null) + { + List factories = new List(); + + foreach (var type in typeof(VolumeManager).Assembly.GetTypes()) + { + foreach (LogicalVolumeFactoryAttribute attr in Attribute.GetCustomAttributes(type, typeof(LogicalVolumeFactoryAttribute), false)) + { + factories.Add((LogicalVolumeFactory)Activator.CreateInstance(type)); + } + } + + s_logicalVolumeFactories = factories; + } + + return s_logicalVolumeFactories; + } + } + + /// + /// Gets the physical volumes held on a disk. + /// + /// The contents of the disk to inspect. + /// An array of volumes. + /// + /// By preference, use the form of this method that takes a disk parameter. + /// If the disk isn't partitioned, this method returns the entire disk contents + /// as a single volume. + /// + public static PhysicalVolumeInfo[] GetPhysicalVolumes(Stream diskContent) + { + return GetPhysicalVolumes(new Raw.Disk(diskContent, Ownership.None)); + } + + /// + /// Gets the physical volumes held on a disk. + /// + /// The disk to inspect. + /// An array of volumes. + /// If the disk isn't partitioned, this method returns the entire disk contents + /// as a single volume. + public static PhysicalVolumeInfo[] GetPhysicalVolumes(VirtualDisk disk) + { + return new VolumeManager(disk).GetPhysicalVolumes(); + } + + /// + /// Adds a disk to the volume manager. + /// + /// The disk to add. + /// The GUID the volume manager will use to identify the disk. + public string AddDisk(VirtualDisk disk) + { + _needScan = true; + int ordinal = _disks.Count; + _disks.Add(disk); + return GetDiskId(ordinal); + } + + /// + /// Adds a disk to the volume manager. + /// + /// The contents of the disk to add. + /// The GUID the volume manager will use to identify the disk. + public string AddDisk(Stream content) + { + return AddDisk(new Raw.Disk(content, Ownership.None)); + } + + /// + /// Gets the physical volumes from all disks added to this volume manager. + /// + /// An array of physical volumes. + public PhysicalVolumeInfo[] GetPhysicalVolumes() + { + if (_needScan) + { + Scan(); + } + + return new List(_physicalVolumes.Values).ToArray(); + } + + /// + /// Gets the logical volumes from all disks added to this volume manager. + /// + /// An array of logical volumes. + public LogicalVolumeInfo[] GetLogicalVolumes() + { + if (_needScan) + { + Scan(); + } + + return new List(_logicalVolumes.Values).ToArray(); + } + + /// + /// Gets a particular volume, based on it's identity. + /// + /// The volume's identity. + /// The volume information for the volume, or returns null. + public VolumeInfo GetVolume(string identity) + { + if (_needScan) + { + Scan(); + } + + PhysicalVolumeInfo pvi; + if (_physicalVolumes.TryGetValue(identity, out pvi)) + { + return pvi; + } + + LogicalVolumeInfo lvi; + if (_logicalVolumes.TryGetValue(identity, out lvi)) + { + return lvi; + } + + return null; + } + + private static void MapPhysicalVolumes(IEnumerable physicalVols, Dictionary result) + { + foreach (var physicalVol in physicalVols) + { + LogicalVolumeInfo lvi = new LogicalVolumeInfo( + physicalVol.PartitionIdentity, + physicalVol, + physicalVol.Open, + physicalVol.Length, + physicalVol.BiosType, + LogicalVolumeStatus.Healthy); + + result.Add(lvi.Identity, lvi); + } + } + + /// + /// Scans all of the disks for their physical and logical volumes. + /// + private void Scan() + { + Dictionary newPhysicalVolumes = ScanForPhysicalVolumes(); + Dictionary newLogicalVolumes = ScanForLogicalVolumes(newPhysicalVolumes.Values); + + _physicalVolumes = newPhysicalVolumes; + _logicalVolumes = newLogicalVolumes; + + _needScan = false; + } + + private Dictionary ScanForLogicalVolumes(IEnumerable physicalVols) + { + List unhandledPhysical = new List(); + Dictionary result = new Dictionary(); + + foreach (PhysicalVolumeInfo pvi in physicalVols) + { + bool handled = false; + foreach (var volFactory in LogicalVolumeFactories) + { + if (volFactory.HandlesPhysicalVolume(pvi)) + { + handled = true; + break; + } + } + + if (!handled) + { + unhandledPhysical.Add(pvi); + } + } + + MapPhysicalVolumes(unhandledPhysical, result); + + foreach (var volFactory in LogicalVolumeFactories) + { + volFactory.MapDisks(_disks, result); + } + + return result; + } + + private Dictionary ScanForPhysicalVolumes() + { + Dictionary result = new Dictionary(); + + // First scan physical volumes + for (int i = 0; i < _disks.Count; ++i) + { + VirtualDisk disk = _disks[i]; + string diskId = GetDiskId(i); + + if (PartitionTable.IsPartitioned(disk.Content)) + { + foreach (var table in PartitionTable.GetPartitionTables(disk)) + { + foreach (var part in table.Partitions) + { + PhysicalVolumeInfo pvi = new PhysicalVolumeInfo(diskId, disk, part); + result.Add(pvi.Identity, pvi); + } + } + } + else + { + PhysicalVolumeInfo pvi = new PhysicalVolumeInfo(diskId, disk); + result.Add(pvi.Identity, pvi); + } + } + + return result; + } + + private string GetDiskId(int ordinal) + { + VirtualDisk disk = _disks[ordinal]; + if (disk.IsPartitioned) + { + Guid guid = disk.Partitions.DiskGuid; + if (guid != Guid.Empty) + { + return "DG" + guid.ToString("B"); + } + } + + int sig = disk.Signature; + if (sig != 0) + { + return "DS" + sig.ToString("X8", CultureInfo.InvariantCulture); + } + + return "DO" + ordinal; + } + } +} diff --git a/DiscUtils/WindowsFileInformation.cs b/DiscUtils/WindowsFileInformation.cs new file mode 100644 index 0000000..5219d70 --- /dev/null +++ b/DiscUtils/WindowsFileInformation.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.IO; + + /// + /// Common information for Windows files. + /// + public class WindowsFileInformation + { + /// + /// Gets or sets the creation time of the file. + /// + public DateTime CreationTime { get; set; } + + /// + /// Gets or sets the last access time of the file. + /// + public DateTime LastAccessTime { get; set; } + + /// + /// Gets or sets the modification time of the file. + /// + public DateTime LastWriteTime { get; set; } + + /// + /// Gets or sets the last time the file was changed. + /// + public DateTime ChangeTime { get; set; } + + /// + /// Gets or sets the file attributes. + /// + public FileAttributes FileAttributes { get; set; } + } +} diff --git a/DiscUtils/WrappingMappedStream.cs b/DiscUtils/WrappingMappedStream.cs new file mode 100644 index 0000000..bb5b9bd --- /dev/null +++ b/DiscUtils/WrappingMappedStream.cs @@ -0,0 +1,178 @@ +// +// Copyright (c) 2008-2012, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System.Collections.Generic; + using System.IO; + + /// + /// Base class for streams that wrap another stream. + /// + /// The type of stream to wrap. + /// + /// Provides the default implementation of methods & properties, so + /// wrapping streams need only override the methods they need to intercept. + /// + internal class WrappingMappedStream : MappedStream + where T : Stream + { + private T _wrapped; + private Ownership _ownership; + private List _extents; + + public WrappingMappedStream(T toWrap, Ownership ownership, IEnumerable extents) + { + _wrapped = toWrap; + _ownership = ownership; + if (extents != null) + { + _extents = new List(extents); + } + } + + public override IEnumerable Extents + { + get + { + if (_extents != null) + { + return _extents; + } + else + { + SparseStream sparse = _wrapped as SparseStream; + if (sparse != null) + { + return sparse.Extents; + } + else + { + return new StreamExtent[] { new StreamExtent(0, _wrapped.Length) }; + } + } + } + } + + public override bool CanRead + { + get { return _wrapped.CanRead; } + } + + public override bool CanSeek + { + get { return _wrapped.CanSeek; } + } + + public override bool CanWrite + { + get { return _wrapped.CanWrite; } + } + + public override long Length + { + get { return _wrapped.Length; } + } + + public override long Position + { + get { return _wrapped.Position; } + set { _wrapped.Position = value; } + } + + protected T WrappedStream + { + get { return _wrapped; } + } + + public override IEnumerable MapContent(long start, long length) + { + MappedStream mapped = _wrapped as MappedStream; + if (mapped != null) + { + return mapped.MapContent(start, length); + } + else + { + return new StreamExtent[] { new StreamExtent(start, length) }; + } + } + + public override void Flush() + { + _wrapped.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _wrapped.SetLength(value); + } + + public override void Clear(int count) + { + SparseStream sparse = _wrapped as SparseStream; + if (sparse != null) + { + sparse.Clear(count); + } + else + { + base.Clear(count); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + _wrapped.Write(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_wrapped != null && _ownership == Ownership.Dispose) + { + _wrapped.Dispose(); + } + + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} diff --git a/DiscUtils/ZeroStream.cs b/DiscUtils/ZeroStream.cs new file mode 100644 index 0000000..cd6250f --- /dev/null +++ b/DiscUtils/ZeroStream.cs @@ -0,0 +1,155 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +namespace DiscUtils +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// A stream that returns Zero's. + /// + internal class ZeroStream : MappedStream + { + private long _length; + private long _position; + private bool _atEof; + + public ZeroStream(long length) + { + _length = length; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get + { + return _position; + } + + set + { + _position = value; + _atEof = false; + } + } + + public override IEnumerable Extents + { + // The stream is entirely sparse + get { return new List(0); } + } + + public override IEnumerable MapContent(long start, long length) + { + return new StreamExtent[0]; + } + + public override void Flush() + { + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_position > _length) + { + _atEof = true; + throw new IOException("Attempt to read beyond end of stream"); + } + + if (_position == _length) + { + if (_atEof) + { + throw new IOException("Attempt to read beyond end of stream"); + } + else + { + _atEof = true; + return 0; + } + } + + int numToClear = (int)Math.Min(count, _length - _position); + Array.Clear(buffer, offset, numToClear); + _position += numToClear; + + return numToClear; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += _length; + } + + _atEof = false; + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of stream"); + } + else + { + _position = effectiveOffset; + return _position; + } + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/FilePickerControl.xaml b/FilePickerControl.xaml new file mode 100644 index 0000000..1fa7f66 --- /dev/null +++ b/FilePickerControl.xaml @@ -0,0 +1,41 @@ + + + + + + + + Change + Clear + + diff --git a/FilePickerControl.xaml.cs b/FilePickerControl.xaml.cs new file mode 100644 index 0000000..0e4a289 --- /dev/null +++ b/FilePickerControl.xaml.cs @@ -0,0 +1,448 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Threading; +using System.Windows; +using System.Windows.Media; + +namespace WPinternals +{ + public class PathChangedEventArgs: EventArgs + { + public PathChangedEventArgs(string NewPath) + : base() + { + this.NewPath = NewPath; + } + + public string NewPath; + } + + public delegate void PathChangedEventHandler( + Object sender, + PathChangedEventArgs e + ); + + /// + /// Interaction logic for FilePickerControl.xaml + /// + public partial class FilePickerBase : System.Windows.Controls.UserControl, INotifyPropertyChanged + { + private SynchronizationContext UIContext; + + public event PropertyChangedEventHandler PropertyChanged = delegate { }; + public event PathChangedEventHandler PathChanged = delegate { }; + + protected void OnPropertyChanged(string propertyName) + { + if (this.PropertyChanged != null) + { + if (SynchronizationContext.Current == UIContext) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + else + { + UIContext.Post((s) => PropertyChanged(this, new PropertyChangedEventArgs(propertyName)), null); + } + } + } + + public FilePickerBase() + { + UIContext = SynchronizationContext.Current; + InitializeComponent(); + } + + public bool AllowNull + { + get + { + return (bool)GetValue(AllowNullProperty); + } + set + { + SetValue(AllowNullProperty, value); + Resize(); + } + } + + public static readonly DependencyProperty AllowNullProperty = + DependencyProperty.Register("AllowNull", typeof(bool), typeof(FilePickerBase), new UIPropertyMetadata(false)); + + public string Caption + { + get + { + return (string)GetValue(CaptionProperty); + } + set + { + SetValue(CaptionProperty, value); + Resize(); + } + } + + public static readonly DependencyProperty CaptionProperty = + DependencyProperty.Register("Caption", typeof(string), typeof(FilePickerBase), new UIPropertyMetadata(null)); + + public string Path + { + get + { + return (string)GetValue(PathProperty); + } + set + { + if ((string)GetValue(PathProperty) != value) + { + SetValue(PathProperty, value); + Resize(); + PathChanged(this, new PathChangedEventArgs(value)); + } + } + } + + public static readonly DependencyProperty PathProperty = + DependencyProperty.Register("Path", typeof(string), typeof(FilePickerBase), new UIPropertyMetadata(null)); + + public string SelectionText + { + get + { + return (string)GetValue(SelectionTextProperty); + } + set + { + SetValue(SelectionTextProperty, value); + Resize(); + } + } + + public static readonly DependencyProperty SelectionTextProperty = + DependencyProperty.Register("SelectionText", typeof(string), typeof(FilePickerBase), new UIPropertyMetadata("")); + + private void SizeChangedHandler(object sender, SizeChangedEventArgs e) + { + Resize(); + } + + protected override Size MeasureOverride(Size availableSize) + { + var resultSize = new Size(availableSize.Width, 0); + + FormattedText formatted = new FormattedText( + "TEST", + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(PathTextBlock.FontFamily, PathTextBlock.FontStyle, PathTextBlock.FontWeight, PathTextBlock.FontStretch), + FontSize, + Foreground + ); + + resultSize.Height = formatted.Height; + + return resultSize; + } + + private void Resize() + { + if (!IsLoaded) + return; + + CaptionTextBlock.Text = Caption; + FormattedText formatted = new FormattedText( + CaptionTextBlock.Text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(CaptionTextBlock.FontFamily, CaptionTextBlock.FontStyle, CaptionTextBlock.FontWeight, CaptionTextBlock.FontStretch), + FontSize, + Foreground + ); + double CaptionWidth = formatted.Width; + if (CaptionWidth > 0) + CaptionWidth += 10; + + bool SelectVisible = (Path == null); + bool ChangeVisible = (Path != null); + bool ClearVisible = ((Path != null) && (AllowNull)); + + double NewWidth = ActualWidth - CaptionWidth; + if (SelectVisible) + NewWidth -= SelectLink.ActualWidth; + if (ChangeVisible) + NewWidth -= (ChangeLink.ActualWidth + 10); + if (ClearVisible) + NewWidth -= (ClearLink.ActualWidth + 10); + + SetText(NewWidth); + + // Calculate the new ActualWidth + // We can't use PathTextBlock.ActualWidth yet, because LayoutUpdated event has not yet been triggered + formatted = new FormattedText( + PathTextBlock.Text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(PathTextBlock.FontFamily, PathTextBlock.FontStyle, PathTextBlock.FontWeight, PathTextBlock.FontStretch), + FontSize, + Foreground + ); + + if (NewWidth < 0) + PathTextBlock.Width = 0; + else if (formatted.Width > NewWidth) + PathTextBlock.Width = NewWidth; + else + PathTextBlock.Width = formatted.Width; + + PathTextBlock.Margin = new Thickness(CaptionWidth, 0, 0, 0); + double Pos = PathTextBlock.Width + CaptionWidth; + + if (SelectVisible) + { + SelectLink.Visibility = Visibility.Visible; + SelectLink.Margin = new Thickness(Pos, 0, 0, 0); + Pos += SelectLink.ActualWidth; + } + else + SelectLink.Visibility = Visibility.Collapsed; + + if (ChangeVisible) + { + ChangeLink.Visibility = Visibility.Visible; + ChangeLink.Margin = new Thickness(Pos + 10, 0, 0, 0); + Pos += ChangeLink.ActualWidth + 10; + } + else + ChangeLink.Visibility = Visibility.Collapsed; + + if (ClearVisible) + { + ClearLink.Visibility = Visibility.Visible; + ClearLink.Margin = new Thickness(Pos + 10, 0, 0, 0); + Pos += ClearLink.ActualWidth + 10; + } + else + ClearLink.Visibility = Visibility.Collapsed; + } + + private void SetText(double MaxWidth) + { + string Text = Path; + + if (System.IO.Path.IsPathRooted(Text)) + { + // It is a valid path + string filename = ""; + try + { + filename = System.IO.Path.GetFileName(Text); + } + catch { } + string directory = ""; + try + { + directory = System.IO.Path.GetDirectoryName(Text); + } + catch { } + FormattedText formatted; + bool widthOK = false; + bool changedWidth = false; + + do + { + formatted = new FormattedText( + "{0}...\\{1}".FormatWith(directory, filename), + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + new Typeface(PathTextBlock.FontFamily, PathTextBlock.FontStyle, PathTextBlock.FontWeight, PathTextBlock.FontStretch), + FontSize, + Foreground + ); + + widthOK = formatted.Width < MaxWidth; + + if (!widthOK) + { + changedWidth = true; + + if (directory.Length > 0) + directory = directory.Substring(0, directory.Length - 1); + + if (directory.Length == 0) + { + Text = "...\\" + filename; + break; + } + } + + } while (!widthOK); + + if (changedWidth && (directory.Length > 0)) + { + Text = "{0}...\\{1}".FormatWith(directory, filename); + } + } + + PathTextBlock.Text = Text; + } + + private void LoadedHandler(object sender, RoutedEventArgs e) + { + Resize(); + } + + private void SelectLink_Click(object sender, RoutedEventArgs e) + { + Select(); + Resize(); + } + + private void ChangeLink_Click(object sender, RoutedEventArgs e) + { + Select(); + Resize(); + } + + private void ClearLink_Click(object sender, RoutedEventArgs e) + { + Path = null; + Resize(); + } + + protected virtual void Select() { } + } + + public class FilePicker : FilePickerBase + { + public FilePicker() + : base() + { + if (SelectionText == "") + SelectionText = "Select file..."; + } + + protected override void Select() + { + Nullable result; + + if (SaveDialog) + { + Microsoft.Win32.SaveFileDialog savedlg = new Microsoft.Win32.SaveFileDialog(); + + if (Path != null) + savedlg.FileName = Path; + else + savedlg.FileName = DefaultFileName; + + // Show open file dialog box + result = savedlg.ShowDialog(); + + // Process open file dialog box results + if (result == true) + { + // Open document + Path = savedlg.FileName; + } + } + else + { + // Select file + Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog(); + + if (Path != null) + dlg.FileName = Path; + else + dlg.FileName = DefaultFileName; + + // Show open file dialog box + result = dlg.ShowDialog(); + + // Process open file dialog box results + if (result == true) + { + // Open document + Path = dlg.FileName; + } + } + } + + public bool SaveDialog + { + get + { + return (bool)GetValue(SaveDialogProperty); + } + set + { + SetValue(SaveDialogProperty, value); + } + } + + public static readonly DependencyProperty SaveDialogProperty = + DependencyProperty.Register("SaveDialog", typeof(bool), typeof(FilePicker), new UIPropertyMetadata(false)); + + public string DefaultFileName + { + get + { + return (string)GetValue(DefaultFileNameProperty); + } + set + { + SetValue(DefaultFileNameProperty, value); + } + } + + public static readonly DependencyProperty DefaultFileNameProperty = + DependencyProperty.Register("DefaultFileName", typeof(string), typeof(FilePicker), new UIPropertyMetadata("")); + } + + public class FolderPicker : FilePickerBase + { + public FolderPicker() + : base() + { + if (SelectionText == "") + SelectionText = "Select folder..."; + } + + protected override void Select() + { + // Select folder + + FolderSelectDialog dlg = new FolderSelectDialog(); + if (Path != null) + dlg.InitialDirectory = Path; + + if (dlg.ShowDialog()) + Path = dlg.FileName; + } + } + + static class Extensions + { + public static string FormatWith(this string s, params object[] args) + { + return string.Format(s, args); + } + } +} diff --git a/FolderSelectDialog.cs b/FolderSelectDialog.cs new file mode 100644 index 0000000..fa5a651 --- /dev/null +++ b/FolderSelectDialog.cs @@ -0,0 +1,338 @@ +// This class was found online. +// Original author is probably: Swizzy +// https://github.com/ttgxdinger/Random/blob/master/CPUKey%20Checker/CPUKey%20Checker/FolderSelectDialog.cs + +using System; +using System.Windows.Forms; +using System.Reflection; + +namespace WPinternals +{ + /// + /// Wraps System.Windows.Forms.OpenFileDialog to make it present + /// a vista-style dialog. + /// + public class FolderSelectDialog + { + // Wrapped dialog + System.Windows.Forms.OpenFileDialog ofd = null; + + /// + /// Default constructor + /// + public FolderSelectDialog() + { + ofd = new System.Windows.Forms.OpenFileDialog(); + + ofd.Filter = "Folders|\n"; + ofd.AddExtension = false; + ofd.CheckFileExists = false; + ofd.DereferenceLinks = true; + ofd.Multiselect = false; + } + + #region Properties + + /// + /// Gets/Sets the initial folder to be selected. A null value selects the current directory. + /// + public string InitialDirectory + { + get { return ofd.InitialDirectory; } + set { ofd.InitialDirectory = value == null || value.Length == 0 ? Environment.CurrentDirectory : value; } + } + + /// + /// Gets/Sets the title to show in the dialog + /// + public string Title + { + get { return ofd.Title; } + set { ofd.Title = value == null ? "Select a folder" : value; } + } + + /// + /// Gets the selected folder + /// + public string FileName + { + get { return ofd.FileName; } + } + + #endregion + + #region Methods + + /// + /// Shows the dialog + /// + /// True if the user presses OK else false + public bool ShowDialog() + { + return ShowDialog(IntPtr.Zero); + } + + /// + /// Shows the dialog + /// + /// Handle of the control to be parent + /// True if the user presses OK else false + public bool ShowDialog(IntPtr hWndOwner) + { + bool flag = false; + + if (Environment.OSVersion.Version.Major >= 6) + { + var r = new Reflector("System.Windows.Forms"); + + uint num = 0; + Type typeIFileDialog = r.GetType("FileDialogNative.IFileDialog"); + object dialog = r.Call(ofd, "CreateVistaDialog"); + r.Call(ofd, "OnBeforeVistaDialog", dialog); + + uint options = (uint)r.CallAs(typeof(System.Windows.Forms.FileDialog), ofd, "GetOptions"); + options |= (uint)r.GetEnum("FileDialogNative.FOS", "FOS_PICKFOLDERS"); + r.CallAs(typeIFileDialog, dialog, "SetOptions", options); + + object pfde = r.New("FileDialog.VistaDialogEvents", ofd); + object[] parameters = new object[] { pfde, num }; + r.CallAs2(typeIFileDialog, dialog, "Advise", parameters); + num = (uint)parameters[1]; + try + { + int num2 = (int)r.CallAs(typeIFileDialog, dialog, "Show", hWndOwner); + flag = 0 == num2; + } + finally + { + r.CallAs(typeIFileDialog, dialog, "Unadvise", num); + GC.KeepAlive(pfde); + } + } + else + { + var fbd = new FolderBrowserDialog(); + fbd.Description = this.Title; + fbd.SelectedPath = this.InitialDirectory; + fbd.ShowNewFolderButton = false; + if (fbd.ShowDialog(new WindowWrapper(hWndOwner)) != DialogResult.OK) return false; + ofd.FileName = fbd.SelectedPath; + flag = true; + } + + return flag; + } + + #endregion + } + + /// + /// Creates IWin32Window around an IntPtr + /// + public class WindowWrapper : System.Windows.Forms.IWin32Window + { + /// + /// Constructor + /// + /// Handle to wrap + public WindowWrapper(IntPtr handle) + { + _hwnd = handle; + } + + /// + /// Original ptr + /// + public IntPtr Handle + { + get { return _hwnd; } + } + + private IntPtr _hwnd; + } + + /// + /// This class is from the Front-End for Dosbox and is used to present a 'vista' dialog box to select folders. + /// Being able to use a vista style dialog box to select folders is much better then using the shell folder browser. + /// http://code.google.com/p/fed/ + /// + /// Example: + /// var r = new Reflector("System.Windows.Forms"); + /// + public class Reflector + { + #region variables + + string m_ns; + Assembly m_asmb; + + #endregion + + #region Constructors + + /// + /// Constructor + /// + /// The namespace containing types to be used + public Reflector(string ns) + : this(ns, ns) + { } + + /// + /// Constructor + /// + /// A specific assembly name (used if the assembly name does not tie exactly with the namespace) + /// The namespace containing types to be used + public Reflector(string an, string ns) + { + m_ns = ns; + m_asmb = null; + foreach (AssemblyName aN in Assembly.GetExecutingAssembly().GetReferencedAssemblies()) + { + if (aN.FullName.StartsWith(an)) + { + m_asmb = Assembly.Load(aN); + break; + } + } + } + + #endregion + + #region Methods + + /// + /// Return a Type instance for a type 'typeName' + /// + /// The name of the type + /// A type instance + public Type GetType(string typeName) + { + Type type = null; + string[] names = typeName.Split('.'); + + if (names.Length > 0) + type = m_asmb.GetType(m_ns + "." + names[0]); + + for (int i = 1; i < names.Length; ++i) + { + type = type.GetNestedType(names[i], BindingFlags.NonPublic); + } + return type; + } + + /// + /// Create a new object of a named type passing along any params + /// + /// The name of the type to create + /// + /// An instantiated type + public object New(string name, params object[] parameters) + { + Type type = GetType(name); + + ConstructorInfo[] ctorInfos = type.GetConstructors(); + foreach (ConstructorInfo ci in ctorInfos) + { + try + { + return ci.Invoke(parameters); + } + catch { } + } + + return null; + } + + /// + /// Calls method 'func' on object 'obj' passing parameters 'parameters' + /// + /// The object on which to excute function 'func' + /// The function to execute + /// The parameters to pass to function 'func' + /// The result of the function invocation + public object Call(object obj, string func, params object[] parameters) + { + return Call2(obj, func, parameters); + } + + /// + /// Calls method 'func' on object 'obj' passing parameters 'parameters' + /// + /// The object on which to excute function 'func' + /// The function to execute + /// The parameters to pass to function 'func' + /// The result of the function invocation + public object Call2(object obj, string func, object[] parameters) + { + return CallAs2(obj.GetType(), obj, func, parameters); + } + + /// + /// Calls method 'func' on object 'obj' which is of type 'type' passing parameters 'parameters' + /// + /// The type of 'obj' + /// The object on which to excute function 'func' + /// The function to execute + /// The parameters to pass to function 'func' + /// The result of the function invocation + public object CallAs(Type type, object obj, string func, params object[] parameters) + { + return CallAs2(type, obj, func, parameters); + } + + /// + /// Calls method 'func' on object 'obj' which is of type 'type' passing parameters 'parameters' + /// + /// The type of 'obj' + /// The object on which to excute function 'func' + /// The function to execute + /// The parameters to pass to function 'func' + /// The result of the function invocation + public object CallAs2(Type type, object obj, string func, object[] parameters) + { + MethodInfo methInfo = type.GetMethod(func, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + return methInfo.Invoke(obj, parameters); + } + + /// + /// Returns the value of property 'prop' of object 'obj' + /// + /// The object containing 'prop' + /// The property name + /// The property value + public object Get(object obj, string prop) + { + return GetAs(obj.GetType(), obj, prop); + } + + /// + /// Returns the value of property 'prop' of object 'obj' which has type 'type' + /// + /// The type of 'obj' + /// The object containing 'prop' + /// The property name + /// The property value + public object GetAs(Type type, object obj, string prop) + { + PropertyInfo propInfo = type.GetProperty(prop, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + return propInfo.GetValue(obj, null); + } + + /// + /// Returns an enum value + /// + /// The name of enum type + /// The name of the value + /// The enum value + public object GetEnum(string typeName, string name) + { + Type type = GetType(typeName); + FieldInfo fieldInfo = type.GetField(name); + return fieldInfo.GetValue(null); + } + + #endregion + + } +} diff --git a/HelperClasses.cs b/HelperClasses.cs new file mode 100644 index 0000000..59e5765 --- /dev/null +++ b/HelperClasses.cs @@ -0,0 +1,2239 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// +// Some of the classes and functions in this file were found online. +// Where possible the original authors are referenced. + +using System; +using System.IO; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Media.Animation; +using System.Windows.Media.Imaging; +using System.Threading; +using System.Windows.Input; +using System.Windows.Threading; +using System.IO.Compression; +using System.Collections; +using System.Reflection; + +namespace WPinternals +{ + internal delegate void SetWorkingStatus(string Message, string SubMessage = null, ulong? MaxProgressValue = null, bool ShowAnimation = true, WPinternalsStatus Status = WPinternalsStatus.Undefined); + internal delegate void UpdateWorkingStatus(string Message, string SubMessage = null, ulong? CurrentProgressValue = null, WPinternalsStatus Status = WPinternalsStatus.Undefined); + internal delegate void ExitSuccess(string Message, string SubMessage = null); + internal delegate void ExitFailure(string Message, string SubMessage = null); + + internal enum WPinternalsStatus + { + Undefined, + Scanning, + Flashing, + Patching, + WaitingForManualReset, + SwitchingMode, + Initializing + }; + + public class BooleanConverter : DependencyObject, IValueConverter + { + public static readonly DependencyProperty OnTrueProperty = + DependencyProperty.Register("OnTrue", typeof(object), typeof(BooleanConverter), + new PropertyMetadata(default(object))); + + public static readonly DependencyProperty OnFalseProperty = + DependencyProperty.Register("OnFalse", typeof(object), typeof(BooleanConverter), + new PropertyMetadata(default(object))); + + public static readonly DependencyProperty OnNullProperty = + DependencyProperty.Register("OnNull", typeof(object), typeof(BooleanConverter), + new PropertyMetadata(default(object))); + + public object OnTrue + { + get { return GetValue(OnTrueProperty); } + set { SetValue(OnTrueProperty, value); } + } + + public object OnFalse + { + get { return GetValue(OnFalseProperty); } + set { SetValue(OnFalseProperty, value); } + } + + public object OnNull + { + get { return GetValue(OnNullProperty); } + set { SetValue(OnNullProperty, value); } + } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + value = value == null + ? OnNull ?? Default(targetType) + : (string.Equals(value.ToString(), false.ToString(), StringComparison.CurrentCultureIgnoreCase) + ? OnFalse + : OnTrue); + return value; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == OnNull) return Default(targetType); + if (value == OnFalse) return false; + if (value == OnTrue) return true; + if (value == null) return null; + + if (OnNull != null && + string.Equals(value.ToString(), OnNull.ToString(), StringComparison.CurrentCultureIgnoreCase)) + return Default(targetType); + + if (OnFalse != null && + string.Equals(value.ToString(), OnFalse.ToString(), StringComparison.CurrentCultureIgnoreCase)) + return false; + + if (OnTrue != null && + string.Equals(value.ToString(), OnTrue.ToString(), StringComparison.CurrentCultureIgnoreCase)) + return true; + + return null; + } + + public static object Default(Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + } + + public class HexConverter : DependencyObject, IValueConverter + { + public static readonly DependencyProperty SeparatorProperty = + DependencyProperty.Register("OnTrue", typeof(object), typeof(HexConverter), + new PropertyMetadata(" ")); + + public object Separator + { + get { return GetValue(SeparatorProperty); } + set { SetValue(SeparatorProperty, value); } + } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is byte[]) + { + byte[] bytes = (byte[])value; + StringBuilder s = new StringBuilder(1000); + for (int i = bytes.GetLowerBound(0); i <= bytes.GetUpperBound(0); i++) + { + if (i != bytes.GetLowerBound(0)) + s.Append(Separator); + s.Append(bytes[i].ToString("X2")); + } + return s.ToString(); + } + else + return ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + public static object Default(Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + } + + public class ObjectToVisibilityConverter : DependencyObject, IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + return "Collapsed"; + else + return "Visible"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + public static object Default(Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + } + + public class InverseObjectToVisibilityConverter : DependencyObject, IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + return "Visible"; + else + return "Collapsed"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + + public static object Default(Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + } + + internal class CollapsibleRun : Run + { + private string CollapsibleText; + + protected override void OnInitialized(EventArgs e) + { + base.OnInitialized(e); + + CollapsibleText = Text; + } + + public Boolean IsVisible + { + get + { + return (Boolean)this.GetValue(IsVisibleProperty); + } + set + { + this.SetValue(IsVisibleProperty, value); + } + } + public static readonly DependencyProperty IsVisibleProperty = DependencyProperty.Register( + "IsVisible", typeof(Boolean), typeof(CollapsibleRun), new PropertyMetadata(true, IsVisibleChanged)); + public static void IsVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if ((bool)e.NewValue) + ((CollapsibleRun)d).Text = ((CollapsibleRun)d).CollapsibleText; + else + ((CollapsibleRun)d).Text = String.Empty; + } + } + + internal class CollapsibleSection : Section + { + public CollapsibleSection() + { + CollapsibleBlocks = new List(); + } + + protected override void OnInitialized(EventArgs e) + { + base.OnInitialized(e); + + foreach (Block Block in Blocks) + CollapsibleBlocks.Add(Block); + + Blocks.Clear(); + } + + public List CollapsibleBlocks { get; private set; } + + public Boolean IsCollapsed + { + get + { + return (Boolean)this.GetValue(IsCollapsedProperty); + } + set + { + this.SetValue(IsCollapsedProperty, value); + + Blocks.Clear(); + + if (IsInitialized) + { + if (!value) + { + foreach (Block Block in CollapsibleBlocks) + Blocks.Add(Block); + } + } + } + } + public static readonly DependencyProperty IsCollapsedProperty = DependencyProperty.Register( + "IsCollapsed", typeof(Boolean), typeof(CollapsibleSection), new PropertyMetadata(false)); + } + + // Overloaded Paragraph class to remove empty Run-elements, caused by auto-formatting new-lines in the XAML + // Use local:Paragraph in a FlowDocument + // This correction only works at run-time + public class Paragraph : System.Windows.Documents.Paragraph + { + protected override void OnInitialized(EventArgs e) + { + base.OnInitialized(e); + int inlinesCount = this.Inlines.Count; + for (int i = 0; i < inlinesCount; i++) + { + System.Windows.Documents.Inline inline = this.Inlines.ElementAt(i); + if (inline is System.Windows.Documents.Run) + { + if ((inline as System.Windows.Documents.Run).Text == Convert.ToChar(32).ToString()) //ACSII 32 is the white space + { + (inline as System.Windows.Documents.Run).Text = string.Empty; + } + } + } + } + } + + internal class ArrivalEventArgs : EventArgs + { + public PhoneInterfaces NewInterface; + public IDisposable NewModel; + + public ArrivalEventArgs(PhoneInterfaces NewInterface, IDisposable NewModel) + : base() + { + this.NewInterface = NewInterface; + this.NewModel = NewModel; + } + } + + internal static class BigEndian + { + public static byte[] GetBytes(object Value) + { + byte[] Bytes; + if (Value is short) + Bytes = BitConverter.GetBytes((short)Value); + else if (Value is ushort) + Bytes = BitConverter.GetBytes((ushort)Value); + else if (Value is int) + Bytes = BitConverter.GetBytes((int)Value); + else if (Value is uint) + Bytes = BitConverter.GetBytes((uint)Value); + else + throw new NotSupportedException(); + + byte[] Result = new byte[Bytes.Length]; + for (int i = 0; i < Bytes.Length; i++) + Result[i] = Bytes[Bytes.Length - 1 - i]; + + return Result; + } + + public static byte[] GetBytes(object Value, int Width) + { + byte[] Result; + byte[] BigEndianBytes = GetBytes(Value); + if (BigEndianBytes.Length == Width) + return BigEndianBytes; + else if (BigEndianBytes.Length > Width) + { + Result = new byte[Width]; + System.Buffer.BlockCopy(BigEndianBytes, BigEndianBytes.Length - Width, Result, 0, Width); + return Result; + } + else + { + Result = new byte[Width]; + System.Buffer.BlockCopy(BigEndianBytes, 0, Result, Width - BigEndianBytes.Length, BigEndianBytes.Length); + return Result; + } + } + + public static UInt16 ToUInt16(byte[] Buffer, int Offset) + { + byte[] Bytes = new byte[2]; + for (int i = 0; i < 2; i++) + Bytes[i] = Buffer[Offset + 1 - i]; + return BitConverter.ToUInt16(Bytes, 0); + } + + public static Int16 ToInt16(byte[] Buffer, int Offset) + { + byte[] Bytes = new byte[2]; + for (int i = 0; i < 2; i++) + Bytes[i] = Buffer[Offset + 1 - i]; + return BitConverter.ToInt16(Bytes, 0); + } + + public static UInt32 ToUInt32(byte[] Buffer, int Offset) + { + byte[] Bytes = new byte[4]; + for (int i = 0; i < 4; i++) + Bytes[i] = Buffer[Offset + 3 - i]; + return BitConverter.ToUInt32(Bytes, 0); + } + + public static Int32 ToInt32(byte[] Buffer, int Offset) + { + byte[] Bytes = new byte[4]; + for (int i = 0; i < 4; i++) + Bytes[i] = Buffer[Offset + 3 - i]; + return BitConverter.ToInt32(Bytes, 0); + } + } + + // This class was found online. + // Original author is probably: mdm20 + // https://stackoverflow.com/questions/5566330/get-gif-to-play-in-wpf-with-gifimage-class/5568703#5568703 + internal class GifImage : Image + { + private bool _isInitialized; + private GifBitmapDecoder _gifDecoder; + private Int32Animation _animation; + + public int FrameIndex + { + get { return (int)GetValue(FrameIndexProperty); } + set { SetValue(FrameIndexProperty, value); } + } + + private void Initialize() + { + _gifDecoder = new GifBitmapDecoder(new Uri("pack://application:,,," + this.GifSource), BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default); + _animation = new Int32Animation(0, _gifDecoder.Frames.Count - 1, new Duration(new TimeSpan(0, 0, 0, _gifDecoder.Frames.Count / 10, (int)((_gifDecoder.Frames.Count / 10.0 - _gifDecoder.Frames.Count / 10) * 1000)))); + _animation.RepeatBehavior = RepeatBehavior.Forever; + this.Source = _gifDecoder.Frames[0]; + + _isInitialized = true; + } + + static GifImage() + { + VisibilityProperty.OverrideMetadata(typeof(GifImage), + new FrameworkPropertyMetadata(VisibilityPropertyChanged)); + } + + private static void VisibilityPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if ((Visibility)e.NewValue == Visibility.Visible) + { + ((GifImage)sender).StartAnimation(); + } + else + { + ((GifImage)sender).StopAnimation(); + } + } + + public static readonly DependencyProperty FrameIndexProperty = + DependencyProperty.Register("FrameIndex", typeof(int), typeof(GifImage), new UIPropertyMetadata(0, new PropertyChangedCallback(ChangingFrameIndex))); + + static void ChangingFrameIndex(DependencyObject obj, DependencyPropertyChangedEventArgs ev) + { + var gifImage = obj as GifImage; + gifImage.Source = gifImage._gifDecoder.Frames[(int)ev.NewValue]; + } + + public bool AutoStart + { + get { return (bool)GetValue(AutoStartProperty); } + set { SetValue(AutoStartProperty, value); } + } + + public static readonly DependencyProperty AutoStartProperty = + DependencyProperty.Register("AutoStart", typeof(bool), typeof(GifImage), new UIPropertyMetadata(false, AutoStartPropertyChanged)); + + private static void AutoStartPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if ((bool)e.NewValue) + (sender as GifImage).StartAnimation(); + } + + public string GifSource + { + get { return (string)GetValue(GifSourceProperty); } + set { SetValue(GifSourceProperty, value); } + } + + public static readonly DependencyProperty GifSourceProperty = + DependencyProperty.Register("GifSource", typeof(string), typeof(GifImage), new UIPropertyMetadata(string.Empty, GifSourcePropertyChanged)); + + private static void GifSourcePropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + (sender as GifImage).Initialize(); + } + + public void StartAnimation() + { + if (!_isInitialized) + this.Initialize(); + + BeginAnimation(FrameIndexProperty, _animation); + } + + public void StopAnimation() + { + BeginAnimation(FrameIndexProperty, null); + } + } + + internal enum LogType + { + FileOnly, + FileAndConsole, + ConsoleOnly + }; + + internal static class LogFile + { + private static StreamWriter w = null; + private static object lockobject = new object(); +#if PREVIEW + private static string LogAction = null; + private static StringBuilder LogBuilder; +#endif + + static LogFile() + { + try + { + if (!Directory.Exists(Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals"))) + Directory.CreateDirectory(Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals")); + w = File.AppendText(Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\WPInternals.log")); + } + catch { } + } + + public static void Log(string logMessage, LogType Type = LogType.FileOnly) + { + if (w == null) return; + + lock (lockobject) + { + if ((Type == LogType.FileOnly) || (Type == LogType.FileAndConsole)) + { + DateTime Now = DateTime.Now; + string Text = Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + ": " + logMessage; + w.WriteLine(Text); + w.Flush(); + +#if PREVIEW + if (LogAction != null) + LogBuilder.AppendLine(Text); +#endif + } + + if ((CommandLine.IsConsoleVisible) && ((Type == LogType.ConsoleOnly) || (Type == LogType.FileAndConsole))) + Console.WriteLine(logMessage); + } + } + + public static void LogException(Exception Ex, LogType Type = LogType.FileAndConsole, string AdditionalInfo = null) + { + string Indent = ""; + Exception CurrentEx = Ex; + while (CurrentEx != null) + { + Log(Indent + "Error: " + RemoveBadChars(CurrentEx.Message).Replace("of type '.' ", "") + (AdditionalInfo == null ? "" : " - " + AdditionalInfo), Type); + AdditionalInfo = null; + if (CurrentEx is WPinternalsException) + Log(Indent + ((WPinternalsException)CurrentEx).SubMessage, Type); +#if DEBUG + if (CurrentEx.StackTrace != null) + Log(Indent + CurrentEx.StackTrace, LogType.FileOnly); +#endif + Indent += " "; + CurrentEx = CurrentEx.InnerException; + } + } + + private static string RemoveBadChars(string Text) + { + return System.Text.RegularExpressions.Regex.Replace(Text, @"[^\u0020-\u007E]+", string.Empty); + } + + public static void DumpLog(StreamReader r) + { + string line; + while ((line = r.ReadLine()) != null) + { + Console.WriteLine(line); + } + } + + public static void LogApplicationVersion() + { + Log("Windows Phone Internals version " + + Assembly.GetExecutingAssembly().GetName().Version.Major.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Minor.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Build.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Revision.ToString(), LogType.FileAndConsole); + Log("Copyright Heathcliff74 / wpinternals.net", LogType.FileAndConsole); + } + + internal static void BeginAction(string Action) + { +#if PREVIEW + if (LogAction == null) + { + LogAction = Action; + LogBuilder = new StringBuilder(); + LogBuilder.AppendLine("Windows Phone Internals version " + + Assembly.GetExecutingAssembly().GetName().Version.Major.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Minor.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Build.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Revision.ToString()); + LogBuilder.AppendLine("Copyright Heathcliff74 / wpinternals.net"); + LogBuilder.AppendLine("Action: " + Action); + if (App.Config.RegistrationName != null) + LogBuilder.AppendLine("Name: " + App.Config.RegistrationName); + if (App.Config.RegistrationEmail != null) + LogBuilder.AppendLine("Mail: " + App.Config.RegistrationEmail); + if (App.Config.RegistrationSkypeID != null) + LogBuilder.AppendLine("Skype: " + App.Config.RegistrationSkypeID); + if (App.Config.RegistrationTelegramID != null) + LogBuilder.AppendLine("Telegram: " + App.Config.RegistrationTelegramID); + if (Environment.MachineName != null) + LogBuilder.AppendLine("Machine: " + Environment.MachineName); + } +#endif + } + + internal static void EndAction() + { + EndAction(null); + } + + internal static void EndAction(string Action) + { +#if PREVIEW + if ((LogAction != null) && ((Action == null) || (LogAction == Action))) + { + Action = LogAction; + LogAction = null; + string FileName = ""; + if (App.Config.RegistrationName != null) + FileName += App.Config.RegistrationName + " - "; + FileName += DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + " - " + Action + " - " + + Assembly.GetExecutingAssembly().GetName().Version.Major.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Minor.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Build.ToString() + "." + + Assembly.GetExecutingAssembly().GetName().Version.Revision.ToString() + ".log"; + + // Normalize filename + try + { + FileName = System.Text.Encoding.ASCII.GetString(System.Text.Encoding.GetEncoding("ISO-8859-8").GetBytes(FileName)); + } + catch { } + FileName = FileName.Replace("?", ""); + + if (Action.ToLower() == "registration") + Uploader.Upload(FileName, LogBuilder.ToString()); + else + { + try + { + Uploader.Upload(FileName, LogBuilder.ToString()); + } + catch { } + } + } +#endif + } + } + + public static class Converter + { + public static string ConvertHexToString(byte[] Bytes, string Separator) + { + StringBuilder s = new StringBuilder(1000); + for (int i = Bytes.GetLowerBound(0); i <= Bytes.GetUpperBound(0); i++) + { + if (i != Bytes.GetLowerBound(0)) + s.Append(Separator); + s.Append(Bytes[i].ToString("X2")); + } + return s.ToString(); + } + + public static byte[] ConvertStringToHex(string HexString) + { + if (HexString.Length % 2 == 1) + throw new Exception("The binary key cannot have an odd number of digits"); + + byte[] arr = new byte[HexString.Length >> 1]; + + for (int i = 0; i < (HexString.Length >> 1); ++i) + { + arr[i] = (byte)((GetHexVal(HexString[i << 1]) << 4) + (GetHexVal(HexString[(i << 1) + 1]))); + } + + return arr; + } + + public static int GetHexVal(char hex) + { + int val = (int)hex; + //For uppercase A-F letters: + //return val - (val < 58 ? 48 : 55); + //For lowercase a-f letters: + //return val - (val < 58 ? 48 : 87); + //Or the two combined, but a bit slower: + return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); + } + + } + + // This class was found online. + // Original author is probably: John Melville + // https://social.msdn.microsoft.com/Forums/en-US/163ef755-ff7b-4ea5-b226-bbe8ef5f4796/is-there-a-pattern-for-calling-an-async-method-synchronously?forum=async + public static class AsyncHelpers + { + /// + /// Execute's an async Task method which has a void return value synchronously + /// + /// Task method to execute + public static void RunSync(Func task) + { + var oldContext = SynchronizationContext.Current; + var synch = new ExclusiveSynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(synch); + synch.Post(async _ => + { + try + { + await task(); + } + catch (Exception e) + { + synch.InnerException = e; + throw; + } + finally + { + synch.EndMessageLoop(); + } + }, null); + synch.BeginMessageLoop(); + + SynchronizationContext.SetSynchronizationContext(oldContext); + } + + /// + /// Execute's an async Task method which has a T return type synchronously + /// + /// Return Type + /// Task method to execute + /// + public static T RunSync(Func> task) + { + var oldContext = SynchronizationContext.Current; + var synch = new ExclusiveSynchronizationContext(); + SynchronizationContext.SetSynchronizationContext(synch); + T ret = default(T); + synch.Post(async _ => + { + try + { + ret = await task(); + } + catch (Exception e) + { + synch.InnerException = e; + throw; + } + finally + { + synch.EndMessageLoop(); + } + }, null); + synch.BeginMessageLoop(); + SynchronizationContext.SetSynchronizationContext(oldContext); + return ret; + } + + private class ExclusiveSynchronizationContext : SynchronizationContext + { + private bool done; + public Exception InnerException { get; set; } + readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false); + readonly Queue> items = + new Queue>(); + + public override void Send(SendOrPostCallback d, object state) + { + throw new NotSupportedException("We cannot send to our same thread"); + } + + public override void Post(SendOrPostCallback d, object state) + { + lock (items) + { + items.Enqueue(Tuple.Create(d, state)); + } + workItemsWaiting.Set(); + } + + public void EndMessageLoop() + { + Post(_ => done = true, null); + } + + public void BeginMessageLoop() + { + while (!done) + { + Tuple task = null; + lock (items) + { + if (items.Count > 0) + { + task = items.Dequeue(); + } + } + if (task != null) + { + task.Item1(task.Item2); + if (InnerException != null) // the method threw an exeption + { + throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException); + } + } + else + { + workItemsWaiting.WaitOne(); + } + } + } + + public override SynchronizationContext CreateCopy() + { + return this; + } + } + } + + // This class is taken from the Prism library by Microsoft Patterns & Practices + // License: http://compositewpf.codeplex.com/license + internal static class WeakEventHandlerManager + { + public static void AddWeakReferenceHandler(ref List handlers, EventHandler handler, int defaultListSize) + { + if (handlers == null) + { + handlers = (defaultListSize > 0) ? new List(defaultListSize) : new List(); + } + handlers.Add(new WeakReference(handler)); + } + + private static void CallHandler(object sender, EventHandler eventHandler) + { + DispatcherProxy proxy = DispatcherProxy.CreateDispatcher(); + if (eventHandler != null) + { + if ((proxy != null) && !proxy.CheckAccess()) + { + proxy.BeginInvoke(new Action(WeakEventHandlerManager.CallHandler), new object[] { sender, eventHandler }); + } + else + { + eventHandler(sender, EventArgs.Empty); + } + } + } + + public static void CallWeakReferenceHandlers(object sender, List handlers) + { + if (handlers != null) + { + EventHandler[] callees = new EventHandler[handlers.Count]; + int count = 0; + count = CleanupOldHandlers(handlers, callees, count); + for (int i = 0; i < count; i++) + { + CallHandler(sender, callees[i]); + } + } + } + + private static int CleanupOldHandlers(List handlers, EventHandler[] callees, int count) + { + for (int i = handlers.Count - 1; i >= 0; i--) + { + WeakReference reference = handlers[i]; + EventHandler target = reference.Target as EventHandler; + if (target == null) + { + handlers.RemoveAt(i); + } + else + { + callees[count] = target; + count++; + } + } + return count; + } + + public static void RemoveWeakReferenceHandler(List handlers, EventHandler handler) + { + if (handlers != null) + { + for (int i = handlers.Count - 1; i >= 0; i--) + { + WeakReference reference = handlers[i]; + EventHandler target = reference.Target as EventHandler; + if ((target == null) || (target == handler)) + { + handlers.RemoveAt(i); + } + } + } + } + + private class DispatcherProxy + { + private Dispatcher innerDispatcher; + + private DispatcherProxy(Dispatcher dispatcher) + { + this.innerDispatcher = dispatcher; + } + + public DispatcherOperation BeginInvoke(Delegate method, params object[] args) + { + return this.innerDispatcher.BeginInvoke(method, DispatcherPriority.Normal, args); + } + + public bool CheckAccess() + { + return this.innerDispatcher.CheckAccess(); + } + + public static WeakEventHandlerManager.DispatcherProxy CreateDispatcher() + { + if (Application.Current == null) + { + return null; + } + return new WeakEventHandlerManager.DispatcherProxy(Application.Current.Dispatcher); + } + } + } + + // This interface is taken from the Prism library by Microsoft Patterns & Practices + // License: http://compositewpf.codeplex.com/license + public interface IActiveAware + { + /// + /// Gets or sets a value indicating whether the object is active. + /// + /// if the object is active; otherwise . + bool IsActive { get; set; } + + /// + /// Notifies that the value for property has changed. + /// + event EventHandler IsActiveChanged; + } + + // This class is taken from the Prism library by Microsoft Patterns & Practices + // License: http://compositewpf.codeplex.com/license + public abstract class DelegateCommandBase : ICommand, IActiveAware + { + private List _canExecuteChangedHandlers; + private bool _isActive; + private readonly Func canExecuteMethod; + private readonly Action executeMethod; + + public event EventHandler CanExecuteChanged + { + add + { + WeakEventHandlerManager.AddWeakReferenceHandler(ref this._canExecuteChangedHandlers, value, 2); + } + remove + { + WeakEventHandlerManager.RemoveWeakReferenceHandler(this._canExecuteChangedHandlers, value); + } + } + + public event EventHandler IsActiveChanged; + + protected DelegateCommandBase(Action executeMethod, Func canExecuteMethod) + { + if ((executeMethod == null) || (canExecuteMethod == null)) + { + throw new ArgumentNullException("executeMethod", "Delegate Command Delegates Cannot Be Null"); + } + this.executeMethod = executeMethod; + this.canExecuteMethod = canExecuteMethod; + } + + protected bool CanExecute(object parameter) + { + if (this.canExecuteMethod != null) + { + return this.canExecuteMethod(parameter); + } + return true; + } + + protected void Execute(object parameter) + { + this.executeMethod(parameter); + } + + protected virtual void OnCanExecuteChanged() + { + WeakEventHandlerManager.CallWeakReferenceHandlers(this, this._canExecuteChangedHandlers); + } + + protected virtual void OnIsActiveChanged() + { + EventHandler isActiveChanged = this.IsActiveChanged; + if (isActiveChanged != null) + { + isActiveChanged(this, EventArgs.Empty); + } + } + + public void RaiseCanExecuteChanged() + { + this.OnCanExecuteChanged(); + } + + bool ICommand.CanExecute(object parameter) + { + return this.CanExecute(parameter); + } + + void ICommand.Execute(object parameter) + { + this.Execute(parameter); + } + + public bool IsActive + { + get + { + return this._isActive; + } + set + { + if (this._isActive != value) + { + this._isActive = value; + this.OnIsActiveChanged(); + } + } + } + } + + // This class is taken from the Prism library by Microsoft Patterns & Practices + // License: http://compositewpf.codeplex.com/license + public class DelegateCommand : DelegateCommandBase + { + public DelegateCommand(Action executeMethod) + : this(executeMethod, () => true) + { + } + + public DelegateCommand(Action executeMethod, Func canExecuteMethod): base(o => executeMethod(), f => canExecuteMethod()) + { + } + + public bool CanExecute() + { + return base.CanExecute(null); + } + + public void Execute() + { + base.Execute(null); + } + } + + internal class FlowDocumentScrollViewerNoMouseWheel: FlowDocumentScrollViewer + { + protected override void OnMouseWheel(MouseWheelEventArgs e) + { + } + } + + internal class ProgressUpdater + { + private DateTime InitTime; + private DateTime LastUpdateTime; + private UInt64 MaxValue; + private Action ProgressUpdateCallback; + internal int ProgressPercentage; + + internal ProgressUpdater(UInt64 MaxValue, Action ProgressUpdateCallback) + { + InitTime = DateTime.Now; + LastUpdateTime = DateTime.Now; + this.MaxValue = MaxValue; + this.ProgressUpdateCallback = ProgressUpdateCallback; + SetProgress(0); + } + + private UInt64 _Progress; + internal UInt64 Progress + { + get + { + return _Progress; + } + } + + internal void SetProgress(UInt64 NewValue) + { + if (_Progress != NewValue) + { + int PreviousProgressPercentage = (int)((double)_Progress / MaxValue * 100); + ProgressPercentage = (int)((double)NewValue / MaxValue * 100); + + _Progress = NewValue; + + if (((DateTime.Now - LastUpdateTime) > TimeSpan.FromSeconds(0.5)) || (ProgressPercentage == 100)) + { +#if DEBUG + Console.WriteLine("Init time: " + InitTime.ToShortTimeString() + " / Now: " + DateTime.Now.ToString() + " / NewValue: " + NewValue.ToString() + " / MaxValue: " + MaxValue.ToString() + " ->> Percentage: " + ProgressPercentage.ToString() + " / Remaining: " + TimeSpan.FromTicks((long)((DateTime.Now - InitTime).Ticks / ((double)NewValue / MaxValue) * ((double)1 - ((double)NewValue / MaxValue)))).ToString()); +#endif + + if (((DateTime.Now - InitTime) < TimeSpan.FromSeconds(30)) && (ProgressPercentage < 15)) + ProgressUpdateCallback(ProgressPercentage, null); + else + ProgressUpdateCallback(ProgressPercentage, TimeSpan.FromTicks((long)((DateTime.Now - InitTime).Ticks / ((double)NewValue / MaxValue) * ((double)1 - ((double)NewValue / MaxValue))))); + + LastUpdateTime = DateTime.Now; + } + } + } + + internal void IncreaseProgress(UInt64 Progress) + { + SetProgress(_Progress + Progress); + } + } + + internal static class Compression + { + internal static Stream GetDecompressedStreamWithSeek(Stream InputStream) + { + long P = InputStream.Position; + byte[] GZipHeader = new byte[3]; + InputStream.Read(GZipHeader, 0, 3); + InputStream.Position = P; + if (StructuralComparisons.StructuralEqualityComparer.Equals(GZipHeader, new byte[] { 0x1F, 0x8B, 0x08 })) + return new GZipStream(InputStream, CompressionMode.Decompress, false); + else + return InputStream; + } + + internal static bool IsCompressedStream(Stream InputStream) + { + byte[] GZipHeader = new byte[3]; + InputStream.Read(GZipHeader, 0, 3); + return StructuralComparisons.StructuralEqualityComparer.Equals(GZipHeader, new byte[] { 0x1F, 0x8B, 0x08 }); + } + + internal static GZipStream GetDecompressedStream(Stream InputStream) + { + return new GZipStream(InputStream, CompressionMode.Decompress, false); + } + } + + internal class WPinternalsException: Exception + { + // Message and SubMessaage are always printable + internal string SubMessage = null; + + internal WPinternalsException() : base() { } + + internal WPinternalsException(string Message) : base(Message) { } + + internal WPinternalsException(string Message, Exception InnerException) : base(Message, InnerException) { } + + internal WPinternalsException(string Message, string SubMessage) : base(Message) { this.SubMessage = SubMessage; } + + internal WPinternalsException(string Message, string SubMessage, Exception InnerException) : base(Message, InnerException) { this.SubMessage = SubMessage; } + } + + // This class is written by: Eugene Beresovsky + // https://stackoverflow.com/questions/13035925/stream-wrapper-to-make-stream-seekable/28036366#28036366 + internal class ReadSeekableStream : Stream + { + private long _underlyingPosition; + private readonly byte[] _seekBackBuffer; + private int _seekBackBufferCount; + private int _seekBackBufferIndex; + private readonly Stream _underlyingStream; + + public ReadSeekableStream(Stream underlyingStream, int seekBackBufferSize) + { + if (!underlyingStream.CanRead) + throw new Exception("Provided stream " + underlyingStream + " is not readable"); + _underlyingStream = underlyingStream; + _seekBackBuffer = new byte[seekBackBufferSize]; + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return true; } } + + public override int Read(byte[] buffer, int offset, int count) + { + int copiedFromBackBufferCount = 0; + if (_seekBackBufferIndex < _seekBackBufferCount) + { + copiedFromBackBufferCount = Math.Min(count, _seekBackBufferCount - _seekBackBufferIndex); + Buffer.BlockCopy(_seekBackBuffer, _seekBackBufferIndex, buffer, offset, copiedFromBackBufferCount); + offset += copiedFromBackBufferCount; + count -= copiedFromBackBufferCount; + _seekBackBufferIndex += copiedFromBackBufferCount; + } + int bytesReadFromUnderlying = 0; + if (count > 0) + { + bytesReadFromUnderlying = _underlyingStream.Read(buffer, offset, count); + if (bytesReadFromUnderlying > 0) + { + _underlyingPosition += bytesReadFromUnderlying; + + var copyToBufferCount = Math.Min(bytesReadFromUnderlying, _seekBackBuffer.Length); + var copyToBufferOffset = Math.Min(_seekBackBufferCount, _seekBackBuffer.Length - copyToBufferCount); + var bufferBytesToMove = Math.Min(_seekBackBufferCount - 1, copyToBufferOffset); + + if (bufferBytesToMove > 0) + Buffer.BlockCopy(_seekBackBuffer, _seekBackBufferCount - bufferBytesToMove, _seekBackBuffer, 0, bufferBytesToMove); + Buffer.BlockCopy(buffer, offset, _seekBackBuffer, copyToBufferOffset, copyToBufferCount); + _seekBackBufferCount = Math.Min(_seekBackBuffer.Length, _seekBackBufferCount + copyToBufferCount); + _seekBackBufferIndex = _seekBackBufferCount; + } + } + return copiedFromBackBufferCount + bytesReadFromUnderlying; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.End) + return SeekFromEnd((int)Math.Max(0, -offset)); + + var relativeOffset = origin == SeekOrigin.Current + ? offset + : offset - Position; + + if (relativeOffset == 0) + return Position; + else if (relativeOffset > 0) + return SeekForward(relativeOffset); + else + return SeekBackwards(-relativeOffset); + } + + private long SeekForward(long origOffset) + { + long offset = origOffset; + var seekBackBufferLength = _seekBackBuffer.Length; + + int backwardSoughtBytes = _seekBackBufferCount - _seekBackBufferIndex; + int seekForwardInBackBuffer = (int)Math.Min(offset, backwardSoughtBytes); + offset -= seekForwardInBackBuffer; + _seekBackBufferIndex += seekForwardInBackBuffer; + + if (offset > 0) + { + // first completely fill seekBackBuffer to remove special cases from while loop below + if (_seekBackBufferCount < seekBackBufferLength) + { + var maxRead = seekBackBufferLength - _seekBackBufferCount; + if (offset < maxRead) + maxRead = (int)offset; + var bytesRead = _underlyingStream.Read(_seekBackBuffer, _seekBackBufferCount, maxRead); + _underlyingPosition += bytesRead; + _seekBackBufferCount += bytesRead; + _seekBackBufferIndex = _seekBackBufferCount; + if (bytesRead < maxRead) + { + if (_seekBackBufferCount < offset) + throw new NotSupportedException("Reached end of stream seeking forward " + origOffset + " bytes"); + return Position; + } + offset -= bytesRead; + } + + // now alternate between filling tempBuffer and seekBackBuffer + bool fillTempBuffer = true; + var tempBuffer = new byte[seekBackBufferLength]; + while (offset > 0) + { + var maxRead = offset < seekBackBufferLength ? (int)offset : seekBackBufferLength; + var bytesRead = _underlyingStream.Read(fillTempBuffer ? tempBuffer : _seekBackBuffer, 0, maxRead); + _underlyingPosition += bytesRead; + var bytesReadDiff = maxRead - bytesRead; + offset -= bytesRead; + if (bytesReadDiff > 0 /* reached end-of-stream */ || offset == 0) + { + if (fillTempBuffer) + { + if (bytesRead > 0) + { + Buffer.BlockCopy(_seekBackBuffer, bytesRead, _seekBackBuffer, 0, bytesReadDiff); + Buffer.BlockCopy(tempBuffer, 0, _seekBackBuffer, bytesReadDiff, bytesRead); + } + } + else + { + if (bytesRead > 0) + Buffer.BlockCopy(_seekBackBuffer, 0, _seekBackBuffer, bytesReadDiff, bytesRead); + Buffer.BlockCopy(tempBuffer, bytesRead, _seekBackBuffer, 0, bytesReadDiff); + } + if (offset > 0) + throw new NotSupportedException("Reached end of stream seeking forward " + origOffset + " bytes"); + } + fillTempBuffer = !fillTempBuffer; + } + } + return Position; + } + + private long SeekBackwards(long offset) + { + var intOffset = (int)offset; + if (offset > int.MaxValue || intOffset > _seekBackBufferIndex) + throw new NotSupportedException("Cannot currently seek backwards more than " + _seekBackBufferIndex + " bytes"); + _seekBackBufferIndex -= intOffset; + return Position; + } + + private long SeekFromEnd(long offset) + { + var intOffset = (int)offset; + var seekBackBufferLength = _seekBackBuffer.Length; + if (offset > int.MaxValue || intOffset > seekBackBufferLength) + throw new NotSupportedException("Cannot seek backwards from end more than " + seekBackBufferLength + " bytes"); + + // first completely fill seekBackBuffer to remove special cases from while loop below + if (_seekBackBufferCount < seekBackBufferLength) + { + var maxRead = seekBackBufferLength - _seekBackBufferCount; + var bytesRead = _underlyingStream.Read(_seekBackBuffer, _seekBackBufferCount, maxRead); + _underlyingPosition += bytesRead; + _seekBackBufferCount += bytesRead; + _seekBackBufferIndex = Math.Max(0, _seekBackBufferCount - intOffset); + if (bytesRead < maxRead) + { + if (_seekBackBufferCount < intOffset) + throw new NotSupportedException("Could not seek backwards from end " + intOffset + " bytes"); + return Position; + } + } + else + { + _seekBackBufferIndex = _seekBackBufferCount; + } + + // now alternate between filling tempBuffer and seekBackBuffer + bool fillTempBuffer = true; + var tempBuffer = new byte[seekBackBufferLength]; + while (true) + { + var bytesRead = _underlyingStream.Read(fillTempBuffer ? tempBuffer : _seekBackBuffer, 0, seekBackBufferLength); + _underlyingPosition += bytesRead; + var bytesReadDiff = seekBackBufferLength - bytesRead; + if (bytesReadDiff > 0) // reached end-of-stream + { + if (fillTempBuffer) + { + if (bytesRead > 0) + { + Buffer.BlockCopy(_seekBackBuffer, bytesRead, _seekBackBuffer, 0, bytesReadDiff); + Buffer.BlockCopy(tempBuffer, 0, _seekBackBuffer, bytesReadDiff, bytesRead); + } + } + else + { + if (bytesRead > 0) + Buffer.BlockCopy(_seekBackBuffer, 0, _seekBackBuffer, bytesReadDiff, bytesRead); + Buffer.BlockCopy(tempBuffer, bytesRead, _seekBackBuffer, 0, bytesReadDiff); + } + _seekBackBufferIndex -= intOffset; + return Position; + } + fillTempBuffer = !fillTempBuffer; + } + } + + public override long Position + { + get { return _underlyingPosition - (_seekBackBufferCount - _seekBackBufferIndex); } + set { Seek(value, SeekOrigin.Begin); } + } + + public override bool CanTimeout { get { return _underlyingStream.CanTimeout; } } + public override bool CanWrite { get { return _underlyingStream.CanWrite; } } + public override long Length { get { return _underlyingStream.Length; } } + public override void SetLength(long value) { _underlyingStream.SetLength(value); } + public override void Write(byte[] buffer, int offset, int count) { _underlyingStream.Write(buffer, offset, count); } + public override void Flush() { _underlyingStream.Flush(); } + public override void Close() { _underlyingStream.Close(); } + public new void Dispose() { _underlyingStream.Dispose(); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + _underlyingStream.Dispose(); + } + } + + } + + // For reading a compressed stream or normal stream + internal class DecompressedStream : Stream + { + private Stream UnderlyingStream; + private bool IsSourceCompressed; + private UInt64 DecompressedLength; + private Int64 ReadPosition = 0; + + // For reading a compressed stream + internal DecompressedStream(Stream InputStream) + { + UnderlyingStream = new ReadSeekableStream(InputStream, 0x100); + + byte[] Signature = new byte["CompressedPartition".Length + 2]; + Signature[0x00] = 0xFF; + Buffer.BlockCopy(Encoding.ASCII.GetBytes("CompressedPartition"), 0, Signature, 0x01, "CompressedPartition".Length); + Signature["CompressedPartition".Length + 1] = 0x00; + + int PrimaryHeaderSize = 0x0A + "CompressedPartition".Length; + byte[] SignatureRead = new byte[Signature.Length]; + UnderlyingStream.Read(SignatureRead, 0, Signature.Length); + + IsSourceCompressed = StructuralComparisons.StructuralEqualityComparer.Equals(Signature, SignatureRead); + if (IsSourceCompressed) + { + byte[] FormatVersionBytes = new byte[4]; + UnderlyingStream.Read(FormatVersionBytes, 0, 4); + if (BitConverter.ToUInt32(FormatVersionBytes, 0) > 1) // Max supported format version = 1 + throw new InvalidDataException(); + + byte[] HeaderSizeBytes = new byte[4]; + UnderlyingStream.Read(HeaderSizeBytes, 0, 4); + UInt32 HeaderSize = BitConverter.ToUInt32(HeaderSizeBytes, 0); + + if (HeaderSize >= (Signature.Length + 0x10)) + { + byte[] DecompressedLengthBytes = new byte[8]; + UnderlyingStream.Read(DecompressedLengthBytes, 0, 8); + DecompressedLength = BitConverter.ToUInt64(DecompressedLengthBytes, 0); + } + else + throw new InvalidDataException(); + + UInt32 HeaderBytesRemaining = (UInt32)(HeaderSize - Signature.Length - 0x10); + if (HeaderBytesRemaining > 0) + { + byte[] HeaderBytes = new byte[HeaderBytesRemaining]; + UnderlyingStream.Read(HeaderBytes, 0, (int)HeaderBytesRemaining); + } + + UnderlyingStream = new GZipStream(UnderlyingStream, CompressionMode.Decompress, false); + } + else + UnderlyingStream.Position = 0; + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override int Read(byte[] buffer, int offset, int count) + { + int RealCount = UnderlyingStream.Read(buffer, offset, count); + ReadPosition += RealCount; + return RealCount; + } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + public override long Position + { + get { return ReadPosition; } + set { throw new NotSupportedException(); } + } + public override bool CanTimeout { get { return UnderlyingStream.CanTimeout; } } + public override bool CanWrite { get { return true; } } + public override long Length + { + get + { + if (IsSourceCompressed) + return (long)DecompressedLength; + else + return UnderlyingStream.Length; + } + } + public override void SetLength(long value) { throw new NotSupportedException(); } + public override void Write(byte[] buffer, int offset, int count) {throw new NotSupportedException(); } + public override void Flush() { UnderlyingStream.Flush(); } + public override void Close() { UnderlyingStream.Close(); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.UnderlyingStream.Dispose(); + } + } + } + + // For writing a compressed stream + internal class CompressedStream: Stream + { + private UInt32 HeaderSize; + private UInt64 WritePosition; + private GZipStream UnderlyingStream; + + internal CompressedStream(Stream OutputStream, UInt64 TotalDecompressedStreamLength) + { + // Write header + HeaderSize = (UInt32)(0x12 + "CompressedPartition".Length); + OutputStream.WriteByte(0xFF); + OutputStream.Write(Encoding.ASCII.GetBytes("CompressedPartition"), 0, "CompressedPartition".Length); + OutputStream.WriteByte(0x00); + OutputStream.Write(BitConverter.GetBytes((UInt32)1), 0, 4); // Format version = 1 + OutputStream.Write(BitConverter.GetBytes(HeaderSize), 0, 4); // Headersize + OutputStream.Write(BitConverter.GetBytes(TotalDecompressedStreamLength), 0, 8); + + this.UnderlyingStream = new GZipStream(OutputStream, CompressionLevel.Optimal, false); + WritePosition = 0; + } + + public override bool CanRead { get { return false; } } + public override bool CanSeek { get { return false; } } + public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } + public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } + public override long Position + { + get { return (long)WritePosition; } + set { throw new NotSupportedException(); } + } + public override bool CanTimeout { get { return UnderlyingStream.CanTimeout; } } + public override bool CanWrite { get { return true; } } + public override long Length { get { return (long)WritePosition; } } + public override void SetLength(long value) { throw new NotSupportedException(); } + public override void Write(byte[] buffer, int offset, int count) + { + WritePosition += (UInt64)count; + UnderlyingStream.Write(buffer, offset, count); + } + public override void Flush() { UnderlyingStream.Flush(); } + public override void Close() { UnderlyingStream.Close(); } + public new void Dispose() { UnderlyingStream.Dispose(); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.UnderlyingStream.Dispose(); + } + } + } + + internal class SeekableStream: Stream + { + private Stream UnderlyingStream; + private Int64 ReadPosition = 0; + private Func StreamInitializer; + private Int64 UnderlyingStreamLength; + + // For reading a compressed stream + internal SeekableStream(Func StreamInitializer, Int64? Length = null) + { + this.StreamInitializer = StreamInitializer; + UnderlyingStream = StreamInitializer(); + if (Length != null) + UnderlyingStreamLength = (Int64)Length; + else + { + try + { + UnderlyingStreamLength = UnderlyingStream.Length; + } + catch + { + throw new ArgumentException("Unknown stream length"); + } + } + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return true; } } + public override int Read(byte[] buffer, int offset, int count) + { + int RealCount = UnderlyingStream.Read(buffer, offset, count); + ReadPosition += RealCount; + return RealCount; + } + public override long Seek(long offset, SeekOrigin origin) + { + if (UnderlyingStream.CanSeek) + { + ReadPosition = UnderlyingStream.Seek(offset, origin); + return ReadPosition; + } + else + { + Int64 NewPosition = 0; + switch (origin) + { + case SeekOrigin.Begin: + NewPosition = offset; + break; + case SeekOrigin.Current: + NewPosition = ReadPosition + offset; + break; + case SeekOrigin.End: + NewPosition = UnderlyingStreamLength - offset; + break; + } + if ((NewPosition < 0) || (NewPosition > UnderlyingStreamLength)) + throw new ArgumentOutOfRangeException(); + if (NewPosition < ReadPosition) + { + UnderlyingStream.Close(); + UnderlyingStream = StreamInitializer(); + ReadPosition = 0; + } + UInt64 Remaining; + byte[] Buffer = new byte[16384]; + while (ReadPosition < NewPosition) + { + Remaining = (UInt64)(NewPosition - ReadPosition); + if (Remaining > (UInt64)Buffer.Length) + Remaining = (UInt64)Buffer.Length; + UnderlyingStream.Read(Buffer, 0, (int)Remaining); + ReadPosition += (long)Remaining; + } + return ReadPosition; + } + } + public override long Position + { + get + { + return ReadPosition; + } + set + { + Seek(value, SeekOrigin.Begin); + } + } + public override bool CanTimeout { get { return UnderlyingStream.CanTimeout; } } + public override bool CanWrite { get { return false; } } + public override long Length + { + get + { + return UnderlyingStreamLength; + } + } + public override void SetLength(long value) { throw new NotSupportedException(); } + public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } + public override void Flush() { throw new NotSupportedException(); } + public override void Close() { UnderlyingStream.Close(); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.UnderlyingStream.Dispose(); + } + } + } + + internal enum ResourceType + { + RT_ACCELERATOR = 9, //Accelerator table. + RT_ANICURSOR = 21, //Animated cursor. + RT_ANIICON = 22, //Animated icon. + RT_BITMAP = 2, //Bitmap resource. + RT_CURSOR = 1, //Hardware-dependent cursor resource. + RT_DIALOG = 5, //Dialog box. + RT_DLGINCLUDE = 17, //Allows + RT_FONT = 8, //Font resource. + RT_FONTDIR = 7, //Font directory resource. + RT_GROUP_CURSOR = ((RT_CURSOR) + 11), //Hardware-independent cursor resource. + RT_GROUP_ICON = ((RT_ICON) + 11), //Hardware-independent icon resource. + RT_HTML = 23, //HTML resource. + RT_ICON = 3, //Hardware-dependent icon resource. + RT_MANIFEST = 24, //Side-by-Side Assembly Manifest. + RT_MENU = 4, //Menu resource. + RT_MESSAGETABLE = 11, //Message-table entry. + RT_PLUGPLAY = 19, //Plug and Play resource. + RT_RCDATA = 10, //Application-defined resource (raw data). + RT_STRING = 6, //String-table entry. + RT_VERSION = 16, //Version resource. + RT_VXD = 20, // + RT_DLGINIT = 240, + RT_TOOLBAR = 241 + }; + + internal class PE + { + internal static byte[] GetResource(byte[] PEfile, int[] Index) + { + // Explanation of PE header here: + // https://msdn.microsoft.com/en-us/library/ms809762.aspx?f=255&MSPPError=-2147217396 + + UInt32 PEPointer = ByteOperations.ReadUInt32(PEfile, 0x3C); + UInt16 OptionalHeaderSize = ByteOperations.ReadUInt16(PEfile, PEPointer + 0x14); + UInt32 SectionTablePointer = PEPointer + 0x18 + OptionalHeaderSize; + UInt16 SectionCount = ByteOperations.ReadUInt16(PEfile, PEPointer + 0x06); + UInt32? ResourceSectionEntryPointer = null; + for (int i = 0; i < SectionCount; i++) + { + string SectionName = ByteOperations.ReadAsciiString(PEfile, (UInt32)(SectionTablePointer + (i * 0x28)), 8); + int e = SectionName.IndexOf('\0'); + if (e >= 0) + SectionName = SectionName.Substring(0, e); + if (SectionName == ".rsrc") + { + ResourceSectionEntryPointer = (UInt32)(SectionTablePointer + (i * 0x28)); + break; + } + } + if (ResourceSectionEntryPointer == null) + throw new WPinternalsException("Resource-section not found"); + UInt32 ResourceRawSize = ByteOperations.ReadUInt32(PEfile, (UInt32)ResourceSectionEntryPointer + 0x10); + UInt32 ResourceRawPointer = ByteOperations.ReadUInt32(PEfile, (UInt32)ResourceSectionEntryPointer + 0x14); + UInt32 ResourceVirtualPointer = ByteOperations.ReadUInt32(PEfile, (UInt32)ResourceSectionEntryPointer + 0x0C); + + UInt32 p = ResourceRawPointer; + for (int i = 0; i < Index.Length; i++) + { + UInt16 ResourceNamedEntryCount = ByteOperations.ReadUInt16(PEfile, p + 0x0c); + UInt16 ResourceIdEntryCount = ByteOperations.ReadUInt16(PEfile, p + 0x0e); + for (int j = ResourceNamedEntryCount; j < ResourceNamedEntryCount + ResourceIdEntryCount; j++) + { + UInt32 ResourceID = ByteOperations.ReadUInt32(PEfile, (UInt32)(p + 0x10 + (j * 8))); + UInt32 NextPointer = ByteOperations.ReadUInt32(PEfile, (UInt32)(p + 0x10 + (j * 8) + 4)); + if (ResourceID == (UInt32)Index[i]) + { + // Check high bit + if (((NextPointer & 0x80000000) == 0) != (i == (Index.Length - 1))) + throw new WPinternalsException("Bad resource path"); + + p = ResourceRawPointer + (NextPointer & 0x7fffffff); + break; + } + } + } + + UInt32 ResourceValuePointer = ByteOperations.ReadUInt32(PEfile, p) - ResourceVirtualPointer + ResourceRawPointer; + UInt32 ResourceValueSize = ByteOperations.ReadUInt32(PEfile, p + 4); + + byte[] ResourceValue = new byte[ResourceValueSize]; + Array.Copy(PEfile, ResourceValuePointer, ResourceValue, 0, ResourceValueSize); + + return ResourceValue; + } + + internal static Version GetFileVersion(byte[] PEfile) + { + byte[] version = PE.GetResource(PEfile, new int[] { (int)ResourceType.RT_VERSION, 1, 1033 }); + + // RT_VERSION format: + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647001(v=vs.85).aspx + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms646997(v=vs.85).aspx + UInt32 FixedFileInfoPointer = 0x28; + UInt16 Major = ByteOperations.ReadUInt16(version, FixedFileInfoPointer + 0x0A); + UInt16 Minor = ByteOperations.ReadUInt16(version, FixedFileInfoPointer + 0x08); + UInt16 Build = ByteOperations.ReadUInt16(version, FixedFileInfoPointer + 0x0E); + UInt16 Revision = ByteOperations.ReadUInt16(version, FixedFileInfoPointer + 0x0C); + + return new Version(Major, Minor, Build, Revision); + } + + internal static Version GetProductVersion(byte[] PEfile) + { + byte[] version = PE.GetResource(PEfile, new int[] { (int)ResourceType.RT_VERSION, 1, 1033 }); + + // RT_VERSION format: + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms647001(v=vs.85).aspx + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms646997(v=vs.85).aspx + UInt32 FixedFileInfoPointer = 0x28; + UInt16 Major = ByteOperations.ReadUInt16(version, FixedFileInfoPointer + 0x12); + UInt16 Minor = ByteOperations.ReadUInt16(version, FixedFileInfoPointer + 0x10); + UInt16 Build = ByteOperations.ReadUInt16(version, FixedFileInfoPointer + 0x16); + UInt16 Revision = ByteOperations.ReadUInt16(version, FixedFileInfoPointer + 0x14); + + return new Version(Major, Minor, Build, Revision); + } + } + +#if PREVIEW + internal static class Uploader + { + internal static List Uploads = new List(); + + internal static void Upload(string FileName, string Text) + { + byte[] byteArray = Encoding.UTF8.GetBytes(Text); + MemoryStream FileStream = new MemoryStream(byteArray); + Upload(FileName, FileStream); + } + + internal static void Upload(string FileName, byte[] Data) + { + Upload(FileName, new MemoryStream(Data)); + } + + internal static void Upload(string FileName, Stream FileStream) + { + Upload(new Uri(@"https://www.wpinternals.net/upload.php", UriKind.Absolute), "uploadedfile", FileName, FileStream); + } + + private static void Upload(Uri Address, string InputName, string FileName, Stream FileStream) + { + System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; + System.Net.Http.HttpClient httpClient = new System.Net.Http.HttpClient(); + System.Net.Http.MultipartFormDataContent form = new System.Net.Http.MultipartFormDataContent(); + System.Net.Http.StreamContent Content = new System.Net.Http.StreamContent(FileStream); + Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain"); + form.Add(Content, InputName, FileName); + Task UploadTask = httpClient.PostAsync(Address, form); + + Uploads.Add( + UploadTask.ContinueWith((t) => + { + Uploads.Remove(t); + httpClient.Dispose(); + }) + ); + } + + internal static void WaitForUploads() + { + Task.WaitAll(Uploads.ToArray()); + } + } +#endif + + internal class AsyncAutoResetEvent + { + readonly LinkedList> waiters = + new LinkedList>(); + + bool isSignaled; + + public AsyncAutoResetEvent(bool signaled) + { + this.isSignaled = signaled; + } + + public Task WaitAsync(TimeSpan timeout) + { + return this.WaitAsync(timeout, CancellationToken.None); + } + + public async Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken) + { + TaskCompletionSource tcs; + + lock (this.waiters) + { + if (this.isSignaled) + { + this.isSignaled = false; + return true; + } + else if (timeout == TimeSpan.Zero) + { + return this.isSignaled; + } + else + { + tcs = new TaskCompletionSource(); + this.waiters.AddLast(tcs); + } + } + + Task winner = await Task.WhenAny(tcs.Task, Task.Delay(timeout, cancellationToken)); + if (winner == tcs.Task) + { + // The task was signaled. + return true; + } + else + { + // We timed-out; remove our reference to the task. + // This is an O(n) operation since waiters is a LinkedList. + lock (this.waiters) + { + bool removed = this.waiters.Remove(tcs); + System.Diagnostics.Debug.Assert(removed); + return false; + } + } + } + + public void Set() + { + TaskCompletionSource toRelease = null; + + lock (this.waiters) + { + if (this.waiters.Count > 0) + { + // Signal the first task in the waiters list. + toRelease = this.waiters.First.Value; + this.waiters.RemoveFirst(); + } + else if (!this.isSignaled) + { + // No tasks are pending + this.isSignaled = true; + } + } + + if (toRelease != null) + { + toRelease.SetResult(true); + } + } + } + + // This class was written by: Rolf Wessels + // https://github.com/rolfwessels/lazycowprojects/tree/master/Wpf + + /// + /// Static class used to attach to wpf control + /// + public static class GridViewColumnResize + { +#region DependencyProperties + + public static readonly DependencyProperty WidthProperty = + DependencyProperty.RegisterAttached("Width", typeof(string), typeof(GridViewColumnResize), + new PropertyMetadata(OnSetWidthCallback)); + + public static readonly DependencyProperty GridViewColumnResizeBehaviorProperty = + DependencyProperty.RegisterAttached("GridViewColumnResizeBehavior", + typeof(GridViewColumnResizeBehavior), typeof(GridViewColumnResize), + null); + + public static readonly DependencyProperty EnabledProperty = + DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(GridViewColumnResize), + new PropertyMetadata(OnSetEnabledCallback)); + + public static readonly DependencyProperty ListViewResizeBehaviorProperty = + DependencyProperty.RegisterAttached("ListViewResizeBehaviorProperty", + typeof(ListViewResizeBehavior), typeof(GridViewColumnResize), null); + +#endregion + + public static string GetWidth(DependencyObject obj) + { + return (string)obj.GetValue(WidthProperty); + } + + public static void SetWidth(DependencyObject obj, string value) + { + obj.SetValue(WidthProperty, value); + } + + public static bool GetEnabled(DependencyObject obj) + { + return (bool)obj.GetValue(EnabledProperty); + } + + public static void SetEnabled(DependencyObject obj, bool value) + { + obj.SetValue(EnabledProperty, value); + } + +#region CallBack + + private static void OnSetWidthCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + { + var element = dependencyObject as GridViewColumn; + if (element != null) + { + GridViewColumnResizeBehavior behavior = GetOrCreateBehavior(element); + behavior.Width = e.NewValue as string; + } + else + { + Console.Error.WriteLine("Error: Expected type GridViewColumn but found " + + dependencyObject.GetType().Name); + } + } + + private static void OnSetEnabledCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + { + var element = dependencyObject as ListView; + if (element != null) + { + ListViewResizeBehavior behavior = GetOrCreateBehavior(element); + behavior.Enabled = (bool)e.NewValue; + } + else + { + Console.Error.WriteLine("Error: Expected type ListView but found " + dependencyObject.GetType().Name); + } + } + + private static ListViewResizeBehavior GetOrCreateBehavior(ListView element) + { + var behavior = element.GetValue(GridViewColumnResizeBehaviorProperty) as ListViewResizeBehavior; + if (behavior == null) + { + behavior = new ListViewResizeBehavior(element); + element.SetValue(ListViewResizeBehaviorProperty, behavior); + } + + return behavior; + } + + private static GridViewColumnResizeBehavior GetOrCreateBehavior(GridViewColumn element) + { + var behavior = element.GetValue(GridViewColumnResizeBehaviorProperty) as GridViewColumnResizeBehavior; + if (behavior == null) + { + behavior = new GridViewColumnResizeBehavior(element); + element.SetValue(GridViewColumnResizeBehaviorProperty, behavior); + } + + return behavior; + } + +#endregion + +#region Nested type: GridViewColumnResizeBehavior + + // This class was written by: Rolf Wessels + // https://github.com/rolfwessels/lazycowprojects/tree/master/Wpf + + /// + /// GridViewColumn class that gets attached to the GridViewColumn control + /// + public class GridViewColumnResizeBehavior + { + private readonly GridViewColumn _element; + + public GridViewColumnResizeBehavior(GridViewColumn element) + { + _element = element; + } + + public string Width { get; set; } + + public bool IsStatic + { + get { return StaticWidth >= 0; } + } + + public double StaticWidth + { + get + { + double result; + return double.TryParse(Width, out result) ? result : -1; + } + } + + public double Percentage + { + get + { + if (!IsStatic) + { + return Mulitplier * 100; + } + return 0; + } + } + + public double Mulitplier + { + get + { + if (Width == "*" || Width == "1*") return 1; + if (Width.EndsWith("*")) + { + double perc; + if (double.TryParse(Width.Substring(0, Width.Length - 1), out perc)) + { + return perc; + } + } + return 1; + } + } + + public void SetWidth(double allowedSpace, double totalPercentage) + { + if (IsStatic) + { + _element.Width = StaticWidth; + } + else + { + double width = Math.Max(allowedSpace * (Percentage / totalPercentage), 0); + _element.Width = width; + } + } + } + +#endregion + +#region Nested type: ListViewResizeBehavior + + // This class was written by: Rolf Wessels + // https://github.com/rolfwessels/lazycowprojects/tree/master/Wpf + + /// + /// ListViewResizeBehavior class that gets attached to the ListView control + /// + public class ListViewResizeBehavior + { + private const int Margin = 25; + private const long RefreshTime = Timeout.Infinite; + private const long Delay = 500; + + private readonly ListView _element; + private readonly Timer _timer; + + public ListViewResizeBehavior(ListView element) + { + if (element == null) throw new ArgumentNullException("element"); + _element = element; + element.Loaded += OnLoaded; + + // Action for resizing and re-enable the size lookup + // This stops the columns from constantly resizing to improve performance + Action resizeAndEnableSize = () => + { + Resize(); + _element.SizeChanged += OnSizeChanged; + }; + _timer = new Timer(x => Application.Current.Dispatcher.BeginInvoke(resizeAndEnableSize), null, Delay, + RefreshTime); + } + + public bool Enabled { get; set; } + + + private void OnLoaded(object sender, RoutedEventArgs e) + { + _element.SizeChanged += OnSizeChanged; + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + if (e.WidthChanged) + { + _element.SizeChanged -= OnSizeChanged; + _timer.Change(Delay, RefreshTime); + } + } + + private void Resize() + { + if (Enabled) + { + double totalWidth = _element.ActualWidth; + var gv = _element.View as GridView; + if (gv != null) + { + double allowedSpace = totalWidth - GetAllocatedSpace(gv); + allowedSpace = allowedSpace - Margin; + double totalPercentage = GridViewColumnResizeBehaviors(gv).Sum(x => x.Percentage); + foreach (GridViewColumnResizeBehavior behavior in GridViewColumnResizeBehaviors(gv)) + { + behavior.SetWidth(allowedSpace, totalPercentage); + } + } + } + } + + private static IEnumerable GridViewColumnResizeBehaviors(GridView gv) + { + foreach (GridViewColumn t in gv.Columns) + { + var gridViewColumnResizeBehavior = + t.GetValue(GridViewColumnResizeBehaviorProperty) as GridViewColumnResizeBehavior; + if (gridViewColumnResizeBehavior != null) + { + yield return gridViewColumnResizeBehavior; + } + } + } + + private static double GetAllocatedSpace(GridView gv) + { + double totalWidth = 0; + foreach (GridViewColumn t in gv.Columns) + { + var gridViewColumnResizeBehavior = + t.GetValue(GridViewColumnResizeBehaviorProperty) as GridViewColumnResizeBehavior; + if (gridViewColumnResizeBehavior != null) + { + if (gridViewColumnResizeBehavior.IsStatic) + { + totalWidth += gridViewColumnResizeBehavior.StaticWidth; + } + } + else + { + totalWidth += t.ActualWidth; + } + } + return totalWidth; + } + } + +#endregion + } + + internal static class ExtensionMethods + { + // This method was written by: Lawrence Johnston + // https://stackoverflow.com/a/22078975 + public static async Task TimeoutAfter(this Task task, TimeSpan timeout) + { + + using (var timeoutCancellationTokenSource = new CancellationTokenSource()) + { + + var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)); + if (completedTask == task) + { + timeoutCancellationTokenSource.Cancel(); + return await task; // Very important in order to propagate exceptions + } + else + { + throw new TimeoutException("The operation has timed out."); + } + } + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f879c1c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Logo-Small.png b/Logo-Small.png new file mode 100644 index 0000000000000000000000000000000000000000..d5eda8874e3215a085d15e98ca8555c73a022010 GIT binary patch literal 10176 zcmV;xCqLMUP)-Ia57b#qGtA1!Xb{SN52zx^#VG&l&c*=$I!*FlQ{4#Z~7LR2bye@jbC z^Im_Cf0uF9;lt3w4?he&_0&_)%P+qSO;6840>KKjyu1$J&><3u40U&R9K?4JF6tVa zy`gW8eN%gT%NhUi{PUl<^fRB?>u>*WBbu6;j{EO#xZwuq@n8K4>L3uHfq?;t&F4cR zsT7jPHX$yH0nN@#HTU-qoPqD|zp{ID_MgTmOOKOQ4P7i{SV-6Ay+j>S8KNnlCJ=CE zzxvv1r(SZ&C3}6VAOF~fb;%`{LC-$>9Msj>39(2dNVp_`M51+QZIKT#h_leh(BOf- zfuXZ;9Rt_)Pmuq4f+_wzO>7@pG9*M&ciF27B1K&ULA2nK`4L3mM39*kC#9>4a2=te zqbP!bl0gDH|h5$r?jS&O{wtwX!8vucFWd#-y{WumAqe z&W^96z+QjjjZ?0=`s%&D<&S?n^Z4VS?#x4DGsMqKup~d5UADYRlSgKFru@dTqh?=s zBVmaTi38Onl>S;=6R5@w!Ajf`sU#iIa@rFwXZ@*i&Y!Lnys28r8b|6fZzVT4x$r6h z-*#I^XUFHey1PHsj>GM{;G7bp58Cxx(Ckb#`m5wI5Gr{Oin;ULxa%7#5goEG6KE!m%l)7yzvI~;DZlB z|MtKG&?R5{8g${IbN2eNoVfAabANycJp-pvF&9H^2D} zMCc$uB(eK>z5~fD*~^JlTP`KCWilJ~bXH=|z?+UVEO%yd3RgC#a%b~e4_wgu;DRBL zEn0$b(H6>;tl>h*94?o2A*2TW|IE~yMl`2#u}F^W{6Y|!opNMmgYB%>RG9QOEyO+6kln6!C&no>m&clHrLh# zmb!VxRxz*IEA};e)wSlVd)Hlc|GEnafjSvlb77Yk{JC}6Qcw#FDIrS{AW=p3-Wi_a zm1)+-lLOQAd-})bzt`D2a9u}F-<6$R-RJf7_nkR9GJM+fzS6S zmT#*j1eSbEXwD>8OzHHRIh{eFiL9xt#Fm1k4h&jO(SVlMc(Qq216on<&z01^a#`xF zS7{2jeNLnnl677d-IXO#1JwnpEB;f40q$P3)U2!KqG8<(n>VZ(tJs>giEXf5;>bCq z&Vp;xS#)o@N*wzjqtQ-<5U*0;a?ZD???9~zkq>p!Vd7N^h|w@zg6ET`V-ard%Vd9UQtNL81H06q3LA z3e6gQgk!Gim&_&Inkj2oH>J!Q=A=z*N!g{=lw;GHcFF8%x7?BOD4ba@_`7{T#Q}{c z7u0(5A)UVv*82+)L!cNn1WR#4sGKl_D=A~7mIduZ-UM0-@N3l&MG!r>&^}NDQnNaU z8r{T_sR346t$p&FTJOFl`{q+jKjD!N5u zP9Zd;)GPXgeodb=t{GFNW1&f`X@?X*+q9>)pk+~Le$5s%1Au1oXQJjn7K3IF!AVC1 zPPwAFj5n48EkzyWCGnaNUC- zV-(?^3551*0kE^@h{O{Q*o!Z|2u)6oK{JcmTN&oMf@v-W*hVkf5X^)ecfg+I_s?L#&(R*oHc$lRx%DMWiWKoxt3v_Xn zP#@Pcpd~&Kn*6xX;G@t2?*T3OPG}_z8c5dKz$RW^XKJoC&N^>0@vnc>e7Djz(Z_pJi^h(qFe)}G5{?Z7c`(H z8_-f1v_?HkXIHH-ylkuFxyD>_enT%NDXkoaH9}!|^F&&>MkcvZf1oM@PXKjE*t*<0 zSDO^`HF4RZCa!EiO8{tz<3f|{Kui7Sp(Wl8Edi7k&Qz;bP{WpVQ7aCQe*mcK8|^sU zc`O!dUrS5NUSn#+x#ymXje+LL)YGQcLw}^3Y6`kB=Vs_qUWPUqU}_Q}768W5#iML} zBFX{4xVluFr%xsLhEx(5krZ%`sU>4JEi`2_OBOgQuoQDVOBp6ft?C7tkw-TLXe>{b z$Ou*E$$@e&(GdPunl4Xg>T+uwO=^{^iHmuf*d~A`N1-hpg{FOfrR|2M+MOQ5Cf`wL zw)cS+&6J9fY^CH*)GF!_QXeDn@pydutw3E@gL;12%*@PQV>%j-{^C(+Zf0uV^s@6I zioPzS7z$3RF6*Ib(|(#Z6{KrYVTLvpVd&CPptu-ApN%v1aDrvXCE3PYifzoNIi^B} zYb<7Y#v;r$S8{Aiy+Bb0Ec2VdWEi{*mMb;S43_7pk@^DJ7`&gNi2aqWE3Ps%nI*P5 zzRFQYH+Y(;gnt}p!lTd(C^XTBgr;jK4TEOh1uge3XrWZG;7eAErWk@OY9mHG0e^pI zSJw?dW#@6Y+G=hKh--FS;nYoQJA~eN=5WK-FdfG+h>4 znZ~7u{pj!Mx(xU=?W5^Z0B+Jv*QQ)_ZPEq)*-2J~jUd%$zEdu#aepl8BZBcfp$Ak*x_1$r@vkEfEH_anZp=qglc*YOlyS9vL_bUt_+dm%gwTaJMQMU2IihxsgdWC>i$ zY-MDXqlyA(Tj^09{VzjP?}CQbw9K~UPi#YTL<$*WsFIWWk>cn)?=3vO?Ry=aou7TX zrRCrQ4?KW9tERmg5O06sr=EHmqSJ`bqS$;pRafRyWRc;8HPi2=S?jltvV_<7b2q*; zx~Ba8xXk+4j6U)j(E$&FzKKZnSE#f=l}7Vd(6JJU5h%?w10^CWP?}?dixnu$vHgWv zPM|cy36^Hqq4EqfQd=N<^Q{Xq=f9AYxjBk5O{U4?6uKweMi;oRci`J@?&#_|Odt>r{`!e0_72u|+<4=S(BR-8 zL?V$+XR6Z!0xM!%wU+ej*1ToIp0{r}iY~FU;+43nVX?cO5POiM$XyLA+w!2fjSW(j zw#Q~yr8kZ8#fOL4tC#i(W!H^KO+Osf2A-O>rQe?Sm4_)|00;o2MG-PNj4&4LiQiM? z-Umsl0+lS!%#$}`3lwRXOp}Iaw9W7?Xw3J9_OA4}{7z`b_vzD)1I^%vqdH$9q43pn zY*knW)O8O~*FOSvoijc@zVGInZ{Ay2`wB2|D|)Dc&vx;p-xv~`?;bP6{xEKb|2!mde|J_@86~N!46*_ylVu4CSr(;I zq!Aib8m3b>!%UDK*&sdg7#Oq&295RJ&=eRnU3_ER7mA{sexbh}} zE<~JPH#|4a7XP4|ymWalo%`ty#?pm-G|~6RxcXnut_4~bHj`6iMV3ZZWEf;wl0lKh znG{)+O_N2rbXkWVHNoTZ>cE^iZZ=#X|%_g=1d8`7e^>dcXss;|3hzg_Z8j!BVQbxVc$N^RzE$n z=EcoR6EkFamPJuy_!LD-KvN_Hba{M1pJkD&I<8Ghv}UxE7j`ydjPaXM9*X=NL$ z>pXo{B+w<%>9_{FQ3Gm`lNO*d8>TV`rZR6sWgzGKvJtdM11p?xM&K0MaQYJk7(J?p zX3NDGTq%dMRqSj@6G9M<0k#ZIE&L-xmt%5mh;Pw`IQZt0nyHG?7M4wKj*}Pf8yFh@ zYIk?fXL|<6zA!k;`QZdx{oC0!ZzoBXBvVwdkgA5)=<3WmU6ovCs1hQkDlTTL;}Q-q zAzV#dhC$PA&mMO}1NUP-9yBz2$iRJVI&uM#vzDOCf@=f<{$D`t`Z5lWKYMC=y7>n` z_`%+Q+K!iAdI=hz7;jn-c^+dKYfEe$EaYjEVsNu^Pz%&(N?FyX4C}_EX)FI9t1=&^ zG7sEP89GP}!%4K^^u_bpV5*RdW=q8cTrNkm6%3df0Gp)n|8-7a`2C^X-h zTD6z$YxbIZ)rt65Ty?+DS~7DqaXxv?+&)PaJUB2sabs6k_to9~!`BW?vu>T_YMv&F zeEnp3nnTm%Ht3q1jG@WOfLW5UG$|QJom7DPQ1Z1&l|YwJFX`V^)7}lumUc;PSNS^3j_l0TR1%7aw3s=UT{iw>teFW@ zq{RmZ$ELs92U2KH-|#;S&2sLT;A)>Gih?6lB`lz8i*lx}pk--uTDCT;;c7D)zAmj@ z)TeYy`jmbdILB3xHP(!~p-C)h=Y}=sS+!Rqv`xRTgMhmO)Gl;I{1B7LY`%IgHjcN? zx6n1N(UBo&l*qcAswoXHwFL%Co#ug*vH}`YY&s6=fMQE!+SRQ_blYV~F_oo&%CbHU zNR7Z*9am-kWHIkeR*F^tOYAJzho=_4Op`}v`KFRdXo9uC)S&9Mh_-e#fE#t9Z~CSq zGcfWG zgEREorWdu(k|d#VnmV__(3Ny-Jy5K^VB+d?M!r646c{olp)q4#F{CZ);GRU_Mcgo_ z9jlgtM_{Q2hUQp*0?zOoAQfIV2h{cEn{V!oy3jz6Ox}3o2sAn|^GQ$>UZiPDB!((Q zXDQ=+P{3C}0T%&a67&qJp)&L+BdJ?$sLU+3Br%nNLhZs-=0sJN38FJO^avhR8G2OV ziRCk{SSe?V)`}n%R;Q`U_f2zEPXTqA77eg!*_Z~VCS_=(rv&<@W5x!plOXM)&dt-9 z`(^#;QjOY^4{E%{kj7UEtNoQ2aGz;qV-qSb@m8ZNrnHwf0FVjdcwlJs^JDY;J7@TYm&uzE3R9QYa7|Sg&s_6y z%{4#URQC_fvi<!U?Hsw)`}aBl4od= z{2h`cIK(y-Ra|{$6Bq&I(LSkBBf-mH7MUB+q+8HryZ18!*w;YQFIF=f3pKV-E@6x0 zllEva?TD5$_GmS0i`Da>W>$=`#@d=HP|s{wGx8<5Ylylier#l7?kmInJ?Hk1jD2p5 z#J`6qG`$H7%+l1ddlFB;e+$&EYd|{uv_v9l{{HvBzjx|->CgWGO;LrHk<`^5vZ^#q zmZxSZn^7_iohYCqAbDhw4OGTaMc1$)>(;CeRawItgNKVw!p%v`w#v3_KK4Md(GF06 z!Jm(7{iXD#A1TkUL{E<~HXlP_akN=6Uz?Hw7pef3TfKeUzuFifZM_VM?MB^!*2)I7 zj90Y-4P9MBq3vuzCANcm4wc&t8sYT)o-`7TtKE!P8lB`$DZ4E?f)N-KR)|gsDD;) z<;+IpKS;{b7+IO8kYx!PfX1N8q8x@i!pElLiYPi67jYXWy)r@Lz_4NSLEU+`$}-;V zW?JWkBWiCUF7wulTxF2aKTiBE=zlH zn(Y?@8`L(=vCz7SnjKdZMh_k{=rYYQYimW&50*e{QI2G*m2jq33uNkuHw6I35d^$c z2-||>#|4gF9IovO0P8~F3{L}AzWJt`ZrVF_q4CtyPdC5))|;Qe<8fyXOf1|u$<_UC zK^hw&D>6)qJjJ2P<2<@NhE5VzSlHMH)Uu-!anIoJ%ETH=72g04S;|AVmbC9snGsW2qnWlh9;m_r`!}7%2L=q8i`eG!B^(2^*$_cI}$-GkbO(J=)uXz^`_x_!it z^@1q0OtBCL(6(0AD%oT94AC^nQ~DKnLi=678D2LuJbWgdPRE|to~6QLa zD&h8n1vO`DWCc`^1`jmjkvMXGk*k(sDI>grvH7oobUQ~=Wq1HSkEM(X*~;h|I{V{p zD_4!JuPK(aM~AhmyG@97yN$r=<-1HswlQhhHX+BXt$CtoYRqK=D6|y1{e(f=-ZQKu za+R7lRYx?zdTn@ya)dx=zXbrh>h(9?IQX-l{cNu>?T+iNyAA{Q%B!z74Gat%06F`U z?YQ)$KBFW+;y0*B<(B_qFZAQt{rc{f%q*e$X>S`l>?Wjy_ z&AQj^#o&^);vJu7-#)Wsd5NmZ^5}{rXc?kh&@!M$BP%F0tUjt^JLwjyXWFAXhdU-@ z2igaikYk~3ZS^{_UT_B*y8VPgYwQ`q<#I4xt(xO#%i)oN`~Eio>ubQupVQah-;93X z?}Us4`}ac+Kk^9l>T9nx4UdfM>+0@42%5!D0caQC@okrN^$vV(guL=gq9`y%*A=BK zeM!gB=X891M!#f88&|OLPRc4WryOgRoO{_`kI(Q`uZ$2E@1`g+45~au1vQZlj11;L z0WcvjG%G-5YuJPxJr+3@nhtZLjTU0Zgd{(L3CZj-A%xrJ;hS7mEFd^YgrR0p) zs|sJOI5;u?5`lpK9-h!~<;cj$&dl(HkK1p*9s5}6z~EprfQF{aGeJ*sJ|5S4X-7xL z7Y3&3_s%ZcaCA*>onx$+dB&neV1&&=BWx9#Vdt{75@9M6{NCZ|Z!Td-TzlIk9R%Ff zy+f1VpJXXsq$!eIj;U(lnW`?Xsp93DYp!7;>y~Mr{&})ILnBG!M5-hLfQ4xE%@Dd& z!vPICe+$|&NR3CAYK}tt027k>a3;k6ZWH3$+B5Vd%6V(FUY2?)aNp?cA3Hh-H@D+( zmou2mQ_xRaqOqsW@IM>poP9Qg7GGdWnt*fKH!(SR5Zu@y9IpLhTzkvmj;@{?hUXU_ zCy4`dG(+7oD=`0VXpaBS3-TO|w3%EWOJnm?Nti^HhNv`Yh)&-Ovl^RF+r8cnw4)|u z%Q+rvLf$h2r4VsbUyLQ+*2WI0SC zZzf1&X@o+NhAC8Oczda)0S&EZ4QTIeLUg;F<1P~-`M^mC2JIc62g{}Gkz&>msujep zQmk)i;%T6+n?UOS%;eP6DL5PsyVsAUy51|$H}KhKpM{t#Ruj0%10$oOAYFEx*V)x^ zPD>l%!p?!IFON`V56p?eZ6rm8M3Sc&q|G>$ER9mB(g?OxLqk(zqn>S @8~3CmkEg*1BIm8Uxim~`9ROW*sp+*`W8s>mypQhPoO{U z;duA>HyNm_LXFB#@4K(*$m_2kXl-l5dO8BW=`PgrKM427fN5^%avcA5xeXC?`Sfx&_p&YkT|p z&J>A6O=llEwAYVuZ{ps2??v}Snp#?0_kp+fRL~xrg~#L0?I5&Y)YaW{&CoRS_L)WV zi=@rO3`GU=(V;L+kyv8vY(d4=w}#IDH4}2oaSVVK)_4msxwo2G0_{h4-{8ZYogFuV z7waNAoqpiRkt5jm{&%6S_X>2%jqZ#9#i2dj0RZolI6MxWq@3SHXuGU;aN>qB>IT}7 z_fnKFkEViG>B{sfLm6LVDPtnG8l7QoudT(7nS{jObsY0SlMsy?4k_Hl=!OT$&T^!E zps)NMs9o0pU}vr=XXFu>zVoeOo-f-3yEFT zBu|$%c6Rsvth2N8Yrq*^FflQ)|Ad&xpQ!Pj?|cV(;)y4q?w+3J{(%9kAqTbYY&@>* zqOMNDmA%80-x{Nd|IhqJXq2YTuhF%6baibLTU*m4um&F>&;sqb^YY_;pEBqfH>={v!F*GeG~#}YZ@FH+6U6pso)`gYHJMk z8DIv!I5e3mEJNAAF%*nEebyk*XN*FS7FUdE^O`AX5m_1^-Vj-` zt`%D)#8gB#Iy*Y<$M8S%4Bh;L|? z{S2sGUq@H2XXoZlot~cF>s$HziZ6WO3(zyqJcG_JoBI0t_79JYoCe(H*=_A@mjH)( z82wo5;57Z;XIC79(?UmIcL(7{kOn`89-pvStY$Rbo{T+;Z+`Qe*iY1Ev)Rp5D)kg} zE(qXVKp?bTPQbr?4KOR82WI0+Oo@2>sYkYI*U2V%kK-FR-U$8muYZNmpU(@R?Z*=c zpKNPy|1{{g4xvXUz`z{D;c(4YTye!-f2${B|Ni~EevkfS?B3p9^k>*NKlJd!&Hw(u u1I^Dq_gwSik3Ww6`AKJZGETOJ~w9OVyA70000Q=l(W1eMLx2JWinK^^m*Q5O;_j~h ze7?Wyeh>G9b27#r<7Aw(_ma8RoGVI0O%WISB{l#6z*U0EX#oI01oFbiME`Fk?FR+& z2g6NCRUTs*6_tz#D1{@3s1`x_AV8(WsIkaQFa&-*t`pHA?+JPFZgsv3C z$T$prsg&$Wt$L8y2k5}`) zvl64eId2W@ic?_>Vy` zU>|ln(RTeX^N25YB7^Hj@xIIYY4b-VoL&&cRAnZ~VS&{+YC z=h1^Pwn(k}R-3HC`DNeb@1dOesZemZU zH0M3#;Vr8ejI%3Z%zN$BsTuY08vJ~@PO~C)yG*?D``>sUb@JKjZp24n=d~x?{Ly2t zhto9hCnat|7(YZ@9YyJXfAsv=ge^K6Xsd`ujYSh;GrHW;=6W^-!OG2D4m|Cd-T@@0 zJ>9TKdS`q!6+8KorhVdT(rQ1u#3ku<@}!p~QhTne7*FLpoZ)}}QNsCbL-qdZIFQso zV7tvA;ep4`qD$1C4F zY+tutpE>!RXI3@bA5}H4`K~-avC%vod764(i(fZ?xWA%Vth1j*sSh$*owOSZNu0C?ER8g;0Y3aco2OX7#%D=(O)R z@I0|laV22yV%O8uVfXIuPR%u2E%p0j8L5L@?C24~_;*r&3DdwYMCGgdm@C~4dw$~8 zHq3k=n*AEPX7Cu@pa9NJ(g82BH>!3j z`a7u$NRS`5l!U}LZM+q~{icooukh%WK;lNcDq!bdgz59&II4OoN#`{x^=$F`r>2~L zqbgsMS2EpsO1AtSy6e^kz%pW$# zxu>T;`2x&=4d2!7dS0(WM$o%?J4evuK2nbZ3wrYU_d^P#I<|Z4g80!Am<98vu;BCM z#R%mX`UM+~Lku69p64fwpfO*-*c)!D7RtQkg8ygN?o|5*#nf0iT@h?2W5Z|{!(QhB z@zLs+HLo}tOK-$(yJ%WtTHyffO38G>_RYq5n%k zqm`9nr+&BPhHKjXX@-~PD4ivD?Lh1M*|sZRRtd?+y|%XRFKOnF=e$?^t9peF3{Q?5 zzNG$E%Pwsxbd-BeliivUPd+-E!;1gGe?aU{9KeH4kwk35gB!*>DP=puwFTdRq&r}{ z(p3XAqDfX|Ai!5J${RSYW+D&$mTsbM=ReC40Gt5WrVe%1Nc`>{gc+(%;*7-b~3zQ2CtLjk%Oj3D0a^f-~^8>8ho^97o`4J|+ zU1uwW#w*ZEa?5GA{H`pDFlNf zVeP`}TJNG6I6rx`>wKATxrCDt%my*2Ds|>%3ekvrUvajd$7A3fJ(>cxx$NffZqxrS zs0!!^$e|L5lS!j}%fDLCGx8{JeFsu`(T9)Gh|0 z)*W{ZD+1Sr_kFvSGzPssTL>0d(YYsFj9?HYGlcxK=%#??)cPig>xkGo7@Q-F@oR=+- z#l6^icPDC|>#qnrMYJ@XJX7-XVE8!7*i`9e?Ym=M(UBihdtm2Cm#oS@cu`S!Us-oyHi|{8gji;d&ZrFdO7OOi~=7mN)kx4zGwJPLx z@2~V#wWY-&CTG7?;uTaesKWQ$5Dy}@wEtoFHt!+}#1YPTe%R3nMu6fi`mKcl?1Pxs zsx>+1!EZvEwyQtd>Mf@h3+=xRZI*J61>PSW@{!@;3Eu!!B<>_4%z8FtC*xxXUVegN zdY{f?gdxUs__t(&h?w&M%=xqg*q}lJKtY(zqZkt}xs>0-o?n9LG}_IMg$B$)U(JuH zy9Y?v$)rL^{VypMoK`14M*=87Wb;5MJ!6M`){JH{&1zdw)6PrY!Kfjq6I#(7FBBmV z@Bpnj?5r4dCMD@ANjS46-YPn>S%Up+h|Z384dWo>xIK}0;p$+?eKt7Mw=Lv>jo_|8 zlxqAwVW(dxo2a|bsi2C<_&cm&a#r8_{1|7NX>!}{|Lum7?ibBV*TA3=Wn0trhj5%K zyFIG57GLX28uUM#7%OMw(($tM%p=zd%^_|2<{P61-y%FwqF5Zs#Ar;9B8t;TPE0GM`F0Z#*y z@%djryb?V#ZqZS^w{^27FfN1D`;{xZ8>%S&FQ-mBDU{YTkL8Ddi2Z6{@X_{TX5dhf z_367p>DNwtG0IeL$H%ORRmInQ1;L9Qke!pG~OH~|uoG*ep*`%34K?Lu0Y0L^g(-I4y zWu%Mf#OZf#HVDu&nHyRYUD=B4M)D}>ea~_kQu4Lfj1@8|FyjS5qqNjO5dA;pwBZxh zSO|h*1VBnF?EtY0hHz6;GVI<6wXh5jkco6cF=C7TElwR~wWtzq|1JCfZUcU51;sv5 z-ik?FZv%FTy&mbBcq8$6bxg?-)K+;_ZD-qg|I2KT?^7b3JO(Ec>TD1eVNQ{4o~6I< zQw32x%evV27I@Ii>3Ka3jl}_^stBT%OayHQ>qO7V^fXY>(qSpe!6xKsoLcDd|)93owm#0-^d9gD7(K1Fq3mw{v2biir^wCdfWk)AE z@+oxTnWHEqYYF2Q(~rL3{_3twd;30^kl<{T0mnq}2RNL2ZHDK}`5&pIdH2U)&C-xW zeyZ>otjv%^J}nhdy%-1xONkh|Y)dCg>unLv9vrrT$!Isnso6gXCxOC)3AImvjHXEt zA7XZMowM%S8a7V&Vc2+CKScp@6ZN^cYhrdMaS!i-BltnXLf14B4=JG$anF;9g(Xj0 z{o|9lbAfB!G)Hd&9@LsvFWWOpKECmK>g4bp$??5xMZv8S4p{5PbxR0V<0gXES(T_M z!JozXo;%Ajxu+I*a*hTXUYZ~g_H>o1@n=GRJ_qlE@fAmjxx2EvfJvJAou2Bq<+hHs z(IXz2JKgXNfSd<*i)Uo&PEew2qy;&msIhEQ#4ZYtVt_c2my}S>)rFN{*H4$gn^FeT zyvv7%nZ6q{Ha7UIOv$(yCj~Lk{}NTEy`Kmqhpli0rL)5)*N)SL(c!`qWYEEca+G=S z%kF3>og%F_vDPRtbO((_>Z}*5fY)5-qFbc%+eYlEb<@w_N_<{s7p ziRw?Ku0@Zlxg4szl_nV++sf6H!X~!Hw#&62?WcprFI7iTtDbk;BLCYX zClALc!pE>Ur${bIz8^QiWQBhx%?VqKJ?IuU3Fuq!=)_hxx@0*y#pqe&C_P=w1&Bwq ztU3q6Sd2Hv>SA!!3e_7k#=hg6_Gh37pYP!8h~g8QT%>l1;=j_r;7znvw{!0q`d;ZZ zsM(Sh#d!~_CJhvMj|sN`U_p9RrTU4$%t4x+uh|(Z@?|E76#C>);ab0@Ww60Thlzf@ zgb*uChIY8S{JFjmw~R{|p%1nAF;Kxo1TC7y1YPEZw2oWf-=sIaDX%pv9pcX>vOU}e z$|Tr%holq7(d6DUgHzF8@GOnG0`a)CWNDsvx*@xFMZa^rw0zD_J!t~htliGh!c!l>Zs3qqgK0bE$LnuiL=8CRAxem3FIUgek_MJ~o1FXgd`XaI& z{JML(6|+_R{%ESHz#z;lSv?!@+Jvl{ZEful2PQr|k;zJ<4>K9c!ws&bB6@EJo7vRG zRWtNzc1DWQ5F3+2IetC^$-{XzZ<|FRKBf|uQn3Ld#Zq*Z*qeM?pVXq!F!ehdzjI`b zf)L*svPMPdZ(nN+iZC=b+Y97ks7&D;fr;2i#7IKGZ^1+gNADULWxFj{4f-NE^;4J` z!l)IcDTdK|T-s7_azB1}<8d>!i-DIVb{;BQq8_;4A63%obMhxVP{gln>o{SQTFmgi zw2S9OZtMf-b``j+b5VnwwaA0}0#y~g9yJc@@zXlS0d zhsSWyIbdf0zCK9Fxx5Hfr172pb-1v5Huvgysd2y8Sn~Py%LR}1F|tnrYbuD6sh1FY zRfoy$!^)L^DJH%6Bce!E|36!I)tpe`xP;{mv+P~veDu=Z#=f^LSE}<^2nPpGVMKCM z_V7>=La~D26z-|X>myn&i<&CH!YKqpZjMXY!22mc;h#FT%nWy{+S(Bn548ah?=f`y zM6^CnZX3ohw@BZUJpe3TED*{xQ~+pG8xKZvrOj$T%r$r9j=lmlHuHe{UJq_nguQGqSTOA#Qv*~ z^RDlYhu^4W`3*-UvpCKBmTvVP_QffCn>`0-9{H~g=uHa9;yW!|eH~7$q-i>C)tj`O z+g+9#ygHO@h#E_xHu%;q|D)74`|s5WdYukQN~VYjf|GaxpL_3&HvclL;e5WB{eRtq zvNAKB7M+sT(@T~8{(Vt$ZPR`3DCKtWqGic0G?ExP=mw{7A%t!+2J^oSAK!Q%Yeb}= zA04Y1=Yt1U!qfz41&pK&jI!x}DoJ#prr31PG0Gv(qLbmuW4HxLC4pkG6=1Y_+ziEe z@B*fE4p4CcDYDuR(qW8*!@(od76bA?ITD*Xoniuo5#7(#RW(|e4Ckz)U+F>RCB*Lg z6xEXf)l=0JZn-oWuOuZ~{{}j{6x~n67PWXRkGm;ti|M`D7S{c}Ci&Jr@^zVz{PnEp zgzwj^In${|-W8{dv^Ha6_QyU-r`r|JOjw}j?AV^~)pUG|i>K`R5GNjP%U<{Q8=5cZ zr$#aQer2Qd2M0Xl0pqO%Pg;}k$K!ob^rLRZd(KTrZT)0ai~s#~TXv=WeEsvd;jX{x zZyLXoOE*{1Pi0npakcf2I0D2-cC>C{Yv`}J~)0!tWi1t&Ee=MBsl8zRMW+-=C%+L~B`!m_JxasI_=`~niLV}GFxp~pfr$CRE>kU3yPXdlz9RIY|Z~I_IGxNpB5#?Ocn=!6;X6*8JX5~!u&Oz zQ84^v#>+C4LEH2gG7HLA#trxg4 zJzx*oJI-?3;?>daDMxvSkm^Zh3ZL(tuz3Bfz53I~7KN=+Y3(hk71-W3)%kMO&-nc| zul&;e!i#RBO|nyL1K+EUZMdt8P0xvnAHDzU538&HWopoAre3ICe?Pc>E$)5396{}e zE1%r}o+h4EKk_~+zNh}n(jk9bSPB?rFyR;m{4! z#HI9N08yB#}$vnbHo{Ey`yAK_^_QVeyN%@8J7xflTeG({E$=1O#Z%363ii_WLW1olsbvG{*~+aDFta;XWNT zSNiPHH`wlJ{hR-Fs8~f;H}#Ep_I+KPq~Bhdt#KFK3tWzhx;udi+z5&(-@PgOV7w`z z=AYBuG{--%MvxhI*ZE9SiUdWD=WFa|ThW@@C~2E0=E99{EeS7OH?-ETkT4c#*O@ay z1f2$}F7S!CQ6l!3-lWA&yiWj&dItLhYs|m=5B9fWGhS#3K$)dPUU2a~6R7{je;hCUaTc&QOcH3UI8cWW64ZgDa>{mItt-@um+JvjcwD=q;zGwWr znC56`dX?%=h1;vaR$N%DVAGU8U|eQ@#wVcq^o5pgdj!2_Ng%a5_d>0r`KQg`<4myi z)#$4~Ylx6UO6^1vGb?ljvOLRO*ORv(Xm4cwQ2urBDPFV38V1&Odoe&`_QzuOjjQ|G z^&6j)(e;(`<610BhPU}8vpT}lPJ>i-xt{{t9H$kqE4>dcAL@u)y8wxO$;{ z+-GfoWd$H-qW)>T`|>~5>ha3=$XmQa_si_ori=k=Gcw$SPo#_$7;Xs_x`#uQWtG_~ z0?_trWM5p+IQ!acAG=*%G;fTB$JGDYiNW`HCnsB#jejkCW6KrdWD%z6FxJr%@mj^= z?+BxUBVZs${R_S2F9#ng1b;CDDdG$G-#Gx1U9O^}3m(?f7<`+0paUl}^2S2A!_W3= zs5G>s%DyIHGNJ?{3Q!}FQjB2^TpvcZ2z1)3!U1GmUh#KVO zGGZGb_3w_1PJBl3heu5)Qd9R7xKwu|KG>)}{uEVIf5a|ku>4Um{?hSd4a=)cqsF`U zPL01HfCQO`qC0}XYMyRo+G*7AN#Dg&t~!0k<0Ed0mGKV)fz8wOf@`BPU*mk(rTSUZ z0iUrbGJpw`>F?0Fneq*R365hO3!i~R;Zh*sIO!(Ygf%#W`9`Xgi}r~~JPF2eM_ZqA z$99V>tmGbDQLNdw9n!$qz`n*C@XQzhwtH1`!gBW|%JC7W!|CT2&gUWRpW-x<&U+IW zJA^M4smF3b4G3o=p;?EK3c|7%An zWn<38mis5%2)@eCWX1~R`TzCTsDEy@5}Hp<%v`Nyv#7UJ+Vec62o?&5=E6~KitD3u zzgj0Cb|a&3MPPu^bc!0%WoXA#X3E! zyV&rJo{rldoI=;+v|cyMG-kce_U~g+e&VDq-!bXw6?UvFo9R2+)%1M*%0ngK8n)4=I#b7c#WTfAX14HflM>2u& z))ZhMJ{z;%0%q_A0NNiZ8=@qG^+Oo(8?{`zOa|*z#n!1ZocB@*okB3)oW2Np(iI}>KYurF2Txxzb2wfZFN3QZ6nu*ThdG0BQw+f zB*G3-_DDXm9woP4{5Hn4`8H9J#-Aa<2SuYXyvB9=F(pdbn02wa@2B?~eS=Qj)p6T% znXTi`Tz96ry22wQ#k@HE*}o7O&WdeJ=0rCiYVm6Su2>T<`lrX;y%sy9g3$6*0~JL$ zgRK@ojP@THpn@Ps#*!ElkbyD9P!ZB%x67#uv^1=q;jD_%(1~<5eBEEWcWOdp|7GK_8T*S3gr(gm3Jr++AjPFzjD>x(K@6 zgQc|A0Ni*FO(<2_J8o5Z_KU+xdLsWU>-`GRx24lQnkrsT;2soy^kKw2Mlm50CPAZ- z0dvFf)!ne9F9>`v#l_Z=WYup{)<`?oC3d6Q=zJa?ekfwTXh2i1S2cR+dQ#Z(erw`K zkzbQNvzPid{Jt_~*gw$YptP1o1le@B*R8XSN*DvZc0v<$T1GNNLKBFh0(oL`=I@3l zo_A!VoR>ig0!HwPv4VS)s<5}0zI9XUN38GW>0VUS`FOa`wI<#LFsMMeDvqfGOn)bq z=il$_=uia^#Y1UD8g^(4LiladxRa#lV-bKt9!SQEU`Cdd3KgXfBniw^s9I&9r09v> zNsqY-4OR!ErlBEiE$dx1@m1OgDH$%^t7Oot>tyy0hsKvowV)&{+6+Jv9h8EnJrYGh zEM_3G$4YJ%RGp3;jXU|?aJawoqNh4Ms;PQ&yb1EC@%X*8&18GW?wyCH;4aGHaC)2p zRaEg?y|Vo2%yOsm6abOl!b=@y#vr-Lac{2s$*rOj8nAm3aZ!7?FB5_&-(k32lv&fy3a0Fg-qi@2Eh z2MOMP#7K6^Jcas=dx&2Y`!CeDoer=Z2g+X7$!m^l{nJXr zp5By^QO^E_S@DQZ)$A}q|D%$WFaB>SZ(ST4o#ZH}938jgT#93YiUqj9FbI6P7<5|s{PANEhkCq zRWT#T?k&)c4~9x)#`+DsyNITubn6Zmn_ElK;Jv9$%a^0gPh*0I5Ghz||7cGSVd|xT zb6dOxrN@H82qOj}ZMM0oRPRyAR)m$8hNJ@z`xlK4`#Y@;8$LFF{NRl+@8utBFgtd9 z=i(tm=}*Ox`pLeWV|j7!5C1Rk8eEQ63-RK@rWpdgh1YBLV17DdY-|ksFNtkCM$UeD zP90u~>#v_f1!IKcC2>^bnoDyDYA}1v%U?@bbqm=}He_oFs{f4vlv-tfA_HoT3r6KF zuoS@$wbgnh{+YcdR_o>2WFMeye78;FF0`MtX0fz$1R>wfajB(t!#564jz`Pza5ZVZ z8ZVSCM%d^&2js2yZG!+T+~Ii`SpBfE>Iwc=3?u{^_67 zC^7;+bPpucpuU|rqZN1hFq_D5{GR~hP0)zr0*&{Es?_cb)%DV~!b80#7VvFLUlFo` z?f&T3|KoFvbq{N+t53!(t^g zm}AqeC5QGv*a{Lk*}ZOIxd$00%5+$hrr!$8#XzexaDO`u8OL zo}J8|B6#m3fB-3>NiwcgOUA0%AHVQ==OOKf!MYE4S-k)8Lz`60CTDSx~g zR{1q#WNJyc{&h!;ckxL`$aKtd%f#pOZNIhgBT~_9jvLXqKIie{{5;w-UbKo4)U5(I z)l2H`gSd%E^9UT0YuD0g`iRS8(FX8{>=F|*P&<=O7CHXms03Vjs3Zv-;sj&(Y%Q%w z&Q9FUt+R0X9gHRN(I6CqzS#m}JP*A$5$Wb)dI))WCtxe>kWc+JEj440ui!`j-0iXaU#bWrZf5G>C1S9rAEXE1W-l_zn z_-CJ7PNmO3i#s|x700PE6VDYlt6mk$|0e_Em0MhEF_WiyOvmh;Ra{;Dyq@7g_Q2L# z_5A83(+@kC+?R(yJZ&O%+Gzoda2#Cer{68_NALR_uRPr-LIROce|B|Lrqh;+5s$XI zmyiu5mcTWGn^*PS`4*Oa2e0+8LCJR!v#`Is9w583toBO*;^m6qKp@ZF&@5MB} zUei%?@+-z?DxNr}Q;SIMZ`1E+1^J=&SB)^>0PJ$s$O~?aUL@PT5Ra>5md?0T78ju* zN!o$93G9lrrRU&8?&`OtClV3V2EnlNlHTyXg7X~`^G%xYpfqR^r`MhM{+W~ra2V{E zj&DrYZW-$Eoqx~1QfhC1Kb2ULbN6W37r&A(R=?*TMrP(cORO$&dqwr;C~d4@GIaWr zZlU~8*fb0Fbd)_(iD+05{w&C5E51Y*Tb{4`=#|8QG=P7I=|ErOQ)i;=rKPIx8SVka z!?Yf53#cgEXOkSuEqH33-#|$faujsoLsBo9;OwRR@}L3u2vmFGEqJb8!R~y*coVnQ zFc0w?8ZV05^%NMf!d^iyn}+wyg<8!`nP>^aW(HW~iOj}tURR1A|k*}0ZvzG?316_i{U zBQtw7A8O=pm{8?}cmDII`9G<0u5-mDvghCP>@(N1T#QG1b=As%0dQzHka0v9GuA30 zy4M-UVkEPdL!Zy0WA?A8-N4`C?{;W~u~b!5JMGYD?P&4+ENM)HzG7Sx&XT~kjOW%O zDv=3FAw4ZCl$HYuCT;e@jU8M@L#3if@y7{pGaZn`PjJg|k=ix~5Xp*1QX>>2#tsj6 z=zKblLvnE}=U)^mjuo-rHcT@GA=84UaZvnTh}b2C&kNh0?NC1>a*`=VG61oU3lAgX zxHb-5bb4psE?STrP>m8>A{R~TgUij&tcmlR%ZFxl_B`^FMo>bT6*J>j$_6H%ib1Jn z+79r;41{F`KkwD)<$~mg*vAIf#gj`u)$CkRpNGX`6jVF!6QsW6c;UCxiZ2$2s7S+O zqT+G^Cw>`SvrfC+E3k@C$6%j`bicNQ<2d1;J#7 zIInJxv^>!k;|s;I8|P+&J#H+)~8Rq<3`tx1CvU2-9;x3B|jM)C3o^acz*o z<|ynACp&*=4HrI?w7!Q{d&}R~XNSAKA10msPOkT#_Rk|UgDP$oQSXIgs z_NZPdyI;X7W=u@-K3CIW%70$y%hQb2;3M@T58RVt)=J{M%@hsyrB^wlG?KnY#O-@s ztOopcz%SQFi$5Q;Sq<+2??_S3*JExf?rjUG~qKz0xMWad*W^)J9@%ev7`Q7)552IAZ`Bk=@-|b zM@E@aUZ%#%x?CjsTw0UjRuTpxNKJv5nCyH@E-~EEo z=Y=;9SJ&?EhTfdoR8LuwVj^JNR7$i!1_U6%r5MYK0Sdulgro(-Jw~A5qF_xJ2y0B+ zb98R<$XjM0@oIJdKpy~e9f>7hQl{9&WVGoOIv%_y10R9hC7VnYfzTTtlmb7qIYWR# z&uUExAQsj^YMSU{JEw=mZPCf5h5Qtfaf~5MGi>g1ua;ub71zt$Q&g+Ng} zIPPRB*(Xma5eK%A!2mcPF1J zT#qIi^EX#V7v3MNR5|Y6{W&FOk?`3$`G-#>=Ju^dtiiJ{?pmzTap5osCVEt{#}m^w(&NuFOibxEb0IQbunW3;{WV z!<*M8mVOr0c}v={!FQ&XQt(8^N`DW72OwZDDNGrlq}JHHlg!OJih>YPf$~wuWt^S- z2~#xVrsN4#lhBj=RkPWuxgOGzd-cWOODk(E(ePo83R+h(KR|M+krjk}7h99}Dq{Rj>wEQF@=2xep~FO(7sfu#%s*>xtt zrFmJH5*Xo_jQgU{T0u%iDP;F`1U-yE#%?+BYD8p4I*DiS#;uZ{dobT7SaYOpXAqst zxDn9WtCb2Q66GGmI2CPJ+f|KR3dWN;efaW^O720=L``-8d>; z;Cr#Hu0WNi zetyyR8jdoB>IO9KJ|+(SDjYxSdb6k^g)K=1cZ$h%i5hQA*!hZVrb?At_v(xBe~et5 z6mfj&&6tlHeJ$>BfUi@hMlE{rB>UT=<#=_(@p$oir?7t9w{}Oh8(F%Z7+ikFaW{=N zD`){5%xG(BwF)&$y{RslzAR%i!I2Z<8SCA>l^)ew7`D)>R~@_ji-u2ieguRr2?{P+ zT4FLFP!poZfvyrWPRJq!icVV;q^SV13$rLy6M)Ux#1hdCO03J`jiNGHt7ENjV=*A! z8K6SL#zS(Y|DI)~C3Anh0x<}U#)1mXg-mU8svAEjZoS`E*k+mbFYMKy)L0Ms+2%Yx zyfBK~WM?JoXD0tWT^SL)3&ybSDbEs?(EE z%1A1c?wp-dr{!px2*!qz!g-~!7`gJ1%IA|yynpso3i!m(bXb>ob6c5ydVr2^YWhfj;bA2UeoN?;a?%tw%M`je1SogF zjCpZlaZ-H&Cc>8J(>&Q13pY#hc}H^LBIg%Z^Q%rCFgtGn0KQ9akOSGP;YogpvY%og z!#K3?`U`2g|FgCemVMlb4^fY!T$kMc%Ikk>B-^E^mL;Zi{bE_+8XGB@QWq5iwwtKl ztA!Cgn!%L4JpH%d)U{p6AjPwldd8>BpHVJxs~ld}`_-s&O?eoGdk1Zx$^|g~wX|WQ zy#y4RAWOhY18gGbXL31ki5N$7h**bnR?YiJI68dqioj~_lwx_wkn%P>5sc4mW{u+@WN-`*ukE8C$_ zRJ_Q{GI%#|l$3%$O43-qQi9qMv z6Bc=)L0A;lGT1P4By)*Ju}O~}Ey-nWb+Q?(@lh89Nr=%>>BxGa#ZAMI@CUBJ^B$LM zEmec(BM$lc+q|%x9}2J!Ni$FpyBJ=jg%2Xu@w}MI;$JG6E#qA%ePauAii9m?W(YT3 zbWw zOqzb5(M42f7@R>VNKYG+MhxX(Mc(+dXEmGpRbiUmon3sE{g~V`gF&LYLS=^e3lw|3 zZu=1wB(Y!9#Fr+tvO_c@GPwrwe7&DD+!= z$=dA*=gx<#6F`wlEprs;PW!j?I>jNSz7IT`v8h^d6td!n0GOAm_7jg2=0s3g4zwXRx|>yTcT%I_o1R!=?5wN zJK@3T7PdFq825JTAS#!u8`MX!vM=TUwa>ihj|L^<(VA1}B z`=>Xeu@ zG6`#~Kq`A*5dDrFfDQ%~p(bE!eg5O!i8;VZTf~OMgG1e^V5S^uru|Luu+CL-C0fz< z=UokhN|`9B3ExeK5?3(XPyon;#n}Blk1?1845iJ$WauUvB$S&XMor2FC5$DbCW(K= zPzZ->gOp%j*P>IVtcHfh)w@4M0fexXC!TPY_3_TW7}Ve^H^D_WmWwY z?SkHCJdGq8iR)T6&{P6oT43#FNbo86lnHevjp&`a)@pMk3!TdmqWw2f!OISaq8oG3 zcM+ofYKC9JL@Cw0#A@ZdDia7fu(vfFJ$ua{SJh~rH^#m~s#KG!a=IdNEu3lmd)AgO z#)&tZDlBOC5;+H_>HNPKj1=4*z8ZftklNk4n_Ij-N&8tlJ2>oNpsyH_at%N z(aEpdF~P9IqaAKkE)sES9MJw*>5+^mGQvVdJ2E^AJch|`m*(2I2MnSf|5aNEmT+utZG2~%O-?~ z%Iz<9QR9#X%$T&OhZP>AX^R5eo5<#6B82s_!r8H56JaF%#&|>)dYT|;9Hs{Lwszd~ zYbjQQ5fSvZP8n1xMMXgh(9}eSyhWAS{EpO81M-QuK~N=`C^Q+BW&DH(2$?x{FbQ%F zQh%?<>?<2OP@B6eMP?T^cwUjEFT}HFRqOdU$cr1h^O>t)e6@Rf-G#*{@9`WDAwR^g2S4u-CpjS_WU?Q;?|C z!<|o|$RV8B!b-15^ZV*`p&UxLXZsjw+k+ZRZKTTlMS@uNTV_CH$Py;RNY&y>2Wt?tQAyK zz!X2-C;(yaHYGm@7KaC6nlhM_~?k_x1 z>ZJhU66x~Y>HKm1w2o4C-(NCe6(ihHV0Be)U%=z{$y4IU1r8*oTVxZO^bZ@H@rXRq z8uVIcd#JP$T4{(D=WZSZlBJxFf_N=ephf@z%b#OIz0Gv`iJ;3~Jo{-gS6;h8x+S9=djBscJ&{#s0)A2F$2WF%Z65AY{t^kDTw9(sxxNq$qOyR0FgFN*{BQ1oNyK z1>rgS`{7$c#$0mXVH8G@@XjnY(=2yVEc(47N)_lNz;0sLf_H7OFg;Ge$ggpwC(Y+z zQ)g+XCg+?*C)B z|Jb(nq&7W)j1&$aDnx;bswg%EXgd=UPplt4Sl8IqLaUYxy_issThSc zI4vnwFfA!qB(Q)sm;?$F-W<1%O)8S%)=iSa9!ivown|{vHb8@xi~N!!kau+uVfIAN zj@lA-i;LP87W@p(jFyNj1Rk-7VSzV^%Syxb@9pHbD( z60YdcyWdI&2fr{Uvjvww@Ak(yg#?)CplM0Gh@?*owqi^mI+c*+)dV6jY~|;Pey-37 zK8cNl)`J9DG$tETs2Z}el!K@?@=!I-O*G(8h*&3}2W#fmIa$>hy_{8wpkSrYJ$&_3 z*IZ87ok(H1AcRo61BzD4*`NEy-so|m*m@;GluX<+XOgA$ulZVU)1O5U?L_G=np{gU zh?^Bq)Qm^L0V+hcb_Ez26UJ0W1z=~HHhdoh^Wgz+!5mJ(Ndd$#=FPk=QJ8)w_ygAa z!s6A*!i@$Hta929jR}nri^=qRUJ!h=6pThBzsC#JB1s^iL|E{E;4{(|Fu*sGq12(% z*V=KLly0e_v=$k1IVn;yHtXJ{u5hcIbuI5y^-U28u+Ph}0 zAf@(5%vP16wKui*E=slbs=a^te1GTUyyxVc{B?8hbDw+fbMO0g3S4vYJfcrn!hP?H zxSKXq3BN20QcZ`4EWk&8ll?mHtV@73B*dEAAo2JQ*)YcgoF3m<;GVeVLJJ^=e zID;_6iPt3Esy!P>qldTg5k!dJIs|K>@ge-r zBU31lRe*T1j}gcyeE}^TVIjnN$l{^UqVLz9lVWZ0`l^65>B6v;35ezv;nN=g?2C;1 z;&!@&41VE`LhsWN@U3vkizyskvNU(`Eo*HUP)jzVs+IT4;?s-AC4`&8;434(nN`Wmu0xP??)x zyhGVuc#?deF6Xt&Ov;HWJ9+W4JJWjWqML0=!_dzrHfUmz!m_>a2o=*2`P-hK9r^qi z0+qPVs5M%AR0=Juv-mr3TojUmc&8<*!}OULUbfCuG&oUc`<(%;a`8b&apRRD;vM5r z0T8!TR6HesF3g}+S1tp;MZZVD|4t*|eL14mVKf~ukli~H(S9Tx2}T_ZOp0h|W}wSU zz;bI4-hBrVM`&(J7fmaZoY&5=&)M$s9d1ct|Bj9N(!lbV)T9o>+P_(|nA;rSKK7r5 zKjAEFL?u4jlI#^@C4M`iLv(BXO?^YTc-}R;w$tR)TE&F8?*)l>)=_~QZXS~=+CTxe zQP8b_h_0L0YeqHBpw>St>Uexfl!gHn7DZ4uGyK|4vy1Lo* zKI_SjWqNGRws`ds+O4VhI1}!v*mE7&Doc#6CMSrBzh|+~jnnVr7K@P9c0WFhy2qwi zIisN=Q~Dkj>^wJ8`G#`_m-H&J#aLgcx?>O=Uz-*Nf6fDlO@{O{6qys!3a)*F;Mmy! z=`+IEB~kg6`}I9;i#(v?sT<NNKBKgK=>o}9!Nl=@57F4S z_olwdD%kV=xno5LSkUl*)T|8D`C0W<&T)GXt}3x<`{NM)A~vs>F@6QRzG0&`U&0`n zr?IP8y%&mJ=d&Y#;k&sI%aJFX3N`Edc;ohAqj|rsx->Q{336&C{`0kRp|b>svd?8j zf1=_DCiXuWTc62fMidK6Jo)K(KW$#f&OW*p7!Pmp)*4%O<0q60LzM7yZvB^g#jzhD z?|LVB8V3+?goVbYKc|PU33}X!&+cdSqu)`5;A%_x|PSik+0dH3qg5RF}#FqrMJz2=?cZp8L*9qCN; zR*y=^@=NO;Ih`FBp1YnALp)WTl=_Nrkkd%@c^6#f4pPgrF;)`IM zJ7F<=c0Uo1;t8;?LnZ%O#-V8%CFwPpPCoRPU5a(uT|vjWi0qb2!;^!I=44VzF!wSO zAMDsMY3|GZ`;t_b)xo(X-LSM^bF}Pdo<28Y#i!?;372pR9g{5Teeq#W`j34mO-jbD z4E8S(WuKp|)hG&inoO&8?pyD_dGE#%!6Ji|L zlNajA{&Oui`J6@h?q_%6+$j#Sf28!hp^8FCSHgK!2^-7Nvq)jT#naPZ{rhu zYYS6vz?r!)Y2ZF?7H<@Gtc_?8=G`yX&-Ah-!TiKZ9XOjTdI ztsrX5ZC;b^f5a0Eo;okiu1?nOu$o0Bi?f$885Ltl?k2l0eptNu3{-+_o;*}qtVVL+ z!zOZ!HpS^m8Yv0+o1?BJMKkfB0Df7S@A#;vfN?ln<~yxL*E#^{b&*kkp44wJ=i|Xc zmZQ=W-#+Q(Lu~LpZr0$}bk9_`mNh4_^J`D1Rcp8!j@pcIBeh6behb#oxT4|=aX8E6 zZLM;@+Y{kb@PcI~9t5q9ILH-pFCTMYhbEH$5nVDmP;E?XWlS1TiDk2T1=i^Ha=1vO zIm_v|_@rl)=y;ko*01eJraQ>}!{_G26|?mK++&j(MCK0A6nU$*Db)o)K00+lG;r|l zPoJfZr!V{253D@|(2Dja%*`mX;6XiRJM!NO-VQr+v0PA#rXp ze7c$H`QtiNsa`Z+Z-bf?%N1m;h5j5k%2ifyyd9C^ajENL}gxMrnHl5sr;kv0M6@uoO zaMl!!EZ!wQ$Jm8ATWaAt*7|8T37K)X)p+*elx+``N3dlL9@Z@gPUOf_77E4y;{|X4 zHHG5|%1A0%7bQO#-yGegihW*J2)~Tg$b)TA!kiXN_KA}XS*NrvS@Y%_03RL(zlEVH z>X3)M7}MV}_8!L+R`kXCCB3f5PE3WS_{%B|tD>D5!b1=;?$`Q9@w?pV4<#&I#Y8*X z98$*liyjf)9|wzO?o8O1_#ba>as)XQyYmLUh%toZVnCF(f$-m9M ze92bh1Ac3G;FXEf>=UEJ#bHr{M5zD9!4()oDIP8X*wod3x`hi=C;eIK=aaRZO{Xi( zZIF)1L_H3Dh;S`qH#$%PJ{BRL%1H!MZHp1lSDTl~6y5kJHXrEdl+|a^4pZ#Px1MDF zNsDPH77h8Vv47*W9ll4f=y`eejY&0R&VkO!RA+}t2bP2EM>fPq#=5Cm6>8<>0ig!X zf@O}j#UVU+C|Ur&7*!0zL6w^z+CdN3O3+;{aOP8F|vW2QkKP15htpCkk z*WJku@b}2w%=Xk<&-N@Wzfv4AHB(gFG*_qgn326ZdV0pr#iesndFS42G}YLwUpKx9 z9#{am{&ARJ3~J-FRO5(D)JtMF$|?#SS!U<(-x*2bkiX{T)yui;RF-FP8R?pqcR!x0 z4BBmdEu#}@fwgGyGP->iM_%9hmw6!_%;02bwf`fZxkBrO&^KI|!*Wi9$}k*gc>uQt z&!eyD+~NT=q5@si;G&{7-8tibeTyJ|p{uuEg6Q1Q2hqF-6g>B^9B?d;>s#QyQJUf- zSX{0^O`-oA4oD_3>Td&xOKoCFSLe55x*BURm8|<@^w@A#anzrMI41GtBj~$VnCP+G zND57-z{k)hpF~+W^|zlbN!y4jh&s7YXu4Jq)?;ep5woTPaOYGty9%L?kBlH?)%cX8 zCbWgLtQQ5K^+;u zfwqK%yD|S>I?QKSbB4LS`EQup#5J7GCGQq za^4G5wH@|;f2aAAi#&DQoXP1V$lZz7Heyscza_mzLC!s;7T|rDv4M}0-xkS>Kt?WvpoWnZj55iFTNLlub z7vk<5-(v~_nrW=D2lkOS25;Uycm5aZEh(0nKWz4GUFqTY=5pEi)>4_+h0+5t8A;zo zn)=fpO9_}3tcPKGKC{rr3VKLcQ(-m3GpV!=7TchC^wQ*quk&TXFc+zm8-vr3`sBJ}LXCPUySbLI8a@6nfwF3AMB z2AMweq%b!45nvo2(k1lj`A!{}X&{F7<(O$tRW%T#EK&aG`!*0O3+$UH8$Oqtas)W` zFMMA0<6cVXeq3ce|1~B|-d%0)*RIaH*X6p*KX&8Bf@j#ww7i4!)=8B3>gJcF+}Q=dThj^Yvj?tz;#D;mP2_XXKqtFuO#WlY>GJkk+gNG+HBSRL4tVP=Ih#AY z+a~YZ86d*Jz`VyQ;T_CZRVOm=X?I(kSSZS?WQ#?~K|K^fIFH~VVZwuT3&SG$Aatq5 zM=@j4u)jY!P_RdHUQ|2*QI80gW08ztT5+@VoN9VJ9tQ9%FUWC+hwX?B_Vf3M)g#L~ zgh2A3w{j*K zSjo(~Ot)Yn5jtuuQg>O{VPa}HTOJoW_P`PdqEExaD50v&P_t>raS6zk`Z#36?Qwyj zHK%D<`#PT$k_1^cK@>kgz55)%4gb8s{1KsD5R2 zDr`p>i2pI_`w`6SzrZAc+mH&F4@#Yyw3WgEib~3cb z!#Mh6QMQ*F82Qcf#7^)37Ri`C{n;KcHYKw6Q4w!+bb|JiUHh_xpwy7pu% z>i^1GG;WSQsG?`u!wU&Tkc&EXzsvCkLdgEY zDM*GMz`)Zz3YKbxHEHYb|BmBd2bhFHYe-y4^!vS);RG;8eA=0mTuMIn}w^X)J z+G}A9;h@2*G@^J#vcEO7_|@${d^DJI^XDaM`FvoUTNVs;a5JMZU6?6)3V&4XQv*mQ z7NeDC2`04!hj~YUH1W7IYMGLslOy=s`8ZQE$X_dj)KDV1VpkyHSW$83L`&e4ETSKv z3yNQ=9bpknRs44JX7Qp21z8631K;zalS6Si;GdH{vz5Rx@lt2i<3Hucg+#v8SbmKJ zTNZJGeLUSij`p<*Z}Cu;t%a{=xDwv=Oq4qSxGAmGRE*wpq>uq}GG2T=9cIN}Af5P#46YX0+boZ_M|ds6SGURU*Pdt&zDXD~&s)%S z)qPnu(kJv@w#|X#wiy8LCT!z0K?ZBO_6dI+%rGkA`C2S(KV6@8{8e>x+Q(}{!*M9% zghJo{Qokem8V{NP3Sq%W*VocM@dGVP-F$>m{^g-1}&P5Wz`ZQuypf<8;WlU8u(zVRkcLGucO zd;`;*GoF|w7%v{g#qX%W11Mu6L|fx(mF~woywu>fa$?%1o?`SCG3jj}D_Bd6dp~?~ zNE+(m;nEL=2M}uD)9$Rl2c*2i8irOe0ySML88lx=0m`5dq`Wg&5&cV%X<&lMKF7A- zs8U-ti@vte2oW$(|Gv?T2QS|65sbe6<9o(W^)B~6rYSPHT3oXv_ch1vs#CNt8oYS~ ztsc}x0bg&G8}8qf1}+{8$G4^2MvP9TK;%0-HNG~_Zh<|t9Pbg}lEek!`?4=2LnN%@ z;t-$^zp6>fOBr~v{XL$0#l3Ok{UJ_hrx6nP`Qu|?J`g0e+U+f2g3YvQ<3nLgb14?R z@uAbe327VocPZYJOiwe^Mq_UO&L(qP$oU;_a1uZQa{VUn3}^pmRxkODrm;9Zz2TN1 zudi|uOe=(69!N-WNl&m8H2?V}O1rR)S5*NOrNx8NI?GbTegVD5#3a*+rg$q`BLp5| z6;barXZH!pV33p)Z^Motoc#!(JU{oXz+c~s$qJL1O7Rk-(5EbketwMLNKEE4*^l2X zLW2NhtoHQ!<&2sDFwXU8HQw5iY1mg#aH|;N-~xa!{^P=>Mqe9;kCvm4fUUg5czMIc zRR_tbD;_{?GZUSm&AhFvewgoTka${oF>8_FNN+T@r}u46G>_auC4c(yzWyhc zr^dAyD1A}%XD?-exJN~g-8&nW zLWWB@xfR^ItkJfNV!)#-8}!c}k}Mh}Up=;xVLdro&S%>$XEySyLo6Zb?w@g8I>)La zS?;Q7f4rF44+CGB)JR1ifOMEWv;++NnNOctzKHy^NXIh)C|ak5haxCN%X|M-5Is!U zmsF8u06S<1X{0zB=d&|&&GSw4czYSy(csI41g&ip>=B%M*Kr4`UHpG`+Xax0unG;> z`ovV6wC2@#36l@zSOFZ>955Fu^0m0*nFjB@#dGhw9KOAWLD5DdYcGL6FhUMDA^VyT-1s?i*WWCvMe zUP)`>h=0eDKwGf#rX{zJRGfhD@Kc&4K`YDFEM3h_r}qVT$^bB<*;4?t8309^Dk|1* zj_SD0_T6kgj#mp}s)}Qx4O&m=SFo#*D^o+!?y|2Z_%rd8X)33%G;@3kHnXH?V$6F3 z7A54!6h&8(>Aaz|i~#8nsDTgmQ`M{lMo=G)zLFW-gY*vxCd{c9i7gV4o<0ll=CdQ> zyJEYeupRgKuzmPeE0{J zHp3wpDfA?M5;5{alO=-#MqSpNu^YDLa9s309S_PBiJ|1H0=-7a%@YDjLYYpYBg@QE z8cnDnq*fkCp2$>TJT;#BEMd)ELxh7PII1V9 zW)r8awQu^-xiy&Is!72sUCF!NzzZF~=HmaVfzK7+@~u2pSJsnQ2RPZJ#Cv_8{(aX5 zry_TysN?1zfin9J*FD?VIHv(8W9X^pO--!>?n9hT!wkt)O~ca+xj~vP?;WVXtkFn2 zV)(()BlxL6BJN?5qDp#dAg&>VYgUMc-XjgK$;sh6dF5fr@{Q#B)BRijl!v62@56}R zKYh$XT2zs%|G1pAp5U&2>wF~20A-5wj=ni*oIlU}Usm$w2Cc|sjQUc1%DD6yZE3{cfwCoNJ@d zG5H{&z#0^}??&E}lM_kGTa;5%`$)llg6oUSPf>hb9jfJ}nZa^pa8i z;ePn&P99?>{*m=pcU_k*-*q*j5e3r5r2Qc>$~|^2hn@Q^!xElH-%ly=C$lm;pjxNYaxi=X2l$13n>d54?OW{5R>{57`zn}8nWh(^yk?&37 zxDgjVQVP00FUjW$qCM~#kBf$MP>wJX+JQxFnnJ~;No+W$uDi^^SZA1(X(NHu94J5??v2Dfs*){`5*Mt4z692Z#*T92Mx7O_Pmf9hfQM)JoChpnviUS0 zM9pb^L&&(riHc#|VIZ<6NjnZ|fMhyQ%Ixe@+=tSm6Ew{o$jBLm^2D5{ld-6vaosbBg5BfZ#_IzaLE@ zfyn{+Ye$n(RyU!2a*)lrceIv3N5k-pmr^LVnZQlmq13I(9R3D3ol#296i232Z5s&{ zNV)m5OyAkWR~cuqI9H8JN}sJ{XUFY582t}YcY1D8c6|Si$=u%V=OyWdD-Qi$F7PH& z0>zB-`XQWb#eaVIKr!tLND1BlNaDBD!~{wCm(46Y^4<^tFW1y~G4!>&*!T1Uk%VKM z?!LBfrhOs`pwB|<4CDTyUVO=218kg|XO>{Z-6PcZq{*slIJ^lAUyo^6kG`pECUeTlwqT$aQH~~7COCNU}w_TfEtlXt2 z%P6GW?5J2y%08uh0RYxYDN~hq+Q4YrDE^Rq+2pl$zNxh=8@w@^p2RL2&e(pt61+=t z;J5pqd+~m~X$`s%x%ozZ*@t_>gaUB0k~3&WoiVbbF^YSrQZH&#@S0DmKTfijH+-ls z7VoM5Vm`|J!vU3g2k5}|E8WsXN@=8gHcs(c%(mQ$VwxC*e&{iYDWe#uNDUpi^9kIjGMC2>3|@b^OQNI| zTy_N+`7hnbEPkKzQjAeLUZJ{scKt^=zU81>WL0EbBxL(|jxpGC>>|w|cx&a5Bq)Q? zEeWTY!L`Ayv;^*>zNba-=`?&@;HyUhi#0XiL(vD>nzTxO?EC7!Vv<2B5A3yTC1~D{ zau~(#i19Ew%2=EF+V6;U??JSzY?6M^#J#=0NjP$%u;N9tTKpI1Y!_bvh(x{Ojk?D% z&*KfN=DS)9-YiB^EG`gGjxcUbfzQyTmnBLco*ZzB2jJSt2rFSnk zIsw2Efkt0>H!@|RGYMkFy7P#LvXP6@gIEd1mY|xM#KdC`oqH7u=z>D4+uhpBcH@ZoSC58@7r_%J4nEj!eI<_!&6Do%nCx;!wI( zp2I0OgJdSU_igQ=X2^S)m5(G3Z`8R8B{zj(zPW7qXkNa4;iT?kdGd(sOiP$5n2?03&0;qsfigaehDa;dy9ad=p&Wv&`1KbTO_h{@^1EB z5QiC}K?)vgnvYgNZGE8CEXK3J?C41e=GuPT(ncBuWS1MSdJWMPZAbAL%tU5fI(;aM zFV(&uIy{rargU908v29Mu!_C?ob}+gQkmOdaA$d-mOFc^*LyOkLo#{{fNig{-!?mc z*ZRm;hDc#0X4#e+vnN8+PU2pCIa{DAA}=R>`!Dasci;8cu`2m!y8ampW0_|6F1Fiw zEaugDL#3p`UqKyRe>grsbFcsi>U;d*dG@qh<=&{drQaIUt9 zxcgIqIr=gY0!!OQ&65Xbc3lVd*1jPcYc1I%mE>DgeiU6w$(IJrJAfbQ_e-z+TIBTS zW2x2HKkHahgWr4-`~2=(?^Veb35=g?g!80$tEXe|LQ{3CQ>Ax$q3Gd={6nFzq>D<(t@9DKjQC}IwjyH=qY#* zmH2%2seJZG-JHhbN>z=-z|9|Hm=NHGLV>sBhjLaIVpn-L@(AjcAp@g#(RcE<_&Kew zR@9jyNJ7e}G5a|RZ{)+h_O36|+eexrt}>iEJ4IUZ4VS#lMMGvTj2ELTKg-=YO)_2M zjpWKb*&5c-{qEVJ>5~gkXHf9<5O+YxcbuhRnZha`(Rh-g!T2GFh7dAj)st546o~S8 z>xeoVpGx(2k-a1rLEhB|%K~I)4|xrNarlAvXc(KuHogC?U5|KIH}AM9vnd78eoWwo zZu#aA^3QSX*J37@yuG?d0+n4znw&j4@4R!Tk6N6wrc_mIjAB4|YT!_P5u}agqp5U2 z5x%l!xt`z`p7zoxZ9KJERIyz{*Whn;&7k{ee{%Lmt&jOC6c`csD7Q#jpqRjA3J`-! zc#j%WE2_E2%4b&%YvT|)oOVw0)Mge`V}DloJ0Ihk9)I-!MI*;gGOtN-4Grm8ZC3AJ4Dnr7pJKY6$BrUCmyQS1Kt5-hAesiLyBS zF_qUummY3W?|(wbq_(q?JjSRXE_b@RvOVL@I)OWBm^a;W+Vtu+XI&~P$;{&Hwy%`y zUuA0I@f@keYmadY8Wpwsx7EJ*2P;JL}Kh~frJDfuwW`bw~?;%9Ev@=S6Cf|B0yFB(P zesFTQXvH-jm;7LidA}Dnf0m8ew)!iU%X6{)*eF_X`=Rt|-U>6W)MSU0$EZEj)&xtV zxp-*XNc08X(KloRL0e&98L&|(H6AI;cncrDS4NP2|4+Ii_K*9x1oo8=52nY!!UD>rbmIXkKt44>?tr4#PKs~X6Or#Ig&Hmi7j&T#Gi zn`N6<_lVA&)9gh~5$>f>-yQCDN`()G5CJ@!lz%zGxznX4)gJl}^Pu<+Hbq7iwF4jUXZlufJd6a#fAv;0xw5yZ;yZ}+) z5sTBiC!9pTb)bUw4L`Q7aV}3(Sch331V68H^OJu$hz@p8dDh$|V?;QMQfo#>TC0Ys z6^;6>CbKDg7^_u79b2K+Xkw3U zuG|SDZ=%BKMXE+pa6>D5`|aB=Asjf(Tg$SqH3Oftd3}2>5F|@HMs0CGD;({+Ez(}5 zI4G6y*KW4(Ap_&G?;E!h8C|`Ko2B<}7V?rl6y9}t+HtI039S3JUw#M8u90@=a6I|B zdQ_;R1OaZMVhUYb9#5~lPVOn~rDsl8i&o2Z+1DwQ=u=O2NG(m-C2hl`mjMG>qT|HJ zvVWzy*if^?%aHsdZ=+mHheQQcg>G z{CuUsT5iaO&I|dDjI{qzvju-9EBV#j+Xhje^Yr4$67yFiuQneTCbB#KSc6)6V2SjGU=`77?-~xewNJg*BhiBD0S*)gv(?sepACvEDYUiH^bFVOi9)x?}=xf82L2v;A( zPc*|$Nu{Z?85)h&sO=hbhPQV}A52jStIgsWrnoAh#LkUrf9^+JNibmcB-x`8R9HzV zE^YmFVBN2MKIz*1wivIy=<}%gm|4T$o?&S~{v-9v2NvZb2Rsc;kau`!08^EAdNND~ z4?BcrGfnZ5C`)mDIK>8s2d$Fkqy6Uf!*B55GOGN?$A!SYfK{n3zWu}*0M@^q6ePQv zfa@3%#rH!2nv56$oKm-eqxhqsa)XzV4O`?$d}R(B%jNG>4XU8a$<5zuD*-pVB$;E| z#Jdh(6qj77py$UkwNpnUy`~*{!XT#!Aks&VHM)U4Xk@qeO_yg_^*UOmHhAXy_<588OQ*b^$cJT5tGS!e2-%DE z)X(GJd<*{S9pWPZ??8k)*4k{C-owB+&@eO*g8eW#{0j7H!jA3v&Hny9m2Nv3+#`I1!GS+GNgzTCatn3*IR zNpS9u=WDSEgt@TvbpmK85(-)zB;m=DrKx#9swOb@aBVyR9F7ev8i1)*nlE#}7)Dys6$rjkAl5Z+xD;v% z+lsY`Rtngfty=0l`}OsAvuMj(XN}Ph=3@V$|I>xUGP!%)+2y~``$g9%KTU$4P00B`Ti=+o`qy%$Yw+=SZv0^o62N`%O>B#cJ` z(+-Faddl$M9w|Qa>TB(g7eAg)t{U$onWu?W%li}ihFJ-xNGOV=rujuc$!egvpp>0!h<2#%Xi8hY_V)ENT}T}Z1pFg8ang`rQG+B`@6OpXKs_IOjt zi}kV@-YxaznY0pz$x$VcW=^;Qrgj%^7BqU-Eu_U;{-q?p-X+%?8f*bb)YaV<#=dWJ z?W}&^DAGT@?C(8&&`$nwSkD7?*s-g+`uL493R`&;q6^!-XRO>S#l^sx`&j)5fC!dp z>U$id<4Atf6Rvw!=a=)_72o;V=kvB;=XaE{tqVyP)InnuO#-{5ZHIjtc=Hxi6aZSO zVaL;CO}Emz^ea$v`*9f;n)16W-_V}{V>s{t7K&KnP>p9HG_65MYMAiYU=46JdlpxD z6GNfkfK|h%u-{kbkps(eijv1AsF7^h*Q;kf-?)8m1g~njubY^01arD|H;$%`yUOr# zV67?n?PW`U-^QfyJWxzkP@I64!9i)7S;UUmg^hYW{M&B$_w+zlsxIam=(|U65UY^zX*vrM{n$t$xhm?=^UXDdL{l#|v8>k<^Nv0% zpHvtx#{Xo@5ew_aE(f}rT(c}49}Chv1Z=94jA7s>B2OMsE8blf?_Hd;jA*X-x;mam z-gizFX%M#yo-;nV{HQmyuqWSK8gOvkaD%yjI{2)IEo1*EFk#}FPw`QVSo1?d?$`rT z{F8|M{^K0(d8)$4tl8dc7hB6n$BO+)rkSN`tFO&AYR_)*EW7NVO-TW96`bv-6H(gD zOUV2WT2$A34Ydj+<+YHb3_MGZt>|W$U-6lK_~CjyZK&5MrEwk*-n`D=-EXW$dg__T z7e4ue07~eIH3r7NyCgAEi9hh?29z~16_TgFjK{L$LG$F_9FKB(P%U?Q1M_&PUhR^c z?z7-b@BPu$BHj|bp76QyuM)wKgVC-n1q-9=spkh-J}}lgt%{$p^j!Sc zR}xSKKurrzj#RS(xH*YL>2`iFzo%TcLHB?VB3F>3(;hv^-I+Ob_uBu(zdFR9twl_n zmPm8c_iGi3I`AOTal1@rX0r@(2s+c=5aV>T$m4Rc)y$UM+DMghu)3YA{g;MtJLCwHTT8_vv8nI2pTF7B?p|C1(sGCCP|`~amk?}`GmV!1Q{++|+e zT)*`@)bFvue|j947I=M)yw`|8ZW|1YJcic|>LBn#bB*{m120C~`HXTAXf{BKqRD_V z0OP+z>&T$J@^p-PkFJ&rAL%GX!bG^4(xY|pg?)w6@HJQGrcLjU-#p&mFu!?qn_m8> zI;p557-3zJQk@{`C9vj|sPLGOYl9`NdlxI#IQFP!tSh2%omtepG=v1=^!QoRo0;;G zd!Q*iteoX!+xhf0_ii#76{C5_I=xKtSQIq`pQEE@QM4`{bObsb$ zEv!;Q!7Cux1K7b3MXEXJ1oNQBhhgE!hS<>z9*C20o9{}V(c|AjAsRJ*FV_hBj)T&R zCC`32DJ4I>wk|Tc85jlRC4p9)T2z}(7;_Nv7BQ4$os49!-z;kl$4r}hKdHZhf*Hz z;i*1}K-YlP93^hz3xUS|<$Q{J0n58@$FrrJ!_Gnq^u-m1_~tM>l*U$S@!|l)G!Y07 zDr<^OmWj?Hv?(>3jF#&j0vVYQ$GkFP4>TUMm|}{?`%uZo>3{6Wo52rumIc5TWL$dEAHY zG06gOfk_0K`uVY?oOm@70F_#9sKkB87bTh|sI5q-APSwNaC4~@<%0Rzd`yV_L@ZAO z5oloEQkwx34Oz;!(Cl>0yfw5Ig7M6#dUEgPEp*u~CTIFAnM@z#NW&xx%{7w32 zJibd5Z$($$AZBy#;nEC+gq)0zy!l4ljE32Jn7Jo3-+w#Vc!5|~$mGdu$BDc3^uenA z7oH6F%&6WB|HI=C4(Dk1M5=gax-T0lZ(r6Baju%Q!DJ{Y9T^~*C;C0I zKCBR@=V?FRu!h&HRy=AnZn216rV-Yo^$CYySs+UcVPsd8)`Qz0kEkG z$`E!@C!h>L0?PX%^E^dOhiH_{+lm-z3ewubFPsyE3u3ON>@$(_uOEYGQCMLW9vN;! zCkeHp#y%H_LAb5V0~BA*+nei$`M|5l1Saq>-L$pKQw-SH0oh&zF_qnZbZzO;eq1J@ z(oO7(-<}2d{S2W0ZN-aII1z$ANTmH)u0+w&A}6o0)ca>WiEpa%vNtY8=4Rl$<0k3g zuTsRvOI?w<;Jcluvyba88SWM*Q&fl6$HrVBo~;Lw6M?J8!q|L8Jf-^&AhAiHH;4`r z8wx7X)x*`}NWu9ED)My7Gt!!dE8pT-_jpzl+|W(yN3rrS=Pp3=*%~=c-{ble^mCBp zN{rUExQiqOmeuA7GeaYheGkcLqiK!r-2xfqUh(yv%$XKqMHQ~}!_Ri&NNA%|ZesHd z&m$py)9QUtNI6bP^T-k{5t-!|RB9CZyAohQ5qD7Zg=DvsWS39olbAWdtJ~|YmCm2y z`+B(S1@LE#cVC&OSz7n_4=R-x!qyYMJUx<04JmykMn0M-4ZsdL`e?}f-rHFJ$=<1e zS2l-(hiU?-F3@qxDEw{ZFLe~?)vx#0B(&f!n;(JFH$3>XK+$sGn=lq4h*N}D81#AT z6adXaWA>d!448#b{Sq15ju(c9o+k~$zSZ2qP(w7SK${pt6V_t!29lgH=;kMB_#aEo z)R}G=8EW-qJiit^p$;hw7>pBZgYl}ZV^ z8zU-O$trz1yd!3mS56QlG}5IKPU~@0m7iLU5k;4$S}E_-sRZBqWM}01{c()^w--Iq zw{*}+)0Y+EZ+6(G2R@?8$@><_R&qJ)5^;MPE_Jv6?D~Y;2Jx5e36v`CGLx>OE38Id z;XGrzeQQQUcz(ZC(!P2BU)1^{<4HoVZ;x~yfu@5C*>^(98xRe;hA_-hf}F`N0^ijn zVmj}DMjb$GsKmQSRt1TzfHD2buK5(yR^>Pq(!ClJU{4H}Au(zTVN;3+lCtTc z*8EB5)=e3jLZdIPwxDV7Jx9D}o?o>lRk{l_ri;t}qQXNaP8HwBxZm9f{Y9qO z^c851y((3^e1|c}ls{mbD$AIAM$t$A9ehLoxYoy5p!t4f>Sg-xRQL1g(lu8qi>%-S z;@_T}frhR(8=E}6y~Cfs-<_$uZU}Nu54IjOpJ-elF4=y0u^%uKL{Jx#a@gXVQ}J%( zy^xMt^0b=%m$stbyPzw=qa@|F6@U216)(!&B^jU{h@a9@M+|1bC|IKJ$9g$dF?A^8 zB&kt^IQ`a~tyl-Zaufwa15(R5i*Dm^a_CJ(K4#<-8T8A+5db`qK;MI*(N7K`pu~;q z0HNt2)%dWqSL|Z_0I2cfzaF88V2Md+JlSX>*?_6nYpb|n^IU0^w-?1cO<*j~E(xbj zu)ci9)w6pz*j;G;FIW26u0?wM`KO2!3^Lj+d^V~3Qs?(7WXr+cj08C$y8k(g!A#0Q z!)UvZd>;4CPNw?jD6$1myD|~+>^;;0z$U#m?ML;~$ECl12}ByB1gwJcS;RoFb^$hh z+<1_Tddj+3pWwbsG;1hQ!YZ+bwiqVi7+aI_*Fn%Ow$7l~L4HxEhi=z)C^Us%_s`&e z&vh<^QomLP z+IsCw-MxZ(?0>5y+v|RKa=&Huc0!_#>ctC>z~k%U^@*9CjLM|wt!3sU(P(JL;Aj!( zsB_!2o+>x*YwPA<{LQn^ZvM-jW_s-ZN78jiQ~m${d#`J+Y(m7nviFu;5$+}9A{iMW zD1 z8Lh0H_s^lcce9On!T6VuCrKIgL|6(aEa9FQ{iY`f4d}^GzND)3KC1M8h|r2(gL56D%g89e_qCCHy_cyBr+ze&ChS2 z{O9QTGgPUb*HNSD)_Duo8wi^`hsmkL<%RkHBzCwmUF zrQO!CX==vusihv=}DqM$!=l`%Vivk-^9Tx8Vl!H)|=_f*D`N%h1ULxF=im-9a; zdnezIK3+K6^Ikr!bgS0`YbCxmkZVC2W_*#W<(nEqdKeJpMRM;S6X$AKG4g21sUcre zm%PP29>|08g30G+SYVa#7sCdhUtQX0^So1i1rXSMM0s*gUfqdw%L=C6LSM(gT z%?6&zm--)%a-o+3@)M(l`cl-a_MrT7*|GkL8rIbmFs$40o1R0Z&PLUJ$_vTBo!{b4 znwrCze??dq-T~ygc?eT)AgJhnAGVTX^M*ffpT=e=^8H}!=N@5RtskP$5+d$8IWR9L zC3;AHw=~r+B65N3wwq^1XD12kc@wGmE~qPl(qMY~*5hqe<4sPZfY1U;bR!^m#Xw`c zxK-&NK?N-Irg>yKivQE;jRZ{7xggTzpIHxrbWJr|(tLU|6g2aQH&!^2g1cIV#O7}V z6+uA4Z+NwxwuF6Pc**12$?}H8Ii=zg*5#)KHag-Dc`;B#9k;8k9LAV5$Jt#ym6_0X z(=P9VtWI(FYvz)e>Ge)`>QQ3X{o|Qlqf!&fo@J$KktIehp2+olnB+BaN1eIUa zJt&1o^VS=?CF3GS7r$Q*T38C@th`=0|HVu zFMVzaxveX`n*ONC8AMIBBfOTO;Cg!!-#r~mrGXIO)x^S@bo{18A0p^Cq@b`8Q3d!9 z`^VVY4NL9i!kr6)Y4>B!Q~sknZ*XfMy!6Ci7=NduI=;}={|6A4#{^t=0_=Nc{bVBK zKpvmQ6tF3L`fsl}j{+G#{+W09J%09%sO6Q41pve!@%y(x@1+O*H#q&8^Uai7*1H-% z+$*V?_0OMrIH2@^MuW#oLYpG9(#rruUq@n^eIgAeM7#Bx%4~!B41gd}D8b&OuW?o> zrg;uG(L_2GU9__6j{Hl?R1YHqmjv=6-M0_ODihnG#%FR74&Iavc zt8(Dj512@DwD#PwlUhf~9#(oQ?dsN1elu$Rjq(wP$4Wg&dV;}8^%!v=3zu~yjagkl zOoj2)g{jeS)lq1<87rccx@tz;cTpy)`Xev(%MlXRI%HuDv&|cqd;Rsy#jlgtv;GC9 zS6v;A7<9UGaGno z{LVVFWuR%@6-kTjg+T6R@T5$=-ox^q>*0obO45~OR=+mb^X~@->sS}Z5@|vCa;v{G z(BlPz`I(|1uBwq?C$*&f^&+c;ry_hr;;T3AG~Pf!V)67(bOj~~jQ6tAbH<3tJ2u0h zRpeUY^Z>{<%k%0UJ_jkX{SN(%7FC)7;DvN};o#(i_hasPavX^ zzSG@v%fln5@nD|GJ%kk1T(q-?8NH-zH78&rN_HpsS3vi%^{C76$g=S55sGMwv2_@? zF_>39X7;Vn7II8Nf%e`ceCli9sEAHyqBB$LhVMwCb4tqwP2}h5?Y{5!?&&3nQa4!{ zmW(Fet}$>q34#2v%pa#hV*NT$X}w6Nw{<%auf4sR8mrfvMjMp4+|i4*XxaYR$Fx#Z zi|S&;X(!>?$nrC#L&L%q+pf}c6(t3}i?6vqQWlQ?cB#3mV;T;19Ui=e!l<$`?x%W$ zbxWjO8C&SSi)-KfJ?`SC_9J+%@o;_N>vZ!)!Jyva4S!0lokm+0;9k z^yTd007jU>3qnM%?q&KCMe&o@`p4*JB;LS#LyI{1ZM2v5KPN+3A0p*G-c*n2c}*at zzoMeV$tr!xt-Ulw&G6g%=5&W2y^ThnMweG!4FRZRK%qVlcM>`3!#h^E4hSdlSP!Bh z(Dg{Uf*3my@n_pCC()cnzmb?LDLdlD#EkcQ#n7c)Kniy7Ho?-N2LJ!Uaa=bJFRzDpI1QCU2Zyf*-jfw@@Rnok|xi; z)AWNUcPDtggXmpG{frw@sSxI?Gj}ZJ(LqGPB6mc?cNcqQ?+JP#5)+g^BMASQSl#+X zh5`F?Cfn$1g&xP;Ojg6MZEBu<*$Ych-#{4NnXzSqukg3_Cvmt{(~|=S#nc1l?(AH_ zFyXk8i`#8fPk`BJ|JepfiyO$|pYbf+r?TBXqSs})BS zgMgdf=I3ON+5LQFZMR*SJ78iN!*a{A;pKk3rkGV@ejwfQ6U>h-(f5izT%ujAo;J&B zlaXbi($$NX4%g1h-Gq91$z0-0Z~J*w#J=AWY>cV^Ap9rS$Ldn;`Tzwdk;Wc*YUd+OmVPoby(;3M>m3+R`!Q^uw!dmC^kXN@**KNs>q;$6s=G!fDk>xa> zS4**YUf=@zJIX3a2h~!*eDbF65?9wEW-N5}!$(H~>&EzZLqh=TlpH?w$Vuo25b079 z>B(4lbMVKwetaddgntu_Q%8ucke(coLes|ZhzN$_$BF$$yqucftZ8)kvj<93;_=d9 z8`1Dah5{yp)3@Q-?=Mr?QFpx5R;=B~{tQxQMdgY)rCn50Yn-;`dY%3P2vYi|k$=cc z4&P^FHeDu=&zFTrdtOxjCxN>2H1hOT_hA_8RrDJ!sy*-vxq18Z=`^XT_JE6{j6HMV zpwz4BDX;Z=LB<~r{O2=It?2l(>ZLzVCi{Y-D7k+=xFM{oWPKl>BLdc!%&q5UakVqW zd;7!RqIE@J+zF1|7vUtTJYX!W8G+kC9|~)weIXt_8RL9Qk5Jc7|48u58&l3cnpH13 zV$^?H)D>r*6nP6Mz&MP7vlP(GH{Bgl{=VOagsr|Ga6SoQO!$>&ss=pM{l3|_R+}O9 zx>B81*ypi)f1Cz^OhA9gBf(kYz?*WEhUK>}zn)*cr!>=73B1_Acgxpzy2L?a)Eyhq zO!|^O24=KUa?H8#p@1}YK{+oq3jP>VtXph4+geNV{@)+(-nDL>?HxDo-K7_0P~n{q zxHJ~|J9$^_kgIC=yzS*PowKd)@jEI?k^#Htlq{A}{C|%|G_B%0X^De$j`Y4|HNREc z=#ODI1*uoPa<4u8yd{^Z5Xj7xdg9yS3%#CRXuDgzS41{;Mj>51G|NxQsAsZ#=mQ5^ zk42Chgd6aDPlEG2F`#%zF^#61MymqzQZb}_j3?MqWMsWa{o*+nD$1$k_M|KFe99I3 z5ej2%HuQz&KQLt>{1H%Mh7~xipTmNZM`clIJ|IApmVD$zRxp~VL9Y7QxE$MX3o%OU z+&6^alt6(sHe^DTYPPrRSil5-m8AE_1|>EHJ3LqOLEw<8dp_G|-~WB~?N(0DbkUTz zzY7#@*Y-b!>v4rXO_Q&;2c6O)(jF(hDkkfmXqznSz89@)g`qGQtXEjtr;&UEq+XBo5)F3B#!yJwg zgCoq~G~#p|f@(sR#=idEijR+Lzmk3z`o*VjkX@5%P$3~vVqmV;aPt|5CyQ-TR|>!pyUVKRb3t^c%JTB;!~N5wnDdUVeD`mY(bDbJ_hkRj;3_{nd;gUNYc z1!GEH$032n4Te|WmaAhaxDt+q!e543P7ot2hv~2@y~1Fm+aLpWg#$2lscX1R#}gx+ z`ar0uf3^G@Tr8-h)O-4ITaa<6HGXz)5}f~t0!TL3U~|+2{!B%Il13WL;u9Ujt~n7$ z3SR+z%kr&<-vC-OTQS_ITVat@>~O}H1rb;=jHKA*sAtLs z&&L@G9`TyG$t|zyN*QcQCl=@m8Sm86uj(q1hn*_L(2vKY@Kbo?@jghWVP++Df2s+i?4A(N^$2+h@uC`FWCpGePc|NB_#kX~w5f5zvtsRBXX zEw6la8=W#nM@-R3b-7wR*9OgpvsErMo@@N&wsHAv-t5si$)VdI z55Zg*w-8g!*PZu|SJP4p$+krz;wn}j=Xh^RdO{L!S&y243O{JNPDs-l-uB+V8_;; z@7{#FZB}vtM@ml#`kVLamp4>P_N3Iv+qUVjRM*Z_;%{8Qunzvl>T#_~(&-XokQ2GM z*^vPm`q_jw{8f=R%THhL;>q}&C!u6BbldGU?VL-yTmOm0u3f(v%@yjNE;Jl!oDE2R zG`or%Lus%2YIKEps zMs96w8T?OH)0R8(JGQ>woW8HJr1`x_-%XyWnhqOHZ3v#m83~r{snDr-%^fv`@0xOk zJ+$R!2=#P~2gtTWrP$r70z7#~<@Bzj=^_~Gn;-MHCCMObu@Ivn{ zN|C-8O3o&s-HTy3SYGa>Y@tNqSSTC&x7@B*RsrYzNi(a_KLZPNH7L&-ZovFmcw(m? z@M^imWt=!#01B+Pxp$@Z<^jP;apqX*XmT5mS$)XiRo(u(PSP)z)x=BQoS%RE<@SGQ zWl7B?UIqEE(-d?i&*LMO#opnf4f-T!#eS=eHlgWd@QShtetK9(_`F>%2C*S%3kjz7 zD7AILf%GNGRLK{g8b|`o+6VX>R0uRCG2ORxaJ3g2I{JcxeN^WPX%Z@8VC-W&#dq8R z0p16KH402V5NL;3gSBojuuWP+v7Ro19a>J`=&*%4Y#sdV;9t*FfOs1pgVD!c7_^%h zY9d(wqskn=ay)!Vj|eq87(vsv@`8HI4ad7!257aSmN^ZWicu05aeE2SA`?uKEK4+4P;!J(>Kb5euBUFq&GE%^UMMQU23BFeP}C(h$0H|FoDD`Y{Mufa$ zCF8_kUO;R${NLJ^VDNQQkjUO{iYA{55tlSAIAFJ<9SB(7t;3qU581A6{>G(n;C%MI zk@UdqtBd&F@UbFm;Ce2o?$hF@l`Ywq0sLKZB_jEr0i&8Rk2CErY?SRbB zLJas1z&Fnmj&{1a2TlcUUO6$JIM_!YWJ2|E>8Qik^evyrwCM1J!n-*}ApC(4g)LaW zze-T2(YV8jx>tskT!Yn7n_bY*M$NUqD|KockjZyup8}KHNuJ#tSHZkUbQTHz2H{Ka z`%y#n5BuO7To&(#9+uPros zpk>K#_4I*|L8YHlKm&yG=x8lL4mZOa7QGE*1WV= z*ZdQoFYmR}Po(}#f%1EtZk%D;5BHWrLj*+UlG$+!QOtHkSPPijbWGT3rr|AFDG;@j zI%ZTD(11I%y`{fOZ5bubmnpXbydn?r~T#C zSw=OR&;D)hPC0AnhTH6`&|XeT*ay`mU{*YlZ9yD-7XL>yxw@5|VL@}n?#}B+&Z2>^ z#qCkcIO;MH*TZvj^O#u$g;Bj9H5|)z-^MWw#**pN10nA8(Y0_IIEX@DP`K-I;6ro4I}RU-_ydMjz86`mT?1#zNf`#h0pps?wM3UpOtwEX$D1!>i#z1f5Uto+c52)Lq?~;|NB3H(?^JLvx)+ciYc<-lmAoMHu@0jDI${ zzQ9u_;o{7)njS7oAPE{1zE9K94gM;>)`It%*HmBu6_^5hnx4P-UKY#s;4A(iAA|6O zE`E@Qt4$%%4c5NPy~FQ$gNAG2`qekD-j&$~LB^759Jso-(iYWzKimsDx;R{a_cWrJ zMPhU9))}Jda{K#@%C#<~yX|21$tcP%^Rb8}g<|a#&3hgX>(5DB>=)#Sr?>rECXkNB z{bz+AAy^)9ayq!-!wCME*m1V%5=o-Vn%B7Q$0YcED4dn0pm!9&na4Hz0?&t=j| z)b(*bs%URjw6AJ7bAi2T|F2?0(*r24qrt@-_i?TrjDIToCQ7J&`@~i#OBLUc+&%Y| z)q~Rqvg!Uarqbsq|=nI|0v$ zf3VCx)$fOflt-n8f0iK#u@Ovzm$xOl2xG#s6(CtDJ zE_H`0i`F-Bzv{WJe`|i~ymndA?lwJ3-b7#lSE8c^;ISM0H$0z#Kqil`wz)svt@Jw< z$m3W0L?wG)>Z>ph{~HSV2>D#d8;kE{i_Lcz#+$QpEOR!ZcM8`wEB*V^KMG2%ef7_h zvb`n6%WK>J`fKX?xq4>6LTI)ZkDi^zBYR^x>QVoRi;4JRXZ!J(*MF53<`=I2DLp*&^dYLvvpj8dKZsK zzI0zx{o}lr`@?$@?rc5s@A0rqPxgDo;0w9M*m=#I++cnQVby^E+L(X}GJ&;nab)XB znnQcnNlrK8LdIU`X}qC{+ckQ)7&z8 z46JvS2!-Of!urEW4v}#nt4$kIHm9q{4AX^m93~EX5{VJbe)l= zc;0KdKkUat@}ZRrQ%!j<4$Ugw{vgsI3UB}{De+YDf*`d2)1LqwPeG;CRlf~+U`MXy zp(%7!%l}A&S&-+Gp~j3Ta+9fM0KW8!uMyV3J6f-8wpH%+Y0}12;CAidhvyDdcNdm$ zgaW0~`40o|82zSZZNY@Ex9d)Ec^h-00tV&AhCOCQ+Vr`wKkyz34(*w!k05kmPhgK1 z(w9z)(xy#KKqCLBut^LH+soFM}nZE>vTGVZfxaWos(S(7nlH2`Xb3sGK>rKrn$Jo$ushz)**GYqLiU%Owh)*I9IwjIj zRH(BWWK!c@q(0hQ&=ABti#CmoyZvdevyp|Ec{b!LPu?Q0@$e|nJEk} z1wj|y2I`O}-_@-_D2&g1;y!No4pKin@FRKqTf&nI*I5<3=P)}yd1jo0g1`;D4G=+6 zpyh)>{9vHV$kcJ~1S3YB`zcM|k2e-vl3AS&$4;G`oaf3Vt=%EBBj8xe786Y;Zh>Wy z_i||6bgoBc<_jZ=3U%?A6}R7{Fz%_c{D46JpbpDaj*shwq^?s_y4(d|LM;e(`^A`L zDK@(&hie|M+*+iaSJ13XMDv7ZV8zUN4McG2)^Fkty2ztvzGA(e&>qZzkDlr7qw=ior?I%$SsBGJEhua({TNPRZH-!8qHCBd<#=QIV-N*ue4j@F9G&nm))E{kL!&ln1*)AZLJ4M0kbLe zG4V5U9n5I?hai@8k{~V2jW9=ss;)%#3ULfmTG-EqFcqSXXcPa*53S`~nWw>9qcW>S z;xT{M8WZm>CQqw!3`FHze9KjmFNmL2>^|Lx9OCj*& zi&J?1p$YVk{MD<#Z&|^-jbEfvs)CL_`tBTlT2otiR89zFDjenRMA``G6$^XlB=Zn4 zsK%TmgM=|9cVDSwOqywXXcrJ)an7Qh3>-v6!TMAvR&c=q1sJJ^_-GIKkev`v`bFrC z)d%(>^!Vs)Qz3ASYF|QK{FGF^0iBlZw{mKakCgiT)BDr6ecsPSqYH^pHN(94$PZW$ z90NEJeL>{n)7_^$Q~+JzqF$uV9oGeq^G{#eQj7l4u%4ZlDren|g7%pwYVW+xyb4oN zY#@}`E@<7_FMPN^-?-;q{A6g$%tX771D9Vot{_CE6<|(`5R9XlqWpEX^zaV^C)0(I zhHx7&CZ8m%kP@@ux5+8wyoKlHhT!Dm9a4dOJHCyC6C2Z9C~FkZaNdOZH4v7w zL}PNGe)#!S+zvBQdy01dTL)au<1CkMPYYh0t7ir#fy+#%8cl$rKqqS!a7AAlRgfT$1}3%sbFW{|kZb0djIa^h}-mp7#^*1+)5%&H|)0+~*j z0wnPr<+t5fo_ND2;~5QwlWVOcYZ<|q7wjKh6tAZ_SU%JaLTiQ7Y|e97Tnml2 z&|>-*HszRJ;nzkRKBF_ZCkfIf;6jQB+_Ys@R~K83ktw%R`S^nlC!_yR->Pd6&97THejBevzal|AH3dEoUdQ=-i`FfYtZakh}4;xkKGIjrNHi zuog8NwjYFcVpVCXR&@!1sC+EB?edJI($V*8db&GNAHerd{GrLeXYz2xd?Aa*3Dph%~H7KlJjG+}+DD zLs~jK`FAKUJ(x1|n9ua&+Ri{Ed?Y!esAW*Lj|Rj z9lJ{;73zNQS2z9sZF0Dv9Tn^OUdOtX#efv|QVs2LCk(pYLO#inWm%=kH@P)Jt{3bw zIN%!hvT>*^qStUGUiX&r1#keF_TM*MzsO6yV_K`=#dsRg^E!v_X7{Jdx1%b~BYQ`h z3SP)u_{2A%*Go&mycH7$Y@pa^?}_Et^T}L9u}z?uBn#6#w+y8jmEB1=`2bzGwzJrr z&x|(iZ|ZMNZxwiyq6IEgEX8QLNLz5{2Rvyyrdgx`Mj;Qjdyp*kc z9SR1bQD;DKFUeiq`x=C+WmMF}x_`nBOK>L;^+ZBRQG(0Y%sKR!?iJeuwYHDhwJTLm z%RyRo9k_N;frb^*R0xNFx*3^A#vzQ0<=KH8FTpe%*Ut2w6u)*gRB?{*qTIEu>3l?G z{U-4lq~fQ=Yp{^wvQx#oa^s>eA_<0aW42b^E2n`*55xKHVK!_qzm2~Ok!!8faD@J9B04fZfnp_W(n+&Ea3<5c$AULT&_{`Xa8uHQ;%?en{1HcHgAAFeB= zgHS!r3@kIC+a!Dy`B3pBHvC>7I2Fpjq?}^}!Bb$+eUcp*quQr-h?~B6N9gMVUre6_ z>PC1k2eB$oY<-gN!{yF63~W5Idp8>FbW@zi0PWKxg#YLlMIRMKhQfy0RNQ!cgohl? zqk&TaL**uN4ihd*PI}4^)(2l&Exf68oviDgD){eQEo94kuDpAUT{_!ez{@Qp&eH8% zmeI_Pbi$tmYP95XP0k2feV5Oetot_M8kY|kS@34&#`&L8d=$+aDX~D7 zOF_*4AjR&Fd~4eiRL}eduJHXFFw;&n&KRJ-)@0-E5BI>Cnt&eUmcm#>RP(VFh9TeN z7l0YL^8>mIePZj<=bi8eUU6oyFXNx5o+(3#r7EJu=A2*RM7|~2QD3D`@Gn@99|CL2 zTK}q$Nl04A*gKW|Ztbt+7VJ{NI=>dzqAgb9pEjJdQp5_7$Qe-l;^N5QRLNuC!bX<3$txIIrS<*#77uns6cSQlXB-kh3Kf=LR02E7*0 zH3OyYIm9#~gunY3|7sk*aRj`- z%X(Yr)+I^@M_Wz7P^3F@<}49#t&vO?xb``4_=I1@)Bfk#)OF+6Ulg_v9%u(rnEPlr zRSZ*l(08s_dzekg3j%ABGFZnRQ}VQ%j6Vrn0QkJPrv!mFBv|~MJ$f+}FkEt0_DO8> zL$t!9-xIH=7QeX3=|34M^+h`7l`N(&ZX|ADa1*aYxjF;v(LRX2_xoh=kALb-Q=*_S z0BRIXV@x#{A_eE)$Q)TC6y1vENxUU0NfKA-mFp4_?eIeE-}3M3B9)MXuXGLh`G0`Z zn>)W3H+_BLUcS{65NlLuKfaw*=a1ZVJa&Fh4c0nIr&r;Ln>QO-(v$|VQqw-B$PaLN z7W5sLg~vcaLk8y|=hs_6~OA9e#IqJ~jh$FHvY4W!3zj58Jva z|COEMUQVVbQKbpb0jtMYvB(4Qxl&Rzl}6k+ba?95U55xI$p>d^vwYdNq)6I7D>g4{ zKD@kBx*wx?6o1k2p~j7uo4TwHmo7l8-f6W07w|1=cCM%X5Num?8eybC4uuPZbAE1BicDE_NBz59$nRa zM`6R9yx=gw$Fx;9QXz3uv$;QV&B%Q`{ijBM zc=T^b{$u2FD@$eRy#O|vQioaXBv|E<`2d5nOVmjQ^B9fUw=xl13ve-%f2xe+?oT$b z5QAlgczPWarnSq>ZOn!znbsGaTFPR@@1=v0;q)AM%J{yh$;uZjM2UeM+3=iN#oM2? zMr+FuyhNkIL=5+A{oL5K^0YTe;4LRZRcn2>&(|kVVW#iMVDCVLlDYu9=W$?xyDOp@ zS;~PYyTz^0qd-QD+!Ol$UdHs+bWK4wAPEUsImlTW`cyIGFm^IQeb?!9)8csMOhUkW z;v>Z<)#JB{z~TfM#gGwHwLQ}@>cRFHGx$8)yhg6gA_`0*|e2s6b%d5_8Za= zEfOmMsC-mxNI2Rj(T*%(Gouh8@|Od3`m7VBd(2rq-<7e8D_x3$Q%ECEd0P0Y4j`(Il%xjP&YYptvMb zKhNHbj}CP(9Ask*8s)%9Z_#0-|CB_7@`qyTG+qYP(Xl;oQP+5s(dnWlbmzstYmU>n zo|b|WdYlrywi;(ceneTY&zxM7_xS(-tp{=Q=&4yJWE)xg$oSbn1f~Xfhrn&YMn-2ixR{aL10{yLufsNa z^-QSd#@Y|crAMCq3lz4v_gqrF-z-fYhVHLOIfSNsQ#G*nEMZ=d(o=cZn)@?^!R1fL zT3^bK?tmR7z4ogWsds#o$*C-16Qv-DISxX9IQ<*lZrVUx3 z-}H#aesk5j$zZ>T{XfSG>-b#|PU#zF7dv>9my0zc}>Zq-OM=ULwn8^v!;ba2q(o#Z+Td7ZRzp z^l^&s&?{M+*?nQ%0h=H#K2VH+K1Jdnlwh6KOJ?}tW;N7>)x`iOVD##yxsFYIFRKP0 zLeLG2|4-LCs{_(U($5-Fc%Z;&r`U}GkGH*4y@O91lAgRFgJga@3b}<%pI^*REF143 zcR6}q<-6?O^E79-0A93Jq}Lv;xb>!DF7(a42@qBkO$vid7Fgrk)1ck$dc1iq$(bC{}(Qkm{xIZ&N2I@4 zZ-o-lWJq{PU>#poY93MOR3t?hx+E<|7Tx*}>CYdc)8nP`V{A}xlvmvWVljp4Qn>xp zIiqO*n`_&5n%oH%WWoAQu9+i0Zz@A!cYEf#X;+!SNEZ=)Fi%}CZhT`fEevevM#hW5 z(}&?RImCe|a(N>9KfKff7U)2+7!$(-VlUswQp*E_YZ_~;FoWAyAgUc5NuO92r;e4^y?aO&J=6t)x?6kwf=9%7D5Th$VZkZ9J?r}*Rv zGIVpQNg~ZWsJYS0p-|$a!u8(NRWx2{dHsTvuPu@^e4zl_#jtQs_3wL2pYwx}>^x;U z1uQ6!EAiyi(KP8^&3~i-j%L%c{l%|@#9_;z%QT@kvhhgOCB?@dh>t(_(3R!HsNeLMAlIeo=!+Siiy4Wc(=P>S85e)-UlFY!*B7jcQ_#*b z=+MHTi3KaVWUBOVkKS&bxxFkhdC*9~4ut`;NjXz6D1nTC^6;!wex%fs_lbG8T z5+7|?BnHGJa&jsKubKR}?~MPMu93b0Fgw&~T*i;tUxD)m1lcF^J;$m_Du6=-f}lRM z{^p=*TpKlEOctxtCyUKtlg~GGn$m4csrA~>b$;e6mf~I2_0i7{sjwTI#5k?FWGoL` z1G>yBQo)w=QV44NDYvoEd19O-!f0O0lgT}tm(>`YpDxrpYcTh*$9gRFq+ZsYTK6AV zIgRUb(U93|w+zZ!mD<-IGvE0L3*(sO4@o?4RI{7iYQY0o=7 z89gAc`_1e1iN#CZrqt?ET@BI7&nPFC>peJ~yDOFgARgfj&_%qoX3j%V`*`WxNBxXW zg%1;$1#F?IRKWEj3a}OoBq-)2@s$PEX}ZdCKgd_M;|t$}bF{?3M3m*9pDLO1!O?<` z+u6ATL^@ybJqfVRNl^d5BljRX&7$_mZ>9Q6u_%|%c<@4xOVhiBRPXD=r?+3klxh@< z7)SI^-{@E=HouK10gtocfw01rv1Sn5;r*hz^;3Y&f#5eWXn)tA@YfjVEwpV!ab|H+LI_=jAHdQ-M_PZJ|BiDQTnWWh5T2cp9 zeJ*Pb;G6rTJsrAa?6k3?YC&J8NDCK=LVSF5bjJnWQ4C~+g7f@v-TKj734zklG&HlvTjsK+rp;6Fb!0Bhl2+UZY! zB+|ng$O^cSo ztFsE^<)qQu>MaTvn@6@4Y0~x44_w?|d*E`6{C=|m;rMqDC90mZH?TRKLr=Rd@l#FD{3bF zq}hbAbdE)TQewCN2SJ`_6OFVP43C$I8JqTJKax*(Sd14feO@4TgBzZ!H) zIl@GlMDc|T18;$g2pwg#cgOjGRi3W6AXvj*FOw+NjLD?jn90USO7raEGoMw)v}0P8 zOD{k8L^3E|MMK4-r}p0he_er>@z6`bXEz~{(a;-d067uP#w|&S=$GII-{(xYFm))U zE?^^_(KbuEPjhfN&4hZ2QE&P()7Q(XPk)-YQt{zaZf&cDplaVB2G0`K-BD{#$(#n_ zy%3HZ4#Pwaf9}K>#Vhd|^8reW4zh82Q8L?6x)LJPF+PIPl>jQP73oLz|I8bT^(jlM za|5HQpMB$Skw~jI^T$c_v!s50F8mah^hlPt4qVr_z6Yw7X`H^vee)_^`H7VNb{`?;5fSWCN~Z& zaF2>Cjw&5ccT*wWJfyj~Y)DkNA}uXt$7q48^}a*Qaq=eYooS*6vhQ2>`(Gg_3Lmi+ z#dceU>qbV)Im@O%wy)1gcHaR?BxCG%#Byb>r3bJald#?u97V^@hbV7z<; zDk$O39*;F`*OIUcr;B29JK79=U|;f(=4Tn7rxpyQK&^x-q;nhtvno&&zA%}W-uRk! zd-ewhD0|D1UFDBXU9R-6uV2krpQ|}lvg21Sk;|jMcP5CZ;zJalvR3FedS(^>zWL&tp+jZs&Ma{O<4hJD+P`9oRH%K>f<|Z?;c;9RK*hL_v-YYW!W)n>&H>5WHzc?>hF&Ox zq>JTXAi~+}Z+KQBd@pIYeKVlmko^>_@9Ulddl$Cgj?8VHHr#IC18z_N&tOKoDR60A zQea)%2vp!4qHG{k{>24jkWa>%1j?7RHtK~v?p)CnFI_HaR%Bm1v^`llz7@6nQ2I7q z?B02+fj#ONa-ReO>{?G5jbtGjIvyIm5}NbUHp4Nlzkq`Vy@DcPs3h@7W|ES*9)+IO z@9PkF_`h#_?x&;$LF_150D^ApUNM%2@grqOkw;i=PzfH!-H05aXw|LI5Gon35H!k{ zh?8i&SynJ-UKcC$Xc(k#Y7!oi9u9&B$toZO-QRsW@Tek$i}V}SLUblX+MrqzX&{~- z%#5o-y{^XKl1+L`G4=D&pwwZ}=hATvRj-b1qgC#C?Q8~C0{_Fm|FpfOwI#F`q#zNv z%*3Ot(IE8x<2R`bcLPs~sB9p2Htu8uUz8U#hyoFd0h>6q~ZEc%g?qD z>|!RgQK2J-7dVf`VRX8DOfvh9w(76i4xfPdZ#NQBR%|*Is%gCG-x_wGmE(K%a`U;; zm}KO%w!VvJqf#De36Y<-$De6A9zXRf7zqcck?Ap#K?f4{Lyr66Y0WH znU3FlV5DpR2?JKTkf7n9^V_@kCkRlVG{{i#$Nhdp@1eE-!mOTMoJ^D)lOFvn`e96( z%@n`{TF@iLb_6L|eMAImRZ{ScE=%LS88~h?Rb^4WkYqh5vuVo)t-CwWe z(Ox&8BL469zh?)ug_*pHH}L7wWr1czmK7o1dEi1ipxo>r9Nm1uwd~*q#Fo~4vwQ-@ z#_F61g8M++Y>QYbTivf=g(gukMPOlN%1 ztUUR}7niBJmz>UaCJfX!y~+-stcAJ-BJFI^!ps|C49{PbMDx9~l_D(L#EhkduryWrn3zCV#Z=t!KW%25;< z&QLd#qs%}3DX+5Wtl95mcJyBefLEJ%7wWXr(~3De42}3c{MIXI4V8a^K{Mz(2ar6; zB!ho@JBguR?^-w~wHN|1jy0RB0@#%l*>{m_g}{xf5}vMVwO990uA5FJy0Kr{|Nc+K zQRko?GoYORd_l{8Racn+Zf`^T!FfJzr&xg`DC{pP2yusqUYg=Y zoEnuD?I!NT>v4UvcK>V-+($q_-QImCe{VA)g;DU2qA;a>s|RxavCMjm@nPy2Md}en zS#`ij-MFyA|Izf7VNJg8`x_u7pfnOnY%oAdL>fl3fy5+~RzRc#32B(5q&P;1G#ed~ zk^(A7NR2M(7LacEJ)iIK|G(nJUT{3m{oMC`UFUU1&f{SHJEA#}#g)|)MLIW5eh;MW z7Z0y|$`Df5gix^W(j`hBr!k|{m-ARyXxGrSsQY)*n1McX{qz3aQ5Je% z>U2-H;dB|}ky1~|Q4l*_*05Ow4>uTjLCU!_RPO_iytQWLueZ1oVsdfX`@AOs)c@;W z%Ra5YWKitJh(6!bLe<+hTst`;w-kXIF8ZM1{M+D*Jz`4tkfYc>gedB%Lo%qaht<78 z!}_;Hgs()ncIIS?-tGA6aGbhh%qECaKI>tDgcJ1-p;!b6hn48@#k1**UblJi9~8_lJDHxu)-lPH;mE&QXurzrFQnbDe4 z0_Ck7N@~G_-S4Zu5V34gL;SjhXMSz#bvvtcB~A&y5)1*5uv~W+UrVltM_8La1K-=C zUhqha*D*0(=hw!|h~#|^y$upnBslUei|EVI_I`5VM~m5H5mE}pszRH#dPU2kD@_QZ zC-OUwA94!sJa#)hBr)dn!3f3lF8BtElu-zjDg*V?C##}yFfXWx2yQr^R^)B|z5D9lZt#G*@=h#_WKOOymiU-h^9XbDtX^P;E^5X)HtFt5AmMPL&U2R=H zHg}y@bUnS)lV}K?Fgqaaa}T~0L1e;rPtV#wiY8A~xw`h302bm4VM;r^Gu#K{b9ULyR}t!F{&&zt zAU(!ldV{c#A{jb=Y96(ViBIBetv2+6?(rc0IR0+HeaYe3ZKa}WI;Vv;PA^9*!kJgT zu*Df?;JzYRb1DexNuzK)E@_b?<#ux2;C4D2m;%YTJ{`QteAg}!@-st!IN&&)LA{Pp zex3H>^d*8>d zzx&Lj=*M;64`7Vc9vxm(^Ry+xvSdBq%FSQU(H_lkX$ra_JPf-dL0nK zO__qFa0>q5=OL>KfPiv=ld)^A2!NQ=*NjUSMp$?oq5%T#hVRo+pKD-(#OS)Vf)N!{ za7APTg?Lz(?$f)$5A;T<0nZK}WHV#0g*knzK0|b%=xfKe{?ZfkO0!Hqbx?r; zWZQljWTfbP^?SWa9zcN=CZ7<} zm#K)nrL-gGdG)>Ad`LS8A$CtH_fg_6zh3Ia$BskZfkj!M&Oo-=9?S37na4^y`0ETS zpyUE7ZZVg)=<`;N=7RECSpc8V^0XO}=;*dP+bXTP1agVPYG{lN8e=GEM2n7K+U<@c zJZ>5+ek0)r&NW;UxAK3YNkr|ZfL%=O5NoP(+q5>pj1IcJBy3MEY`=C_I5Lspg}E&r zqns{mUw*`J&fnA^KKIXzvOM+PI5hE}ss%58=A#>FcLVoG8jnejY|dem#CJweZVm;Y zN<#JwIDJV>KX7&M58ugx-U=S9$}exi2l4NwOrp|xI1xnenGhim4WBoi7Ef89kkG75bN=|x%q=Xy_gfAVo1jvR;k#Gihd$+3oj54~2l+Td3;HlD#AtnPX)BP@~bivxvK*pz>hZde+m5=3(~XXFg>`MFC5CBeOEtM6JXbW0qG?( z7=531Qz_B|pTDYsAmMY<0ZdHJC@2wn@SN3y^|4Gj|C1SA`G5-*%k$CfTS1p>vsZKU zvuC4znf@DvUI`#}H)EFsE7e)k$sI`Y+y61y%9?rn-vp|Pe?8Qq)Y=5%jy=Kf23+;9 zh@g`ptvcQj=|10pa#e3$iu>1~TZ1tuNTw4wq~X;6qSVOFmJ@)xtb+t&rlDILu9~yP zi0sv`{~QAEg&E&G7j6Yd9$9W?`C&J1yZ=p25;Ll zB`cQ_{NIb{c$>c4yll`N;z9<~w!ajimQ|dwD~uWZqt6#Z3DSS_j)X<%JBLu*q&t;n zot@yGpVW$`2?@-$WWfS+7?@pc-1+>q#Ilc)G7X2Gz+W`#quO42+e}yE`8$lUNrU6P zSHfRqLE@Y>#m)wkjFQ16R0d(-$PN1N2NKz@Ugud%LTvA(bhGrZ2!5X6rH31Wvx?fN zo(RE(G*HTkULvXiU*ul6tJ59+}32v-Eo3t+3#h*@~<-8$?LQuA5ld)kHg8DWZEnUY*6#|$FaGDBCYr5O69cY z8qKt2t3oGn-x5`8Hbk#VNO&mc2Qc%%-TB5+_T53IAKA;h>yqxXTz0CW8&wZQ1WTxk z9%wV(DWi)q=vasiv@SEQz_j3lWl?3gGbm@F(F@iQBwzf)@RO{L6qevg;Yg*zAJBGQ zvfsOn+faE|1lQ^%0FE1adpb8p&hDF|I{6y_%@03&=ly(8PPMpMm(=&f5+SpmiSMy# zo(Vm~>S2shxMkO>IuOg_QJeCzWHFDgeCuVUTnvd_`$pIQBUz?>$}D;bk1)X@XM zINH4Fg%pmsp^wix>K?rAVF~q1u{qW=xthL(<+auF;^=DRSWS58^_MT;SefQH9Ah4e zvfC^BBDnO0m3H$M(!NvEevFVVB2Ydq>;X#3VsK*aF8$IFil!x|t6}8g)Grq#!VH*% zDp80QCI0}Sq{gx;==vlCKe8KB*nJ*xCGD0d9>%_*w|bEtTvaN=Za9-^FQJKM(AcBg z`ulma%P7HQvYhucNfrzPoLw31tQu6TD%)$-&<`}`Gr0X7rGFZCjDld6j$PI5$J*z` zmQRFz6=__XPB#wU8PjHUav;{_3v)=`MIPl9aL92eWi>yZ9Ao%#(#J0ie4XZIFWw_uw^=$t2G(beDH1E8660KEI;g$)E(&b8nDNSJ>(SqIoFtr) zLW_7mSa3j)Ubn|POv%d33{9_sUMJVr{`e5RJ5AL1*1+sx`K2#!Z^85v0m8yhB->SA z_ZCpHC(dEJsWeghYK3ElpW=65PyP2n8|A^=)_bG4e zA$co_mBgbep*+3X_KzSzns^6Hll`~)j0MxXD9kFjP}Wt~%Pd~}o6vf5?DUhAmMXf1 z)Arl5IaROEBmy0s(lbBb+A}6i8wt|amJW=b{5)_4ReGPat!bR;vy#ZL$jhv%MBcjY z`QmtNY%o(EAi67FmbwI;Gol(k5HVESx&r57lQElSwb?PX;Z?J|U~fJMcasFC&Mvi(SC>`)}1fdm2-nh@$}B%+{k(6oL}eNz|#qT7NF*5jJ# zybMsUo*GeK)H7Oa1wMT{1>G`774*0ffO_@?+a^bZZF`ky$qTsjI(hOFkp5)DvtIZ! ztpXjdm~5?>l{R$TwXMF#LfE+KE#wa3RA?e<>Vb#gT<+}T^}2@Nz6Q>-1TA# zzDVc4*MH0BWO7dC9-G->FD0L%+}g6AVcF>XCeOC+(A|$Dcf}HasgCs8+ke7jd0!%< z(&%Ge6@xg>xVkt7n>;``=2^fpBuFOHcX559`+CN4QRg;x362uu98u#m1Oi+4L8!9w zASiy#vQLz=O{j1lTOCica|jv&K|qDgP8#s2SO@ACbxLx{fYeJq|9de&J9s4mQ2I-| zGv+stn(q=`cQq~u z!+ZebcU)Isds25Y9Q8>l5`ez2v)b6V00c{t3{ggafcQasEj?%l+JHD_V?+vEI8Xqs zHRNjt<5WN9RvuR=KS6s8XNk&2%ZGa^> zGotIxRw(UC&h-=Rv#TBM7#j2ecjM&99dga#q>B>N;ghewW_D0dH?GGWofW3+>@B!~ z>1kmH8u!r9_fC9Fy+Rn**lbHBHKbJ|m*E}m5hVKn^bkfyXZ29vmLy1zegIMrIeq@j z4H3d2ybiEMDr{v}FBvQnQC@_h^FPoqTr(~)_JSD8FsU;=A+|dqsYTHWv;Qx7y!sCO z0qN$mMc(8aG5>kE_RZecJ~cg!tVuF29r^Y-L(0Bz`{0wf%Vzq%0>0KD@8RfP2_(Hn zO0rdzv%8N-)jhP?HDZy8sF`8i@XEdGalg4a~o zn;llStV@VbCs)e-C_RB43Mdikq3LTCU~TYp*20l!gB$T1BR1I`R#<^i$1ZNRO_oi5 z5QR0*v*0N9Sax+`!&ijypX1t0*~(RkuxgVE<>=Y&uiL6OgTG5(_3<+NCPz=+PERl; z#j8o-CqO}V{vj>I`5qdpD?0`07rR@APk5hZ@ZkS?d1uZ@OY+L!_q!41LM{awe@0v4&$L?#-!OTsG6=RKNAf^MF z1VfkB^G`r4D3g3J18bVJXxOYxuFj2|y#&^$_+OF4Q_cv6Dzct8reD&&gb5&8VSwjA zY@103D59Efp%VSy=F%lrxBg?KEbnxpv61e+)H!DPc)j6n`$4`z(gKY0p$fr7E5}XS zmmHpYr*ee_`xaanFfLV3jI>X<76*(O3aVR_|UslASNE_hA-RSmN_1?y>LVNaoM}MZ*hE+DFqN6nassr?RGc2BGxt z?9EA`Ds{~sUUnYoE#h?@vx5yMvXp%N$En`4fGeNk(eRM&(5GCl?{_MGlzU&2gCmnR zVETMpcLsmBGP3|Hs^+}~sGvi6IQ31!X^IB(Yyg3v!hc7e&R^E%kMqn80KD@0AwV&Qr0m@W)~z&hHiXe)W_!$1oR^bQNgbj|TPoi6%Fto6&(Ik3PT*k9&XJ3o67G(9?e-69wj1#F3-! z_U6f>UU;JPzh9udibb)b#jMv~UU2OyzP^K8WxRhS6oJ|m*6ZFF38yk`NHD{YKuY=L zdC4$+DU&yFV0VY@Q#XVxy%zx|?Wq`M+|F`x$!sC?9m@E%F_jc427677rC+fUqF}*; zau1MSK0Z5epSTeuF55WR{aD#LBWb65MSGQ&iyN)-Du?ElQyF{>P`uGO++m21;8FEp-vk)EFscnHWBExfKyb|q0= z9|VmJ?=?wpoL+3<3F4O>3x^8U^N-ZuoR-le1>lGqfH#zHjhJ{=s1}Oti7TRzRw_@Q z8jTYhtTcBZdI2Xw=XtCQSN};%8|*@pzy_}S5UcRk|)c%DiuGv3ECanEOGOGHw zVUDESD*smmg4{&g5XJv~ha?h3vJxFKSYa)wTtA(HlGAU*=Uuw0XBhom`EqEv_6LAT z<(>bqq+UOs-UKJ&r3!14R2g||AX*s|qN%J5K&lf6$AHEX#iJ^18&#*EUD?&{iiM7y z&&L7@(c-}AbomwQtDr&yh?7{bdlE$SA*iM=)X;jZZiYKCGXKde01I=8n-4e{9nAzS z7f+NEmDY92rkfIkD6Ejtz}Bip_#qbv>4qMN>SR?dFINP0JMQpK>#CQIg8R}~!Ap*V zyC#(h4XwzUjkS5>zp4SC|Cqlu?+)5U0wTeP>OrS_ku-G_!s~1Xql?d@Tr;Fjhx^$T z_Ifkr0~h!w3;fUPZ;s7+sW@I(xBDNk1qhzrZAZ>CZ3zxOpi@HF!wRs1($u*2rNSq7 zs(kTW;sp<1h8yG^h|WEXdZX>E9Yd_W=&D6;;_iSFBtqEr{caWNUAUaC)tV%w;~gM- zi#X@!AKo$}gd{Oc3OAYHYNzz0TdHmMlC61z09%xpD_=TMz<$oY^*^WmJpbOmq2-_M zx-)c?&FJYntV`gkR?C}3w zK1z72vTnf@v7N|%WgWqObOSFWjm>C@5Aj#z7Y=8CrN~bK!|#olNWzwp+SIh#AXGZI zP_Hl{HOwL8CpxqxuYwp!r3uZD1{Nph=}BBOByjlGxo&#PzZ?FFit6cBg<&#>GTafW z_OcY3FHW=X$!HF9n8}s{k0=;VlK zBC`J~ASnm0)WnF?z(7(9o%mVtE@O~H1retZ87pwg%C9=Ue974g6JN3S={K+6jWD>t z1P&i8ep6V#B1_~~EU8%HBVnWf2&Enm7y2oX#G?n0?N8*m6Q{2s9l*eZ`N2}F8Q~bw zsU+2DcH7~0;632LF>4?OSl_907za=I`{CQ|jNMe~W_M{2XcPyhLp72FZ&IZO7Z}(} zCx@VnIlGd!fPla2lkZ&cwI?TmSMSC%E>t{RKi@U8?+#TDoKG`)l;uKuel5!38#L zkczxSCq!kDklS;)A&FD;$VT$g$nk#*Cb1kLAUu^B>Ltp$Z_5qgtCgCDA_EY6?Yt>C zQQ91a(3rHf4NRv}AMO}=djd$A{EInRElLEKEVumUkqEh_I;e{rz_`U)Vc*ybc#|$- zA=CN}e}3WZ=A-f?K@b=5Z1%76O2>&udoY<|Lfp5{FOD4CUsJIrlfYs>9wdEF=}Y4n zGZoh}0bwM>!^Sw^ll9DeO7@*^C1Eo_U%sPKbxtE~)1t){u@Df({vXGHd|B@-kV0CH z957r~tHW`-BdAv}zz^pLR413P&;&lL3PBe?7w_%wGA3$y@naAO(SFS6a9Z+;F4zwitz#H~p(jgUay5P;+^M8G?A>dxAm&+?BGkfC2Pv zr10+hAfxCMCqTD<%pGsD99jY}JP#g#V>lk zIk1Z&?}Iem*~}dJdhm8ly@2MC)h+3zgv?C8{wGGlX|&`lL9*p=XDLpVQ_LQAL~Sn= zrjKUV>yDeh8%sR5W!w0gEEfNB;Vm38cmp{V=iZN`Ij+jA-icQ+ddu{;K8zih6r8nD zNOvA5;NpJ(FTk=PK_zmFFOn;Q3c$U0V3az3no7?IeUzMcxbNE=x0IzS1njM4w?H{L zWCKrhMVT)Y&#3^-@t? zZ`re6=zX`5z3~{(9}GTy;EMPpEtTiC;1|R(1m6{jrSJTL#e=aMmxt(F#Fh(a| zD+cq)0%xH!hYBA{9)-Hl0b^>TBSr6WgW~cPj-1agPE=22(4CDhPiVh}NU?)zR0qQJqdmAX7Uk zTKe}rlzmCB`wubld`kE19hLMFK0ag(b60c;-X+^wpk{|f;dylv*>em`-cReJU`-BW z@xN(}lnVkYf|kC2T#UE^!_WTi(ra81?SH?waC|+*5|r5YJ2Lj$XB}vsJ05vgvk30^QJ%f zjOz3*r5x-xo+}I=!*>n1GTk*cMFOx1NFp}Ui!=_P_Sa_P645qY!o)f~vhPC2oH@lN zDw*1#wo^TCQ>3=MXEQ{X0iOZ~-HNJz0=z%_cmp?Yw2(kzvTbQKqyOk|_JRN-mltli zpV&=Zcs0iA|9OratUimp3c4IK{-5BVe|75~Np}{)46-e zP|pKj{hDQ&-6_fOCy(sOI{bbw^MUL6sfhplwUny{*;t^+df{?VVycZNal*9379)C1 zdxhd$+f8)-&~z=0XiL0exv34`V!--<@4VMGwi|WF$aEvu#NCQ$y?W ze6%~Gy<2cqOmi5wo`G((kRuDjPxQlA{hvhZ-x76Y*a|Unve_hyOnEjy>~nr^J9vIG zo;Q51e1DFj87>?NwjVT|B#B{(UkMmln^q5Se4R`Cd1h__tsk5=c%m-;=nLI?_Qnj~ z-JlD@h?b+82%Yr$qr7j4OJXuV)Vfp=CtkOl1SQFhf%z;5FfS>w5(=TiGXb+t^DGE- z2PkmEZc6_Sm5 zez6Zv;MZ#r^nql`tt-m%Yrv!HfUVj~bz5^1Ao3Zm!%LRpXwV+6@@pfW(z^cLr@E*I zy4$HsJw9|PxF0puX?(gXTO{;CtA{WRg|d{J*Sd*?ljRSM=aU4iUA3KIg{AmAub5Ja zQ<#QFzjIgPYM7EdYcC$msivc-iK#SkP2@OsVnT|9^_>d(n94>pM6PYMo$tisSAw72 zLYWqVs}5r1JwnXsvv&p675h?WK~AJYrFoNWS(M?^@n}vJh)|szt+X3?_>xF8>Q-qI zHL~U`VCnt`_REeR268oC!YBPm6`dcb$eiJo;0&Dj>Y+(fgkUf#1oW6q(-m$_f_oc% z@^*0YBxcwJNhwX$lLfgYP1SXjKmuzX;EI^ur*o@Y&7SZJV{iD&cbOH+Uhu~}Z+uRZ zzlc(}y2Q#^KzYKbCwaxRnE%@{D89s0&LwK^;)2(*-rv;kV6EbZCEfI3m+t)6bBZ-d zz~hK{F@5{}U6aPY9015O=bPm?=5|+pdD>C>Lq^ow4BN^2euy;hq_vFyQm3hIn*W(| ze}>GN@Z{}g*TW-?^Sl|$ZE=Rzu=~r;@y#;A@IoS(;Y-|=;_d&e4gf{qKvZA%q}LB9 zqblceZ*XN2t!W(C{x>jyh=O-diap?b+^~17IlcU|>&-9q#@|pOBF$*-_?71Nf$zTd z?te@vR6aff*}rVAkXUy9$veij`{tHa^&d)p4dZVyewxP%&)u-ezs3~)w0x%=s}x{{ zAo(!W@uWLop60%kyJ!_)L`1Vr9ZA~^hCje}kj`UB0f0u~2Fv4~xpi`cvVZI0iYR#+nn?KtDH_ErH7Xg&dUiVGs?o(%Bj*BE7jkg^_N7a5 z_YTYM`Gv-XMd5=VmKfPd!DnBGI5-UKr0O#Ii>;;0rxfT%3ML(;%T1m9`BHR%Fo~Cw z*4)2TYcof4`T=tQLF1YynfgL_9hf#=$xX|_(UX!J)@xs~>=WBO9S*o)nI)k~?vR!E z(6>UA5G1HR$_aHagj|#k9r{KxK25qddH5NH*$T!c-4<`lLqw1R*ef^0!dK*N0o5D~ z@PWt&0e>IOzf&<;tbLgGv-HoD$(dwJQku=q*3?c@4OZ$FOGTv?71ASIivdky3j!E~ z2vUf{78n*wq`qZETo1W8H2}a%_@qMS!}~X-spR2%+ozP$zda^&8yq{^+;6*$yL2ki zKr57`z>%qEZ^W{D?$_t$NzOQw5vdIbw}t4l=teLO%WzUdg& z`rSlK>Z=aC$f>W?_`CRK4Y?J=jhNbwW-}-MQd%~*ufNC0RutgQQ1anbkw1Yrm+5_| z(Jeg?9X%Y!)yDPwH(fjn)}w=g7}JLZ#lI*S!w$z9Z%BCR!sI`2k##`_32V2M|ASgk zt}R><7jf|_^XI%voG!*6?>}UXBpQ3aEG10Y#_{ILptwG1?z5}LV?ne{)Lvn- z4QyS&VS?(b&7CjIB`O2&n?Vs+J~7T%Y05Y;yHV-w`%-*!F%ZtjFJ3qiQ_$z?QJ6)c zwIs$!IECKBQ;Fk&cw!I=`moAkh9u@YS|3P)G9>~gPtdq96;J);uP`B&98yFkt zzkL~?)%97Kqcu%^T&24h!KxX1?C`{hm>jHWpWc^7*fd@U0nrFeP%j?B9_C4OJS*pK zeMN@-8Bbhu67RJlPcHp87_%%L&TjVc;#-23$g9q5Jv;N42nrB?(kd%j-&%ZVfheAD z!l41Bg1p@YQ_r)O>*l3eeD^;OgR{W8qnfla zp-10<7fHkGbD~5D2 z>wrL7Tc{C4Z9({Ib!TmRiH=a3+k{Q(u6K{(sgyM=9B6NmJ4?c`TYL!pe_Rzi!%Cg= zjf`$UX>-oE1W$j7KF3K5G{BFLuy&hvi%T zH8uQ91VqS(dXZeF8fey-+V1?vrw=GEmrvfu{iyXD>78=SjKuiNv>pJV?mKd)zn{zO z|9iJ@>=N*IWq70I;7+kgVvSgF1s#^v8Mgk{kk}?vTgIU}ZiBmpzhq zIlU}saOd=J_|K=K-q*2y-*U{f4+W|3BtkZXZm{sr$72uf0&ks=f?*}18>YY|p1cQS zCV1J?K!OgeONHh~zjf!kql6{s=d0WZ_VGXhaTt536wpL|RHn2#%K$o?t$uK>33vYp z;Ck(*0YY)kINXk(ev?tKmzKa)-)A{5du|6B_|?fV z?|0sVjmM8rz0s|k$-DTEi$1G679g@ZlH%-jZ@`Npc;yd!DM0p4vC*v@YT;vb@N3-R z?Aq+V6TmX-taH#s!3}SDEPHt~^7CcS9jG+U8LwuW=)iN;7jM+YRag^_Rp+aNG*^GX zNjQmdQurY%&co^Iar3K!OIAS@5$p|Li(NHZB)P~akYaHDq;mTA4yHJEW3rfy>ma{X zF#CjOy8YDho{`U(VPj2F2)h?l@EJEh$|x&1QTv%u4iv+k=-g%Sv@1zyr=i5WOQ{m3 z&yG`PwL;&~rUCfKtL!pd9?Ft#I_d7pTS41?<+}qz)K)|h^xKx68(hDsctQK~ipMRh z_ko5>3Y=>{ymV$ks1~TpeRQczhTivjSQRJSwy>Q4N;TfUm&8-NU`uC)MnN@k^YvKv zR?}jJ_#i7OKZvWA><_0GP>35E8n;|KS^r`lIqP*BF=#RsPe)rLH^7kGQ93)}!U;Nh zGdlFJL!|cCXZlI+@v-1YMl=Iad@u;r3Ks1>7!O9H(A^YLV09(q79PS3rIXA*Dh zc})WUmX!Pq6_WKw94Vl3-t-6;l2LK^*!R(+gDnL0nF_g;H^tzVvceTX%tglJ>b=Y- zJ@BH18gBpN-iNYtIx(qSmXMEp9G26M`OK!)L!I44#MDGhrnKV9x1oNk$NsCrW+${( z2h?io{S)sw-83F@lUoMuWV_FpZEH5cuY1_l&Ah7OoK_nAWYM47)YD>PLV{NKkQpv?bb>r1$E`Xi$w9lwD= z>s!*~njkDaS~<)NH00F6BQ+&UpZ4Di^0oU8LfI7d-@QNrk# zkzj`#F_WFpEfxD~IYcn3m{Uz%|ym!9tOWIVR$2(vhlUEV-Dl~Lh zAk5g)<$lBVV^!nk$nhdcM$;pl_5QZUG=QeEW*TBM!!7 zhKm}1v;d$kzGe`qsnVnI(l9$N+N<5Kv|iLm7pF~uJrKIVxz;EV71PtEO2J7qv@Xt^ zS>PfWa8o!}M?R6A@R%>=@uOgrf6hc|ic@U|7%!C-TB#-kb8HlHa0DP#eM=r)+r z_A3t`5wSX~MY)3C?(9ELb)MjFq2;;#NMZx1m+F@LhKU|?$Tx+0*@cEN&mUV&Kl(o5 zK&lF&g8{q_tL6ne-a6t9c)iCR=N>}{sXRy)mIO=pL#+0Zu8AWd2j|AVpR_rG(Q^rFq<)=n$TY!;5t!Pg-U1~3X_YK1f0(G>s4$qtPhbZO;sev~_hd1UIj_}SUS zVP?<=^Sm+L#LEpwN9#0L07x`nY( z$7P_!_4+wOJJwvr78l#|iypD6y%$}J0&%lxd-Or>c6=*8LtQ3xXHNu;d{U8#h1w5@ z<*Vu;wZA?^p;7F5$oG24@u@9fvppKJkCxUK9(Ib5C*@RIm3^6Wvp{VamCsW#xsH78 z*?{`<+HP{_5&f9u-INoMQbN+U{^wm(@u^El}yD}!4sk?=`W4hZ*H zVlEdPQPjm4)-4%OPl>Ok~Mf*j2f8brl+ovf{e=_>k2{VeB@->tz(u zCoaNEUjm4^BJRV$>Oet5#iEup9l3^)WlW!a+`jUbAXb+vHLdUe!d>Kfs0!<|39SR+ zyip{;{k-|D_SM-1^RC3tq@6*~$lpV{iEYg&A9v0-Gq)O+$LlWA5UC)pba8}LjtGp1 z{=ePj;NYm}R`0XKCX=wr-QHdpmi!JQWUn@10>(XZoE`*{IgvWpoAIe?y`EAF`{+L9 z&pZ|5uXTlM`XR=zgoe{j}CUu3MNaKn~pZmx~21b4y z5Xh?Dh@%BcVW5zS0;vzy&v-D1MggBi-dw!4qu=hE*7u|Z7ETfp3yw=u4|_kq_(OQl zY2O?wr3PJ$TVqq*j;q_Yn?X6rou~2p5vdv?x=eMLn5%3o^xy1Sdw*-r!7a^rUz7Ej zk0|7GJmVkGfs&?{c17}Uqg(zUqbngHri7^M=zOR!{{`WBy%QsK2XqILh z?Ottk?=xGNK0PUKG0!>KEiNu9zSkgEjVfUX&!>bF=;J{=A&pn+Xh=OSZ`YF)|92l3 zqBpq>ilIs4lbFqpS1H@aC5tR^>i+Th5K0h~=$fw$XRod5OL!{|rxb3?&9>E*t0sX7 zZFlfQHx_nI>j-R2BQ&;5{VLXDo5z24SWMS7FyAwui1zBw8|~J~I90OO9~}EfJ?q%R4H!GbW_dv&rsPh#`cjKCD+mM~GKWf76I&jEFue1jGTgFd2 z6h*0rls0kV2k|c}gOPsjy+;5+#^O~aKIz{#Te)PlxLxWRqZS<@Ss{1C*IiAw&RuJ=Cw8X6-jfSx3&!6olhF|I;-3jf`_k zXrp8uS}L*ClymFq-q}-z)`G&)%jT%bp%0d9`x!0+WQGPLkkeCIV$6J&EFs2uhuGdf z7EDp@n4(1}zlX-$CT8x(c1W56>5HB3vJN=I*&jTa*uenHtyT3^e2KHS$#gRIcFa>? zU6s-L-fv~WEKr`fQk&u0dggTH$K;|>u_aq-ek%#?%Z7&6C9wM!0_cO&ZUQ0ypTs}0 z|9|jJ6n6o4fp5I$&{mWGsy0DwYTD}TRCf2wZ#}y5$bZq!P{m!*&=lwcKd9MFeI9&v zY}Py4{7h~Y@GTpzm5=C7y)W%^b+Oqac(o|GW}PALwS727Dj8^`cvtVy8X(}L=8iX5 zIHkS$@Vy=itSUgOKDoO^75}dxwPtG$Ljy<1>2|}rzTCq5aW7fjz~+7Ba{&-xDyeBh zF0wZ04U*)82Vv~wKtpTwhEG|PQuyyr<^3rVoXvQBV$Aqg0_~={45b%?1d;lpAc)Xy zp*Ql!l7c*-xDNME3vUa}OjIYaZR9z1aNFICY2*kup8_VLZ}wqTAA! zv~5qS7-TEwv6ZS&l2arZ4&n_W)$|C*M*2rXry6&cPuk`fC_>dX*CwdL^ToF7wd^0wkO2LHhZ&R#7qiz6u zblOO6X<9qf?$MLyQ|bZ_nm@*A16EsT&6Wmc4htli@cNlRcemCzeFL+9YW^8^!fin9 zQkA@;uO$~Dy8vHU=q6N5`e?LwEUyByCi~}EIR`&;ZI{D77#c9O#1ec^xdl1eB!V|+WR=B%Uw(fti zi?RX&6smjNoVI-lEb(P7GaO8A-caC~BI8x!_Hm88K-qD&Z%Qrw+eHI}C(mMlvH;JN zId47< z2~eS${TlKtL;UHJb)r;#3tTZd&T6*vKhzT2U4%+XYz|RA?L3M?!h!FND(E}sq5_lI zncr=-H{=~>uzfI0kmY8PNckR1-SLXJP1Bxg>`5WOk4fax>u1Nb4RKPLFj@^492Xc^ zPdOO*WI^h1LZu8hTDIl*H9F_NAZV&p3ZcJf{@ZvU&cl0d_D6|S((GOjyvyQ4<}h*e zZtqW4zvf<@3V}6+LH49@Hz%C(=%InPgFHvLY!u7bF}Jng`q~_7(#vAR(8$Q_%2Vz6 zJTn7ct>5MYIfk_0rO52!_WS6~6%QNPoiF3wW$wI0Ly=KY5e0!3CTC3W_1GJRBjwpU z+!UJsiOm!XJ_R2AMmH`1Mw?rcs!bVfo$TSJQKOP6++!eKBPvbUpispdWs+QCcsH1r zy18uj1st^*Rt2dE{EXjzko>Z$0sruZ31J)4z@w(Kw|N3y_zI?m^t?<+UXb=AAkFzz5`?8YO;8xfxOWn}>n}zY@xnG_` zIqp3B6HRqLPd?djGBc5hQT{{!ZvPPU>FN6{plpjM;avr`Ov4S1SF4eST6=FZrX@Ju z0>$hBf%)%FW>KM!1fz6OW@f=z^Y;alx2qIczCs+7V`(nl$B8;)mSZ2ndN!$dZ@Ow4 zvw~q#d4eQ((kCaWgulhfXe>3VafvAc(7kt!r+{M8+mu4GqjLZ2bcj1n<#ex8jyIp(hS*!ezRUaY%Ic;*61J$lbl%l-7Th?z^X$K)~2x0OfP5D+3(I~q#S8J=nAJo|v5aY>s z1@!oJgI#kGq3l1ks;H18xZZd&-Ew;2t{MtXXH|0bz;pHcR?l=v@<$Vt>T=`lB{*0u z5%e+NiTCY~PX!w?qj-W9B4k9l$uo;TAphSp8N+`$uXZJq(WR{+Xo`*w)~}KMnJou^ zj`wdnbI@j3X1f33O~v99^T{9x8GXG#)0XEjO`&YQ{{<>yQ*f+<$QJozO*vQ0wX}_; z6J9vDd39h*X{a`|bF=@$#aw7hlHaa!l5$)E#VF&QU&U3>axzFm#a4!w` z8i%n@YlO3trf+}BRi;_OY}dxC9Qn{?knns+Kv&c5$~@v$`1Nv_jOEmMv!eCD1gNcU zJaz@Lcd{#%J-oerM8@UU*MF`4lic;`dhusZ>q|Xpfg7-O4!hSn-)3O@ie>$d6o- zOJP+LT@FrQ*QV0s#=@pKl_EE+j%Cv|k<26c9+BXp>h>&G9MMQvd z^^X00kAElTG5?c3F%;|kcgG$8s?ZKcd-6aFP`6FA?6?|vEcz6&szuq$5e_YPLYROtEdfW|;1}1Te-U>1UP=gNfH( zyh_5Gud7e>7Pumk#g<&Zct-HCqsD;-YSh_s_hTck`M1 zdw_r&-`t=Uhdvy~i^Q)c1ckk?)h6*@$F4rqtiMlo<5e1%PAhx{oS*j3i^*=LZom$$&>kq9DgRlf6oj5<=GeQlF|0Y^068J zOpoi1P$?}jZshekb~u*UzxlO3U`U2u#n12FiROOrU)2$Wj{#ZQ<-s;C0yk7pQN|TK z8clm{zSNpDYa;2U`4OhWHK;FY*I`(4O%bt*dFt_M-;%xv!64o zN~8AQ{)<+;Co$b}qcRP5aNRg<-Z;1<&l7yk&--$OPvU0T7lW^Z8#bf#m9EQ9+spa% za{sT2hUF#wOhiO|VaF3U+Dd#bObe=Kv#(6}Mz5m-p}F*~hNqeEfCWlZ9-Ad#Jdu!# zfQmHuQCaQ-ApwV9R%aBx2+rIm2iWKSr zFg>r+>WS8-q!%k`9@P=OR!LwuxKwyjdOMmS$Hz7`f_{ftb!=6T;nw@k2jJF8UE%Oa zhLr;9^RYbgnY9{E+RId>`;D)E3l6s?H6v`F@zoWk2V}Xg-6^N>D3M5Y!=F$3ok>N!)q;TIEI&=^6%0iulS-vQyq6z zSL#-K1(9RsZC+Y4nC4IJ*71R_UomfzbiT;eb00o8PYNRkBmM}qPn!A8NaT0%xRWq- zJ0}Bqd6(Y7Bh|K3fBrspK7g1V1=G9_efoHHik&o;Ou8ls3}Ha@fLiwEwxQc@!%uNe|x_FZ@m9E8@_Go06~K``OTvpC5iH1hE%zTEb;rlKnsoGm>vF&O=@IpPip?%2sYND>yE_yz{*ll za$#E4c_00K)<4ITk$>fpC40(TgvPHBlXnoor__Q03Q(FpC;r(dOtaK>)v;wvSS{mU9-5psl5#`nJIOaa>L``HB9 zdCBUpJxPI|_`qFBNGp;D*IXB00wIiYj${5*;# z|Gq1Gw_P|}AV59U?w`Xg4Q={|j1P0lt>Ib;BEMS8d^-reG9Vp?cguSoRF5_-ei&r*PLBuXdr>r=tS{#88VyLM&dc@H zC@^_^qC!;ojW`w#XZdDVUBE7Xk~aiC^*0|?v-4*n%XrP}C>jmWi6CJ&pKX~CB$P78 zJEzn7%GTf*A~3yW->p)Yd9Y7pc~{hnIWD5TF~gzz*e1B}b>n>3+P~9VrvT7axF2SD zrmkE&om#bATe%#W>elTy92LB_7PJt(Y%@5tZ6sK|dsC=}(`YCVqVuOvC8!l=DnvaPvrqnW-NXQc8xn?-7+(-NzknNl6*Lj_RUW>gB zbS6kQwc1r(p#dCYk6jiYOvX!53^N|Wz+l|W1&ZUndI*;SB#f{m*G`N`w zN~q*FLQAv3TfX;3d`E93g*px1cGAa=_!gFLU{d`i4NEkxhLJI4amWsrB9Kt?5mjyX zy;?WD_x@#L!oVV|$>A${ukTYK*AG|{)j6f$G8r^F*?M)iRla-e1>wKr^M^543EvoP zKHcv6P#>PnHlA)n>uD&iNqxGv<)9Of40v@XU<^FBJ~e-uP|ol*BW0g-?%*RXK{O2K zcB3&7UwL!8Es}h^l}3qk3H30(vRek#H)*}jlx>J^>HU=E<3rc1o z+uOS1@13}#Ys1E`Z8XrQ=9@oPvuZBZzRMu}(u!I~$Mmhf`%dkvTfcfPsdY`9k=%EV z=dD99f+*tV1*op zP2lt99@88ys|_esrS0ss@w`yXI>?14{8SK(%j-2xg#PWw`NYfYI~q)L9QuobK|n&H z{NiL4@DOjb)3b!1h_H+YjoFbkPpM6-4%%cA#o`Uot3II(#+zks(v&iyK6PAPt`srj zB-WV&dcv0$iTJS*eWTWi;B9P9$C{&rw%frOXJN%W-5q)vDR|JM3>qy*MskzS`5}Zj zFsE@PR~O%t6g50)usHB~9*=!^epwMHum!()be^oL(jJ2|adsd>k;PoqfBZ~*FanbA zeJ%gljm3m-OtCFwr9`g1XBLZK24m?9L0n-MTH*mo+dQVokQ;4 z`a3R6JN{G3X*R)gU9+>Y#tMddx0}i~*2WIe_jp6gRX_OU-R39dW>U+F3Gl1m|UQj zv)o)yBzNVx^v3WaOjeKXc0_-nHy^RUHpXKWn&034af7M;vr$$T^OC)nC28K^0DsWQta$)Pr{Q@?l&PNr5d1=$N_+7!> zfq`yi{bpykP${BPP2wn*5L~>_p9+#^bF)aSI_SpOp-GU-O6vCJFX{T1Xn}~z5#4#q zf0(h|uZ_6i!UP_`M8S{&6!eTgNB8hS8V+qpRpIaZ-x^zb(w4#`+rOu&VMKk>RB60^ z4CzG~JyVXr_nhbMbIfJqjBw+oECv~9snq$2Xnarr+DkjzyIBW@S0G0KL5H*yRgqOi zmk)lm0t$v$r=FyZ@kDaafnN22k>RrEF$Y>kCWf$I)r*pmc#i?u;w)+~ zdZ5kAK;X4Hyy9{Q6miK80Ri$|=jdeK{;cKu<$;G4tg12CuSy&S4NP)knQ(U;ftrg9#*Z8@1Ka^=3fF(Ibsh(!-Us2zI<{bVm0RBCCV~Avmt-()FL|onZoR-M*nPaZ zK&rr)`uk@7Ng^u4D#D9m?dMD&5gI))ZX?rAmIT-$t_2lt++UKuZ)@~JR)_nEaeVjr zbt6M{A_QFa)zpHM>iM>^T-<$nk1hkP5{)Xyzr_&2Y0KUGr_CRMBq=9zqE(S1r6`%n zL{#S|oOBSA7WNd%6UDQ;ej5?bU1w;$g?EvtqT~cp;RB(8qhBdj$1B2Bk^$1+!5r(O*E4q-u z&JUb(b9I|mx69jXUHP85K_q2199Mk4f3PXTNuFv}sHNbeo7B%6fp^4P1=i=Xo$n{p zB6c++p(QStQFdem%N1*Nc(p;$6_jhnUK#>!>}T+6*`>!Hh_G#RoR+Q-bD0ptAo|W3 zNvjJvPl5VyuW$|JQqsf;gvE@pbTHYf-plN;Z3mCP^ft&(P%sA!6Hm1(_=mOTH)7gi z!0sD}E*eRS749inQ79}n8QkQ;ck!}C*SI^1AzcOkgVqhx^1mA^AT{mB7f z4C{<{Nk+1`QsY>1rFqV`e0~W-RD1{dI!ZxLO0B@laHt7?VyF=4PogsMNUahe=4Y*B zE@=ly&cju}5HT2lJS$F-ZpZaZEzUfWN?{6(8_!AvXt$7}0vCpuq z>GlFZV}3K`jt0z_^TvCd2#^*95HZ6y-%b9ae6|?u*_$lQIhTAUm`!-&MtAa4*}aLs z0Zk&IXY795tbBXD>Vg*;fgfiABbH)I?BrmuYVp?Ofs1 zM%l|OWaWYZiLp4sIUTCrB#%%`dwCx*V|%J5ksPX>Ly_NqG)nocfAKpSOD8$L;aA{K z#C+iq&-svd@<5V*`D2mp=eRLJOROeI`Rbd&6yi$}&mSMIz6iUo}isB{~@4p0A48O#W>99Q7$pbPc|L2O>q z7qp%n1)1PnY1&UYQVCxk4nJ}dSdD}X0Mj7{Enh$Q;a~G*tvXW9Z5?*oPeJ|^&4WV; zEd>w&*i2Koz(f$yNeLoE5ksS{-aGoqD3%IH%9(*eRawCUDGzae{? zvJGeePLx`h%W!QYc}}d|Gp}J{+vgxPhAD?rbjj^=OaZSa({(6szHC=T2A-jgpI(c+ ze6#~6R^3kr_fr_O#uS@}1aE+u)YC8Z-{P2kG$H-bE1QPgxuUk0T2eOwsGMY!_xD9d z48}*ni?f9FAHXGFOvR;6YtU2-I`xDuSC%iifoi5Jt?Jt=LX<)%7kN<>4GN!2l0JKN zR{O3iSt1NcAvA7Omh`9VDrG)IH5OVjI9nu)0s(33pqgNS_RcTu`w`dWtNetsgn4A= zA;7J~%SCd*!aimV%e8pzbMktkO0F@d; z5E_3mh$;YvJmS!x(Yn%7a2s@uS#e<>T2P_;tHUfpn;20)*l&|objn1jvX-_2c5b9w zXi9B@pnop_8Bd+(NqHLi&jvI3e(v9H8xPv~rRSy1*dN31^5a^4AyIcf{MNL}Ze&NS zWm2%HTz-bZ6Blu9%EH2xI_iRAuzDH8=&v}|TsB7|*J_yXg!l`QYlSO7a(+Dpj$`{c z=#)%e;xrDD@5lF=_!EQO$xg?xEJWe&fAEu?@17(Iq)LkduKkMbdEj5rEelYKK#J;d zM)>|$q2ccd!wrsHiNn@BBAP?>kEhKD>`K?AHgz6GU?R*2LyBRe5VO-va2+pVa_`~9 zTogRNKFCDjDcaP}MdU`tv}wq3YYwK5Ar#{Hrgrzu^nY@=0esT;%;B zI^u&fB9wxZ`F*|$k^RbvmbZRi6{SR27Xs^Dv(e0%?8T8bN26J?y|is!_{j=XZ9GC} zpA2VF3)TrN;x#hgKYaGQdA3i_`GQst^1R8t>XBs2)koLK5{DHWO~pp!%c_6whClfF z$`q*_R#~-25|6; z3;Wgpd3~7G^!i8|J;>(UQj(^fHG&-$hg`ZXdTMQs+a-@K?HVq*XnWGCw)f;(4nE zad~6Ykd282K?=i{%D2pIc8q^#g`$-CwYYQBJ^v>$D7$B#OeO9u6Xk7b6_cu|)nYY}RI>gr(B>H{2$fsr$kJvVBTQIvq(Mt8c)eco`;fy(^AhwKEneGIa;_exv1wgR@rz_zN zxm}*tTi&V1CWKfqav-pv z{`&eAo{`l;)mpt-bv&EfMTgas9)^G;YfrJ}Uv zc(rPkI^2eRHwBdM8I6(wPW^A;9%wGfg>xIhqveI6{oW!lV>6Q-cB^`W^4Repwg}`U zi7?@;51Fs!Px{}LKJ$FF_x|yC$wzAPq=(IfC0X+~mxCX&VOU~uA`GnaCA<0eDVJ7wktnj=aUWmm@vS-+5 z50B5cH*WvxMMoqv8i|Wa^wir#P;r61t@E&v^_#hB>x-sU)ml-+>PwT%jDu&5+hbWh zUoxrRGVTvSmiaH{{ouO$X1CMTr}14E{_^rC@mM3*9+ZfOAiR2%<=-{pg{@L(P4Kto zl%(zTCDWkS!7)P?epE!Gn~=>eDY&R5w92Uw{NibOXrt*>F~V%~gCdd06T4tQqm@9y zMt4nM%rGZr+{_;VMIFRFEv?I|4z?j5+6jm|tV`sue+zA_$u z)8<7kv@}OlT-0GBX4t=Pp7sbbB=C)yiiHE~*C&(eEHCHkWiy`&Rl40frQW7O1lm?5}_q`~rOw2ZmkMjPPbt zWJ2=$LQ@4QakZEH#|1z`YaFyB%kGY$gzWRCN#R%BAJhiHyF9*Tx2t{-1OImc)onC- ze12#xavB!#J&hlfW3>qjDNt)cn=Y(e3HPq3|Ld6{)R^j`Xq@zLJkW&>8as5lBknLk zYaj?CH{ou~k$c@PodV~$y>s49dRmGVv;4M`-B|ah+-G;3afU$kw>Z~F3dG{olM5cMlU71t6aP!5|qAier7H#N}|Q&j#3DC*oBZ#8M)K(J?$L^!6d(%F5TJE0S=A=v|VD>ak2%)}t9;0EBjx0RHZI-M<`xFF0q;}90} zjSI5{()+=mK#A@2Tv%AjCif*Z6o&5Zew-BUFCnuOlpq$w+^~#JLE@s>d!3|8=zr!HQWZYE-`}P9j2J#F=Xz93~ z6q^ZyL=(tw@3WlV7l$bcGT&sT)A&W?4N?sjPTd*b13;MgP-17ag79!E?#NI1MYV0A_;;cwk2aCxaJXZRDN-Ae(g++CAl(iV1gz1BP z`h}c-!Tl_;*6%tf{1hS;Z(CEMwNPi|}kJerN25HCMvGne4 z;ZJAHQd5*|f-2{Eu$=v!c~8Y`-wW6OV-bQYUpeuc%<0fnIm}?ZdS?Ms6bxh#X}m-O zHO+=%J6*0_9}o2`nt?wqFh!UQp$@STLpJPv3P{xCMFVOke1 zZ{|NMm98ftUncwcw)zEukS3VQDXN|&_WF(98Z_qc0B_7CEW58q40b1Os++Q*J8na+ zzu6+aOmp?JvwVHv)&1b@k)PyP`@L0G7@&QgZW@)LTyJ^;w<*ERVz9-3DmMBfUG3{- zzw{D|TQ~O{AuIZM4aWl8PvTg@K3xC6tNK)#(pJ9*tbHFRc22KKOL96bZ;h^Yr(`Z~ z7nDZogj`pfR3n(0a!`nR{^NT4?#+>|SmvKwQvd(&0FxT_6ssSNsVjiPk~tUtTn|tl;={Go|{LcNqR$j-1U99KdUOExI(bxX*!hZ{H_bX0N`j!m8-Xqem zyl&F}RDh|!_gN-$OU#CjHz$Z^y}fw~1fm;s#GB?wwI6nQu$CYA|0tf8Aiy?dm>67R xzUfgIn2`Vg;Nw;QWe!i%e?R&^e@#~YQ9*RDbnQ(^JcfXe_6-B|ay7e%{|5pC@eTk0 literal 0 HcmV?d00001 diff --git a/Models/ByteOperations.cs b/Models/ByteOperations.cs new file mode 100644 index 0000000..ba5f656 --- /dev/null +++ b/Models/ByteOperations.cs @@ -0,0 +1,381 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// These functions assume same endianness for the CPU architecture and the raw data it reads from or writes to. + +using System; +using System.IO; + +namespace WPinternals +{ + internal static class ByteOperations + { + internal static string ReadAsciiString(byte[] ByteArray, UInt32 Offset, UInt32 Length) + { + byte[] Bytes = new byte[Length]; + Buffer.BlockCopy(ByteArray, (int)Offset, Bytes, 0, (int)Length); + return System.Text.Encoding.ASCII.GetString(Bytes); + } + + internal static string ReadUnicodeString(byte[] ByteArray, UInt32 Offset, UInt32 Length) + { + byte[] Bytes = new byte[Length]; + Buffer.BlockCopy(ByteArray, (int)Offset, Bytes, 0, (int)Length); + return System.Text.Encoding.Unicode.GetString(Bytes); + } + + internal static void WriteAsciiString(byte[] ByteArray, UInt32 Offset, string Text, UInt32? MaxBufferLength = null) + { + if (MaxBufferLength != null) + Array.Clear(ByteArray, (int)Offset, (int)MaxBufferLength); + + byte[] TextBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(Text); + int WriteLength = TextBytes.Length; + if (WriteLength > MaxBufferLength) + WriteLength = (int)MaxBufferLength; + + Buffer.BlockCopy(TextBytes, 0, ByteArray, (int)Offset, WriteLength); + } + + internal static void WriteUnicodeString(byte[] ByteArray, UInt32 Offset, string Text, UInt32? MaxBufferLength = null) + { + if (MaxBufferLength != null) + Array.Clear(ByteArray, (int)Offset, (int)MaxBufferLength); + + byte[] TextBytes = System.Text.UnicodeEncoding.Unicode.GetBytes(Text); + int WriteLength = TextBytes.Length; + if (WriteLength > MaxBufferLength) + WriteLength = (int)MaxBufferLength; + + Buffer.BlockCopy(TextBytes, 0, ByteArray, (int)Offset, WriteLength); + } + + internal static UInt32 ReadUInt32(byte[] ByteArray, UInt32 Offset) + { + return BitConverter.ToUInt32(ByteArray, (int)Offset); + } + + internal static void WriteUInt32(byte[] ByteArray, UInt32 Offset, UInt32 Value) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(Value), 0, ByteArray, (int)Offset, 4); + } + + internal static Int32 ReadInt32(byte[] ByteArray, UInt32 Offset) + { + return BitConverter.ToInt32(ByteArray, (int)Offset); + } + + internal static void WriteInt32(byte[] ByteArray, UInt32 Offset, Int32 Value) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(Value), 0, ByteArray, (int)Offset, 4); + } + + internal static UInt16 ReadUInt16(byte[] ByteArray, UInt32 Offset) + { + return BitConverter.ToUInt16(ByteArray, (int)Offset); + } + + internal static void WriteUInt16(byte[] ByteArray, UInt32 Offset, UInt16 Value) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(Value), 0, ByteArray, (int)Offset, 2); + } + + internal static Int16 ReadInt16(byte[] ByteArray, UInt32 Offset) + { + return BitConverter.ToInt16(ByteArray, (int)Offset); + } + + internal static void WriteInt16(byte[] ByteArray, UInt32 Offset, Int16 Value) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(Value), 0, ByteArray, (int)Offset, 2); + } + + internal static byte ReadUInt8(byte[] ByteArray, UInt32 Offset) + { + return ByteArray[Offset]; + } + + internal static void WriteUInt8(byte[] ByteArray, UInt32 Offset, byte Value) + { + ByteArray[Offset] = Value; + } + + internal static UInt32 ReadUInt24(byte[] ByteArray, UInt32 Offset) + { + return (UInt32)(ByteArray[Offset] + (ByteArray[Offset + 1] << 8) + (ByteArray[Offset + 2] << 16)); + } + + internal static void WriteUInt24(byte[] ByteArray, UInt32 Offset, UInt32 Value) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(Value), 0, ByteArray, (int)Offset, 3); + } + + internal static UInt64 ReadUInt64(byte[] ByteArray, UInt32 Offset) + { + return BitConverter.ToUInt64(ByteArray, (int)Offset); + } + + internal static void WriteUInt64(byte[] ByteArray, UInt32 Offset, UInt64 Value) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(Value), 0, ByteArray, (int)Offset, 8); + } + + internal static Guid ReadGuid(byte[] ByteArray, UInt32 Offset) + { + byte[] GuidBuffer = new byte[0x10]; + Buffer.BlockCopy(ByteArray, (int)Offset, GuidBuffer, 0, 0x10); + return new Guid(GuidBuffer); + } + + internal static void WriteGuid(byte[] ByteArray, UInt32 Offset, Guid Value) + { + Buffer.BlockCopy(Value.ToByteArray(), 0, ByteArray, (int)Offset, 0x10); + } + + internal static UInt32 Align(UInt32 Base, UInt32 Offset, UInt32 Alignment) + { + if (((Offset - Base) % Alignment) == 0) + return Offset; + else + return ((((Offset - Base) / Alignment) + 1) * Alignment) + Base; + } + + internal static UInt32? FindPatternInFile(string FileName, byte[] Pattern, byte[] Mask, out byte[] OutPattern) + { + // The mask is optional. + // In the mask 0x00 means the value must match, and 0xFF means that this position is a wildcard. + + UInt32? Result = null; + + FileStream Stream = new FileStream(FileName, FileMode.Open, FileAccess.Read); + + byte[] Buffer = new byte[0x10000 + Pattern.Length - 1]; + UInt32 BufferReadPosition = 0; // Position in buffer where file-chunk is being read. + UInt32 BytesInBuffer = 0; + UInt32 BytesRead; + UInt32 SearchPositionFile = 0; + UInt32 SearchPositionBuffer = 0; + UInt32 BufferFileOffset = 0; // Offset in file where data from buffer is located. + bool Match = false; + int i; + + OutPattern = null; + + while (SearchPositionFile <= (Stream.Length - Pattern.Length)) + { + if ((SearchPositionBuffer + Pattern.Length) > BytesInBuffer) + { + // Need to read next chunk + if ((BytesInBuffer - SearchPositionBuffer) > 0) + { + System.Buffer.BlockCopy(Buffer, (int)SearchPositionBuffer, Buffer, 0, (int)(BytesInBuffer - SearchPositionBuffer)); + } + BufferReadPosition = BytesInBuffer - SearchPositionBuffer; + BytesInBuffer -= SearchPositionBuffer; + BufferFileOffset += SearchPositionBuffer; + SearchPositionBuffer = 0; + + BytesRead = (UInt32)Stream.Read(Buffer, (int)BufferReadPosition, Buffer.Length - (int)BufferReadPosition); + BytesInBuffer += BytesRead; + } + + Match = true; + for (i = 0; i < Pattern.Length; i++) + { + if (Buffer[SearchPositionBuffer + i] != Pattern[i]) + if ((Mask == null) || (Mask[i] == 0)) + { + Match = false; + break; + } + } + + if (Match) + { + Result = SearchPositionFile; + + OutPattern = new byte[Pattern.Length]; + System.Buffer.BlockCopy(Buffer, (int)SearchPositionBuffer, OutPattern, 0, Pattern.Length); + break; + } + + SearchPositionBuffer++; + SearchPositionFile++; + } + + Stream.Close(); + + return Result; + } + + internal static UInt32? FindAscii(byte[] SourceBuffer, string Pattern) + { + return FindPattern(SourceBuffer, System.Text.ASCIIEncoding.ASCII.GetBytes((string)Pattern), null, null); + } + + internal static UInt32? FindUnicode(byte[] SourceBuffer, string Pattern) + { + return FindPattern(SourceBuffer, System.Text.UnicodeEncoding.Unicode.GetBytes((string)Pattern), null, null); + } + + internal static UInt32? FindUint(byte[] SourceBuffer, UInt32 Pattern) + { + return FindPattern(SourceBuffer, BitConverter.GetBytes((UInt32)Pattern), null, null); + } + + internal static UInt32? FindPattern(byte[] SourceBuffer, byte[] Pattern, byte[] Mask, byte[] OutPattern) + { + return FindPattern(SourceBuffer, 0, null, Pattern, Mask, OutPattern); + } + + internal static bool Compare(byte[] Array1, byte[] Array2) + { + return System.Collections.StructuralComparisons.StructuralEqualityComparer.Equals(Array1, Array2); + } + + internal static UInt32? FindPattern(byte[] SourceBuffer, uint SourceOffset, uint? SourceSize, byte[] Pattern, byte[] Mask, byte[] OutPattern) + { + // The mask is optional. + // In the mask 0x00 means the value must match, and 0xFF means that this position is a wildcard. + + UInt32? Result = null; + + UInt32 SearchPosition = SourceOffset; + bool Match = false; + int i; + + while ((SearchPosition <= (SourceBuffer.Length - Pattern.Length)) && ((SourceSize == null) || (SearchPosition <= (SourceOffset + SourceSize - Pattern.Length)))) + { + Match = true; + for (i = 0; i < Pattern.Length; i++) + { + if (SourceBuffer[SearchPosition + i] != Pattern[i]) + if ((Mask == null) || (Mask[i] == 0)) + { + Match = false; + break; + } + } + + if (Match) + { + Result = SearchPosition; + + if (OutPattern != null) + System.Buffer.BlockCopy(SourceBuffer, (int)SearchPosition, OutPattern, 0, Pattern.Length); + break; + } + + SearchPosition++; + } + + return Result; + } + + internal static byte CalculateChecksum8(byte[] Buffer, UInt32 Offset, UInt32 Size) + { + byte Checksum = 0; + + for (UInt32 i = Offset; i < (Offset + Size); i++) + Checksum += Buffer[i]; + + return (byte)(0x100 - Checksum); + } + + internal static UInt16 CalculateChecksum16(byte[] Buffer, UInt32 Offset, UInt32 Size) + { + UInt16 Checksum = 0; + + for (UInt32 i = Offset; i < (Offset + Size - 1); i += 2) + Checksum += BitConverter.ToUInt16(Buffer, (int)i); + + return (UInt16)(0x10000 - Checksum); + } + + private static UInt32[] CRC32Table = new UInt32[] { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, + 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, + 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, + 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, 0x35B5A8FA, 0x42B2986C, + 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, + 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, + 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, + 0x91646C97, 0xE6635C01, 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950, + 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, + 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, + 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, + 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, + 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, + 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, 0xD6D6A3E8, 0xA1D1937E, + 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, + 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, + 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, + 0x72076785, 0x05005713, 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242, + 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, + 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, + 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, + 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + internal static UInt32 CRC32(byte[] Input, UInt32 Offset, UInt32 Length) + { + if ((Input == null) || ((Offset + Length) > Input.Length)) + throw new ArgumentException(); + + unchecked + { + uint crc = (uint)(((uint)0) ^ (-1)); + for (var i = Offset; i < (Offset + Length); i++) + { + crc = (crc >> 8) ^ CRC32Table[ (crc ^ Input[i]) & 0xFF ]; + } + crc = (uint)(crc ^ (-1)); + + if (crc < 0) + { + crc += (uint)4294967296; + } + + return crc; + } + } + } +} diff --git a/Models/FFU.cs b/Models/FFU.cs new file mode 100644 index 0000000..228345a --- /dev/null +++ b/Models/FFU.cs @@ -0,0 +1,449 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Linq; + +namespace WPinternals +{ + internal class FFU + { + internal int ChunkSize; + internal string Path; + internal byte[] SecurityHeader; + internal byte[] ImageHeader; + internal byte[] StoreHeader; + private int?[] ChunkIndexes; + private FileStream FFUFile = null; + private int FileOpenCount = 0; + + internal string PlatformID; + internal GPT GPT; + + internal UInt64 TotalSize; + internal UInt64 HeaderSize; + internal UInt64 PayloadSize; + internal UInt64 TotalChunkCount; + + internal FFU(string Path) + { + this.Path = Path; + + try + { + OpenFile(); + + // Read Security Header + byte[] ShortSecurityHeader = new byte[0x20]; + FFUFile.Read(ShortSecurityHeader, 0, 0x20); + if (ByteOperations.ReadAsciiString(ShortSecurityHeader, 0x04, 0x0C) != "SignedImage ") + throw new BadImageFormatException(); + ChunkSize = ByteOperations.ReadInt32(ShortSecurityHeader, 0x10) * 1024; + UInt32 SecurityHeaderSize = ByteOperations.ReadUInt32(ShortSecurityHeader, 0x00); + UInt32 CatalogSize = ByteOperations.ReadUInt32(ShortSecurityHeader, 0x18); + UInt32 HashTableSize = ByteOperations.ReadUInt32(ShortSecurityHeader, 0x1C); + SecurityHeader = new byte[RoundUpToChunks(SecurityHeaderSize + CatalogSize + HashTableSize)]; + FFUFile.Seek(0, SeekOrigin.Begin); + FFUFile.Read(SecurityHeader, 0, SecurityHeader.Length); + + // Read Image Header + byte[] ShortImageHeader = new byte[0x1C]; + FFUFile.Read(ShortImageHeader, 0, 0x1C); + if (ByteOperations.ReadAsciiString(ShortImageHeader, 0x04, 0x0C) != "ImageFlash ") + throw new BadImageFormatException(); + UInt32 ImageHeaderSize = ByteOperations.ReadUInt32(ShortImageHeader, 0x00); + UInt32 ManifestSize = ByteOperations.ReadUInt32(ShortImageHeader, 0x10); + ImageHeader = new byte[RoundUpToChunks(ImageHeaderSize + ManifestSize)]; + FFUFile.Seek(SecurityHeader.Length, SeekOrigin.Begin); + FFUFile.Read(ImageHeader, 0, ImageHeader.Length); + + // Read Store Header + byte[] ShortStoreHeader = new byte[248]; + FFUFile.Read(ShortStoreHeader, 0, 248); + PlatformID = ByteOperations.ReadAsciiString(ShortStoreHeader, 0x0C, 192).TrimEnd(new char[] { (char)0, ' ' }); + int WriteDescriptorCount = ByteOperations.ReadInt32(ShortStoreHeader, 208); + UInt32 WriteDescriptorLength = ByteOperations.ReadUInt32(ShortStoreHeader, 212); + UInt32 ValidateDescriptorLength = ByteOperations.ReadUInt32(ShortStoreHeader, 220); + StoreHeader = new byte[RoundUpToChunks(248 + WriteDescriptorLength + ValidateDescriptorLength)]; + FFUFile.Seek(SecurityHeader.Length + ImageHeader.Length, SeekOrigin.Begin); + FFUFile.Read(StoreHeader, 0, StoreHeader.Length); + + // Parse Chunk Indexes + int HighestChunkIndex = 0; + UInt32 LocationCount; + int ChunkIndex; + int ChunkCount; + int DiskAccessMethod; + UInt32 WriteDescriptorEntryOffset = 248 + ValidateDescriptorLength; + int FFUChunkIndex = 0; + for (int i = 0; i < WriteDescriptorCount; i++) + { + LocationCount = ByteOperations.ReadUInt32(StoreHeader, WriteDescriptorEntryOffset + 0x00); + ChunkCount = ByteOperations.ReadInt32(StoreHeader, WriteDescriptorEntryOffset + 0x04); + + for (int j = 0; j < LocationCount; j++) + { + DiskAccessMethod = ByteOperations.ReadInt32(StoreHeader, (UInt32)(WriteDescriptorEntryOffset + 0x08 + (j * 0x08))); + ChunkIndex = ByteOperations.ReadInt32(StoreHeader, (UInt32)(WriteDescriptorEntryOffset + 0x0C + (j * 0x08))); + + if (DiskAccessMethod == 0) // 0 = From begin, 2 = From end. We ignore chunks at end of disk. These contain secondairy GPT. + { + if ((ChunkIndex + ChunkCount - 1) > HighestChunkIndex) + HighestChunkIndex = ChunkIndex + ChunkCount - 1; + } + } + WriteDescriptorEntryOffset += 8 + (LocationCount * 0x08); + FFUChunkIndex += ChunkCount; + } + ChunkIndexes = new int?[HighestChunkIndex + 1]; + WriteDescriptorEntryOffset = 248 + ValidateDescriptorLength; + FFUChunkIndex = 0; + for (int i = 0; i < WriteDescriptorCount; i++) + { + LocationCount = ByteOperations.ReadUInt32(StoreHeader, WriteDescriptorEntryOffset + 0x00); + ChunkCount = ByteOperations.ReadInt32(StoreHeader, WriteDescriptorEntryOffset + 0x04); + + for (int j = 0; j < LocationCount; j++) + { + DiskAccessMethod = ByteOperations.ReadInt32(StoreHeader, (UInt32)(WriteDescriptorEntryOffset + 0x08 + (j * 0x08))); + ChunkIndex = ByteOperations.ReadInt32(StoreHeader, (UInt32)(WriteDescriptorEntryOffset + 0x0C + (j * 0x08))); + + if (DiskAccessMethod == 0) // 0 = From begin, 2 = From end. We ignore chunks at end of disk. These contain secondairy GPT. + { + for (int k = 0; k < ChunkCount; k++) + { + ChunkIndexes[ChunkIndex + k] = FFUChunkIndex + k; + } + } + } + WriteDescriptorEntryOffset += 8 + (LocationCount * 0x08); + FFUChunkIndex += ChunkCount; + } + + byte[] GPTBuffer = GetSectors(0x01, 0x21); + GPT = new GPT(GPTBuffer); + + HeaderSize = (UInt64)(SecurityHeader.Length + ImageHeader.Length + StoreHeader.Length); + + TotalChunkCount = (UInt64)FFUChunkIndex; + PayloadSize = TotalChunkCount * (UInt64)ChunkSize; + TotalSize = HeaderSize + PayloadSize; + + if (TotalSize != (UInt64)FFUFile.Length) + throw new WPinternalsException("Bad FFU file", "Bad FFU file: " + Path + "." + Environment.NewLine + "Expected size: " + TotalSize.ToString() + ". Actual size: " + FFUFile.Length + "."); + } + catch (WPinternalsException) + { + throw; + } + catch (Exception Ex) + { + throw new WPinternalsException("Bad FFU file", "Bad FFU file: " + Path + "." + Environment.NewLine + Ex.Message, Ex); + } + finally + { + CloseFile(); + } + } + + internal static bool IsFFU(string FileName) + { + bool Result = false; + + FileStream FFUFile = new FileStream(FileName, FileMode.Open, FileAccess.Read); + + byte[] Signature = new byte[0x10]; + FFUFile.Read(Signature, 0, 0x10); + + Result = (ByteOperations.ReadAsciiString(Signature, 0x04, 0x0C) == "SignedImage "); + + FFUFile.Close(); + + return Result; + } + + private void OpenFile() + { + if (FFUFile == null) + { + FFUFile = new FileStream(Path, FileMode.Open, FileAccess.Read); + FileOpenCount = 0; + } + FileOpenCount++; + } + + private void CloseFile() + { + FileOpenCount--; + if (FileOpenCount == 0) + { + FFUFile.Close(); + FFUFile = null; + } + } + + private void FileSeek(long Position) + { + // https://social.msdn.microsoft.com/Forums/vstudio/en-US/2e67ca57-3556-4275-accd-58b7df30d424/unnecessary-filestreamseek-and-setting-filestreamposition-has-huge-effect-on-performance?forum=csharpgeneral + + if (FFUFile != null) + { + if (FFUFile.Position != Position) + FFUFile.Seek(Position, SeekOrigin.Begin); + } + } + + internal UInt32 RoundUpToChunks(UInt32 Size) + { + if ((Size % ChunkSize) > 0) + return (UInt32)(((Size / ChunkSize) + 1) * ChunkSize); + else + return Size; + } + + internal UInt32 RoundDownToChunks(UInt32 Size) + { + if ((Size % ChunkSize) > 0) + return (UInt32)((Size / ChunkSize) * ChunkSize); + else + return Size; + } + + internal byte[] GetSectors(int StartSector, int SectorCount) + { + int FirstChunk = GetChunkIndexFromSectorIndex(StartSector); + int LastChunk = GetChunkIndexFromSectorIndex(StartSector + SectorCount - 1); + + byte[] Buffer = new byte[ChunkSize]; + + OpenFile(); + + byte[] Result = new byte[SectorCount * 0x200]; + + int ResultOffset = 0; + + for (int j = FirstChunk; j <= LastChunk; j++) + { + GetChunk(Buffer, j); + + int FirstSector = 0; + int LastSector = (ChunkSize / 0x200) - 1; + + if (j == FirstChunk) + FirstSector = GetSectorNumberInChunkFromSectorIndex(StartSector); + + if (j == LastChunk) + LastSector = GetSectorNumberInChunkFromSectorIndex(StartSector + SectorCount - 1); + + int Offset = FirstSector * 0x200; + int Size = (LastSector - FirstSector + 1) * 0x200; + + System.Buffer.BlockCopy(Buffer, Offset, Result, ResultOffset, Size); + + ResultOffset += Size; + } + + CloseFile(); + + return Result; + } + + internal byte[] GetPartition(string Name) + { + Partition Target = GPT.Partitions.Where(p => (string.Compare(p.Name, Name, true) == 0)).FirstOrDefault(); + if (Target == null) + throw new ArgumentOutOfRangeException(); + return GetSectors((int)Target.FirstSector, (int)(Target.LastSector - Target.FirstSector + 1)); + } + + internal void WritePartition(string Name, string FilePath, bool Compress = false) + { + WritePartition(Name, FilePath, null, null, Compress); + } + + internal void WritePartition(string Name, string FilePath, Action ProgressUpdateCallback, bool Compress = false) + { + WritePartition(Name, FilePath, ProgressUpdateCallback, null, Compress); + } + + internal void WritePartition(string Name, string FilePath, ProgressUpdater UpdaterPerSector, bool Compress = false) + { + WritePartition(Name, FilePath, null, UpdaterPerSector, Compress); + } + + private void WritePartition(string Name, string FilePath, Action ProgressUpdateCallback, ProgressUpdater UpdaterPerSector, bool Compress = false) + { + Partition Target = GPT.Partitions.Where(p => (string.Compare(p.Name, Name, true) == 0)).FirstOrDefault(); + if (Target == null) + throw new ArgumentOutOfRangeException(); + + int FirstChunk = GetChunkIndexFromSectorIndex((int)Target.FirstSector); + int LastChunk = GetChunkIndexFromSectorIndex((int)Target.LastSector); + + ProgressUpdater Updater = UpdaterPerSector; + if ((Updater == null) && (ProgressUpdateCallback != null)) + Updater = new ProgressUpdater(Target.LastSector - Target.FirstSector + 1, ProgressUpdateCallback); + + byte[] Buffer = new byte[ChunkSize]; + + OpenFile(); + + FileStream OutputFile = new FileStream(FilePath, FileMode.Create, FileAccess.Write); + Stream OutStream = OutputFile; + + // We use gzip compression + // + // LZMA is about 60 times slower (compression is twice as good, but compressed size is already really small, so it doesnt matter much) + // OutStream = new LZMACompressionStream(OutputFile, System.IO.Compression.CompressionMode.Compress, false); + // + // DeflateStream is a raw compression stream without recognizable header + // Deflate has almost no performance penalty + // OutStream = new DeflateStream(OutputFile, CompressionLevel.Optimal, false); + // + // GZip can be recognized. It always starts with 1F 8B 08 (1F 8B is the magic value, 08 is the Deflate compression method) + // With GZip compression, dump time goes from 1m to 1m37s. So that doesnt matter much. + if (Compress) + { + OutStream = new CompressedStream(OutputFile, (Target.LastSector - Target.FirstSector + 1) * 0x200); + } + + for (int j = FirstChunk; j <= LastChunk; j++) + { + GetChunk(Buffer, j); + + int FirstSector = 0; + int LastSector = (ChunkSize / 0x200) - 1; + + if (j == FirstChunk) + FirstSector = GetSectorNumberInChunkFromSectorIndex((int)Target.FirstSector); + + if (j == LastChunk) + LastSector = GetSectorNumberInChunkFromSectorIndex((int)Target.LastSector); + + int Offset = FirstSector * 0x200; + int Size = (LastSector - FirstSector + 1) * 0x200; + + OutStream.Write(Buffer, Offset, Size); + + if (Updater != null) + Updater.IncreaseProgress((UInt64)(ChunkSize / 0x200)); + } + + OutStream.Close(); + + CloseFile(); + } + + private byte[] GetChunk(int ChunkIndex) + { + long BaseOffset = (long)SecurityHeader.Length + ImageHeader.Length + StoreHeader.Length; + if (ChunkIndexes[ChunkIndex] == null) + return new byte[ChunkSize]; + else + { + OpenFile(); + FileSeek(BaseOffset + ((long)ChunkIndexes[ChunkIndex] * ChunkSize)); + byte[] Chunk = new byte[ChunkSize]; + FFUFile.Read(Chunk, 0, ChunkSize); + CloseFile(); + return Chunk; + } + } + + private void GetChunk(byte[] Chunk, int ChunkIndex) + { + long BaseOffset = SecurityHeader.Length + ImageHeader.Length + StoreHeader.Length; + if (ChunkIndexes[ChunkIndex] == null) + Array.Clear(Chunk, 0, ChunkSize); + else + { + OpenFile(); + FileSeek(BaseOffset + ((long)ChunkIndexes[ChunkIndex] * ChunkSize)); + FFUFile.Read(Chunk, 0, ChunkSize); + CloseFile(); + } + } + + private int GetChunkIndexFromSectorIndex(int SectorIndex) + { + int SectorsPerChunk = ChunkSize / 0x200; + return SectorIndex / SectorsPerChunk; + } + + private int GetSectorNumberInChunkFromSectorIndex(int SectorIndex) + { + int SectorsPerChunk = ChunkSize / 0x200; + return SectorIndex % SectorsPerChunk; + } + + internal bool IsPartitionPresentInFFU(string PartitionName) + { + Partition Target = GPT.GetPartition(PartitionName); + if (Target == null) + throw new InvalidOperationException("Partitionname is not found!"); + int ChunkIndex = GetChunkIndexFromSectorIndex((int)Target.FirstSector); + return (ChunkIndexes[ChunkIndex] != null); + } + + private int GetChunkIndexFromSectorIndex(ulong p) + { + throw new NotImplementedException(); + } + + internal string GetFirmwareVersion() + { + string Result = null; + + Partition Plat = GPT.GetPartition("PLAT"); + if (Plat != null) + { + byte[] Data = GetPartition("PLAT"); + uint? Offset = ByteOperations.FindAscii(Data, "SWVERSION="); + if (Offset != null) + { + uint Start = (uint)Offset + 10; + uint Length = (uint)ByteOperations.FindPattern(Data, Start, 0x100, new byte[] { 0x00 }, null, null) - Start; + uint? Offset0D = ByteOperations.FindPattern(Data, Start, 0x100, new byte[] { 0x0D }, null, null); + if ((Offset0D != null) && (Offset0D < (Start + Length))) + Length = (uint)Offset0D - Start; + Result = ByteOperations.ReadAsciiString(Data, Start, Length); + } + } + + return Result; + } + + internal string GetOSVersion() + { + byte[] efiesp = GetPartition("EFIESP"); + MemoryStream s = new MemoryStream(efiesp); + DiscUtils.Fat.FatFileSystem fs = new DiscUtils.Fat.FatFileSystem(s); + Stream mss = fs.OpenFile(@"Windows\System32\Boot\mobilestartup.efi", FileMode.Open, FileAccess.Read); + MemoryStream msms = new MemoryStream(); + mss.CopyTo(msms); + byte[] mobilestartup = msms.ToArray(); + Version OSVersion = PE.GetProductVersion(mobilestartup); + s.Close(); + + return OSVersion.ToString(); + } + } +} diff --git a/Models/GPT.cs b/Models/GPT.cs new file mode 100644 index 0000000..9cb6b5e --- /dev/null +++ b/Models/GPT.cs @@ -0,0 +1,644 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Xml.Serialization; + +namespace WPinternals +{ + [XmlType("Partitions")] + public class GPT + { + private byte[] GPTBuffer; + private UInt32 HeaderOffset; + private UInt32 HeaderSize; + private UInt32 TableOffset; + private UInt32 TableSize; + private UInt32 PartitionEntrySize; + private UInt32 MaxPartitions; + internal UInt64 FirstUsableSector; + internal UInt64 LastUsableSector; + internal bool HasChanged = false; + + [XmlElement("Partition")] + public List Partitions = new List(); + + public GPT() // Only for serialization + { + } + + internal GPT(byte[] GPTBuffer) + { + this.GPTBuffer = GPTBuffer; + UInt32? TempHeaderOffset = ByteOperations.FindAscii(GPTBuffer, "EFI PART"); + if (TempHeaderOffset == null) + throw new WPinternalsException("Bad GPT"); + HeaderOffset = (UInt32)TempHeaderOffset; + HeaderSize = ByteOperations.ReadUInt32(GPTBuffer, HeaderOffset + 0x0C); + TableOffset = HeaderOffset + 0x200; + FirstUsableSector = ByteOperations.ReadUInt64(GPTBuffer, HeaderOffset + 0x28); + LastUsableSector = ByteOperations.ReadUInt64(GPTBuffer, HeaderOffset + 0x30); + MaxPartitions = ByteOperations.ReadUInt32(GPTBuffer, HeaderOffset + 0x50); + PartitionEntrySize = ByteOperations.ReadUInt32(GPTBuffer, HeaderOffset + 0x54); + TableSize = MaxPartitions * PartitionEntrySize; + if ((TableOffset + TableSize) > GPTBuffer.Length) + throw new WPinternalsException("Bad GPT"); + + UInt32 PartitionOffset = TableOffset; + + while (PartitionOffset < (TableOffset + TableSize)) + { + string Name = ByteOperations.ReadUnicodeString(GPTBuffer, PartitionOffset + 0x38, 0x48).TrimEnd(new char[] { (char)0, ' ' }); + if (Name.Length == 0) + break; + Partition CurrentPartition = new Partition(); + CurrentPartition.Name = Name; + CurrentPartition.FirstSector = ByteOperations.ReadUInt64(GPTBuffer, PartitionOffset + 0x20); + CurrentPartition.LastSector = ByteOperations.ReadUInt64(GPTBuffer, PartitionOffset + 0x28); + CurrentPartition.PartitionTypeGuid = ByteOperations.ReadGuid(GPTBuffer, PartitionOffset + 0x00); + CurrentPartition.PartitionGuid = ByteOperations.ReadGuid(GPTBuffer, PartitionOffset + 0x10); + CurrentPartition.Attributes = ByteOperations.ReadUInt64(GPTBuffer, PartitionOffset + 0x30); + Partitions.Add(CurrentPartition); + PartitionOffset += PartitionEntrySize; + } + + HasChanged = false; + } + + internal Partition GetPartition(string Name) + { + return Partitions.Where(p => (string.Compare(p.Name, Name, true) == 0)).FirstOrDefault(); + } + + // Magic! + // SecureBoot hack for Bootloader Spec A starts here + internal byte[] InsertHack() + { + Partition HackPartition = Partitions.Where(p => (p.Name == "HACK")).FirstOrDefault(); + Partition SBL1 = Partitions.Where(p => (p.Name == "SBL1")).FirstOrDefault(); + Partition SBL2 = Partitions.Where(p => (p.Name == "SBL2")).FirstOrDefault(); + + if ((SBL1 == null) || (SBL2 == null)) + throw new WPinternalsException("Bad GPT"); + + if (HackPartition == null) + { + HackPartition = new Partition(); + HackPartition.Name = "HACK"; + HackPartition.Attributes = SBL2.Attributes; + HackPartition.FirstSector = SBL1.LastSector; + HackPartition.LastSector = SBL1.LastSector; + + HackPartition.PartitionTypeGuid = SBL2.PartitionTypeGuid; + HackPartition.PartitionGuid = SBL2.PartitionGuid; + + Partitions.Add(HackPartition); + + SBL1.LastSector--; + + SBL2.PartitionTypeGuid = new Guid(new byte[] { 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74 }); + SBL2.PartitionGuid = new Guid(new byte[] { 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74 }); + } + + HasChanged = true; + + return Rebuild(); + } + + internal byte[] RemoveHack() + { + Partition HackPartition = Partitions.Where(p => (p.Name == "HACK")).FirstOrDefault(); + Partition SBL1 = Partitions.Where(p => (p.Name == "SBL1")).FirstOrDefault(); + Partition SBL2 = Partitions.Where(p => (p.Name == "SBL2")).FirstOrDefault(); + + if ((SBL1 == null) || (SBL2 == null)) + throw new WPinternalsException("Bad GPT"); + + if (HackPartition != null) + { + SBL2.PartitionTypeGuid = HackPartition.PartitionTypeGuid; + SBL2.PartitionGuid = HackPartition.PartitionGuid; + + Partitions.Remove(HackPartition); + + SBL1.LastSector++; + } + + HasChanged = true; + + return Rebuild(); + } + + internal byte[] Rebuild() + { + if (GPTBuffer == null) + { + TableSize = 0x4200; + TableOffset = 0; + GPTBuffer = new byte[TableSize]; + } + else + Array.Clear(GPTBuffer, (int)TableOffset, (int)TableSize); + + UInt32 PartitionOffset = TableOffset; + foreach (Partition CurrentPartition in Partitions) + { + ByteOperations.WriteGuid(GPTBuffer, PartitionOffset + 0x00, CurrentPartition.PartitionTypeGuid); + ByteOperations.WriteGuid(GPTBuffer, PartitionOffset + 0x10, CurrentPartition.PartitionGuid); + ByteOperations.WriteUInt64(GPTBuffer, PartitionOffset + 0x20, CurrentPartition.FirstSector); + ByteOperations.WriteUInt64(GPTBuffer, PartitionOffset + 0x28, CurrentPartition.LastSector); + ByteOperations.WriteUInt64(GPTBuffer, PartitionOffset + 0x30, CurrentPartition.Attributes); + ByteOperations.WriteUnicodeString(GPTBuffer, PartitionOffset + 0x38, CurrentPartition.Name, 0x48); + + PartitionOffset += PartitionEntrySize; + } + + ByteOperations.WriteUInt32(GPTBuffer, HeaderOffset + 0x58, ByteOperations.CRC32(GPTBuffer, TableOffset, TableSize)); + ByteOperations.WriteUInt32(GPTBuffer, HeaderOffset + 0x10, 0); + ByteOperations.WriteUInt32(GPTBuffer, HeaderOffset + 0x10, ByteOperations.CRC32(GPTBuffer, HeaderOffset, HeaderSize)); + + return GPTBuffer; + } + + internal void MergePartitionsFromFile(string Path, bool RoundToChunks) + { + MergePartitions(File.ReadAllText(Path), RoundToChunks); + } + + internal void MergePartitionsFromStream(Stream Partitions, bool RoundToChunks) + { + using (TextReader tr = new StreamReader(Partitions)) + { + MergePartitions(tr.ReadToEnd(), RoundToChunks); + } + } + + internal void MergePartitions(string Xml, bool RoundToChunks, ZipArchive Archive = null) + { + GPT GptToMerge; + if (Xml == null) + GptToMerge = new GPT(); + else + { + XmlSerializer x = new XmlSerializer(typeof(GPT), ""); + MemoryStream s = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(Xml)); + GptToMerge = (GPT)x.Deserialize(s); + s.Dispose(); + } + + if (Archive != null) + { + foreach (Partition NewPartition in GptToMerge.Partitions) + { + ZipArchiveEntry Entry = Archive.Entries.Where(e => ((string.Compare(e.Name, NewPartition.Name, true) == 0) || (e.Name.StartsWith(NewPartition.Name + ".", true, System.Globalization.CultureInfo.GetCultureInfo("en-US"))))).FirstOrDefault(); + if (Entry == null) + { + // There is a partition entry in the xml, but this partition is not present in the archive. + + Partition OldPartition = GetPartition(NewPartition.Name); + if (OldPartition == null) + { + // The partition entry in the xml is also not present in the current partition table. + // It must have a know position and length. + + if (NewPartition.LastSector == 0) + { + throw new WPinternalsException("Unknown length for partition \"" + NewPartition.Name + "\""); + } + } + else + { + // The partition entry in the xml is also present in the current partition table. + // But since the partition is not present in the archive, the partition cannot be relocated. + // If the location of the new partition is specified, it must be the same as the current partition. + + if ((NewPartition.FirstSector != 0) && (NewPartition.FirstSector != OldPartition.FirstSector)) + throw new WPinternalsException("Incorrect location for partition \"" + NewPartition.Name + "\""); + if ((NewPartition.LastSector != 0) && (NewPartition.LastSector != OldPartition.LastSector)) + throw new WPinternalsException("Incorrect length for partition \"" + NewPartition.Name + "\""); + + NewPartition.FirstSector = OldPartition.FirstSector; + NewPartition.LastSector = OldPartition.LastSector; + } + } + else + { + // The partition in the xml is also present in the archive. + // If the length is specified in the xml, it must match the file in the archive. + + ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; + using (DecompressedStream DecompressedStream = new DecompressedStream(Entry.Open())) + { + try + { + StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; + } + catch { } + } + + if (NewPartition.LastSector == 0) + { + NewPartition.SizeInSectors = StreamLengthInSectors; + } + else + { + if (NewPartition.SizeInSectors != StreamLengthInSectors) + throw new WPinternalsException("Inconsistent length specified for partition \"" + NewPartition.Name + "\""); + } + } + } + } + else + { + foreach (Partition NewPartition in GptToMerge.Partitions) + { + // This is a partition entry in the xml, and there is no archive. + + Partition OldPartition = GetPartition(NewPartition.Name); + if (OldPartition == null) + { + // The partition entry in the xml is also not present in the current partition table. + // It must have a known position and length. + + if (NewPartition.LastSector == 0) + { + throw new WPinternalsException("Unknown length for partition \"" + NewPartition.Name + "\""); + } + } + else + { + // The partition entry in the xml is also present in the current partition table. + // But since the partition is not present in the archive, the partition cannot be relocated. + // If the location of the new partition is specified, it must be the same as the current partition. + + if ((NewPartition.FirstSector != 0) && (NewPartition.FirstSector != OldPartition.FirstSector)) + throw new WPinternalsException("Incorrect location for partition \"" + NewPartition.Name + "\""); + if ((NewPartition.LastSector != 0) && (NewPartition.LastSector != OldPartition.LastSector)) + throw new WPinternalsException("Incorrect length for partition \"" + NewPartition.Name + "\""); + + NewPartition.FirstSector = OldPartition.FirstSector; + NewPartition.LastSector = OldPartition.LastSector; + } + } + } + + List DynamicPartitions = new List(); + if (Archive != null) + { + // Partitions which are present in the archive, and which have no start-sector in the new GPT data (dynamic relocation), + // and which can be clustered to the end of emmc, are first removed from the existing GPT. + IEnumerable SortedPartitions = Partitions.OrderBy(p => p.FirstSector); + for (int i = SortedPartitions.Count() - 1; i >= 0; i--) + { + Partition OldPartition = SortedPartitions.ElementAt(i); + + // Present in archive? + ZipArchiveEntry Entry = Archive.Entries.Where(e => ((string.Compare(e.Name, OldPartition.Name, true) == 0) || (e.Name.StartsWith(OldPartition.Name + ".", true, System.Globalization.CultureInfo.GetCultureInfo("en-US"))))).FirstOrDefault(); + if (Entry != null) + { + // Not present in new GPT or present in GPT without FirstSector? + Partition NewPartition = GptToMerge.GetPartition(OldPartition.Name); + if ((NewPartition == null) || (NewPartition.FirstSector == 0)) + { + DynamicPartitions.Insert(0, OldPartition); + this.Partitions.Remove(OldPartition); + } + else + break; + } + else + break; + } + } + + // All partitions in the new GPT data should have a start-sector and end-sector by now. + // The partitions in the new GPT data will be applied to the current partition-table. + // Existing partitions, which are overwritten by the new partitions will be removed from the existing GPT. + // Existing partition with the same name in the existing GPT is reused (guids and attribs remain, if not specified). + UInt64 LowestSector = 0; + Partition DPP = this.GetPartition("DPP"); + if (DPP != null) + LowestSector = DPP.LastSector + 1; + foreach (Partition NewPartition in GptToMerge.Partitions) + { + // If the new partition is a dynamic partition, then skip it here. It will be added later. + if (DynamicPartitions.Select(p => p.Name).Any(n => string.Compare(n, NewPartition.Name, true) == 0)) + continue; + + // Sanity check + if (NewPartition.FirstSector < LowestSector) + throw new WPinternalsException("Bad sector alignment for partition: " + NewPartition.Name); + + Partition CurrentPartition = this.GetPartition(NewPartition.Name); + if (CurrentPartition == null) + { + CurrentPartition = new Partition(); + CurrentPartition.Name = NewPartition.Name; + this.Partitions.Add(CurrentPartition); + HasChanged = true; + } + + if ((NewPartition.FirstSector != 0) && (NewPartition.FirstSector != CurrentPartition.FirstSector)) + { + CurrentPartition.FirstSector = NewPartition.FirstSector; + HasChanged = true; + } + if ((NewPartition.LastSector != 0) && (NewPartition.LastSector != CurrentPartition.LastSector)) + { + CurrentPartition.LastSector = NewPartition.LastSector; + HasChanged = true; + } + if ((NewPartition.Attributes != 0) && (CurrentPartition.Attributes != NewPartition.Attributes)) + { + CurrentPartition.Attributes = NewPartition.Attributes; + HasChanged = true; + } + + if ((NewPartition.PartitionGuid == null) || (NewPartition.PartitionGuid != CurrentPartition.PartitionGuid)) + HasChanged = true; + if (NewPartition.PartitionGuid != null) + CurrentPartition.PartitionGuid = NewPartition.PartitionGuid; + if (CurrentPartition.PartitionGuid == null) + CurrentPartition.PartitionGuid = Guid.NewGuid(); + + if ((NewPartition.PartitionTypeGuid == null) || (NewPartition.PartitionTypeGuid != CurrentPartition.PartitionTypeGuid)) + HasChanged = true; + if (NewPartition.PartitionTypeGuid != null) + CurrentPartition.PartitionTypeGuid = NewPartition.PartitionTypeGuid; + if (CurrentPartition.PartitionTypeGuid == null) + CurrentPartition.PartitionTypeGuid = Guid.NewGuid(); + + for (int i = this.Partitions.Count - 1; i >= 0; i--) + { + if (this.Partitions[i] != CurrentPartition) + { + if ((CurrentPartition.FirstSector <= this.Partitions[i].LastSector) && (CurrentPartition.LastSector >= this.Partitions[i].FirstSector)) + { + this.Partitions.RemoveAt(i); + HasChanged = true; + } + } + } + } + + if (Archive != null) + { + // All partitions listed in the archive, which are present in the existing GPT, should overwrite the existing partition. + // Check if the sizes of the partitions in the archive do not exceed the size of the partition, as listed in the GPT. + foreach (Partition OldPartition in this.Partitions) + { + ZipArchiveEntry Entry = Archive.Entries.Where(e => ((string.Compare(e.Name, OldPartition.Name, true) == 0) || (e.Name.StartsWith(OldPartition.Name + ".", true, System.Globalization.CultureInfo.GetCultureInfo("en-US"))))).FirstOrDefault(); + if (Entry != null) + { + DecompressedStream DecompressedStream = new DecompressedStream(Entry.Open()); + ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; + try + { + StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; + } + catch { } + DecompressedStream.Close(); + + UInt64 MaxPartitionSizeInSectors = OldPartition.SizeInSectors; + Partition NextPartition = this.Partitions.Where(p => p.FirstSector > OldPartition.FirstSector).OrderBy(p => p.FirstSector).FirstOrDefault(); + if (NextPartition != null) + MaxPartitionSizeInSectors = NextPartition.FirstSector - OldPartition.FirstSector; + if (StreamLengthInSectors > MaxPartitionSizeInSectors) + { + throw new WPinternalsException("Incorrect length for partition \"" + OldPartition.Name + "\""); + } + + if (OldPartition.SizeInSectors != StreamLengthInSectors) + { + OldPartition.SizeInSectors = StreamLengthInSectors; + HasChanged = true; + } + } + } + + // All remaining partitions in the archive, which were listed in the original GPT, + // should be added at the end of the partition-table. + UInt64 FirstFreeSector = 0x5000; + if (this.Partitions.Count > 0) + { + FirstFreeSector = this.Partitions.Max(p => p.LastSector) + 1; + + // Always start a new partition on a new chunk (0x100 sector boundary), to be more flexible during custom flash + if (RoundToChunks && (((double)FirstFreeSector % 0x100) != 0)) + FirstFreeSector = (UInt64)(Math.Ceiling((double)FirstFreeSector / 0x100) * 0x100); + + } + foreach (Partition NewPartition in DynamicPartitions) + { + if (NewPartition.FirstSector != FirstFreeSector) + { + NewPartition.FirstSector = FirstFreeSector; + HasChanged = true; + } + ZipArchiveEntry Entry = Archive.Entries.Where(e => ((string.Compare(e.Name, NewPartition.Name, true) == 0) || (e.Name.StartsWith(NewPartition.Name + ".", true, System.Globalization.CultureInfo.GetCultureInfo("en-US"))))).FirstOrDefault(); + DecompressedStream DecompressedStream = new DecompressedStream(Entry.Open()); + ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; + try + { + StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; + } + catch { } + DecompressedStream.Close(); + if (NewPartition.SizeInSectors != StreamLengthInSectors) + { + NewPartition.SizeInSectors = StreamLengthInSectors; + HasChanged = true; + } + this.Partitions.Add(NewPartition); + FirstFreeSector += StreamLengthInSectors; + + // Always start a new partition on a new chunk (0x100 sector boundary), to be more flexible during custom flash + if (RoundToChunks && (((double)FirstFreeSector % 0x100) != 0)) + FirstFreeSector = (UInt64)(Math.Ceiling((double)FirstFreeSector / 0x100) * 0x100); + } + } + + Rebuild(); + } + + internal void WritePartitions(string Path) + { + string DirPath = System.IO.Path.GetDirectoryName(Path); + if (!Directory.Exists(DirPath)) + Directory.CreateDirectory(DirPath); + + XmlSerializer x = new XmlSerializer(typeof(GPT), ""); + + XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); + ns.Add("", ""); + System.IO.StreamWriter FileWriter = new System.IO.StreamWriter(Path); + x.Serialize(FileWriter, this, ns); + FileWriter.Close(); + } + + internal static GPT ReadPartitions(string Path) + { + XmlSerializer x = new XmlSerializer(typeof(GPT), ""); + using (FileStream s = new FileStream(Path, FileMode.Open)) + { + return (GPT)x.Deserialize(s); + } + } + + internal void RestoreBackupPartitions() + { + // This is necessary, because the partitions and backup-partitions can exchange. + // This may cause the startsector to be higher than the maximum allowed sector for flashing with a Lumia V1 programmer (hardcoded in programmer) + List RevisePartitions = new List(new string[] { "SBL1", "SBL2", "SBL3", "UEFI", "TZ", "RPM", "WINSECAPP" }); + foreach (string RevisePartitionName in RevisePartitions) + { + Partition RevisePartition = GetPartition(RevisePartitionName); + Partition ReviseBackupPartition = GetPartition("BACKUP_" + RevisePartitionName); + if ((RevisePartition != null) && (ReviseBackupPartition != null) && (RevisePartition.FirstSector > ReviseBackupPartition.FirstSector)) + { + ulong OriginalFirstSector = RevisePartition.FirstSector; + ulong OriginalLastSector = RevisePartition.LastSector; + RevisePartition.FirstSector = ReviseBackupPartition.FirstSector; + RevisePartition.LastSector = ReviseBackupPartition.LastSector; + ReviseBackupPartition.FirstSector = OriginalFirstSector; + ReviseBackupPartition.LastSector = OriginalLastSector; + + HasChanged = true; + } + + if (RevisePartition.LastSector >= 0xF400) + throw new WPinternalsException("Unsupported partition layout!"); + } + + } + } + + public class Partition + { + private UInt64 _SizeInSectors; + private UInt64 _FirstSector; + private UInt64 _LastSector; + + public string Name; // 0x48 + public Guid PartitionTypeGuid; // 0x10 + public Guid PartitionGuid; // 0x10 + [XmlIgnore] + internal UInt64 Attributes; // 0x08 + + [XmlIgnore] + internal UInt64 SizeInSectors + { + get + { + if (_SizeInSectors != 0) + return _SizeInSectors; + else + return LastSector - FirstSector + 1; + } + set + { + _SizeInSectors = value; + if (FirstSector != 0) + LastSector = FirstSector + _SizeInSectors - 1; + } + } + + [XmlIgnore] + internal UInt64 FirstSector // 0x08 + { + get + { + return _FirstSector; + } + set + { + _FirstSector = value; + if (_SizeInSectors != 0) + _LastSector = FirstSector + _SizeInSectors - 1; + } + } + + [XmlIgnore] + internal UInt64 LastSector // 0x08 + { + get + { + return _LastSector; + } + set + { + _LastSector = value; + _SizeInSectors = 0; + } + } + + [XmlIgnore] + public string Volume + { + get + { + return @"\\?\Volume" + PartitionGuid.ToString("b") + @"\"; + } + } + + [XmlElement(ElementName = "FirstSector")] + public string FirstSectorAsString + { + get + { + return "0x" + FirstSector.ToString("X16"); + } + set + { + FirstSector = Convert.ToUInt64(value, 16); + } + } + + [XmlElement(ElementName = "LastSector")] + public string LastSectorAsString + { + get + { + return "0x" + LastSector.ToString("X16"); + } + set + { + LastSector = Convert.ToUInt64(value, 16); + } + } + + [XmlElement(ElementName = "Attributes")] + public string AttributesAsString + { + get + { + return "0x" + Attributes.ToString("X16"); + } + set + { + Attributes = Convert.ToUInt64(value, 16); + } + } + } +} diff --git a/Models/LZMA.cs b/Models/LZMA.cs new file mode 100644 index 0000000..c0b2ee2 --- /dev/null +++ b/Models/LZMA.cs @@ -0,0 +1,287 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +// SevenZip LZMA SDK: http://www.7-zip.org/download.html +// Usage: http://stackoverflow.com/questions/7646328/how-to-use-the-7z-sdk-to-compress-and-decompress-a-file + +using SevenZip; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading; + +namespace WPinternals +{ + internal static class LZMA + { + internal static byte[] Decompress(byte[] Input, UInt32 Offset, UInt32 InputSize) + { + byte[] Properties = new byte[5]; + Buffer.BlockCopy(Input, (int)Offset, Properties, 0, 5); + + UInt64 OutputSize = ByteOperations.ReadUInt64(Input, Offset + 5); + + SevenZip.Compression.LZMA.Decoder Coder = new SevenZip.Compression.LZMA.Decoder(); + Coder.SetDecoderProperties(Properties); + + MemoryStream InStream = new MemoryStream(Input, (int)Offset + 0x0D, (int)InputSize - 0x0D); + + byte[] Output = new byte[OutputSize]; + MemoryStream OutStream = new MemoryStream(Output, true); + + Coder.Code(InStream, OutStream, (Int64)InputSize - 0x0D, (Int64)OutputSize, null); + + OutStream.Flush(); + OutStream.Close(); + InStream.Close(); + + return Output; + } + + internal static byte[] Compress(byte[] Input, UInt32 Offset, UInt32 InputSize) + { + SevenZip.Compression.LZMA.Encoder Coder = new SevenZip.Compression.LZMA.Encoder(); + + MemoryStream InStream = new MemoryStream(Input, (int)Offset, (int)InputSize); + MemoryStream OutStream = new MemoryStream(); + + // Write the encoder properties + Coder.WriteCoderProperties(OutStream); + + // Write the decompressed file size + OutStream.Write(BitConverter.GetBytes(InStream.Length), 0, 8); + + // Encode the file + Coder.Code(InStream, OutStream, (Int64)InputSize, -1, null); + + byte[] Output = new byte[OutStream.Length]; + Buffer.BlockCopy(OutStream.GetBuffer(), 0, Output, 0, (int)OutStream.Length); + + OutStream.Flush(); + OutStream.Close(); + InStream.Close(); + + return Output; + } + } + + public class LZMACompressionStream : Stream + { + private SevenZip.Compression.LZMA.Encoder Encoder = null; + private SevenZip.Compression.LZMA.Decoder Decoder = null; + private PumpStream BufferStream; + private Stream stream; + private bool LeaveOpen; + private Thread WorkThread; + + public LZMACompressionStream(Stream stream, CompressionMode mode, bool LeaveOpen, int DictionarySize, int PosStateBits, + int LitContextBits, int LitPosBits, int Algorithm, int NumFastBytes, string MatchFinder, bool EndMarker) + { + this.stream = stream; + this.LeaveOpen = LeaveOpen; + BufferStream = new PumpStream(); + + if (mode == CompressionMode.Compress) + { + Encoder = new SevenZip.Compression.LZMA.Encoder(); + if (DictionarySize != 0) + Encoder.SetCoderProperties( + new SevenZip.CoderPropID[8] {CoderPropID.DictionarySize, CoderPropID.PosStateBits, CoderPropID.LitContextBits, + CoderPropID.LitPosBits, CoderPropID.Algorithm, CoderPropID.NumFastBytes, CoderPropID.MatchFinder, CoderPropID.EndMarker}, + new object[8] { DictionarySize, PosStateBits, LitContextBits, LitPosBits, Algorithm, NumFastBytes, MatchFinder, EndMarker }); + Encoder.WriteCoderProperties(stream); + WorkThread = new Thread(new ThreadStart(Encode)); + } + else + { + byte[] DecoderProperties = new byte[5]; + stream.Read(DecoderProperties, 0, 5); + Decoder = new SevenZip.Compression.LZMA.Decoder(); + Decoder.SetDecoderProperties(DecoderProperties); + WorkThread = new Thread(new ThreadStart(Decode)); + } + + WorkThread.Start(); + } + + public LZMACompressionStream(Stream stream, CompressionMode mode, bool LeaveOpen) + : this(stream, mode, LeaveOpen, 0, 0, 0, 0, 0, 0, null, false) + { + } + + private void Encode() + { + Encoder.Code(BufferStream, stream, -1, -1, null); + if (LeaveOpen == false) + stream.Close(); + } + + private void Decode() + { + Decoder.Code(stream, BufferStream, -1, -1, null); + BufferStream.Close(); + if (LeaveOpen == false) + stream.Close(); + } + + public override void Close() + { + if (Encoder != null) + BufferStream.Close(); + else if (WorkThread.IsAlive) + WorkThread.Abort(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return BufferStream.Read(buffer, offset, count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + BufferStream.Write(buffer, offset, count); + } + + public override bool CanRead { get { return (Decoder != null); } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return (Encoder != null); } } + public override void Flush() { } + public override long Length { get { return 0; } } + public override long Position { get { return 0; } set { } } + public override long Seek(long offset, SeekOrigin origin) { return 0; } + public override void SetLength(long value) { } + } + + public class PumpStream : Stream + { + private Queue BufferQueue; + private int BufferOffset; + private long MaxBufferSize; + private long BufferSize; + private bool Closed; + private bool EOF; + + public PumpStream(long MaxBufferSize, int ReadTimeout, int WriteTimeout) + { + this.MaxBufferSize = MaxBufferSize; + this.ReadTimeout = ReadTimeout; + this.WriteTimeout = WriteTimeout; + BufferQueue = new Queue(); + BufferOffset = 0; + BufferSize = 0; + Closed = false; + EOF = false; + } + + public PumpStream() + : this(16777216, Timeout.Infinite, Timeout.Infinite) + { + } + + public new void Dispose() + { + BufferQueue.Clear(); + } + + public override void Close() + { + Closed = true; + lock (BufferQueue) + Monitor.Pulse(BufferQueue); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int BytesRead = 0; + lock (BufferQueue) + { + while (BytesRead < count && EOF == false) + { + if (BufferQueue.Count > 0) + { + byte[] b = BufferQueue.Peek(); + + if ((b.Length - BufferOffset) <= (count - BytesRead)) + { + Array.Copy(b, BufferOffset, buffer, offset + BytesRead, (b.Length - BufferOffset)); + + BufferQueue.Dequeue(); + BufferSize -= b.Length; + Monitor.Pulse(BufferQueue); + + BytesRead += (b.Length - BufferOffset); + BufferOffset = 0; + } + else + { + Array.Copy(b, BufferOffset, buffer, offset + BytesRead, (count - BytesRead)); + + BufferOffset += (count - BytesRead); + BytesRead += (count - BytesRead); + } + + } + else + { + if (Closed == false) + { + if (Monitor.Wait(BufferQueue, ReadTimeout) == false) + throw new IOException("Could not read from stream: Timeout expired waiting for data to be written."); + } + else + EOF = true; + } + } + } + + return BytesRead; + } + + public override void Write(byte[] buffer, int offset, int count) + { + lock (BufferQueue) + { + while (BufferSize >= MaxBufferSize) + if (Monitor.Wait(BufferQueue, WriteTimeout) == false) + throw new IOException("Could not write to stream: Timeout expired waiting for data to be read."); + + byte[] b = new byte[count]; + Array.Copy(buffer, offset, b, 0, count); + BufferQueue.Enqueue(b); + BufferSize += b.Length; + + Monitor.Pulse(BufferQueue); + } + } + + public override int ReadTimeout { get; set; } + public override int WriteTimeout { get; set; } + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return true; } } + public override void Flush() { } + public override long Length { get { return 0; } } + public override long Position { get { return 0; } set { } } + public override long Seek(long offset, SeekOrigin origin) { return 0; } + public override void SetLength(long value) { } + } +} diff --git a/Models/LumiaDownloadModel.cs b/Models/LumiaDownloadModel.cs new file mode 100644 index 0000000..179100a --- /dev/null +++ b/Models/LumiaDownloadModel.cs @@ -0,0 +1,478 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.IO; +using System.Xml; + +namespace WPinternals +{ + internal static class LumiaDownloadModel + { + internal static string SearchFFU(string ProductType, string ProductCode, string OperatorCode) + { + string FoundProductType; + return SearchFFU(ProductType, ProductCode, OperatorCode, out FoundProductType); + } + + internal static string SearchFFU(string ProductType, string ProductCode, string OperatorCode, out string FoundProductType) + { + if (ProductType == "") + ProductType = null; + if (ProductCode == "") + ProductCode = null; + if (OperatorCode == "") + OperatorCode = null; + + if (ProductCode != null) + { + ProductCode = ProductCode.ToUpper(); + ProductType = null; + OperatorCode = null; + } + if (ProductType != null) + { + ProductType = ProductType.ToUpper(); + if (ProductType.StartsWith("RM") && !ProductType.StartsWith("RM-")) + ProductType = "RM-" + ProductType.Substring(2); + } + if (OperatorCode != null) + OperatorCode = OperatorCode.ToUpper(); + + DiscoveryQueryParameters DiscoveryQueryParams = new DiscoveryQueryParameters + { + manufacturerName = "Microsoft", + manufacturerProductLine = "Lumia", + packageType = "Firmware", + packageClass = "Public", + manufacturerHardwareModel = ProductType, + manufacturerHardwareVariant = ProductCode, + operatorName = OperatorCode + }; + DiscoveryParameters DiscoveryParams = new DiscoveryParameters + { + query = DiscoveryQueryParams + }; + + DataContractJsonSerializer Serializer1 = new DataContractJsonSerializer(typeof(DiscoveryParameters)); + MemoryStream JsonStream1 = new MemoryStream(); + Serializer1.WriteObject(JsonStream1, DiscoveryParams); + JsonStream1.Seek(0L, SeekOrigin.Begin); + string JsonContent = new StreamReader(JsonStream1).ReadToEnd(); + + Uri RequestUri = new Uri("https://api.swrepository.com/rest-api/discovery/1/package"); + + HttpClient HttpClient = new HttpClient(); + HttpClient.DefaultRequestHeaders.UserAgent.TryParseAdd("SoftwareRepository"); + HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + Task HttpPostTask = HttpClient.PostAsync(RequestUri, new StringContent(JsonContent, Encoding.UTF8, "application/json")); + HttpPostTask.Wait(); + HttpResponseMessage Response = HttpPostTask.Result; + + string JsonResultString = ""; + if (Response.StatusCode == HttpStatusCode.OK) + { + Task ReadResponseTask = Response.Content.ReadAsStringAsync(); + ReadResponseTask.Wait(); + JsonResultString = ReadResponseTask.Result; + } + + SoftwarePackage Package = null; + using (MemoryStream JsonStream2 = new MemoryStream(Encoding.UTF8.GetBytes(JsonResultString))) + { + DataContractJsonSerializer Serializer2 = new DataContractJsonSerializer(typeof(SoftwarePackages)); + SoftwarePackages SoftwarePackages = (SoftwarePackages)Serializer2.ReadObject(JsonStream2); + if (SoftwarePackages != null) + { + Package = SoftwarePackages.softwarePackages.FirstOrDefault(); + } + } + + if (Package == null) + throw new WPinternalsException("FFU not found"); + + FoundProductType = Package.manufacturerHardwareModel[0]; + + SoftwareFile FileInfo = Package.files.Where(f => f.fileName.EndsWith(".ffu", StringComparison.OrdinalIgnoreCase)).First(); + + Uri FileInfoUri = new Uri("https://api.swrepository.com/rest-api/discovery/fileurl/1/" + Package.id + "/" + FileInfo.fileName); + Task GetFileInfoTask = HttpClient.GetStringAsync(FileInfoUri); + GetFileInfoTask.Wait(); + string FileInfoString = GetFileInfoTask.Result; + + string FfuUrl = ""; + FileUrlResult FileUrl = null; + using (MemoryStream JsonStream3 = new MemoryStream(Encoding.UTF8.GetBytes(FileInfoString))) + { + DataContractJsonSerializer Serializer3 = new DataContractJsonSerializer(typeof(FileUrlResult)); + FileUrl = (FileUrlResult)Serializer3.ReadObject(JsonStream3); + if (FileUrl != null) + { + FfuUrl = FileUrl.url; + } + } + + HttpClient.Dispose(); + + return FfuUrl; + } + + internal static string[] SearchEmergencyFiles(string ProductType) + { + ProductType = ProductType.ToUpper(); + if (ProductType.StartsWith("RM") && !ProductType.StartsWith("RM-")) + ProductType = "RM-" + ProductType.Substring(2); + + LogFile.Log("Getting Emergency files for: " + ProductType, LogType.FileAndConsole); + + if ((ProductType == "RM-1072") || (ProductType == "RM-1073")) + { + LogFile.Log("Due to mix-up in online-repository, redirecting to emergency files of RM-1113", LogType.FileAndConsole); + ProductType = "RM-1113"; + } + + List Result = new List(); + + WebClient Client = new WebClient(); + string Src; + string FileName; + string Config = null; + try + { + Config = Client.DownloadString(@"https://repairavoidance.blob.core.windows.net/packages/EmergencyFlash/" + ProductType + "/emergency_flash_config.xml"); + } + catch + { + LogFile.Log("Emergency files for " + ProductType + " not found", LogType.FileAndConsole); + return null; + } + Client.Dispose(); + + XmlDocument Doc = new XmlDocument(); + Doc.LoadXml(Config); + + // Hex + XmlNode Node = Doc.SelectSingleNode("//emergency_flash_config/hex_flasher"); + if (Node != null) + { + FileName = Node.Attributes["image_path"].InnerText; + Src = @"https://repairavoidance.blob.core.windows.net/packages/EmergencyFlash/" + ProductType + "/" + FileName; + LogFile.Log("Hex-file: " + Src); + Result.Add(Src); + } + + // Mbn + Node = Doc.SelectSingleNode("//emergency_flash_config/mbn_image"); + if (Node != null) + { + FileName = Node.Attributes["image_path"].InnerText; + Src = @"https://repairavoidance.blob.core.windows.net/packages/EmergencyFlash/" + ProductType + "/" + FileName; + LogFile.Log("Mbn-file: " + Src); + Result.Add(Src); + } + + // Ede + foreach (XmlNode SubNode in Doc.SelectNodes("//emergency_flash_config/first_boot_images/first_boot_image")) + { + FileName = SubNode.Attributes["image_path"].InnerText; + Src = @"https://repairavoidance.blob.core.windows.net/packages/EmergencyFlash/" + ProductType + "/" + FileName; + LogFile.Log("Firehose-programmer-file: " + Src); + Result.Add(Src); + } + + // Edp + foreach (XmlNode SubNode in Doc.SelectNodes("//emergency_flash_config/second_boot_firehose_single_image/firehose_image")) + { + FileName = SubNode.Attributes["image_path"].InnerText; + Src = @"https://repairavoidance.blob.core.windows.net/packages/EmergencyFlash/" + ProductType + "/" + FileName; + LogFile.Log("Firehose-payload-file: " + Src); + Result.Add(Src); + } + + return Result.ToArray(); + } + } + + #pragma warning disable 0649 + [DataContract] + internal class FileUrlResult + { + [DataMember] + internal string url; + + [DataMember] + internal List alternateUrl; + + [DataMember] + internal long fileSize; + + [DataMember] + internal List checksum; + } + #pragma warning restore 0649 + + [DataContract] + public class DiscoveryQueryParameters + { + [DataMember(EmitDefaultValue = false)] + public string customerName; + + [DataMember(EmitDefaultValue = false)] + public ExtendedAttributes extendedAttributes; + + [DataMember(EmitDefaultValue = false)] + public string manufacturerHardwareModel; + + [DataMember(EmitDefaultValue = false)] + public string manufacturerHardwareVariant; + + [DataMember(EmitDefaultValue = false)] + public string manufacturerModelName; + + [DataMember] + public string manufacturerName; + + [DataMember(EmitDefaultValue = false)] + public string manufacturerPackageId; + + [DataMember(EmitDefaultValue = false)] + public string manufacturerPlatformId; + + [DataMember] + public string manufacturerProductLine; + + [DataMember(EmitDefaultValue = false)] + public string manufacturerVariantName; + + [DataMember(EmitDefaultValue = false)] + public string operatorName; + + [DataMember] + public string packageClass; + + [DataMember(EmitDefaultValue = false)] + public string packageRevision; + + [DataMember(EmitDefaultValue = false)] + public string packageState; + + [DataMember(EmitDefaultValue = false)] + public string packageSubRevision; + + [DataMember(EmitDefaultValue = false)] + public string packageSubtitle; + + [DataMember(EmitDefaultValue = false)] + public string packageTitle; + + [DataMember] + public string packageType; + } + + [DataContract] + public class DiscoveryParameters + { + [DataMember(Name = "api-version")] + public string apiVersion; + + [DataMember] + public DiscoveryQueryParameters query; + + [DataMember] + public List condition; + + [DataMember] + public List response; + + public DiscoveryParameters(): this(DiscoveryCondition.Default) + { + } + + public DiscoveryParameters(DiscoveryCondition Condition) + { + this.apiVersion = "1"; + this.query = new DiscoveryQueryParameters(); + this.condition = new List(); + if (Condition == DiscoveryCondition.All) + { + this.condition.Add("all"); + return; + } + if (Condition == DiscoveryCondition.Latest) + { + this.condition.Add("latest"); + return; + } + this.condition.Add("default"); + } + } + + public enum DiscoveryCondition + { + Default, + All, + Latest + } + + [Serializable] + public class ExtendedAttributes : ISerializable + { + public Dictionary Dictionary; + + public ExtendedAttributes() + { + this.Dictionary = new Dictionary(); + } + + protected ExtendedAttributes(SerializationInfo info, StreamingContext context) + { + if (info != null) + { + this.Dictionary = new Dictionary(); + SerializationInfoEnumerator Enumerator = info.GetEnumerator(); + while (Enumerator.MoveNext()) + { + this.Dictionary.Add(Enumerator.Current.Name, (string)Enumerator.Current.Value); + } + } + } + + public virtual void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info != null) + { + foreach (string Current in this.Dictionary.Keys) + { + info.AddValue(Current, this.Dictionary[Current]); + } + } + } + } + + [DataContract] + public class SoftwareFileChecksum + { + [DataMember] + public string type; + + [DataMember] + public string value; + } + + [DataContract] + public class SoftwarePackages + { + [DataMember] + public List softwarePackages; + } + + [DataContract] + public class SoftwarePackage + { + [DataMember] + public List customerName; + + [DataMember] + public ExtendedAttributes extendedAttributes; + + [DataMember] + public List files; + + [DataMember] + public string id; + + [DataMember] + public List manufacturerHardwareModel; + + [DataMember] + public List manufacturerHardwareVariant; + + [DataMember] + public List manufacturerModelName; + + [DataMember] + public string manufacturerName; + + [DataMember] + public string manufacturerPackageId; + + [DataMember] + public List manufacturerPlatformId; + + [DataMember] + public string manufacturerProductLine; + + [DataMember] + public List manufacturerVariantName; + + [DataMember] + public List operatorName; + + [DataMember] + public List packageClass; + + [DataMember] + public string packageDescription; + + [DataMember] + public string packageRevision; + + [DataMember] + public string packageState; + + [DataMember] + public string packageSubRevision; + + [DataMember] + public string packageSubtitle; + + [DataMember] + public string packageTitle; + + [DataMember] + public string packageType; + } + + [DataContract] + public class SoftwareFile + { + [DataMember] + public List checksum; + + [DataMember] + public string fileName; + + [DataMember] + public long fileSize; + + [DataMember] + public string fileType; + } +} diff --git a/Models/MassStorage.cs b/Models/MassStorage.cs new file mode 100644 index 0000000..24942fc --- /dev/null +++ b/Models/MassStorage.cs @@ -0,0 +1,461 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Linq; +using System.Management; +using System.Runtime.InteropServices; + +namespace WPinternals +{ + internal class MassStorage: NokiaPhoneModel + { + internal string Drive = null; + internal string PhysicalDrive = null; + internal string VolumeLabel = null; + internal IntPtr hVolume = (IntPtr)(-1); + internal IntPtr hDrive = (IntPtr)(-1); + private bool OpenWithWriteAccess; + + internal MassStorage(string DevicePath): base(DevicePath) + { + try + { + ManagementObjectCollection coll = new ManagementObjectSearcher("select * from Win32_LogicalDisk").Get(); + foreach (ManagementObject logical in coll) + { + System.Diagnostics.Debug.Print(logical["Name"].ToString()); + + string Label = ""; + foreach (ManagementObject partition in logical.GetRelated("Win32_DiskPartition")) + { + foreach (ManagementObject drive in partition.GetRelated("Win32_DiskDrive")) + { + if ((drive["PNPDeviceID"].ToString().IndexOf("VEN_QUALCOMM&PROD_MMC_STORAGE") >= 0) || + (drive["PNPDeviceID"].ToString().IndexOf("VEN_MSFT&PROD_PHONE_MMC_STOR") >= 0)) + { + Label = (logical["VolumeName"] == null ? "" : logical["VolumeName"].ToString()); + if ((Drive == null) || (string.Compare(Label, "MainOS", true) == 0)) // Always prefer the MainOS drive-mapping + { + Drive = logical["Name"].ToString(); + PhysicalDrive = drive["DeviceID"].ToString(); + VolumeLabel = Label; + } + if (string.Compare(Label, "MainOS", true) == 0) + break; + } + } + if (string.Compare(Label, "MainOS", true) == 0) + break; + } + } + } + catch { } + } + + protected override void Dispose(bool disposing) + { + if (Disposed) + return; + + if (disposing) + { + CloseVolume(); + + base.Dispose(disposing); + } + } + + internal void OpenVolume(bool WriteAccess) + { + OpenWithWriteAccess = false; + + if (IsVolumeOpen()) + throw new Exception("Volume already opened"); + + if (WriteAccess) + { + // Unmounting the volume does not have the desired effect. + // It does not unmount the mountpoints on the phone. + // So the sectors of the filesystems of EFIESP, Data, etc cannot be written. + // Unmounting the mounting points would alter the NTFS structure, which is also an undesired effect. + // Restoring partitions with file-systems can better be done using Flash mode! + + // Open volume + hVolume = NativeMethods.CreateFile( + PhysicalDrive, + NativeMethods.GENERIC_READ | NativeMethods.GENERIC_WRITE, + NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE, + IntPtr.Zero, + NativeMethods.OPEN_EXISTING, + NativeMethods.FILE_FLAG_WRITE_THROUGH | NativeMethods.FILE_FLAG_NO_BUFFERING, // !!! + IntPtr.Zero); + } + else + { + hVolume = NativeMethods.CreateFile( + // @"\\.\" + Drive, + PhysicalDrive, + NativeMethods.GENERIC_READ, + NativeMethods.FILE_SHARE_READ | NativeMethods.FILE_SHARE_WRITE, + IntPtr.Zero, + NativeMethods.OPEN_EXISTING, + 0, + IntPtr.Zero); + } + + if ((int)hVolume == -1) + throw new Exception(Marshal.GetLastWin32Error().ToString()); + + OpenWithWriteAccess = WriteAccess; + } + + internal bool IsVolumeOpen() + { + return (int)(hVolume) != -1; + } + + internal void CloseVolume() + { + if ((int)hDrive != -1) + { + NativeMethods.CloseHandle(hDrive); // This reloads the logical drive!! + hDrive = new IntPtr(-1); + } + + if ((int)hVolume != -1) + { + NativeMethods.CloseHandle(hVolume); + hVolume = new IntPtr(-1); + } + } + + internal void SetSectorPosition(UInt64 Sector) + { + if (!IsVolumeOpen()) + throw new Exception("Volume is not opened"); + + int High = (int)(Sector >> (32 - 9)); + NativeMethods.SetFilePointer(hVolume, (int)(Sector << 9), ref High, EMoveMethod.Begin); + } + + internal void WriteSector(byte[] buffer) + { + if (!IsVolumeOpen()) + throw new Exception("Volume is not opened"); + + if (!OpenWithWriteAccess) + throw new Exception("Volume is not opened with Write Acces"); + + uint count = 0; + bool result = NativeMethods.WriteFile(hVolume, buffer, 512, out count, IntPtr.Zero); + if (!result) + throw new Exception("Exception: 0x" + Marshal.GetLastWin32Error().ToString("X8")); + } + + internal void ReadSector(byte[] buffer) + { + if (!IsVolumeOpen()) + throw new Exception("Volume is not opened"); + + uint count = 1; + bool result = NativeMethods.ReadFile(hVolume, buffer, 512, out count, IntPtr.Zero); + if (!result) + throw new Exception("Exception: 0x" + Marshal.GetLastWin32Error().ToString("X8")); + } + + internal void ReadSectors(byte[] buffer, out uint ActualSectorsRead, uint SizeInSectors = uint.MaxValue) + { + if (!IsVolumeOpen()) + throw new Exception("Volume is not opened"); + + uint count = 1; + bool Result = NativeMethods.ReadFile(hVolume, buffer, (SizeInSectors * 0x200) < buffer.Length ? (SizeInSectors * 0x200) : (uint)buffer.Length, out count, IntPtr.Zero); + ActualSectorsRead = count / 0x200; + if (!Result) + throw new Exception("Failed to read sectors. Exception: 0x" + Marshal.GetLastWin32Error().ToString("X8")); + } + + internal byte[] ReadSectors(UInt64 StartSector, UInt64 SectorCount) + { + byte[] Result = new byte[SectorCount * 0x200]; + + bool VolumeWasOpen = IsVolumeOpen(); + if (!VolumeWasOpen) + OpenVolume(false); + + SetSectorPosition(StartSector); + uint SectorsRead; + ReadSectors(Result, out SectorsRead); + + if (!VolumeWasOpen) + CloseVolume(); + + if (Result == null) + throw new Exception("Failed to read from phone"); + + return Result; + } + + internal void DumpSectors(UInt64 StartSector, UInt64 SectorCount, string Path) + { + DumpSectors(StartSector, SectorCount, Path, null, null, null); + } + + internal void DumpSectors(UInt64 StartSector, UInt64 SectorCount, string Path, Action ProgressUpdateCallback) + { + DumpSectors(StartSector, SectorCount, Path, null, ProgressUpdateCallback, null); + } + + internal void DumpSectors(UInt64 StartSector, UInt64 SectorCount, string Path, ProgressUpdater UpdaterPerSector) + { + DumpSectors(StartSector, SectorCount, Path, null, null, UpdaterPerSector); + } + + internal void DumpSectors(UInt64 StartSector, UInt64 SectorCount, Stream OutputStream) + { + DumpSectors(StartSector, SectorCount, null, OutputStream, null, null); + } + + internal void DumpSectors(UInt64 StartSector, UInt64 SectorCount, Stream OutputStream, Action ProgressUpdateCallback) + { + DumpSectors(StartSector, SectorCount, null, OutputStream, ProgressUpdateCallback, null); + } + + internal void DumpSectors(UInt64 StartSector, UInt64 SectorCount, Stream OutputStream, ProgressUpdater UpdaterPerSector) + { + DumpSectors(StartSector, SectorCount, null, OutputStream, null, UpdaterPerSector); + } + + private void DumpSectors(UInt64 StartSector, UInt64 SectorCount, string Path, Stream OutputStream, Action ProgressUpdateCallback, ProgressUpdater UpdaterPerSector) + { + bool VolumeWasOpen = IsVolumeOpen(); + if (!VolumeWasOpen) + OpenVolume(false); + + SetSectorPosition(StartSector); + ProgressUpdater Progress = UpdaterPerSector; + if ((Progress == null) && (ProgressUpdateCallback != null)) + Progress = new ProgressUpdater(SectorCount, ProgressUpdateCallback); + + byte[] Buffer; + if (SectorCount >= 0x80) + Buffer = new byte[0x10000]; + else + Buffer = new byte[SectorCount * 0x200]; + + Stream Stream; + if (Path == null) + Stream = OutputStream; + else + Stream = File.Open(Path, FileMode.Create); + + using (BinaryWriter Writer = new BinaryWriter(Stream)) + { + uint ActualSectorsRead; + for (UInt64 i = 0; i < SectorCount; i += 0x80) + { + // TODO: Reading sectors and writing to compressed stream should be on different threads. + // Backup of 3 partitions without compression takes about 40 minutes. + // Backup of same partitions with compression takes about 70 minutes. + // Separation reading and compression could potentially speed up a lot. + // BinaryWriter doesnt support async. + // Calling async directly on the EntryStream of a Ziparchive blocks. + + ReadSectors(Buffer, out ActualSectorsRead, (SectorCount - i) >= 0x80 ? 0x80 : (uint)(SectorCount - i)); + Writer.Write(Buffer, 0, (int)ActualSectorsRead * 0x200); + if (Progress != null) + Progress.IncreaseProgress(ActualSectorsRead); + } + Stream.Flush(); + } + + if (!VolumeWasOpen) + CloseVolume(); + } + + internal void BackupPartition(string PartitionName, string Path) + { + BackupPartition(PartitionName, Path, null, null, null); + } + + internal void BackupPartition(string PartitionName, string Path, Action ProgressUpdateCallback) + { + BackupPartition(PartitionName, Path, null, ProgressUpdateCallback, null); + } + + internal void BackupPartition(string PartitionName, string Path, ProgressUpdater UpdaterPerSector) + { + BackupPartition(PartitionName, Path, null, null, UpdaterPerSector); + } + + internal void BackupPartition(string PartitionName, Stream OutputStream) + { + BackupPartition(PartitionName, null, OutputStream, null, null); + } + + internal void BackupPartition(string PartitionName, Stream OutputStream, Action ProgressUpdateCallback) + { + BackupPartition(PartitionName, null, OutputStream, ProgressUpdateCallback, null); + } + + internal void BackupPartition(string PartitionName, Stream OutputStream, ProgressUpdater UpdaterPerSector) + { + BackupPartition(PartitionName, null, OutputStream, null, UpdaterPerSector); + } + + private void BackupPartition(string PartitionName, string Path, Stream OutputStream, Action ProgressUpdateCallback, ProgressUpdater UpdaterPerSector) + { + bool VolumeWasOpen = IsVolumeOpen(); + if (!VolumeWasOpen) + OpenVolume(false); + + SetSectorPosition(1); + byte[] GPTBuffer = ReadSectors(1, 33); + GPT GPT = new GPT(GPTBuffer); + Partition Partition = GPT.Partitions.Where((p) => p.Name == PartitionName).First(); + + DumpSectors(Partition.FirstSector, Partition.LastSector - Partition.FirstSector + 1, Path, OutputStream, ProgressUpdateCallback, UpdaterPerSector); + + if (!VolumeWasOpen) + CloseVolume(); + } + + internal void WriteSectors(UInt64 StartSector, byte[] Buffer) + { + bool Result = true; + + bool VolumeWasOpen = IsVolumeOpen(); + if (!VolumeWasOpen) + OpenVolume(true); + + SetSectorPosition(StartSector); + + uint count = 1; + Result = NativeMethods.WriteFile(hVolume, Buffer, (uint)Buffer.Length, out count, IntPtr.Zero); + + if (!VolumeWasOpen) + CloseVolume(); + + if (!Result) + throw new Exception("Failed to write sectors."); + } + + internal void WriteSectors(byte[] Buffer, uint Length = uint.MaxValue) + { + if (!IsVolumeOpen()) + throw new Exception("Volume is not opened"); + + uint count = 1; + bool Result = NativeMethods.WriteFile(hVolume, Buffer, (Length < Buffer.Length ? Length : (uint)Buffer.Length), out count, IntPtr.Zero); + if (!Result) + throw new Exception("Failed to write sectors. Exception: 0x" + Marshal.GetLastWin32Error().ToString("X8")); + } + + internal void WriteSectors(UInt64 StartSector, string Path) + { + WriteSectors(StartSector, Path, null, null); + } + + internal void WriteSectors(UInt64 StartSector, string Path, Action ProgressUpdateCallback) + { + WriteSectors(StartSector, Path, ProgressUpdateCallback, null); + } + + internal void WriteSectors(UInt64 StartSector, string Path, ProgressUpdater UpdaterPerSector) + { + WriteSectors(StartSector, Path, null, UpdaterPerSector); + } + + private void WriteSectors(UInt64 StartSector, string Path, Action ProgressUpdateCallback, ProgressUpdater UpdaterPerSector) + { + bool VolumeWasOpen = IsVolumeOpen(); + if (!VolumeWasOpen) + OpenVolume(true); + + SetSectorPosition(StartSector); + + byte[] Buffer; + + using (BinaryReader Reader = new BinaryReader(File.Open(Path, FileMode.Open))) + { + ProgressUpdater Progress = UpdaterPerSector; + if ((Progress == null) && (ProgressUpdateCallback != null)) + Progress = new ProgressUpdater((UInt64)(Reader.BaseStream.Length / 0x200), ProgressUpdateCallback); + + if (Reader.BaseStream.Length >= 0x10000) + Buffer = new byte[0x10000]; + else + Buffer = new byte[Reader.BaseStream.Length]; + + int Count; + for (UInt64 i = 0; i < (UInt64)(Reader.BaseStream.Length / 0x200); i += 0x80) + { + Count = Reader.Read(Buffer, 0, Buffer.Length); + + WriteSectors(Buffer, (uint)Count); + + if (Progress != null) + Progress.IncreaseProgress((ulong)Count / 0x200); + } + } + + if (!VolumeWasOpen) + CloseVolume(); + } + + internal void RestorePartition(string Path, string PartitionName) + { + RestorePartition(Path, PartitionName, null, null); + } + + internal void RestorePartition(string Path, string PartitionName, Action ProgressUpdateCallback) + { + RestorePartition(Path, PartitionName, ProgressUpdateCallback, null); + } + + internal void RestorePartition(string Path, string PartitionName, ProgressUpdater UpdaterPerSector) + { + RestorePartition(Path, PartitionName, null, UpdaterPerSector); + } + + private void RestorePartition(string Path, string PartitionName, Action ProgressUpdateCallback, ProgressUpdater UpdaterPerSector) + { + bool VolumeWasOpen = IsVolumeOpen(); + if (!VolumeWasOpen) + OpenVolume(true); + + SetSectorPosition(1); + byte[] GPTBuffer = ReadSectors(1, 33); + GPT GPT = new GPT(GPTBuffer); + Partition Partition = GPT.Partitions.Where((p) => p.Name == PartitionName).First(); + ulong PartitionSize = (Partition.LastSector - Partition.FirstSector + 1) * 0x200; + ulong FileSize = (ulong)new FileInfo(Path).Length; + if (FileSize > PartitionSize) + throw new InvalidOperationException("Partition can not be restored, because its size is too big!"); + + WriteSectors(Partition.FirstSector, Path, ProgressUpdateCallback); + + if (!VolumeWasOpen) + CloseVolume(); + } + } +} diff --git a/Models/NativeMethods.cs b/Models/NativeMethods.cs new file mode 100644 index 0000000..3fa5c8e --- /dev/null +++ b/Models/NativeMethods.cs @@ -0,0 +1,315 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.ConstrainedExecution; +using Microsoft.Win32.SafeHandles; +using System.Security; + +namespace WPinternals +{ + [Flags] + internal enum TokenAccessLevels + { + AssignPrimary = 0x00000001, + Duplicate = 0x00000002, + Impersonate = 0x00000004, + Query = 0x00000008, + QuerySource = 0x00000010, + AdjustPrivileges = 0x00000020, + AdjustGroups = 0x00000040, + AdjustDefault = 0x00000080, + AdjustSessionId = 0x00000100, + + Read = 0x00020000 | Query, + + Write = 0x00020000 | AdjustPrivileges | AdjustGroups | AdjustDefault, + + AllAccess = 0x000F0000 | + AssignPrimary | + Duplicate | + Impersonate | + Query | + QuerySource | + AdjustPrivileges | + AdjustGroups | + AdjustDefault | + AdjustSessionId, + + MaximumAllowed = 0x02000000 + } + + internal enum SecurityImpersonationLevel + { + Anonymous = 0, + Identification = 1, + Impersonation = 2, + Delegation = 3, + } + + internal enum TokenType + { + Primary = 1, + Impersonation = 2, + } + + internal enum EMoveMethod : uint + { + Begin = 0, + Current = 1, + End = 2 + } + + internal sealed class NativeMethods + { + internal const uint SE_PRIVILEGE_DISABLED = 0x00000000; + internal const uint SE_PRIVILEGE_ENABLED = 0x00000002; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct LUID + { + internal uint LowPart; + internal uint HighPart; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct LUID_AND_ATTRIBUTES + { + internal LUID Luid; + internal uint Attributes; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct TOKEN_PRIVILEGE + { + internal uint PrivilegeCount; + internal LUID_AND_ATTRIBUTES Privilege; + } + + internal const string ADVAPI32 = "advapi32.dll"; + internal const string KERNEL32 = "kernel32.dll"; + + internal const int ERROR_SUCCESS = 0x0; + internal const int ERROR_ACCESS_DENIED = 0x5; + internal const int ERROR_NOT_ENOUGH_MEMORY = 0x8; + internal const int ERROR_NO_TOKEN = 0x3f0; + internal const int ERROR_NOT_ALL_ASSIGNED = 0x514; + internal const int ERROR_NO_SUCH_PRIVILEGE = 0x521; + internal const int ERROR_CANT_OPEN_ANONYMOUS = 0x543; + + [DllImport( + KERNEL32, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern bool CloseHandle(IntPtr handle); + + [DllImport( + ADVAPI32, + CharSet = CharSet.Unicode, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern bool AdjustTokenPrivileges( + [In] SafeTokenHandle TokenHandle, + [In] bool DisableAllPrivileges, + [In] ref TOKEN_PRIVILEGE NewState, + [In] uint BufferLength, + [In, Out] ref TOKEN_PRIVILEGE PreviousState, + [In, Out] ref uint ReturnLength); + + [DllImport( + ADVAPI32, + CharSet = CharSet.Auto, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern + bool RevertToSelf(); + + [DllImport( + ADVAPI32, + EntryPoint = "LookupPrivilegeValueW", + CharSet = CharSet.Auto, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern + bool LookupPrivilegeValue( + [In] string lpSystemName, + [In] string lpName, + [In, Out] ref LUID Luid); + + [DllImport( + KERNEL32, + CharSet = CharSet.Auto, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern + IntPtr GetCurrentProcess(); + + [DllImport( + KERNEL32, + CharSet = CharSet.Auto, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern + IntPtr GetCurrentThread(); + + [DllImport( + ADVAPI32, + CharSet = CharSet.Unicode, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern + bool OpenProcessToken( + [In] IntPtr ProcessToken, + [In] TokenAccessLevels DesiredAccess, + [In, Out] ref SafeTokenHandle TokenHandle); + + [DllImport + (ADVAPI32, + CharSet = CharSet.Unicode, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern + bool OpenThreadToken( + [In] IntPtr ThreadToken, + [In] TokenAccessLevels DesiredAccess, + [In] bool OpenAsSelf, + [In, Out] ref SafeTokenHandle TokenHandle); + + [DllImport + (ADVAPI32, + CharSet = CharSet.Unicode, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern + bool DuplicateTokenEx( + [In] SafeTokenHandle ExistingToken, + [In] TokenAccessLevels DesiredAccess, + [In] IntPtr TokenAttributes, + [In] SecurityImpersonationLevel ImpersonationLevel, + [In] TokenType TokenType, + [In, Out] ref SafeTokenHandle NewToken); + + [DllImport + (ADVAPI32, + CharSet = CharSet.Unicode, + SetLastError = true)] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + internal static extern + bool SetThreadToken( + [In] IntPtr Thread, + [In] SafeTokenHandle Token); + + internal const uint FILE_SHARE_READ = 0x00000001; + internal const uint FILE_SHARE_WRITE = 0x00000002; + internal const uint FILE_SHARE_DELETE = 0x00000004; + internal const uint OPEN_EXISTING = 3; + + internal const uint GENERIC_READ = (0x80000000); + internal const uint GENERIC_WRITE = (0x40000000); + + internal const uint FILE_FLAG_WRITE_THROUGH = 0x80000000; + internal const uint FILE_FLAG_NO_BUFFERING = 0x20000000; + internal const uint FILE_READ_ATTRIBUTES = (0x0080); + internal const uint FILE_WRITE_ATTRIBUTES = 0x0100; + internal const uint ERROR_INSUFFICIENT_BUFFER = 122; + internal const uint FILE_BEGIN = 0; + internal const uint FSCTL_LOCK_VOLUME = 0x00090018; + internal const uint FSCTL_DISMOUNT_VOLUME = 0x00090020; + internal const uint FSCTL_UNLOCK_VOLUME = 0x00090022; + internal const uint IOCTL_STORAGE_EJECT_MEDIA = 0x2D4808; + internal const uint IOCTL_STORAGE_LOAD_MEDIA = 0x2D480C; + + internal const Int32 INVALID_HANDLE_VALUE = -1; + internal const Int32 FILE_ATTRIBUTE_NORMAL = 1; + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool WriteFile(IntPtr hFile, byte[] lpBuffer, uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten, IntPtr lpOverlapped); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool ReadFile(IntPtr hFile, [Out] byte[] lpBuffer, uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped); + + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern uint SetFilePointer( + [In] IntPtr hFile, + [In] int lDistanceToMove, + [In, Out] ref int lpDistanceToMoveHigh, + [In] EMoveMethod dwMoveMethod); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern IntPtr CreateFile( + string lpFileName, + uint dwDesiredAccess, + uint dwShareMode, + IntPtr lpSecurityAttributes, + uint dwCreationDisposition, + uint dwFlagsAndAttributes, + IntPtr hTemplateFile); + + [DllImport("Kernel32.dll", SetLastError = false, CharSet = CharSet.Auto)] + public static extern bool DeviceIoControl( + IntPtr hDevice, + uint IoControlCode, + [MarshalAs(UnmanagedType.AsAny)] + [In] object InBuffer, + uint nInBufferSize, + [MarshalAs(UnmanagedType.AsAny)] + [Out] object OutBuffer, + uint nOutBufferSize, + ref uint pBytesReturned, + IntPtr Overlapped + ); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool FlushFileBuffers(IntPtr hFile); + + static NativeMethods() + { + } + } + + internal sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private SafeTokenHandle() : base(true) { } + + // 0 is an Invalid Handle + internal SafeTokenHandle(IntPtr handle) + : base(true) + { + SetHandle(handle); + } + + internal static SafeTokenHandle InvalidHandle + { + get { return new SafeTokenHandle(IntPtr.Zero); } + } + + [DllImport(NativeMethods.KERNEL32, SetLastError = true), + SuppressUnmanagedCodeSecurity, + ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + private static extern bool CloseHandle(IntPtr handle); + + override protected bool ReleaseHandle() + { + return CloseHandle(handle); + } + } +} diff --git a/Models/NokiaFlashModel.cs b/Models/NokiaFlashModel.cs new file mode 100644 index 0000000..27cb539 --- /dev/null +++ b/Models/NokiaFlashModel.cs @@ -0,0 +1,1150 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Linq; + +namespace WPinternals +{ + internal enum FfuProtocol + { + ProtocolSyncV1 = 1, + ProtocolAsyncV1 = 2, + ProtocolSyncV2 = 4, + ProtocolAsyncV2 = 8, + ProtocolAsyncV3 = 16 + } + + internal enum FlashOptions : byte + { + SkipWrite = 1, + SkipHashCheck = 2, + SkipIdCheck = 4, + SkipSignatureCheck = 8 + } + + internal delegate void InterfaceChangedHandler(PhoneInterfaces NewInterface); + + internal class NokiaFlashModel : NokiaPhoneModel + { + private UefiSecurityStatusResponse _SecurityStatus = null; + private PhoneInfo Info = new PhoneInfo(); + + internal event InterfaceChangedHandler InterfaceChanged = delegate { }; + + public NokiaFlashModel(string DevicePath) : base(DevicePath) { } + + public byte[] ReadParam(string Param) + { + byte[] Request = new byte[0x0B]; + const string Header = "NOKXFR"; + + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Param), 0, Request, 7, Param.Length); + + byte[] Response = ExecuteRawMethod(Request); + if ((Response == null) || (Response.Length < 0x10)) return null; + + byte[] Result = new byte[Response[0x10]]; + System.Buffer.BlockCopy(Response, 0x11, Result, 0, Response[0x10]); + return Result; + } + + public string ReadStringParam(string Param) + { + byte[] Bytes = ReadParam(Param); + if (Bytes == null) return null; + return System.Text.ASCIIEncoding.ASCII.GetString(Bytes).Trim(new char[] { '\0' }); + } + + public uint? ReadSecurityFlags() + { + byte[] Response = ReadParam("FCS"); + if ((Response == null) || (Response.Length != 4)) return null; + + // This value is in big endian + return (UInt32)((Response[0] << 24) | (Response[1] << 16) | (Response[2] << 8) | Response[3]); + } + + public UefiSecurityStatusResponse ReadSecurityStatus() + { + if (_SecurityStatus != null) + return _SecurityStatus; + + byte[] Response = ReadParam("SS"); + if (Response == null) return null; + + UefiSecurityStatusResponse Result = new UefiSecurityStatusResponse(); + + Result.IsTestDevice = Response[0]; + Result.PlatformSecureBootStatus = Convert.ToBoolean(Response[1]); + Result.SecureFfuEfuseStatus = Convert.ToBoolean(Response[2]); + Result.DebugStatus = Convert.ToBoolean(Response[3]); + Result.RdcStatus = Convert.ToBoolean(Response[4]); + Result.AuthenticationStatus = Convert.ToBoolean(Response[5]); + Result.UefiSecureBootStatus = Convert.ToBoolean(Response[6]); + Result.CryptoHardwareKey = Convert.ToBoolean(Response[7]); + + _SecurityStatus = Result; + + return Result; + } + + internal bool IsBootLoaderUnlocked() + { + UefiSecurityStatusResponse SecurityStatus = ReadSecurityStatus(); + return (SecurityStatus.AuthenticationStatus || SecurityStatus.RdcStatus || !SecurityStatus.SecureFfuEfuseStatus); + } + + public TerminalResponse GetTerminalResponse() + { + byte[] AsskMask = new byte[0x10] { 1, 0, 16, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64 }; + byte[] Request = new byte[0xAC]; + string Header = "NOKXFT"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + Request[7] = 1; + System.Buffer.BlockCopy(BigEndian.GetBytes(0x18, 4), 0, Request, 0x08, 4); // Subblocktype = 0x18 + System.Buffer.BlockCopy(BigEndian.GetBytes(0x9C, 4), 0, Request, 0x0C, 4); // Subblocklength = 0x9C + System.Buffer.BlockCopy(BigEndian.GetBytes(0x00, 4), 0, Request, 0x10, 4); // AsicIndex = 0x00 + System.Buffer.BlockCopy(AsskMask, 0, Request, 0x14, 0x10); + byte[] TerminalResponse = ExecuteRawMethod(Request); + if ((TerminalResponse != null) && (TerminalResponse.Length > 0x20) && (BigEndian.ToUInt32(TerminalResponse, 0x14) == (TerminalResponse.Length - 0x18)) && (BitConverter.ToUInt32(TerminalResponse, 0x1C) == (TerminalResponse.Length - 0x20))) + { + // Parse Terminal Response from offset 0x18 + TerminalResponse ParsedResponse = Terminal.Parse(TerminalResponse, 0x18); + return ParsedResponse; + } + else return null; + } + + public void SendFfuHeaderV1(byte[] FfuHeader, byte Options = 0) + { + byte[] Request = new byte[FfuHeader.Length + 0x20]; + + string Header = "NOKXFS"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + System.Buffer.BlockCopy(BigEndian.GetBytes(0x0001, 2), 0, Request, 0x06, 2); // Protocol version = 0x0001 + Request[0x08] = 0; // Progress = 0% + Request[0x0B] = 1; // Subblock count = 1 + System.Buffer.BlockCopy(BigEndian.GetBytes(0x0000000B, 4), 0, Request, 0x0C, 4); // Subblock type for header = 0x0B + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuHeader.Length + 0x0C, 4), 0, Request, 0x10, 4); // Subblock length = length of header + 0x0C + System.Buffer.BlockCopy(BigEndian.GetBytes(0x00000000, 4), 0, Request, 0x14, 4); // Header type = 0 + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuHeader.Length, 4), 0, Request, 0x18, 4); // Payload length = length of header + Request[0x1C] = Options; // Header options = 0 + + Buffer.BlockCopy(FfuHeader, 0, Request, 0x20, FfuHeader.Length); + + byte[] Response = ExecuteRawMethod(Request); + if (Response == null) + throw new BadConnectionException(); + int ResultCode = (Response[6] << 8) + Response[7]; + if (ResultCode != 0) + ThrowFlashError(ResultCode); + } + + public void SendFfuHeaderV2(UInt32 TotalHeaderLength, UInt32 OffsetForThisPart, byte[] FfuHeader, byte Options = 0) + { + byte[] Request = new byte[FfuHeader.Length + 0x3C]; + + string Header = "NOKXFS"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + System.Buffer.BlockCopy(BigEndian.GetBytes((int)FfuProtocol.ProtocolSyncV2, 2), 0, Request, 0x06, 2); // Protocol version = 0x0001 + Request[0x08] = 0; // Progress = 0% + Request[0x0B] = 1; // Subblock count = 1 + + System.Buffer.BlockCopy(BigEndian.GetBytes(0x00000021, 4), 0, Request, 0x0C, 4); // Subblock type for header v2 = 0x21 + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuHeader.Length + 0x28, 4), 0, Request, 0x10, 4); // Subblock starts at 0x14, payload starts at 0x3C. + + System.Buffer.BlockCopy(BigEndian.GetBytes(0x00000000, 4), 0, Request, 0x14, 4); // Header type = 0 + System.Buffer.BlockCopy(BigEndian.GetBytes(TotalHeaderLength, 4), 0, Request, 0x18, 4); // Payload length = length of header + Request[0x1C] = Options; // Header options = 0 + + System.Buffer.BlockCopy(BigEndian.GetBytes(OffsetForThisPart, 4), 0, Request, 0x1D, 4); + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuHeader.Length, 4), 0, Request, 0x21, 4); + Request[0x25] = 0; // No Erase + + Buffer.BlockCopy(FfuHeader, 0, Request, 0x3C, FfuHeader.Length); + + byte[] Response = ExecuteRawMethod(Request); + if (Response == null) + throw new BadConnectionException(); + if (Response.Length == 4) + throw new WPinternalsException("Flash protocol v2 not supported"); + int ResultCode = (Response[6] << 8) + Response[7]; + if (ResultCode != 0) + ThrowFlashError(ResultCode); + } + + public void SendFfuPayloadV1(byte[] FfuChunk, int Progress = 0, byte Options = 0) + { + byte[] Request = new byte[FfuChunk.Length + 0x1C]; + + string Header = "NOKXFS"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + System.Buffer.BlockCopy(BigEndian.GetBytes((int)FfuProtocol.ProtocolSyncV1, 2), 0, Request, 0x06, 2); // Protocol version = 0x0001 + Request[0x08] = (byte)Progress; // Progress = 0% (0 - 100) + Request[0x0B] = 1; // Subblock count = 1 + + System.Buffer.BlockCopy(BigEndian.GetBytes(0x0000000C, 4), 0, Request, 0x0C, 4); // Subblock type for ChunkData = 0x0C + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuChunk.Length + 0x08, 4), 0, Request, 0x10, 4); // Subblock length = length of chunk + 0x08 + + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuChunk.Length, 4), 0, Request, 0x14, 4); // Payload length = length of chunk + Request[0x18] = Options; // Data options = 0 (1 = verify) + + Buffer.BlockCopy(FfuChunk, 0, Request, 0x1C, FfuChunk.Length); + + byte[] Response = ExecuteRawMethod(Request); + if (Response == null) + throw new BadConnectionException(); + int ResultCode = (Response[6] << 8) + Response[7]; + if (ResultCode != 0) + ThrowFlashError(ResultCode); + } + + public void SendFfuPayloadV2(byte[] FfuChunk, int Progress = 0, byte Options = 0) + { + byte[] Request = new byte[FfuChunk.Length + 0x20]; + + string Header = "NOKXFS"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + System.Buffer.BlockCopy(BigEndian.GetBytes((int)FfuProtocol.ProtocolSyncV2, 2), 0, Request, 0x06, 2); // Protocol + Request[0x08] = (byte)Progress; // Progress = 0% (0 - 100) + Request[0x0B] = 1; // Subblock count = 1 + + System.Buffer.BlockCopy(BigEndian.GetBytes(0x0000001B, 4), 0, Request, 0x0C, 4); // Subblock type for Payload v2 = 0x1B + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuChunk.Length + 0x0C, 4), 0, Request, 0x10, 4); // Subblock length = length of chunk + 0x08 + + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuChunk.Length, 4), 0, Request, 0x14, 4); // Payload length = length of chunk + Request[0x18] = Options; // Data options = 0 (1 = verify) + + Buffer.BlockCopy(FfuChunk, 0, Request, 0x20, FfuChunk.Length); + + byte[] Response = ExecuteRawMethod(Request); + if (Response == null) + throw new BadConnectionException(); + int ResultCode = (Response[6] << 8) + Response[7]; + if (ResultCode != 0) + ThrowFlashError(ResultCode); + } + + public void SendFfuPayloadV3(byte[] FfuChunk, UInt32 WriteDescriptorIndex, UInt32 CRC, int Progress = 0, byte Options = 0) + { + byte[] Request = new byte[FfuChunk.Length + 0x20]; + + string Header = "NOKXFS"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + System.Buffer.BlockCopy(BigEndian.GetBytes((int)FfuProtocol.ProtocolAsyncV3, 2), 0, Request, 0x06, 2); // Protocol + Request[0x08] = (byte)Progress; // Progress = 0% (0 - 100) + Request[0x0B] = 1; // Subblock count = 1 + + System.Buffer.BlockCopy(BigEndian.GetBytes(0x0000001D, 4), 0, Request, 0x0C, 4); // Subblock type for Payload v2 = 0x1B + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuChunk.Length + 0x2C, 4), 0, Request, 0x10, 4); // Subblock length = length of chunk + 0x08 + + System.Buffer.BlockCopy(BigEndian.GetBytes(FfuChunk.Length, 4), 0, Request, 0x14, 4); // Payload length = length of chunk + Request[0x18] = Options; // Data options = 0 (1 = verify) + System.Buffer.BlockCopy(BigEndian.GetBytes(WriteDescriptorIndex, 4), 0, Request, 0x19, 4); // Payload length = length of chunk + System.Buffer.BlockCopy(BigEndian.GetBytes(CRC, 4), 0, Request, 0x1D, 4); // Payload length = length of chunk + + Buffer.BlockCopy(FfuChunk, 0, Request, 0x40, FfuChunk.Length); + + byte[] Response = ExecuteRawMethod(Request); + if (Response == null) + throw new BadConnectionException(); + int ResultCode = (Response[6] << 8) + Response[7]; + if (ResultCode != 0) + ThrowFlashError(ResultCode); + } + + public void BackupPartitionToRam(string PartitionName) + { + PartitionName = PartitionName.ToUpper(); + if (new string[] { "MODEM_FSG", "MODEM_FS1", "MODEM_FS2", "SSD", "DPP" }.Any(s => s == PartitionName)) + { + byte[] Request = new byte[84]; + string Header = "NOKXFB"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + Request[0x07] = 1; // Subblock count = 1 + Request[0x08] = 6; // Subblock ID = 6 = Create Partition Backup to RAM + System.Buffer.BlockCopy(BigEndian.GetBytes(73, 2), 0, Request, 0x09, 2); // Subblock length = Length of data in subblock including fillers (subblock-ID-field and subblock-length-field are not counted for the subblock-length) + System.Text.UnicodeEncoding.Unicode.GetBytes(PartitionName); + + byte[] PartitionBytes = System.Text.Encoding.Unicode.GetBytes(PartitionName); + Buffer.BlockCopy(PartitionBytes, 0, Request, 0x0C, PartitionBytes.Length); + Request[0x0C + PartitionBytes.Length + 0x00] = 0; // Trailing zero + Request[0x0C + PartitionBytes.Length + 0x01] = 0; + + byte[] Response = ExecuteRawMethod(Request); + int ResultCode = (Response[6] << 8) + Response[7]; + if (ResultCode != 0) + ThrowFlashError(ResultCode); + } + else + throw new WPinternalsException("Specified partition cannot be backupped to RAM"); + } + + public void LoadMmosBinary(UInt32 TotalLength, UInt32 Offset, bool SkipMmosSupportCheck, byte[] MmosPart) + { + byte[] Request = new byte[MmosPart.Length + 0x20]; + string Header = "NOKXFL"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + Request[0x07] = 1; // Subblock count = 1 + System.Buffer.BlockCopy(BigEndian.GetBytes(0x0000001E, 4), 0, Request, 0x08, 4); // Subblock ID = Load MMOS Binary = 0x1E + System.Buffer.BlockCopy(BigEndian.GetBytes(MmosPart.Length + 0x10, 4), 0, Request, 0x0C, 4); // Subblock length = Payload-length + 0x10 + System.Buffer.BlockCopy(BigEndian.GetBytes(TotalLength, 4), 0, Request, 0x10, 4); + System.Buffer.BlockCopy(BigEndian.GetBytes(Offset, 4), 0, Request, 0x14, 4); + if (SkipMmosSupportCheck) + Request[0x18] = 1; + System.Buffer.BlockCopy(BigEndian.GetBytes(MmosPart.Length, 4), 0, Request, 0x1C, 4); + Buffer.BlockCopy(MmosPart, 0, Request, 0x20, MmosPart.Length); + + byte[] Response = ExecuteRawMethod(Request); + int ResultCode = (Response[6] << 8) + Response[7]; + if (ResultCode != 0) + ThrowFlashError(ResultCode); + } + + private void ThrowFlashError(int ErrorCode) + { + string SubMessage; + + switch (ErrorCode) + { + case 0x0008: SubMessage = "Unsupported protocol / Invalid options"; break; + case 0x000F: SubMessage = "Invalid sub block count"; break; + case 0x0010: SubMessage = "Invalid sub block length"; break; + case 0x0012: SubMessage = "Authentication required"; break; + case 0x000E: SubMessage = "Invalid sub block type"; break; + case 0x0013: SubMessage = "Failed async message"; break; + case 0x1000: SubMessage = "Invalid header type"; break; + case 0x1001: SubMessage = "FFU header contain unknown extra data"; break; + case 0x0001: SubMessage = "Couldn't allocate memory"; break; + case 0x1106: SubMessage = "Security header validation failed"; break; + case 0x1105: SubMessage = "Invalid hash table size"; break; + case 0x1104: SubMessage = "Invalid catalog size"; break; + case 0x1103: SubMessage = "Invalid chunk size"; break; + case 0x1102: SubMessage = "Unsupported algorithm"; break; + case 0x1101: SubMessage = "Invalid struct size"; break; + case 0x1100: SubMessage = "Invalid signature"; break; + case 0x1202: SubMessage = "Invalid struct size"; break; + case 0x1203: SubMessage = "Unsupported algorithm"; break; + case 0x1204: SubMessage = "Invalid chunk size"; break; + case 0x1005: SubMessage = "Data not aligned correctly"; break; + case 0x0009: SubMessage = "Locate protocol failed"; break; + case 0x1003: SubMessage = "Hash mismatch"; break; + case 0x1006: SubMessage = "Couldn't find hash from security header for index"; break; + case 0x1004: SubMessage = "Security header import missing / All FFU headers have not been imported"; break; + case 0x1304: SubMessage = "Invalid platform ID"; break; + case 0x1307: SubMessage = "Invalid write descriptor info"; break; + case 0x1306: SubMessage = "Invalid write descriptor info"; break; + case 0x1305: SubMessage = "Invalid block size"; break; + case 0x1303: SubMessage = "Unsupported FFU version"; break; + case 0x1302: SubMessage = "Unsupported struct version"; break; + case 0x1301: SubMessage = "Invalid update type"; break; + case 0x100B: SubMessage = "Too much payload data, all data has already been written"; break; + case 0x1008: SubMessage = "Internal error"; break; + case 0x1007: SubMessage = "Payload data does not contain all data"; break; + case 0x0004: SubMessage = "Flash write failed"; break; + case 0x000D: SubMessage = "Flash verify failed"; break; + case 0x0002: SubMessage = "Flash read failed"; break; + default: SubMessage = "Unknown error"; break; + } + + WPinternalsException Ex = new WPinternalsException("Flash failed!"); + Ex.SubMessage = "Error 0x" + ErrorCode.ToString("X4") + ": " + SubMessage; + + throw Ex; + } + + public void FlashFFU(string FFUPath, bool ResetAfterwards = true, byte Options = 0) + { + FlashFFU(new FFU(FFUPath), ResetAfterwards, Options); + } + + public void FlashFFU(FFU FFU, bool ResetAfterwards = true, byte Options = 0) + { + FlashFFU(FFU, null, ResetAfterwards, Options); + } + + public void FlashFFU(FFU FFU, ProgressUpdater UpdaterPerChunk, bool ResetAfterwards = true, byte Options = 0) + { + LogFile.BeginAction("FlashFFU"); + + ProgressUpdater Progress = UpdaterPerChunk; + + PhoneInfo Info = ReadPhoneInfo(); + if ((Info.SecureFfuSupportedProtocolMask & ((ushort)FfuProtocol.ProtocolSyncV1 | (ushort)FfuProtocol.ProtocolSyncV2)) == 0) + throw new WPinternalsException("Flash failed!", "Protocols not supported"); + + UInt64 CombinedFFUHeaderSize = FFU.HeaderSize; + byte[] FfuHeader = new byte[CombinedFFUHeaderSize]; + using (System.IO.FileStream FfuFile = new System.IO.FileStream(FFU.Path, System.IO.FileMode.Open, System.IO.FileAccess.Read)) + { + FfuFile.Read(FfuHeader, 0, (int)CombinedFFUHeaderSize); + SendFfuHeaderV1(FfuHeader, Options); + + UInt64 Position = CombinedFFUHeaderSize; + byte[] Payload; + int ChunkCount = 0; + + if ((Info.SecureFfuSupportedProtocolMask & (ushort)FfuProtocol.ProtocolSyncV2) == 0) + { + // Protocol v1 + Payload = new byte[FFU.ChunkSize]; + + while (Position < (UInt64)FfuFile.Length) + { + FfuFile.Read(Payload, 0, Payload.Length); + ChunkCount++; + SendFfuPayloadV1(Payload, (int)((double)ChunkCount * 100 / FFU.TotalChunkCount), 0); + Position += (ulong)Payload.Length; + + if (Progress != null) + Progress.IncreaseProgress(1); + } + } + else + { + // Protocol v2 + Payload = new byte[Info.WriteBufferSize]; + + while (Position < (UInt64)FfuFile.Length) + { + UInt32 PayloadSize = Info.WriteBufferSize; + if (((UInt64)FfuFile.Length - Position) < PayloadSize) + { + PayloadSize = (UInt32)((UInt64)FfuFile.Length - Position); + Payload = new byte[PayloadSize]; + } + + FfuFile.Read(Payload, 0, (int)PayloadSize); + ChunkCount += (int)(PayloadSize / FFU.ChunkSize); + SendFfuPayloadV2(Payload, (int)((double)ChunkCount * 100 / FFU.TotalChunkCount), 0); + Position += PayloadSize; + + if (Progress != null) + Progress.IncreaseProgress((ulong)(PayloadSize / FFU.ChunkSize)); + } + } + } + + if (ResetAfterwards) + ResetPhone(); + + LogFile.EndAction("FlashFFU"); + } + + public void FlashSectors(UInt32 StartSector, byte[] Data, int Progress = 0) + { + // Start sector is in UInt32, so max size of eMMC is 2 TB. + + byte[] Request = new byte[Data.Length + 0x40]; + + string Header = "NOKF"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + Request[0x05] = 0; // Device type = 0 + System.Buffer.BlockCopy(BigEndian.GetBytes(StartSector, 4), 0, Request, 0x0B, 4); // Start sector + System.Buffer.BlockCopy(BigEndian.GetBytes(Data.Length / 0x200, 4), 0, Request, 0x0F, 4); // Sector count + Request[0x13] = (byte)Progress; // Progress (0 - 100) + Request[0x18] = 0; // Do Verify + Request[0x19] = 0; // Is Test + + Buffer.BlockCopy(Data, 0, Request, 0x40, Data.Length); + + ExecuteRawMethod(Request); + } + + public void DisableRebootTimeOut() + { + byte[] Request = new byte[4]; + string Header = "NOKD"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + ExecuteRawMethod(Request); + } + + public void Shutdown() + { + byte[] Request = new byte[4]; + string Header = "NOKZ"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + ExecuteRawMethod(Request); + } + + public FlashVersion GetFlashVersion() + { + byte[] Response = ReadParam("FAI"); + if ((Response == null) || (Response.Length < 6)) return null; + + FlashVersion Result = new FlashVersion(); + + Result.ProtocolMajor = Response[1]; + Result.ProtocolMinor = Response[2]; + Result.ApplicationMajor = Response[3]; + Result.ApplicationMinor = Response[4]; + + return Result; + } + + internal GPT ReadGPT() + { + // If this function is used with a locked BootMgr v1, + // then the mode-switching should be done outside this function, + // because the context-switches that are used here are not supported on BootMgr v1. + + // Only works in BootLoader-mode or on unlocked bootloaders in Flash-mode!! + + PhoneInfo Info = ReadPhoneInfo(ExtendedInfo: false); + FlashAppType OriginalAppType = Info.App; + bool Switch = ((Info.App != FlashAppType.BootManager) && Info.SecureFfuEnabled && !Info.Authenticated && !Info.RdcPresent); + if (Switch) + SwitchToBootManagerContext(); + + byte[] Request = new byte[0x04]; + const string Header = "NOKT"; + + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + + byte[] Buffer = ExecuteRawMethod(Request); + if ((Buffer == null) || (Buffer.Length < 0x4408)) + throw new InvalidOperationException("Unable to read GPT!"); + + UInt16 Error = (UInt16)((Buffer[6] << 8) + Buffer[7]); + if (Error > 0) + throw new NotSupportedException("ReadGPT: Error 0x" + Error.ToString("X4")); + + byte[] GPTBuffer = new byte[Buffer.Length - 0x208]; + System.Buffer.BlockCopy(Buffer, 0x208, GPTBuffer, 0, 0x4200); + + if (Switch) + { + if (OriginalAppType == FlashAppType.FlashApp) + SwitchToFlashAppContext(); + else + SwitchToPhoneInfoAppContext(); + } + + return new GPT(GPTBuffer); // NOKT message header and MBR are ignored + } + + internal void WriteGPT(GPT NewGPT) + { + if (!IsBootLoaderUnlocked()) + throw new InvalidOperationException("Bootloader is not unlocked!"); + + byte[] Buffer = NewGPT.Rebuild(); + + UInt32? HeaderOffset = ByteOperations.FindAscii(Buffer, "EFI PART"); + if (HeaderOffset != 0) + throw new BadImageFormatException(); + + FlashSectors(1, Buffer); + } + + internal void FlashRawPartition(string Path, string PartitionName) + { + FlashRawPartition(Path, null, PartitionName, null, null); + } + + internal void FlashRawPartition(string Path, string PartitionName, Action ProgressUpdateCallback) + { + FlashRawPartition(Path, null, PartitionName, ProgressUpdateCallback, null); + } + + internal void FlashRawPartition(string Path, string PartitionName, ProgressUpdater UpdaterPerSector) + { + FlashRawPartition(Path, null, PartitionName, null, UpdaterPerSector); + } + + internal void FlashRawPartition(Stream Stream, string PartitionName, ProgressUpdater UpdaterPerSector) + { + FlashRawPartition(null, Stream, PartitionName, null, UpdaterPerSector); + } + + internal void FlashRawPartition(byte[] Buffer, string PartitionName, ProgressUpdater UpdaterPerSector) + { + FlashRawPartition(null, new MemoryStream(Buffer, false), PartitionName, null, UpdaterPerSector); + } + + private void FlashRawPartition(string Path, Stream Stream, string PartitionName, Action ProgressUpdateCallback, ProgressUpdater UpdaterPerSector) + { + if (!IsBootLoaderUnlocked()) + throw new InvalidOperationException("Bootloader is not unlocked!"); + + GPT GPT = ReadGPT(); + + Partition Partition = GPT.Partitions.Where((p) => p.Name == PartitionName).First(); + ulong PartitionSize = (Partition.LastSector - Partition.FirstSector + 1) * 0x200; + + Stream InputStream = null; + + if (Path != null) + InputStream = new DecompressedStream(File.Open(Path, FileMode.Open)); + else if (Stream != null) + { + if (Stream is DecompressedStream) + InputStream = Stream; + else + InputStream = new DecompressedStream(Stream); + } + + if (InputStream != null) + { + using (InputStream) + { + UInt64? InputStreamLength = null; + try + { + InputStreamLength = (UInt64)InputStream.Length; + } + catch { } + + if ((InputStreamLength != null) && ((UInt64)InputStream.Length > PartitionSize)) + throw new InvalidOperationException("Partition can not be flashed, because its size is too big!"); + + ProgressUpdater Progress = UpdaterPerSector; + if ((Progress == null) && (ProgressUpdateCallback != null) && (InputStreamLength != null)) + Progress = new ProgressUpdater((UInt64)(InputStreamLength / 0x200), ProgressUpdateCallback); + int ProgressPercentage = 0; + + const int FlashBufferSize = 0x200000; // Flash 8 GB phone -> buffersize 0x200000 = 11:45 min, buffersize 0x20000 = 12:30 min + byte[] FlashBuffer = new byte[FlashBufferSize]; + int BytesRead; + UInt64 i = 0; + do + { + BytesRead = InputStream.Read(FlashBuffer, 0, FlashBufferSize); + + byte[] FlashBufferFinalSize; + if (BytesRead > 0) + { + if (BytesRead == FlashBufferSize) + FlashBufferFinalSize = FlashBuffer; + else + { + FlashBufferFinalSize = new byte[BytesRead]; + Buffer.BlockCopy(FlashBuffer, 0, FlashBufferFinalSize, 0, BytesRead); + } + + FlashSectors((UInt32)(Partition.FirstSector + (i / 0x200)), FlashBufferFinalSize, ProgressPercentage); + } + + if (Progress != null) + { + Progress.IncreaseProgress((UInt64)FlashBuffer.Length / 0x200); + ProgressPercentage = Progress.ProgressPercentage; + } + + i += FlashBufferSize; + } + while (BytesRead == FlashBufferSize); + } + } + } + + internal void ErasePartition(string PartitionName) + { + // Partition "Data" can always be erased. + // Other partitions can only be erased when a valid RDC certificate is present or full SX authentication was performed. + if (PartitionName.Length > 0x23) + throw new ArgumentException("PartitionName cannot exceed 0x23 chars!"); + + byte[] Request = new byte[0x50]; + string Header = "NOKXFP"; + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + Request[0x06] = 1; // Protocol version must be 1 + Request[0x07] = 0; // Device type = 0 + + byte[] PartitionBytes = System.Text.Encoding.Unicode.GetBytes(PartitionName); + Buffer.BlockCopy(PartitionBytes, 0, Request, 8, PartitionBytes.Length); + Request[0x08 + PartitionBytes.Length + 0x00] = 0; // Trailing zero + Request[0x08 + PartitionBytes.Length + 0x01] = 0; + + ExecuteRawMethod(Request); + } + + internal FlashAppType GetFlashAppType() + { + byte[] Request; + + Request = new byte[4]; + ByteOperations.WriteAsciiString(Request, 0, "NOKV"); + byte[] Response = ExecuteRawMethod(Request); + if ((Response == null) || (ByteOperations.ReadAsciiString(Response, 0, 4) == "NOKU")) + throw new NotSupportedException(); + return (FlashAppType)Response[5]; + } + + internal PhoneInfo ReadPhoneInfo(bool ExtendedInfo = true) + { + // NOKH = Get Phone Info (IMEI and info from Product.dat) - Not available on some phones, like Lumia 640. + // NOKV = Info Query + + bool PhoneInfoLogged = Info.State != PhoneInfoState.Empty; + PhoneInfo Result = Info; + + if (Result.State == PhoneInfoState.Empty) + { + byte[] Request; + Request = new byte[4]; + ByteOperations.WriteAsciiString(Request, 0, "NOKV"); + byte[] Response = ExecuteRawMethod(Request); + if ((Response != null) && (ByteOperations.ReadAsciiString(Response, 0, 4) != "NOKU")) + { + Result.App = (FlashAppType)Response[5]; + + switch (Result.App) + { + case FlashAppType.BootManager: + Result.BootManagerProtocolVersionMajor = Response[6]; + Result.BootManagerProtocolVersionMinor = Response[7]; + Result.BootManagerVersionMajor = Response[8]; + Result.BootManagerVersionMinor = Response[9]; + break; + case FlashAppType.FlashApp: + Result.FlashAppProtocolVersionMajor = Response[6]; + Result.FlashAppProtocolVersionMinor = Response[7]; + Result.FlashAppVersionMajor = Response[8]; + Result.FlashAppVersionMinor = Response[9]; + break; + case FlashAppType.PhoneInfoApp: + Result.PhoneInfoAppProtocolVersionMajor = Response[6]; + Result.PhoneInfoAppProtocolVersionMinor = Response[7]; + Result.PhoneInfoAppVersionMajor = Response[8]; + Result.PhoneInfoAppVersionMinor = Response[9]; + break; + } + + byte SubblockCount = Response[10]; + int SubblockOffset = 11; + + for (int i = 0; i < SubblockCount; i++) + { + byte SubblockID = Response[SubblockOffset + 0x00]; + UInt16 SubblockLength = BigEndian.ToUInt16(Response, SubblockOffset + 0x01); + int SubblockPayloadOffset = SubblockOffset + 3; + byte SubblockVersion; + switch (SubblockID) + { + case 0x01: + Result.TransferSize = BigEndian.ToUInt32(Response, SubblockPayloadOffset); + break; + case 0x1F: + Result.MmosOverUsbSupported = (Response[SubblockPayloadOffset] == 1); + break; + case 0x04: + if (Result.App == FlashAppType.BootManager) + { + Result.FlashAppProtocolVersionMajor = Response[SubblockPayloadOffset + 0x00]; + Result.FlashAppProtocolVersionMinor = Response[SubblockPayloadOffset + 0x01]; + Result.FlashAppVersionMajor = Response[SubblockPayloadOffset + 0x02]; + Result.FlashAppVersionMinor = Response[SubblockPayloadOffset + 0x03]; + } + else if (Result.App == FlashAppType.FlashApp) + { + Result.SdCardSizeInSectors = BigEndian.ToUInt32(Response, SubblockPayloadOffset); + } + break; + case 0x02: + Result.WriteBufferSize = BigEndian.ToUInt32(Response, SubblockPayloadOffset); + break; + case 0x03: + Result.EmmcSizeInSectors = BigEndian.ToUInt32(Response, SubblockPayloadOffset); + break; + case 0x05: + Result.PlatformID = ByteOperations.ReadAsciiString(Response, (uint)SubblockPayloadOffset, (uint)SubblockLength).Trim(new char[] { ' ', '\0' }); + break; + case 0x0D: + Result.AsyncSupport = (Response[SubblockPayloadOffset + 1] == 1); + break; + case 0x0F: + SubblockVersion = Response[SubblockPayloadOffset]; // 0x03 + Result.PlatformSecureBootEnabled = (Response[SubblockPayloadOffset + 0x01] == 0x01); + Result.SecureFfuEnabled = (Response[SubblockPayloadOffset + 0x02] == 0x01); + Result.JtagDisabled = (Response[SubblockPayloadOffset + 0x03] == 0x01); + Result.RdcPresent = (Response[SubblockPayloadOffset + 0x04] == 0x01); + Result.Authenticated = ((Response[SubblockPayloadOffset + 0x05] == 0x01) || (Response[SubblockPayloadOffset + 0x05] == 0x02)); + Result.UefiSecureBootEnabled = (Response[SubblockPayloadOffset + 0x06] == 0x01); + Result.SecondaryHardwareKeyPresent = (Response[SubblockPayloadOffset + 0x07] == 0x01); + break; + case 0x10: + SubblockVersion = Response[SubblockPayloadOffset]; // 0x01 + Result.SecureFfuSupportedProtocolMask = BigEndian.ToUInt16(Response, SubblockPayloadOffset + 0x01); + break; + case 0x20: + // CRC header info + break; + } + SubblockOffset += SubblockLength + 3; + } + } + + Result.State = PhoneInfoState.Basic; + } + + if ((ExtendedInfo) && (Result.State == PhoneInfoState.Basic)) + { + FlashAppType OriginalType = Result.App; + + try + { + SwitchToPhoneInfoAppContext(); // May throw NotSupportedException + + Result.Type = ReadPhoneInfoVariable("TYPE"); + Result.ProductCode = ReadPhoneInfoVariable("CTR"); + Result.Imei = ReadPhoneInfoVariable("IMEI"); + + SwitchToFlashAppContext(); + DisableRebootTimeOut(); + } + catch { } + + if (Result.App == FlashAppType.FlashApp) + { + Result.Firmware = ReadStringParam("FVER"); + Result.RKH = ReadParam("RRKH"); + } + + try + { + if (OriginalType == FlashAppType.BootManager) + { + SwitchToBootManagerContext(); + } + } + catch { } + + Result.State = PhoneInfoState.Extended; + } + + if (!PhoneInfoLogged) + Result.Log(LogType.FileOnly); + + return Result; + } + + internal void ResetPhone() + { + LogFile.Log("Rebooting phone", LogType.FileAndConsole); + try + { + byte[] Request = new byte[4]; + ByteOperations.WriteAsciiString(Request, 0, "NOKR"); + ExecuteRawVoidMethod(Request); + } + catch + { + LogFile.Log("Sending reset-request failed", LogType.FileOnly); + LogFile.Log("Assuming automatic reset already in progress", LogType.FileOnly); + } + } + + internal void ContinueBoot() + { + LogFile.Log("Continue boot..."); + byte[] Request = new byte[4]; + ByteOperations.WriteAsciiString(Request, 0, "NOKA"); + ExecuteRawVoidMethod(Request); + } + + internal void ResetPhoneToFlashMode() + { + // This only works when the phone is in BootMgr mode. If it is already in FlashApp, it will not reboot. It only makes the phone unresponsive. + LogFile.Log("Rebooting phone to Flash mode..."); + byte[] Request = new byte[4]; + ByteOperations.WriteAsciiString(Request, 0, "NOKS"); + ExecuteRawVoidMethod(Request); + } + + internal void Hello() + { + byte[] Request = new byte[4]; + ByteOperations.WriteAsciiString(Request, 0, "NOKI"); + byte[] Response = ExecuteRawMethod(Request); + if (Response == null) + throw new BadConnectionException(); + if (ByteOperations.ReadAsciiString(Response, 0, 4) != "NOKI") + throw new WPinternalsException("Bad response from phone!"); + } + + internal UInt16 ReadSecureFfuSupportedProtocolMask() + { + return BigEndian.ToUInt16(ReadParam("SFPI"), 0); + } + + internal void SwitchToPhoneInfoAppContext() + { + byte[] Request = new byte[7]; + ByteOperations.WriteAsciiString(Request, 0, "NOKXCBP"); + byte[] Response = ExecuteRawMethod(Request); + if (ByteOperations.ReadAsciiString(Response, 0, 4) == "NOKU") + throw new NotSupportedException(); + UInt16 Error = (UInt16)((Response[6] << 8) + Response[7]); + if (Error > 0) + throw new NotSupportedException("SwitchToPhoneInfoAppContext: Error 0x" + Error.ToString("X4")); + DisableRebootTimeOut(); + Info.App = FlashAppType.PhoneInfoApp; + InterfaceChanged(PhoneInterfaces.Lumia_Flash); + } + + internal void SwitchToFlashAppContext() + { + // SwitchToFlashAppContext() should only be used with BootMgr v2 + // For switching from BootMgr to FlashApp, it will use NOKS + // That will switch to a charging state, whereas a normal context switch will not start charging + // The implementation of NOKS in BootMgr mode has changed in BootMgr v2 + // It does not disconnect / reconnect anymore and the apptype is changed immediately + // NOKS still doesnt return a status + + byte[] Request; + + if (Info.State == PhoneInfoState.Empty) + ReadPhoneInfo(ExtendedInfo: false); + + if (Info.App == FlashAppType.BootManager) + { + if (Info.FlashAppProtocolVersionMajor < 2) + { + // A phone with Bootloader Spec A cannot be switched from BootMgr to FlashApp. + // NOKS will make the phone unresponsive and let you wait for a new arrival, but that would require a PhoneNotifier and that is not available in this context. + // And NOKXCBF is not supported at all. + return; + } + + Request = new byte[4]; + ByteOperations.WriteAsciiString(Request, 0, "NOKS"); // This will let the phone charge + ExecuteRawVoidMethod(Request); // On phone with bootloader Spec A this triggers a reboot, so DisableRebootTimeOut() cannot be called immediately. + } + else if (Info.App == FlashAppType.PhoneInfoApp) + { + Request = new byte[7]; + ByteOperations.WriteAsciiString(Request, 0, "NOKXCBF"); // This will stop charging the phone + byte[] Response = ExecuteRawMethod(Request); + if (ByteOperations.ReadAsciiString(Response, 0, 4) == "NOKU") + throw new NotSupportedException(); + UInt16 Error = (UInt16)((Response[6] << 8) + Response[7]); + if (Error > 0) + throw new NotSupportedException("SwitchToFlashAppContext: Error 0x" + Error.ToString("X4")); + } + + DisableRebootTimeOut(); + + Info.App = FlashAppType.FlashApp; + + // If current Info class was retrieved while in BootMgr mode, then we need to invalidate this data, because it is incomplete. + if (Info.PlatformID == null) + Info.State = PhoneInfoState.Empty; + + InterfaceChanged(PhoneInterfaces.Lumia_Flash); + } + + internal void SwitchToBootManagerContext(bool DisableTimeOut = true) + { + byte[] Request = new byte[7]; + ByteOperations.WriteAsciiString(Request, 0, "NOKXCBB"); + byte[] Response = ExecuteRawMethod(Request); + if (ByteOperations.ReadAsciiString(Response, 0, 4) == "NOKU") + throw new NotSupportedException(); + UInt16 Error = (UInt16)((Response[6] << 8) + Response[7]); + if (Error > 0) + throw new NotSupportedException("SwitchToBootManagerContext: Error 0x" + Error.ToString("X4")); + if (DisableTimeOut) + DisableRebootTimeOut(); + Info.App = FlashAppType.BootManager; + InterfaceChanged(PhoneInterfaces.Lumia_Bootloader); + } + + internal string ReadPhoneInfoVariable(string VariableName) + { + // This function assumes the phone is in Phone Info App context + + byte[] Request = new byte[16]; + ByteOperations.WriteAsciiString(Request, 0, "NOKXPH" + VariableName + "\0"); // BTR or CTR, CTR is public ProductCode + byte[] Response = ExecuteRawMethod(Request); + UInt16 Length = BigEndian.ToUInt16(Response, 6); + string Result = ByteOperations.ReadAsciiString(Response, 8, Length).Trim(new char[] { ' ', '\0' }); + return Result; + } + + internal string ReadProductCode() + { + SwitchToPhoneInfoAppContext(); + string Result = ReadPhoneInfoVariable("CTR"); + SwitchToFlashAppContext(); + return Result; + } + + internal void StartAsyncFlash() + { + byte[] Request = new byte[14]; + ByteOperations.WriteAsciiString(Request, 0, "NOKXFFS"); + Request[8] = 1; // Protocol version must be 1 + Request[9] = 0; // Protocol type must be 0 + ExecuteRawMethod(Request); + } + + internal void EndAsyncFlash() + { + byte[] Request = new byte[7]; + ByteOperations.WriteAsciiString(Request, 0, "NOKXFFE"); + ExecuteRawMethod(Request); + } + + internal enum SecureBootKeyType: byte + { + Retail = 0, + Engineering = 1 + } + + internal void ProvisionSecureBootKeys(SecureBootKeyType KeyType) // Only for Flashmode, not BootManager mode. + { + byte[] Request = new byte[8]; + ByteOperations.WriteAsciiString(Request, 0, "NOKXFK"); + Request[6] = 0; // Options + Request[7] = (byte)KeyType; + byte[] Response = ExecuteRawMethod(Request); + UInt32 Status = ByteOperations.ReadUInt32(Response, 6); + if (Status != 0) + ThrowFlashError((int)Status); + } + } + + internal enum FlashAppType + { + BootManager = 1, + FlashApp = 2, + PhoneInfoApp = 3 + }; + + internal enum PhoneInfoState + { + Empty, + Basic, + Extended + }; + + internal class PhoneInfo + { + public PhoneInfoState State = PhoneInfoState.Empty; + + public string Type; // Extended info + public string ProductCode; // Extended info + public string Imei; // Extended info + public string Firmware; // Extended info + public byte[] RKH; // Extended info + + public FlashAppType App; + + public byte FlashAppVersionMajor; + public byte FlashAppVersionMinor; + public byte FlashAppProtocolVersionMajor; + public byte FlashAppProtocolVersionMinor; + + public byte BootManagerVersionMajor; + public byte BootManagerVersionMinor; + public byte BootManagerProtocolVersionMajor; + public byte BootManagerProtocolVersionMinor; + + public byte PhoneInfoAppVersionMajor; + public byte PhoneInfoAppVersionMinor; + public byte PhoneInfoAppProtocolVersionMajor; + public byte PhoneInfoAppProtocolVersionMinor; + + public UInt32 TransferSize; + public bool MmosOverUsbSupported; + public UInt32 SdCardSizeInSectors; + public UInt32 WriteBufferSize; + public UInt32 EmmcSizeInSectors; + public string PlatformID; + public UInt16 SecureFfuSupportedProtocolMask; + public bool AsyncSupport; + + public bool PlatformSecureBootEnabled; + public bool SecureFfuEnabled; + public bool JtagDisabled; + public bool RdcPresent; + public bool Authenticated; + public bool UefiSecureBootEnabled; + public bool SecondaryHardwareKeyPresent; + + internal void Log(LogType Type) + { + if (State == PhoneInfoState.Extended) + { + if (this.Type != null) + LogFile.Log("Phone type: " + this.Type, Type); + if (ProductCode != null) + LogFile.Log("Product code: " + ProductCode, Type); + if (RKH != null) + LogFile.Log("Root key hash: " + Converter.ConvertHexToString(RKH, ""), Type); + if (Firmware.Length > 0) + LogFile.Log("Firmware version: " + Firmware, Type); + if (!(Type == LogType.ConsoleOnly) && (Imei != null)) + LogFile.Log("IMEI: " + Imei, LogType.FileOnly); + } + + switch (App) + { + case FlashAppType.BootManager: + LogFile.Log("Bootmanager: " + BootManagerVersionMajor + "." + BootManagerVersionMinor, Type); + LogFile.Log("Bootmanager protocol: " + BootManagerProtocolVersionMajor + "." + BootManagerProtocolVersionMinor, Type); + LogFile.Log("Flash app: " + FlashAppVersionMajor + "." + FlashAppVersionMinor, Type); + LogFile.Log("Flash protocol: " + FlashAppProtocolVersionMajor + "." + FlashAppProtocolVersionMinor, Type); + break; + case FlashAppType.FlashApp: + LogFile.Log("Flash app: " + FlashAppVersionMajor + "." + FlashAppVersionMinor, Type); + LogFile.Log("Flash protocol: " + FlashAppProtocolVersionMajor + "." + FlashAppProtocolVersionMinor, Type); + break; + case FlashAppType.PhoneInfoApp: + LogFile.Log("Phone info app: " + PhoneInfoAppVersionMajor + "." + PhoneInfoAppVersionMinor, Type); + LogFile.Log("Phone info protocol: " + PhoneInfoAppProtocolVersionMajor + "." + PhoneInfoAppProtocolVersionMinor, Type); + break; + } + + LogFile.Log("SecureBoot: " + ((!PlatformSecureBootEnabled || !UefiSecureBootEnabled) ? "Disabled" : "Enabled"), Type); + + if ((Type == LogType.ConsoleOnly) || (Type == LogType.FileAndConsole)) + LogFile.Log("Flash app security: " + ((!SecureFfuEnabled || RdcPresent || Authenticated) ? "Disabled" : "Enabled"), LogType.ConsoleOnly); + if ((Type == LogType.FileOnly) || (Type == LogType.FileAndConsole)) + LogFile.Log("Flash app security: " + ((!SecureFfuEnabled || RdcPresent || Authenticated) ? "Disabled" : "Enabled") + " (FFU security: " + (SecureFfuEnabled ? "Enabled" : "Disabled") + ", RDC: " + (RdcPresent ? "Present" : "Not found") + ", Authenticated: " + (Authenticated ? "True" : "False") + ")", LogType.FileOnly); + + LogFile.Log("JTAG: " + (JtagDisabled ? "Disabled" : "Enabled"), Type); + } + } + + internal class UefiSecurityStatusResponse + { + public byte IsTestDevice; + public bool PlatformSecureBootStatus; + public bool SecureFfuEfuseStatus; + public bool DebugStatus; + public bool RdcStatus; + public bool AuthenticationStatus; + public bool UefiSecureBootStatus; + public bool CryptoHardwareKey; + } + + internal class FlashVersion + { + public int ApplicationMajor; + public int ApplicationMinor; + public int ProtocolMajor; + public int ProtocolMinor; + } +} diff --git a/Models/NokiaPhoneModel.cs b/Models/NokiaPhoneModel.cs new file mode 100644 index 0000000..48435e9 --- /dev/null +++ b/Models/NokiaPhoneModel.cs @@ -0,0 +1,347 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using MadWizard.WinUSBNet; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace WPinternals +{ + internal class NokiaPhoneModel: IDisposable + { + protected bool Disposed = false; + private USBDevice Device = null; + private int MessageId = 0; + private object UsbLock = new object(); + + public NokiaPhoneModel(string DevicePath) + { + // Mass Storage device is not WinUSB + try + { + Device = new USBDevice(DevicePath); + } + catch { } + } + + private JToken ExecuteJsonMethodAsJsonToken(string JsonMethod, Dictionary Params, string ResultElement) + { + byte[] Buffer; + int Length; + + lock (UsbLock) + { + string jsonrpc = "2.0"; + int id = MessageId++; + string method = JsonMethod; + Dictionary @params = new Dictionary(); + if (Params != null) + { + foreach (KeyValuePair Param in Params) + { + if (Param.Value is byte[]) + @params.Add(Param.Key, ((byte[])Param.Value).Select(b => (int)b).ToArray()); // convert to int-array + else + @params.Add(Param.Key, Param.Value); + } + } + @params.Add("MessageVersion", 0); + string Request = JsonConvert.SerializeObject(new { jsonrpc, id, method, @params }); + Device.OutputPipe.Write(System.Text.Encoding.ASCII.GetBytes(Request)); + + Buffer = new byte[0x10000]; + Length = Device.InputPipe.Read(Buffer); + } + + Newtonsoft.Json.Linq.JObject ResultMessage = Newtonsoft.Json.Linq.JObject.Parse(System.Text.ASCIIEncoding.ASCII.GetString(Buffer, 0, Length)); + + JToken ResultToken = ResultMessage.Root.SelectToken("result"); + if ((ResultToken == null) || (ResultElement == null)) return null; + return ResultToken.SelectToken(ResultElement); + } + + public void ExecuteJsonMethod(string JsonMethod, Dictionary Params) + { + JToken Token = ExecuteJsonMethodAsJsonToken(JsonMethod, Params, null); + } + + public string ExecuteJsonMethodAsString(string JsonMethod, Dictionary Params, string ResultElement) + { + JToken Token = ExecuteJsonMethodAsJsonToken(JsonMethod, Params, ResultElement); + if (Token == null) return null; + return Token.Value(); + } + + public string ExecuteJsonMethodAsString(string JsonMethod, string ResultElement) + { + JToken Token = ExecuteJsonMethodAsJsonToken(JsonMethod, null, ResultElement); + if (Token == null) return null; + return Token.Value(); + } + + public int ExecuteJsonMethodAsInteger(string JsonMethod, Dictionary Params, string ResultElement) + { + JToken Token = ExecuteJsonMethodAsJsonToken(JsonMethod, Params, ResultElement); + if (Token == null) return 0; + return Token.Value(); + } + + public int ExecuteJsonMethodAsInteger(string JsonMethod, string ResultElement) + { + JToken Token = ExecuteJsonMethodAsJsonToken(JsonMethod, null, ResultElement); + if (Token == null) return 0; + return Token.Value(); + } + + public byte[] ExecuteJsonMethodAsBytes(string JsonMethod, Dictionary Params, string ResultElement) + { + JToken Token = ExecuteJsonMethodAsJsonToken(JsonMethod, Params, ResultElement); + if (Token == null) return null; + return Token.Values().ToArray(); + } + + public byte[] ExecuteJsonMethodAsBytes(string JsonMethod, string ResultElement) + { + JToken Token = ExecuteJsonMethodAsJsonToken(JsonMethod, null, ResultElement); + if (Token == null) return null; + return Token.Values().ToArray(); + } + + public bool? ExecuteJsonMethodAsBoolean(string JsonMethod, string ResultElement) + { + JToken Token = ExecuteJsonMethodAsJsonToken(JsonMethod, null, ResultElement); + if (Token == null) return null; + return Token.Value(); + } + + public void ExecuteJsonMethodAsync(string JsonMethod, Dictionary Params) + { + lock (UsbLock) + { + string jsonrpc = "2.0"; + int id = MessageId++; + string method = JsonMethod; + Dictionary @params = new Dictionary(); + if (Params != null) + { + foreach (KeyValuePair Param in Params) + { + if (Param.Value is byte[]) + @params.Add(Param.Key, ((byte[])Param.Value).Select(b => (int)b).ToArray()); // convert to int-array + else + @params.Add(Param.Key, Param.Value); + } + } + @params.Add("MessageVersion", 0); + string Request = JsonConvert.SerializeObject(new { jsonrpc, id, method, @params }); + + byte[] OutBuffer = System.Text.Encoding.ASCII.GetBytes(Request); + Device.OutputPipe.BeginWrite(OutBuffer, 0, OutBuffer.Length, (AsyncResultWrite) => + { + Device.OutputPipe.EndWrite(AsyncResultWrite); + }, null); + } + } + + public void ExecuteJsonMethodAsync(string JsonMethod) + { + ExecuteJsonMethod(JsonMethod, null); + } + + public delegate void JsonMethodCallbackString(object State, string Result); + + public void ExecuteJsonMethodAsStringAsync(string JsonMethod, Dictionary Params, string ResultElement, object State, JsonMethodCallbackString Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, Params, ResultElement, State, (ReturnState, Token) => { Callback(ReturnState, Token.Value()); }); + } + + public void ExecuteJsonMethodAsStringAsync(string JsonMethod, string ResultElement, object State, JsonMethodCallbackString Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, null, ResultElement, State, (ReturnState, Token) => { Callback(ReturnState, Token.Value()); }); + } + + public delegate void JsonMethodCallbackBoolean(object State, bool Result); + + public void ExecuteJsonMethodAsBooleanAsync(string JsonMethod, Dictionary Params, string ResultElement, object State, JsonMethodCallbackBoolean Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, Params, ResultElement, State, (ReturnState, Token) => { Callback(ReturnState, Token.Value()); }); + } + + public void ExecuteJsonMethodAsBooleanAsync(string JsonMethod, string ResultElement, object State, JsonMethodCallbackBoolean Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, null, ResultElement, State, (ReturnState, Token) => { Callback(ReturnState, Token.Value()); }); + } + + public delegate void JsonMethodCallbackBytes(object State, byte[] Result); + + public void ExecuteJsonMethodAsBytesAsync(string JsonMethod, Dictionary Params, string ResultElement, object State, JsonMethodCallbackBytes Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, Params, ResultElement, State, (ReturnState, Token) => { Callback(ReturnState, Token.Values().ToArray()); }); + } + + public void ExecuteJsonMethodAsBytesAsync(string JsonMethod, string ResultElement, object State, JsonMethodCallbackBytes Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, null, ResultElement, State, (ReturnState, Token) => { Callback(ReturnState, Token.Values().ToArray()); }); + } + + public delegate void JsonMethodCallbackInteger(object State, int Result); + + public void ExecuteJsonMethodAsIntegerAsync(string JsonMethod, Dictionary Params, string ResultElement, object State, JsonMethodCallbackInteger Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, Params, ResultElement, State, (ReturnState, Token) => { Callback(ReturnState, Token.Value()); }); + } + + public void ExecuteJsonMethodAsIntegerAsync(string JsonMethod, string ResultElement, object State, JsonMethodCallbackInteger Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, null, ResultElement, State, (ReturnState, Token) => { Callback(ReturnState, Token.Value()); }); + } + + public delegate void JsonMethodCallbackToken(object State, JToken Result); + + public void ExecuteJsonMethodAsTokenAsync(string JsonMethod, Dictionary Params, string ResultElement, object State, JsonMethodCallbackToken Callback) + { + byte[] Buffer; + int Length; + + lock (UsbLock) + { + string jsonrpc = "2.0"; + int id = MessageId++; + string method = JsonMethod; + Dictionary @params = new Dictionary(); + if (Params != null) + { + foreach (KeyValuePair Param in Params) + { + if (Param.Value is byte[]) + @params.Add(Param.Key, ((byte[])Param.Value).Select(b => (int)b).ToArray()); // convert to int-array + else + @params.Add(Param.Key, Param.Value); + } + } + @params.Add("MessageVersion", 0); + string Request = JsonConvert.SerializeObject(new { jsonrpc, id, method, @params }); + + byte[] OutBuffer = System.Text.Encoding.ASCII.GetBytes(Request); + Device.OutputPipe.BeginWrite(OutBuffer, 0, OutBuffer.Length, (AsyncResultWrite) => + { + Device.OutputPipe.EndWrite(AsyncResultWrite); + Buffer = new byte[0x10000]; + Device.InputPipe.BeginRead(Buffer, 0, 0x10000, (AsyncResultRead) => + { + Length = Device.InputPipe.EndRead(AsyncResultRead); + + Newtonsoft.Json.Linq.JObject ResultMessage = Newtonsoft.Json.Linq.JObject.Parse(System.Text.ASCIIEncoding.ASCII.GetString(Buffer, 0, Length)); + + JToken ResultToken = ResultMessage.Root.SelectToken("result"); + if ((ResultToken == null) || (ResultElement == null)) Callback(AsyncResultRead.AsyncState, null); + Callback(AsyncResultRead.AsyncState, ResultToken.SelectToken(ResultElement)); + }, AsyncResultWrite.AsyncState); + }, State); + } + } + + public void ExecuteJsonMethodAsTokenAsync(string JsonMethod, string ResultElement, object State, JsonMethodCallbackToken Callback) + { + ExecuteJsonMethodAsTokenAsync(JsonMethod, null, ResultElement, State, Callback); + } + + public byte[] ExecuteRawMethod(byte[] RawMethod) + { + return ExecuteRawMethod(RawMethod, RawMethod.Length); + } + + public byte[] ExecuteRawMethod(byte[] RawMethod, int Length) + { + byte[] Buffer = new byte[0x8000]; // Should be at least 0x4408 for receiving the GPT packet. + byte[] Result = null; + lock (UsbLock) + { + Device.OutputPipe.Write(RawMethod, 0, Length); + try + { + int OutputLength = Device.InputPipe.Read(Buffer); + Result = new byte[OutputLength]; + System.Buffer.BlockCopy(Buffer, 0, Result, 0, OutputLength); + } + catch { } // Reboot command looses connection + } + return Result; + } + + public void ExecuteRawVoidMethod(byte[] RawMethod) + { + ExecuteRawVoidMethod(RawMethod, RawMethod.Length); + } + + public void ExecuteRawVoidMethod(byte[] RawMethod, int Length) + { + lock (UsbLock) + { + Device.OutputPipe.Write(RawMethod, 0, Length); + } + } + + /// + /// Disposes the UsbDevice including all unmanaged WinUSB handles. This function + /// should be called when the UsbDevice object is no longer in use, otherwise + /// unmanaged handles will remain open until the garbage collector finalizes the + /// object. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Finalizer for the UsbDevice. Disposes all unmanaged handles. + /// + ~NokiaPhoneModel() + { + Dispose(false); + } + + /// + /// Disposes the object + /// + /// Indicates wether Dispose was called manually (true) or by + /// the garbage collector (false) via the destructor. + protected virtual void Dispose(bool disposing) + { + if (Disposed) + return; + + if (disposing) + { + if (Device != null) + Device.Dispose(); + } + + // Clean unmanaged resources here. + // (none currently) + + Disposed = true; + } + } +} diff --git a/Models/PatchEngine.cs b/Models/PatchEngine.cs new file mode 100644 index 0000000..290f115 --- /dev/null +++ b/Models/PatchEngine.cs @@ -0,0 +1,597 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.AccessControl; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Xml; +using System.Xml.Serialization; + +namespace WPinternals +{ + internal class PatchEngine + { + internal List PatchDefinitions = new List(); + internal readonly List TargetRedirections = new List(); + + internal PatchEngine() { } + + internal PatchEngine(string PatchDefinitionsXmlString) + { + XmlSerializer x = new XmlSerializer(PatchDefinitions.GetType(), null, new Type[] { }, new XmlRootAttribute("PatchDefinitions"), ""); + MemoryStream s = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(PatchDefinitionsXmlString)); + PatchDefinitions = (List)x.Deserialize(s); + } + + internal void WriteDefinitions(string FilePath) + { + XmlSerializer x = new XmlSerializer(PatchDefinitions.GetType(), null, new Type[] { }, new XmlRootAttribute("PatchDefinitions"), ""); + + XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); + ns.Add("", ""); + + System.IO.StreamWriter FileWriter = new System.IO.StreamWriter(FilePath); + XmlWriter XmlWriter = XmlWriter.Create(FileWriter, new XmlWriterSettings() { OmitXmlDeclaration = true, Indent = true, NewLineHandling = NewLineHandling.Entitize }); + + FileWriter.WriteLine(""); + FileWriter.WriteLine(""); + FileWriter.WriteLine(""); + FileWriter.WriteLine(""); + + x.Serialize(XmlWriter, PatchDefinitions, ns); + + FileWriter.Close(); + } + + private string _TargetPath = null; + internal string TargetPath + { + get + { + return _TargetPath; + } + set + { + _TargetPath = value.TrimEnd(new char[] { '\\' }); + } + } + + + private DiscUtils.DiscFileSystem _TargetImage = null; + internal DiscUtils.DiscFileSystem TargetImage + { + get + { + return _TargetImage; + } + set + { + _TargetImage = value; + _TargetPath = ""; + } + } + + internal bool Patch(string PatchDefinition) + { + bool Result = false; + List LoadedFiles = new List(); + + LogFile.Log("Attempt patch: " + PatchDefinition); + + // Find a matching TargetVersion + PatchDefinition Definition = PatchDefinitions.Single(d => string.Compare(d.Name, PatchDefinition, true) == 0); + TargetVersion MatchedVersion = null; + int VersionIndex = 0; + foreach (TargetVersion CurrentVersion in Definition.TargetVersions) + { + bool Match = true; + int FileIndex = 0; + + foreach (TargetFile CurrentTargetFile in CurrentVersion.TargetFiles) + { + // Determine target path + string TargetPath = null; + foreach (TargetRedirection CurrentRedirection in TargetRedirections) + { + if (CurrentTargetFile.Path.StartsWith(CurrentRedirection.RelativePath, StringComparison.OrdinalIgnoreCase)) + { + TargetPath = Path.Combine(CurrentRedirection.TargetPath + "\\", CurrentTargetFile.Path); + break; + } + } + if (TargetPath == null) + TargetPath = Path.Combine(this.TargetPath + "\\", CurrentTargetFile.Path); + + // Lookup file + FilePatcher CurrentFile = LoadedFiles.SingleOrDefault(f => string.Compare(f.FilePath, TargetPath, true) == 0); + if (CurrentFile == null) + { + if ((TargetImage != null) && (!TargetPath.Contains(':'))) + CurrentFile = new FilePatcher(TargetPath, TargetImage.OpenFile(TargetPath, FileMode.Open, FileAccess.ReadWrite)); + else + CurrentFile = new FilePatcher(TargetPath); + LoadedFiles.Add(CurrentFile); + } + + // Compare hash + if (!StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentTargetFile.HashOriginal) && + !StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentTargetFile.HashPatched)) + { + Match = false; + + foreach (TargetFile CurrentObsoleteFile in CurrentTargetFile.Obsolete) + { + if (StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentObsoleteFile.HashPatched)) + { + Match = true; // Found match after all. File is patched with an obsolete version of this patch. + break; + } + } + + if (!Match) + { + LogFile.Log("Pattern: " + VersionIndex.ToString() + ", " + FileIndex.ToString()); + break; + } + } + + FileIndex++; + } + + if (Match) + { + MatchedVersion = CurrentVersion; + break; + } + + VersionIndex++; + } + + if (MatchedVersion != null) + { + LogFile.Log("Apply: " + MatchedVersion.Description); + + foreach (TargetFile CurrentTargetFile in MatchedVersion.TargetFiles) + { + FilePatcher CurrentFile = LoadedFiles.SingleOrDefault(f => string.Compare(f.FilePath, Path.Combine(TargetPath + "\\", CurrentTargetFile.Path), true) == 0); + + if (!StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentTargetFile.HashPatched)) + { + CurrentFile.StartPatching(); + + if (!StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentTargetFile.HashOriginal)) + { + // File is already patched, but with an older version of this patch. + // First unpatch back to original. + + foreach (TargetFile CurrentObsoleteFile in CurrentTargetFile.Obsolete) + { + if (StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentObsoleteFile.HashPatched)) + { + foreach (Patch CurrentPatch in CurrentObsoleteFile.Patches) + { + CurrentFile.ApplyPatch(CurrentPatch.Address, CurrentPatch.OriginalBytes); + } + break; + } + } + } + + foreach (Patch CurrentPatch in CurrentTargetFile.Patches) + { + CurrentFile.ApplyPatch(CurrentPatch.Address, CurrentPatch.PatchedBytes); + } + + CurrentFile.FinishPatching(); + } + } + + Result = true; + } + + return Result; + } + + internal void Restore(string PatchDefinition) + { + List LoadedFiles = new List(); + + try + { + // Find a matching TargetVersion + PatchDefinition Definition = PatchDefinitions.Single(d => string.Compare(d.Name, PatchDefinition, true) == 0); + TargetVersion MatchedVersion = null; + foreach (TargetVersion CurrentVersion in Definition.TargetVersions) + { + bool Match = true; + + foreach (TargetFile CurrentTargetFile in CurrentVersion.TargetFiles) + { + // Determine target path + string TargetPath = null; + foreach (TargetRedirection CurrentRedirection in TargetRedirections) + { + if (CurrentTargetFile.Path.StartsWith(CurrentRedirection.RelativePath, StringComparison.OrdinalIgnoreCase)) + { + TargetPath = Path.Combine(CurrentRedirection.TargetPath, CurrentTargetFile.Path); + break; + } + } + if (TargetPath == null) + TargetPath = Path.Combine(this.TargetPath, CurrentTargetFile.Path); + + // Lookup file + FilePatcher CurrentFile = LoadedFiles.SingleOrDefault(f => string.Compare(f.FilePath, TargetPath, true) == 0); + if (CurrentFile == null) + { + CurrentFile = new FilePatcher(TargetPath); + LoadedFiles.Add(CurrentFile); + } + + // Compare hash + if (!StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentTargetFile.HashOriginal) && + !StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentTargetFile.HashPatched)) + { + Match = false; + + foreach (TargetFile CurrentObsoleteFile in CurrentTargetFile.Obsolete) + { + if (StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentObsoleteFile.HashPatched)) + { + Match = true; // Found match after all. File is patched with an obsolete version of this patch. + break; + } + } + + if (!Match) + break; + } + } + + if (Match) + { + MatchedVersion = CurrentVersion; + break; + } + } + + if (MatchedVersion != null) + { + foreach (TargetFile CurrentTargetFile in MatchedVersion.TargetFiles) + { + FilePatcher CurrentFile = LoadedFiles.SingleOrDefault(f => string.Compare(f.FilePath, Path.Combine(TargetPath, CurrentTargetFile.Path), true) == 0); + + if (!StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentTargetFile.HashOriginal)) + { + CurrentFile.StartPatching(); + + if (StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentTargetFile.HashPatched)) + { + foreach (Patch CurrentPatch in CurrentTargetFile.Patches) + { + CurrentFile.ApplyPatch(CurrentPatch.Address, CurrentPatch.OriginalBytes); + } + } + else + { + foreach (TargetFile CurrentObsoleteFile in CurrentTargetFile.Obsolete) + { + if (StructuralComparisons.StructuralEqualityComparer.Equals(CurrentFile.Hash, CurrentObsoleteFile.HashPatched)) + { + foreach (Patch CurrentPatch in CurrentObsoleteFile.Patches) + { + CurrentFile.ApplyPatch(CurrentPatch.Address, CurrentPatch.OriginalBytes); + } + break; + } + } + } + + CurrentFile.FinishPatching(); + } + } + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + } + } + + internal class TargetRedirection + { + private string _RelativePath; + private string _TargetPath; + + internal TargetRedirection(string RelativePath, string TargetPath) + { + this.RelativePath = RelativePath; + this.TargetPath = TargetPath; + } + + internal string RelativePath + { + get + { + return _RelativePath; + } + set + { + _RelativePath = value.TrimStart(new char[] { '\\' }).TrimEnd(new char[] { '\\' }); + } + } + + internal string TargetPath + { + get + { + return _TargetPath; + } + set + { + _TargetPath = value.TrimEnd(new char[] { '\\' }); + } + } + } + + internal class FilePatcher + { + internal byte[] Hash = null; + internal string FilePath; + internal FileSecurity OriginalACL; + internal Privilege TakeOwnershipPrivilege; + internal Privilege RestorePrivilege; + internal Stream Stream = null; + + internal FilePatcher(string FilePath) + { + this.FilePath = FilePath; + using (FileStream stream = File.OpenRead(FilePath)) + { + SHA1Managed sha = new SHA1Managed(); + Hash = sha.ComputeHash(stream); + } + } + + internal FilePatcher(string FilePath, Stream FileStream) + { + if (!FileStream.CanSeek || !FileStream.CanWrite) + throw new WPinternalsException("Incorrect filestream"); + + this.FilePath = FilePath; + this.Stream = FileStream; + FileStream.Position = 0; + SHA1Managed sha = new SHA1Managed(); + Hash = sha.ComputeHash(FileStream); + FileStream.Position = 0; + } + + internal void StartPatching() + { + if (FilePath.Contains(':')) + { + // Enable Take Ownership AND Restore ownership to original owner + // Take Ownership Privilge is not enough. + // We need Restore Privilege. + RestorePrivilege = new Privilege(Privilege.Restore); + RestorePrivilege.Enable(); + + if ((Environment.OSVersion.Version.Major == 6) && (Environment.OSVersion.Version.Minor <= 1)) + { + // On Vista or 7 + TakeOwnershipPrivilege = new Privilege(Privilege.TakeOwnership); + TakeOwnershipPrivilege.Enable(); + } + + // Backup original owner and ACL + OriginalACL = File.GetAccessControl(FilePath); + + // And take the original security to create new security rules. + FileSecurity NewACL = File.GetAccessControl(FilePath); + + // Take ownership + NewACL.SetOwner(WindowsIdentity.GetCurrent().User); + File.SetAccessControl(FilePath, NewACL); + + // And create a new access rule + NewACL.SetAccessRule(new FileSystemAccessRule(WindowsIdentity.GetCurrent().User, FileSystemRights.FullControl, AccessControlType.Allow)); + File.SetAccessControl(FilePath, NewACL); + + // Open the file for patching + Stream = new FileStream(FilePath, FileMode.Open, FileAccess.ReadWrite); + } + } + + internal void ApplyPatch(UInt32 Offset, byte[] Bytes) + { + Stream.Position = Offset; + Stream.Write(Bytes, 0, Bytes.Length); + } + + internal void FinishPatching() + { + // Close file + Stream.Close(); + + if (FilePath.Contains(':')) + { + // Restore original owner and access rules. + // The OriginalACL cannot be reused directly. + FileSecurity NewACL = File.GetAccessControl(FilePath); + NewACL.SetSecurityDescriptorBinaryForm(OriginalACL.GetSecurityDescriptorBinaryForm()); + File.SetAccessControl(FilePath, NewACL); + + // Revert to self + RestorePrivilege.Revert(); + RestorePrivilege.Disable(); + + if ((Environment.OSVersion.Version.Major == 6) && (Environment.OSVersion.Version.Minor <= 1)) + { + // On Vista or 7 + TakeOwnershipPrivilege.Revert(); + TakeOwnershipPrivilege.Disable(); + } + } + } + } + + public class PatchDefinition // Must be public to be serializable + { + [XmlAttribute] + public string Name; + + public List TargetVersions = new List(); + } + + public class TargetVersion // Must be public to be serializable + { + [XmlAttribute] + public string Description; + + public List TargetFiles = new List(); + } + + public class TargetFile // Must be public to be serializable + { + private string _Path; + [XmlAttribute] + public string Path + { + get + { + return _Path; + } + set + { + _Path = value.TrimStart(new char[] { '\\' }); + } + } + + [XmlIgnore] + public byte[] HashOriginal; + [XmlAttribute("HashOriginal")] + public string HashOriginalAsString + { + get + { + return Converter.ConvertHexToString(HashOriginal, ""); + } + set + { + HashOriginal = Converter.ConvertStringToHex(value); + } + } + + [XmlIgnore] + public byte[] HashPatched; + [XmlAttribute("HashPatched")] + public string HashPatchedAsString + { + get + { + return Converter.ConvertHexToString(HashPatched, ""); + } + set + { + HashPatched = Converter.ConvertStringToHex(value); + } + } + + public List Patches = new List(); + public List Obsolete = new List(); + } + + public class Patch // Must be public to be serializable + { + [XmlIgnore] + public UInt32 Address; + [XmlAttribute("Address")] + public string AddressAsString + { + get + { + return "0x" + Address.ToString("X8"); + } + set + { + string NewValue = value; + if (NewValue.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + NewValue = NewValue.Substring(2); + Address = Convert.ToUInt32(NewValue, 16); + } + } + + [XmlIgnore] + public byte[] OriginalBytes; + [XmlAttribute("OriginalBytes")] + public string OriginalBytesAsString + { + get + { + return Converter.ConvertHexToString(OriginalBytes, ""); + } + set + { + OriginalBytes = Converter.ConvertStringToHex(value); + } + } + + [XmlIgnore] + public byte[] PatchedBytes; + [XmlAttribute("PatchedBytes")] + public string PatchedBytesAsString + { + get + { + return Converter.ConvertHexToString(PatchedBytes, ""); + } + set + { + PatchedBytes = Converter.ConvertStringToHex(value); + } + } + } +} diff --git a/Models/Privileges.cs b/Models/Privileges.cs new file mode 100644 index 0000000..795e5c5 --- /dev/null +++ b/Models/Privileges.cs @@ -0,0 +1,667 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Specialized; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.ConstrainedExecution; +using System.Threading; + +namespace WPinternals +{ + using Luid = NativeMethods.LUID; + using Win32Exception = System.ComponentModel.Win32Exception; + using PrivilegeNotHeldException = System.Security.AccessControl.PrivilegeNotHeldException; + + internal delegate void PrivilegedCallback(object state); + + internal sealed class Privilege + { + #region Private static members + private static LocalDataStoreSlot tlsSlot = Thread.AllocateDataSlot(); + private static HybridDictionary privileges = new HybridDictionary(); + private static HybridDictionary luids = new HybridDictionary(); + private static ReaderWriterLock privilegeLock = new ReaderWriterLock(); + #endregion + + #region Private members + private bool needToRevert = false; + private bool initialState = false; + private bool stateWasChanged = false; + private Luid luid; + private readonly Thread currentThread = Thread.CurrentThread; + private TlsContents tlsContents = null; + #endregion + + #region Privilege names + public const string CreateToken = "SeCreateTokenPrivilege"; + public const string AssignPrimaryToken = "SeAssignPrimaryTokenPrivilege"; + public const string LockMemory = "SeLockMemoryPrivilege"; + public const string IncreaseQuota = "SeIncreaseQuotaPrivilege"; + public const string UnsolicitedInput = "SeUnsolicitedInputPrivilege"; + public const string MachineAccount = "SeMachineAccountPrivilege"; + public const string TrustedComputingBase = "SeTcbPrivilege"; + public const string Security = "SeSecurityPrivilege"; + public const string TakeOwnership = "SeTakeOwnershipPrivilege"; + public const string LoadDriver = "SeLoadDriverPrivilege"; + public const string SystemProfile = "SeSystemProfilePrivilege"; + public const string SystemTime = "SeSystemtimePrivilege"; + public const string ProfileSingleProcess = "SeProfileSingleProcessPrivilege"; + public const string IncreaseBasePriority = "SeIncreaseBasePriorityPrivilege"; + public const string CreatePageFile = "SeCreatePagefilePrivilege"; + public const string CreatePermanent = "SeCreatePermanentPrivilege"; + public const string Backup = "SeBackupPrivilege"; + public const string Restore = "SeRestorePrivilege"; + public const string Shutdown = "SeShutdownPrivilege"; + public const string Debug = "SeDebugPrivilege"; + public const string Audit = "SeAuditPrivilege"; + public const string SystemEnvironment = "SeSystemEnvironmentPrivilege"; + public const string ChangeNotify = "SeChangeNotifyPrivilege"; + public const string RemoteShutdown = "SeRemoteShutdownPrivilege"; + public const string Undock = "SeUndockPrivilege"; + public const string SyncAgent = "SeSyncAgentPrivilege"; + public const string EnableDelegation = "SeEnableDelegationPrivilege"; + public const string ManageVolume = "SeManageVolumePrivilege"; + public const string Impersonate = "SeImpersonatePrivilege"; + public const string CreateGlobal = "SeCreateGlobalPrivilege"; + public const string TrustedCredentialManagerAccess = "SeTrustedCredManAccessPrivilege"; + public const string ReserveProcessor = "SeReserveProcessorPrivilege"; + #endregion + + #region LUID caching logic + + // + // This routine is a wrapper around a hashtable containing mappings + // of privilege names to luids + // + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + private static Luid LuidFromPrivilege(string privilege) + { + Luid luid; + luid.LowPart = 0; + luid.HighPart = 0; + + // + // Look up the privilege LUID inside the cache + // + + RuntimeHelpers.PrepareConstrainedRegions(); + + try + { + privilegeLock.AcquireReaderLock(Timeout.Infinite); + + if (luids.Contains(privilege)) + { + luid = (Luid)luids[privilege]; + + privilegeLock.ReleaseReaderLock(); + } + else + { + privilegeLock.ReleaseReaderLock(); + + if (false == NativeMethods.LookupPrivilegeValue(null, privilege, ref luid)) + { + int error = Marshal.GetLastWin32Error(); + + if (error == NativeMethods.ERROR_NOT_ENOUGH_MEMORY) + { + throw new OutOfMemoryException(); + } + else if (error == NativeMethods.ERROR_ACCESS_DENIED) + { + throw new UnauthorizedAccessException("Caller does not have the rights to look up privilege local unique identifier"); + } + else if (error == NativeMethods.ERROR_NO_SUCH_PRIVILEGE) + { + throw new ArgumentException( + string.Format("{0} is not a valid privilege name", privilege), + "privilege"); + } + else + { + throw new Win32Exception(error); + } + } + + privilegeLock.AcquireWriterLock(Timeout.Infinite); + } + } + finally + { + if (privilegeLock.IsReaderLockHeld) + { + privilegeLock.ReleaseReaderLock(); + } + + if (privilegeLock.IsWriterLockHeld) + { + if (!luids.Contains(privilege)) + { + luids[privilege] = luid; + privileges[luid] = privilege; + } + + privilegeLock.ReleaseWriterLock(); + } + } + + return luid; + } + #endregion + + #region Nested classes + private sealed class TlsContents : IDisposable + { + private bool disposed = false; + private int referenceCount = 1; + private SafeTokenHandle threadHandle = new SafeTokenHandle(IntPtr.Zero); + private bool isImpersonating = false; + + private static SafeTokenHandle processHandle = new SafeTokenHandle(IntPtr.Zero); + private static readonly object syncRoot = new object(); + + #region Constructor and finalizer + public TlsContents() + { + int error = 0; + int cachingError = 0; + bool success = true; + + if (processHandle.IsInvalid) + { + lock (syncRoot) + { + if (processHandle.IsInvalid) + { + if (false == NativeMethods.OpenProcessToken( + NativeMethods.GetCurrentProcess(), + TokenAccessLevels.Duplicate, + ref processHandle)) + { + cachingError = Marshal.GetLastWin32Error(); + success = false; + } + } + } + } + + RuntimeHelpers.PrepareConstrainedRegions(); + + try + { + // Open the thread token; if there is no thread token, + // copy the process token onto the thread + + if (false == NativeMethods.OpenThreadToken( + NativeMethods.GetCurrentThread(), + TokenAccessLevels.Query | TokenAccessLevels.AdjustPrivileges, + true, + ref this.threadHandle)) + { + if (success == true) + { + error = Marshal.GetLastWin32Error(); + + if (error != NativeMethods.ERROR_NO_TOKEN) + { + success = false; + } + + if (success == true) + { + error = 0; + + if (false == NativeMethods.DuplicateTokenEx( + processHandle, + TokenAccessLevels.Impersonate | TokenAccessLevels.Query | TokenAccessLevels.AdjustPrivileges, + IntPtr.Zero, + SecurityImpersonationLevel.Impersonation, + TokenType.Impersonation, + ref this.threadHandle)) + { + error = Marshal.GetLastWin32Error(); + success = false; + } + } + + if (success == true) + { + if (false == NativeMethods.SetThreadToken( + IntPtr.Zero, + this.threadHandle)) + { + error = Marshal.GetLastWin32Error(); + success = false; + } + } + + if (success == true) + { + // This thread is now impersonating; it needs to be reverted to its original state + + this.isImpersonating = true; + } + } + else + { + error = cachingError; + } + } + else + { + success = true; + } + } + finally + { + if (!success) + { + Dispose(); + } + } + + if (error == NativeMethods.ERROR_NOT_ENOUGH_MEMORY) + { + throw new OutOfMemoryException(); + } + else if (error == NativeMethods.ERROR_ACCESS_DENIED || + error == NativeMethods.ERROR_CANT_OPEN_ANONYMOUS) + { + throw new UnauthorizedAccessException("The caller does not have the rights to perform the operation"); + } + else if (error != 0) + { + throw new Win32Exception(error); + } + } + + ~TlsContents() + { + if (!this.disposed) + { + Dispose(false); + } + } + #endregion + + #region IDisposable implementation + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (this.disposed) return; + + if (this.threadHandle != null) + { + this.threadHandle.Dispose(); + this.threadHandle = null; + } + + if (this.isImpersonating) + { + NativeMethods.RevertToSelf(); + } + + this.disposed = true; + } + #endregion + + #region Reference-counting + public void IncrementReferenceCount() + { + this.referenceCount++; + } + + public int DecrementReferenceCount() + { + int result = --this.referenceCount; + + if (result == 0) + { + Dispose(); + } + + return result; + } + + public int ReferenceCountValue + { + get { return this.referenceCount; } + } + #endregion + + #region Properties + public SafeTokenHandle ThreadHandle + { + get { return this.threadHandle; } + } + + public bool IsImpersonating + { + get { return this.isImpersonating; } + } + #endregion + } + #endregion + + #region Constructor + public Privilege(string privilegeName) + { + if (privilegeName == null) + { + throw new ArgumentNullException("privilegeName"); + } + + this.luid = LuidFromPrivilege(privilegeName); + } + #endregion + + #region Public methods and properties + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + public void Enable() + { + this.ToggleState(true); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + public void Disable() + { + this.ToggleState(false); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + public void Revert() + { + int error = 0; + + // All privilege operations must take place on the same thread + + if (!this.currentThread.Equals(Thread.CurrentThread)) + { + throw new InvalidOperationException("Operation must take place on the thread that created the object"); + } + + if (!this.NeedToRevert) + { + return; + } + + // This code must be eagerly prepared and non-interruptible. + + RuntimeHelpers.PrepareConstrainedRegions(); + + try + { + // The payload is entirely in the finally block + // This is how we ensure that the code will not be + // interrupted by catastrophic exceptions + } + finally + { + bool success = true; + + try + { + // Only call AdjustTokenPrivileges if we're not going to be reverting to self, + // on this Revert, since doing the latter obliterates the thread token anyway + + if (this.stateWasChanged && + (this.tlsContents.ReferenceCountValue > 1 || + !this.tlsContents.IsImpersonating)) + { + NativeMethods.TOKEN_PRIVILEGE newState = new NativeMethods.TOKEN_PRIVILEGE(); + newState.PrivilegeCount = 1; + newState.Privilege.Luid = this.luid; + newState.Privilege.Attributes = (this.initialState ? NativeMethods.SE_PRIVILEGE_ENABLED : NativeMethods.SE_PRIVILEGE_DISABLED); + + NativeMethods.TOKEN_PRIVILEGE previousState = new NativeMethods.TOKEN_PRIVILEGE(); + uint previousSize = 0; + + if (false == NativeMethods.AdjustTokenPrivileges( + this.tlsContents.ThreadHandle, + false, + ref newState, + (uint)Marshal.SizeOf(previousState), + ref previousState, + ref previousSize)) + { + error = Marshal.GetLastWin32Error(); + success = false; + } + } + } + finally + { + if (success) + { + this.Reset(); + } + } + } + + if (error == NativeMethods.ERROR_NOT_ENOUGH_MEMORY) + { + throw new OutOfMemoryException(); + } + else if (error == NativeMethods.ERROR_ACCESS_DENIED) + { + throw new UnauthorizedAccessException("Caller does not have the permission to change the privilege"); + } + else if (error != 0) + { + throw new Win32Exception(error); + } + } + + public bool NeedToRevert + { + get { return this.needToRevert; } + } + + public static void RunWithPrivilege(string privilege, bool enabled, PrivilegedCallback callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException("callback"); + } + + Privilege p = new Privilege(privilege); + + RuntimeHelpers.PrepareConstrainedRegions(); + + try + { + if (enabled) + { + p.Enable(); + } + else + { + p.Disable(); + } + + callback(state); + } + catch + { + p.Revert(); + throw; + } + finally + { + p.Revert(); + } + } + #endregion + + #region Private implementation + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + private void ToggleState(bool enable) + { + int error = 0; + + // All privilege operations must take place on the same thread + + if (!this.currentThread.Equals(Thread.CurrentThread)) + { + throw new InvalidOperationException("Operation must take place on the thread that created the object"); + } + + // This privilege was already altered and needs to be reverted before it can be altered again + + if (this.NeedToRevert) + { + throw new InvalidOperationException("Must revert the privilege prior to attempting this operation"); + } + + // Need to make this block of code non-interruptible so that it would preserve + // consistency of thread oken state even in the face of catastrophic exceptions + + RuntimeHelpers.PrepareConstrainedRegions(); + + try + { + // The payload is entirely in the finally block + // This is how we ensure that the code will not be + // interrupted by catastrophic exceptions + } + finally + { + try + { + // Retrieve TLS state + + this.tlsContents = Thread.GetData(tlsSlot) as TlsContents; + + if (this.tlsContents == null) + { + this.tlsContents = new TlsContents(); + Thread.SetData(tlsSlot, this.tlsContents); + } + else + { + this.tlsContents.IncrementReferenceCount(); + } + + NativeMethods.TOKEN_PRIVILEGE newState = new NativeMethods.TOKEN_PRIVILEGE(); + newState.PrivilegeCount = 1; + newState.Privilege.Luid = this.luid; + newState.Privilege.Attributes = enable ? NativeMethods.SE_PRIVILEGE_ENABLED : NativeMethods.SE_PRIVILEGE_DISABLED; + + NativeMethods.TOKEN_PRIVILEGE previousState = new NativeMethods.TOKEN_PRIVILEGE(); + uint previousSize = 0; + + // Place the new privilege on the thread token and remember the previous state. + + if (false == NativeMethods.AdjustTokenPrivileges( + this.tlsContents.ThreadHandle, + false, + ref newState, + (uint)Marshal.SizeOf(previousState), + ref previousState, + ref previousSize)) + { + error = Marshal.GetLastWin32Error(); + } + else if (NativeMethods.ERROR_NOT_ALL_ASSIGNED == Marshal.GetLastWin32Error()) + { + error = NativeMethods.ERROR_NOT_ALL_ASSIGNED; + } + else + { + // This is the initial state that revert will have to go back to + + this.initialState = ((previousState.Privilege.Attributes & NativeMethods.SE_PRIVILEGE_ENABLED) != 0); + + // Remember whether state has changed at all + + this.stateWasChanged = (this.initialState != enable); + + // If we had to impersonate, or if the privilege state changed we'll need to revert + + this.needToRevert = this.tlsContents.IsImpersonating || this.stateWasChanged; + } + } + finally + { + if (!this.needToRevert) + { + this.Reset(); + } + } + } + + if (error == NativeMethods.ERROR_NOT_ALL_ASSIGNED) + { + throw new PrivilegeNotHeldException(privileges[this.luid] as string); + } + if (error == NativeMethods.ERROR_NOT_ENOUGH_MEMORY) + { + throw new OutOfMemoryException(); + } + else if (error == NativeMethods.ERROR_ACCESS_DENIED || + error == NativeMethods.ERROR_CANT_OPEN_ANONYMOUS) + { + throw new UnauthorizedAccessException("The caller does not have the right to change the privilege"); + } + else if (error != 0) + { + throw new Win32Exception(error); + } + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] + private void Reset() + { + RuntimeHelpers.PrepareConstrainedRegions(); + + try + { + // Payload is in the finally block + // as a way to guarantee execution + } + finally + { + this.stateWasChanged = false; + this.initialState = false; + this.needToRevert = false; + + if (this.tlsContents != null) + { + if (0 == this.tlsContents.DecrementReferenceCount()) + { + this.tlsContents = null; + Thread.SetData(tlsSlot, null); + } + } + } + } + #endregion + } +} + diff --git a/Models/QualcommDownload.cs b/Models/QualcommDownload.cs new file mode 100644 index 0000000..750c4b0 --- /dev/null +++ b/Models/QualcommDownload.cs @@ -0,0 +1,151 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Linq; + +namespace WPinternals +{ + internal class QualcommDownload + { + private QualcommSerial Serial; + + public QualcommDownload(QualcommSerial Serial) + { + this.Serial = Serial; + } + + public bool IsAlive() + { + try + { + Serial.SendCommand(new byte[] { 0x06 }, new byte[] { 0x02 }); + return true; + } + catch + { + return false; + } + } + + public void SendToPhoneMemory(UInt32 Address, Stream Data, UInt32 Length = UInt32.MaxValue) + { + long Remaining; + if (Length > (Data.Length - Data.Position)) + Remaining = Data.Length - Data.Position; + else + Remaining = Length; + UInt32 CurrentLength; + byte[] Buffer = new byte[0x107]; + Buffer[0] = 0x0F; + System.Buffer.BlockCopy(BitConverter.GetBytes((UInt16)0x100).Reverse().ToArray(), 0, Buffer, 5, 2); // Length is in Big Endian + UInt32 CurrentAddress = Address; + while (Remaining > 0) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(CurrentAddress).Reverse().ToArray(), 0, Buffer, 1, 4); // Address is in Big Endian + + if (Remaining >= 0x100) + CurrentLength = 0x100; + else + CurrentLength = (UInt32)Remaining; + CurrentLength = (UInt32)(Data.Read(Buffer, 7, (int)CurrentLength)); + Serial.SendCommand(Buffer, new byte[] { 0x02 }); + + CurrentAddress += CurrentLength; + Remaining -= CurrentLength; + } + } + + public void SendToPhoneMemory(UInt32 Address, byte[] Data, UInt32 Offset = 0, UInt32 Length = UInt32.MaxValue) + { + long Remaining; + if (Offset > (Data.Length - 1)) + throw new ArgumentException("Wrong offset"); + if (Length > (Data.Length - Offset)) + Remaining = Data.Length - Offset; + else + Remaining = Length; + UInt32 CurrentLength; + UInt32 CurrentOffset = Offset; + byte[] Buffer = new byte[0x107]; + UInt32 CurrentAddress = Address; + byte[] CurrentBytes; + while (Remaining > 0) + { + + if (Remaining >= 0x100) + { + CurrentLength = 0x100; + CurrentBytes = Buffer; + } + else + { + CurrentLength = (UInt32)Remaining; + CurrentBytes = new byte[CurrentLength + 7]; + } + CurrentBytes[0] = 0x0F; + System.Buffer.BlockCopy(BitConverter.GetBytes(CurrentAddress).Reverse().ToArray(), 0, CurrentBytes, 1, 4); // Address is in Big Endian + System.Buffer.BlockCopy(BitConverter.GetBytes((UInt16)CurrentLength).Reverse().ToArray(), 0, CurrentBytes, 5, 2); // Length is in Big Endian + System.Buffer.BlockCopy(Data, (int)CurrentOffset, CurrentBytes, 7, (int)CurrentLength); + + Serial.SendCommand(CurrentBytes, new byte[] { 0x02 }); + + CurrentAddress += CurrentLength; + CurrentOffset += CurrentLength; + Remaining -= CurrentLength; + } + } + + public void StartBootloader(UInt32 Address) + { + byte[] Buffer = new byte[5]; + Buffer[0] = 0x05; + System.Buffer.BlockCopy(BitConverter.GetBytes(Address).Reverse().ToArray(), 0, Buffer, 1, 4); // Address is in Big Endian + Serial.SendCommand(Buffer, new byte[] { 0x02 }); + } + + // Reset interface. Interface becomes unresponsive. + public void Reset() + { + Serial.SendCommand(new byte[] { 0x0A }, new byte[] { 0x02 }); + } + + // This also resets interface. This does not actually reboot the phone. The interface becomes unresponsive. + public void Shutdown() + { + Serial.SendCommand(new byte[] { 0x0E }, new byte[] { 0x02 }); + } + + // This command only works on 9008 interface. + public byte[] GetRKH() + { + byte[] Response = Serial.SendCommand(new byte[] { 0x18 }, new byte[] { 0x18, 0x01, 0x00 }); + byte[] Result = new byte[0x20]; + Buffer.BlockCopy(Response, 3, Result, 0, 0x20); + return Result; + } + + public void CloseSerial() + { + Serial.Close(); + } + } +} diff --git a/Models/QualcommFlasher.cs b/Models/QualcommFlasher.cs new file mode 100644 index 0000000..bcc3201 --- /dev/null +++ b/Models/QualcommFlasher.cs @@ -0,0 +1,226 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; + +namespace WPinternals +{ + internal enum FlashUnit + { + Bytes, + Sectors + } + + internal class QualcommFlasher + { + private QualcommSerial Serial; + + public QualcommFlasher(QualcommSerial Serial) + { + this.Serial = Serial; + } + + public void CloseSerial() + { + Serial.Close(); + } + + public void Hello() + { + byte[] Command = new byte[] + { + 0x01, // Hello command + 0x51, 0x43, 0x4F, 0x4D, 0x20, 0x66, 0x61, 0x73, 0x74, 0x20, 0x64, 0x6F, 0x77, 0x6E, 0x6C, 0x6F, // "QCOM fast download protocol host" + 0x61, 0x64, 0x20, 0x70, 0x72, 0x6F, 0x74, 0x6F, 0x63, 0x6F, 0x6C, 0x20, 0x68, 0x6F, 0x73, 0x74, + 0x02, + 0x02, // Protocol version - Must be at least 0x02 + 0x01 + }; + + Serial.SendCommand(Command, new byte[] { 0x02 }); + } + + public void SetSecurityMode(byte Mode) + { + byte[] Command = new byte[2]; + Command[0] = 0x17; + Command[1] = Mode; + + Serial.SendCommand(Command, new byte[] { 0x18 }); + } + + // Use PartitionID 0x21 + public void OpenPartition(byte PartitionID) + { + byte[] Command = new byte[2]; + Command[0] = 0x1B; + Command[1] = PartitionID; + + Serial.SendCommand(Command, new byte[] { 0x1C }); + } + + public void ClosePartition() + { + Serial.SendCommand(new byte[] { 0x15 }, new byte[] { 0x16 }); + } + + public void Flash(UInt32 StartInBytes, Stream Data, UInt32 LengthInBytes = UInt32.MaxValue) + { + Flash(StartInBytes, Data, null, null, LengthInBytes); + } + + public void Flash(UInt32 StartInBytes, Stream Data, Action ProgressUpdateCallback, UInt32 LengthInBytes = UInt32.MaxValue) + { + Flash(StartInBytes, Data, ProgressUpdateCallback, null, LengthInBytes); + } + + public void Flash(UInt32 StartInBytes, Stream Data, ProgressUpdater UpdaterPerSector, UInt32 LengthInBytes = UInt32.MaxValue) + { + Flash(StartInBytes, Data, null, UpdaterPerSector, LengthInBytes); + } + + public void Flash(UInt32 StartInBytes, Stream Data, Action ProgressUpdateCallback, ProgressUpdater UpdaterPerSector, UInt32 LengthInBytes = UInt32.MaxValue) + { + long Remaining; + if ((LengthInBytes == UInt32.MaxValue) || (LengthInBytes > (Data.Length - Data.Position))) + Remaining = Data.Length - Data.Position; + else + Remaining = LengthInBytes; + UInt32 CurrentLength; + byte[] Buffer = new byte[0x405]; + byte[] ResponsePattern = new byte[5]; + byte[] FinalCommand; + Buffer[0] = 0x07; + ResponsePattern[0] = 0x08; + UInt32 CurrentPosition = StartInBytes; + + ProgressUpdater Progress = UpdaterPerSector; + if ((Progress == null) && (ProgressUpdateCallback != null)) + Progress = new ProgressUpdater(GetSectorCount((UInt64)Remaining), ProgressUpdateCallback); + + while (Remaining > 0) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(CurrentPosition), 0, Buffer, 1, 4); // Start is in bytes and in Little Endian (on Samsung devices start is in sectors!) + System.Buffer.BlockCopy(BitConverter.GetBytes(CurrentPosition), 0, ResponsePattern, 1, 4); // Start is in bytes and in Little Endian (on Samsung devices start is in sectors!) + + if (Remaining >= 0x400) + CurrentLength = 0x400; + else + CurrentLength = (UInt32)Remaining; + + CurrentLength = (uint)Data.Read(Buffer, 5, (int)CurrentLength); + + if (CurrentLength < 0x400) + { + FinalCommand = new byte[CurrentLength + 5]; + System.Buffer.BlockCopy(Buffer, 0, FinalCommand, 0, (int)CurrentLength + 5); + } + else + FinalCommand = Buffer; + + Serial.SendCommand(FinalCommand, ResponsePattern); + + CurrentPosition += CurrentLength; + Remaining -= CurrentLength; + + if (Progress != null) + Progress.IncreaseProgress(GetSectorCount(CurrentLength)); + } + } + + public void Flash(UInt32 StartInBytes, byte[] Data, UInt32 OffsetInBytes = 0, UInt32 LengthInBytes = UInt32.MaxValue) + { + Flash(StartInBytes, Data, null, null, OffsetInBytes, LengthInBytes); + } + + public void Flash(UInt32 StartInBytes, byte[] Data, Action ProgressUpdateCallback, UInt32 OffsetInBytes = 0, UInt32 LengthInBytes = UInt32.MaxValue) + { + Flash(StartInBytes, Data, ProgressUpdateCallback, null, OffsetInBytes, LengthInBytes); + } + + public void Flash(UInt32 StartInBytes, byte[] Data, ProgressUpdater UpdaterPerSector, UInt32 OffsetInBytes = 0, UInt32 LengthInBytes = UInt32.MaxValue) + { + Flash(StartInBytes, Data, null, UpdaterPerSector, OffsetInBytes, LengthInBytes); + } + + public void Flash(UInt32 StartInBytes, byte[] Data, Action ProgressUpdateCallback, ProgressUpdater UpdaterPerSector, UInt32 OffsetInBytes = 0, UInt32 LengthInBytes = UInt32.MaxValue) + { + long RemainingBytes; + if (OffsetInBytes > (Data.Length - 1)) + throw new ArgumentException("Wrong offset"); + if ((LengthInBytes == UInt32.MaxValue) || (LengthInBytes > (Data.Length - OffsetInBytes))) + RemainingBytes = Data.Length - OffsetInBytes; + else + RemainingBytes = LengthInBytes; + UInt32 CurrentLength; + UInt32 CurrentOffset = OffsetInBytes; + byte[] Buffer = new byte[0x405]; + byte[] ResponsePattern = new byte[5]; + byte[] FinalCommand; + Buffer[0] = 0x07; + ResponsePattern[0] = 0x08; + UInt32 CurrentPosition = StartInBytes; + + ProgressUpdater Progress = UpdaterPerSector; + if ((Progress == null) && (ProgressUpdateCallback != null)) + Progress = new ProgressUpdater(GetSectorCount((UInt64)RemainingBytes), ProgressUpdateCallback); + + while (RemainingBytes > 0) + { + System.Buffer.BlockCopy(BitConverter.GetBytes(CurrentPosition), 0, Buffer, 1, 4); // Start position is in bytes and in Little Endian (on Samsung phones the start position is in Sectors!!) + System.Buffer.BlockCopy(BitConverter.GetBytes(CurrentPosition), 0, ResponsePattern, 1, 4); // Start position is in bytes and in Little Endian (on Samsung phones the start position is in Sectors!!) + + if (RemainingBytes >= 0x400) + CurrentLength = 0x400; + else + CurrentLength = (UInt32)RemainingBytes; + System.Buffer.BlockCopy(Data, (int)CurrentOffset, Buffer, 5, (int)CurrentLength); + + if (CurrentLength < 0x400) + { + FinalCommand = new byte[CurrentLength + 5]; + System.Buffer.BlockCopy(Buffer, 0, FinalCommand, 0, (int)CurrentLength + 5); + } + else + FinalCommand = Buffer; + + Serial.SendCommand(FinalCommand, ResponsePattern); + + CurrentPosition += CurrentLength; + CurrentOffset += CurrentLength; + RemainingBytes -= CurrentLength; + + if (Progress != null) + Progress.IncreaseProgress(GetSectorCount(CurrentLength)); + } + } + + public UInt64 GetSectorCount(UInt64 ByteCount) + { + return (ByteCount / 0x200) + ((ByteCount % 0x200) > 0 ? (UInt64)1 : (UInt64)0); + } + + public void Reboot() + { + Serial.SendCommand(new byte[] { 0x0B }, new byte[] { 0x0C }); + } + } +} diff --git a/Models/QualcommLoader.cs b/Models/QualcommLoader.cs new file mode 100644 index 0000000..99c1321 --- /dev/null +++ b/Models/QualcommLoader.cs @@ -0,0 +1,109 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; + +namespace WPinternals +{ + internal static class QualcommLoaders + { + internal static List GetPossibleLoadersForRootKeyHash(string Path, byte[] RootKeyHash) + { + List Result = new List(); + + try + { + IEnumerable FilePaths = Directory.EnumerateFiles(Path); + foreach (string FilePath in FilePaths) + { + try + { + FileInfo Info = new FileInfo(FilePath); + if (Info.Length <= 0x80000) + { + QualcommPartition Loader; + +#if DEBUG + System.Diagnostics.Debug.Print("Evaluating loader: " + FilePath); +#endif + + byte[] Binary = ParseAsHexFile(FilePath); + if (Binary == null) + Loader = new QualcommPartition(FilePath); + else + Loader = new QualcommPartition(Binary); + + if ((StructuralComparisons.StructuralEqualityComparer.Equals(Loader.RootKeyHash, RootKeyHash)) + && (ByteOperations.FindUnicode(Loader.Binary, "QHSUSB_ARMPRG") != null)) // To detect that this is a loader, and not SBL1 or something. V1 loaders are QHSUSB_ARMPRG. V2 loaders are QHSUSB__BULK. Only V1 supported for now, because V2 only accepts signed payload. + { + Result.Add(Loader); + } + } + } + catch { } + } + } + catch { } + + return Result; + } + + internal static byte[] ParseAsHexFile(string FilePath) + { + byte[] Result = null; + + try + { + string[] Lines = File.ReadAllLines(FilePath); + byte[] Buffer = null; + int BufferSize = 0; + + foreach (string Line in Lines) + { + if (Line[0] != ':') + throw new BadImageFormatException(); + + byte[] LineBytes = Converter.ConvertStringToHex(Line.Substring(1)); + + if ((LineBytes[0] + 5) != LineBytes.Length) + throw new BadImageFormatException(); + + if (Buffer == null) + Buffer = new byte[0x40000]; + + if (LineBytes[3] == 0) // This is mem data + { + System.Buffer.BlockCopy(LineBytes, 4, Buffer, BufferSize, LineBytes[0]); + BufferSize += LineBytes[0]; + } + } + + Result = new byte[BufferSize]; + System.Buffer.BlockCopy(Buffer, 0, Result, 0, BufferSize); + } + catch { } + + return Result; + } + } +} diff --git a/Models/QualcommPartition.cs b/Models/QualcommPartition.cs new file mode 100644 index 0000000..9a7332f --- /dev/null +++ b/Models/QualcommPartition.cs @@ -0,0 +1,166 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace WPinternals +{ + internal enum QualcommPartitionHeaderType + { + Long, + Short + }; + + internal class QualcommPartition + { + internal byte[] Binary; + internal uint HeaderOffset; + internal QualcommPartitionHeaderType HeaderType; + internal uint ImageOffset; + internal uint ImageAddress; + internal uint ImageSize; + internal uint CodeSize; + internal uint SignatureAddress; + internal uint SignatureSize; + internal uint SignatureOffset; + internal uint CertificatesAddress; + internal uint CertificatesSize; + internal uint CertificatesOffset; + internal byte[] RootKeyHash = null; + + internal QualcommPartition(string Path): this(File.ReadAllBytes(Path)) { } + + internal QualcommPartition(byte[] Binary, uint Offset = 0) + { +#if DEBUG + System.Diagnostics.Debug.Print("Loader: " + Converter.ConvertHexToString(new SHA256Managed().ComputeHash(Binary, 0, Binary.Length), "")); +#endif + + this.Binary = Binary; + + byte[] LongHeaderPattern = new byte[] { 0xD1, 0xDC, 0x4B, 0x84, 0x34, 0x10, 0xD7, 0x73, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + byte[] LongHeaderMask = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + if (ByteOperations.FindPattern(Binary, Offset, 4, new byte[] { 0x7F, 0x45, 0x4C, 0x46 }, new byte[] { 0x00, 0x00, 0x00, 0x00 }, null) == 0) + { + // This is an ELF image + // First program header is a reference to the elf-header + // Second program header is a reference to the signed hash-table + HeaderType = QualcommPartitionHeaderType.Short; + UInt32 ProgramHeaderOffset; + UInt16 ProgramHeaderEntrySize; + UInt32 HashTableProgramHeaderOffset; + if (Binary[Offset + 0x04] == 1) + { + // 32-bit elf image + ProgramHeaderOffset = Offset + ByteOperations.ReadUInt32(Binary, Offset + 0x1c); + ProgramHeaderEntrySize = ByteOperations.ReadUInt16(Binary, Offset + 0x2a); + HashTableProgramHeaderOffset = ProgramHeaderOffset + ProgramHeaderEntrySize; + ImageOffset = Offset + ByteOperations.ReadUInt32(Binary, HashTableProgramHeaderOffset + 0x04); + HeaderOffset = ImageOffset + 8; + } + else if (Binary[Offset + 0x04] == 2) + { + // 64-bit elf image + ProgramHeaderOffset = Offset + ByteOperations.ReadUInt32(Binary, Offset + 0x20); + ProgramHeaderEntrySize = ByteOperations.ReadUInt16(Binary, Offset + 0x36); + HashTableProgramHeaderOffset = ProgramHeaderOffset + ProgramHeaderEntrySize; + ImageOffset = Offset + (UInt32)ByteOperations.ReadUInt64(Binary, HashTableProgramHeaderOffset + 0x08); + HeaderOffset = ImageOffset + 8; + } + else + throw new WPinternalsException("Invalid programmer"); + } + else if (ByteOperations.FindPattern(Binary, Offset, (uint)LongHeaderPattern.Length, LongHeaderPattern, LongHeaderMask, null) == null) + { + HeaderType = QualcommPartitionHeaderType.Short; + ImageOffset = Offset; + HeaderOffset = ImageOffset + 8; + } + else + { + HeaderType = QualcommPartitionHeaderType.Long; + ImageOffset = Offset; + HeaderOffset = ImageOffset + (uint)LongHeaderPattern.Length; + } + + if (ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X00) != 0) + ImageOffset = ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X00); + else if (HeaderType == QualcommPartitionHeaderType.Short) + ImageOffset += 0x28; + else + ImageOffset += 0x50; + + ImageAddress = ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X04); + ImageSize = ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X08); + CodeSize = ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X0C); + SignatureAddress = ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X10); + SignatureSize = ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X14); + SignatureOffset = SignatureAddress - ImageAddress + ImageOffset; + CertificatesAddress = ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X18); + CertificatesSize = ByteOperations.ReadUInt32(Binary, HeaderOffset + 0X1C); + CertificatesOffset = CertificatesAddress - ImageAddress + ImageOffset; + + uint CurrentCertificateOffset = CertificatesOffset; + uint CertificateSize = 0; + while (CurrentCertificateOffset < (CertificatesOffset + CertificatesSize)) + { + if ((Binary[CurrentCertificateOffset] == 0x30) && (Binary[CurrentCertificateOffset + 1] == 0x82)) + { + CertificateSize = (uint)(Binary[CurrentCertificateOffset + 2] * 0x100) + Binary[CurrentCertificateOffset + 3] + 4; // Big endian! + + if ((CurrentCertificateOffset + CertificateSize) == (CertificatesOffset + CertificatesSize)) + { + // This is the last certificate. So this is the root key. + RootKeyHash = new SHA256Managed().ComputeHash(Binary, (int)CurrentCertificateOffset, (int)CertificateSize); + +#if DEBUG + System.Diagnostics.Debug.Print("RKH: " + Converter.ConvertHexToString(RootKeyHash, "")); +#endif + } +#if DEBUG + else + { + System.Diagnostics.Debug.Print("Cert: " + Converter.ConvertHexToString(new SHA256Managed().ComputeHash(Binary, (int)CurrentCertificateOffset, (int)CertificateSize), "")); + } +#endif + CurrentCertificateOffset += CertificateSize; + } + else + { + if ((RootKeyHash == null) && (CurrentCertificateOffset > CertificatesOffset)) + { + CurrentCertificateOffset -= CertificateSize; + + // This is the last certificate. So this is the root key. + RootKeyHash = new SHA256Managed().ComputeHash(Binary, (int)CurrentCertificateOffset, (int)CertificateSize); + +#if DEBUG + System.Diagnostics.Debug.Print("RKH: " + Converter.ConvertHexToString(RootKeyHash, "")); +#endif + } + break; + } + } + } + } +} diff --git a/Models/QualcommSahara.cs b/Models/QualcommSahara.cs new file mode 100644 index 0000000..4f8c213 --- /dev/null +++ b/Models/QualcommSahara.cs @@ -0,0 +1,313 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading.Tasks; + +namespace WPinternals +{ + internal enum SaharaMode: uint + { + ImageTransferPending = 0x00, + ImagetransferComplete = 0x01, + MemoryDebug = 0x02, + Command = 0x03 + } + + internal delegate void ReadyHandler(); + + internal class QualcommSahara + { + private QualcommSerial Serial; + + public QualcommSahara(QualcommSerial Serial) + { + Serial.EncodeCommands = false; + Serial.DecodeResponses = false; + this.Serial = Serial; + } + + public bool SendImage(string Path) + { + bool Result = true; + + LogFile.Log("Sending programmer: " + Path, LogType.FileOnly); + + int Step = 0; + UInt32 Offset = 0; + UInt32 Length = 0; + + byte[] ImageBuffer = null; + try + { + Step = 1; + byte[] Hello = null; + Hello = Serial.GetResponse(new byte[] { 0x01, 0x00, 0x00, 0x00 }); + + // Incoming Hello packet: + // 00000001 = Hello command id + // xxxxxxxx = Length + // xxxxxxxx = Protocol version + // xxxxxxxx = Supported version + // xxxxxxxx = Max packet length + // xxxxxxxx = Expected mode + // 6 dwords reserved space + LogFile.Log("Protocol: 0x" + ByteOperations.ReadUInt32(Hello, 0x08).ToString("X8"), LogType.FileOnly); + LogFile.Log("Supported: 0x" + ByteOperations.ReadUInt32(Hello, 0x0C).ToString("X8"), LogType.FileOnly); + LogFile.Log("MaxLength: 0x" + ByteOperations.ReadUInt32(Hello, 0x10).ToString("X8"), LogType.FileOnly); + LogFile.Log("Mode: 0x" + ByteOperations.ReadUInt32(Hello, 0x14).ToString("X8"), LogType.FileOnly); + + // Packet: + // 00000002 = Hello response command id + // 00000030 = Length + // 00000002 = Protocol version + // 00000001 = Supported version + // 00000000 = Status OK + // 00000000 = Mode + // rest is reserved space + Step = 2; + byte[] HelloResponse = new byte[] { + 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + Serial.SendData(HelloResponse); + + Step = 3; + using (System.IO.FileStream FileStream = new System.IO.FileStream(Path, System.IO.FileMode.Open, System.IO.FileAccess.Read)) + { + while (true) + { + Step = 4; + byte[] ReadDataRequest = Serial.GetResponse(null); + UInt32 ResponseID = ByteOperations.ReadUInt32(ReadDataRequest, 0); + if (ResponseID == 4) + break; + if (ResponseID != 3) + { + Step = 5; + throw new BadConnectionException(); + } + + Offset = ByteOperations.ReadUInt32(ReadDataRequest, 0x0C); + Length = ByteOperations.ReadUInt32(ReadDataRequest, 0x10); + if ((ImageBuffer == null) || (ImageBuffer.Length != Length)) + ImageBuffer = new byte[Length]; + if (FileStream.Position != Offset) + FileStream.Seek(Offset, System.IO.SeekOrigin.Begin); + + Step = 6; + FileStream.Read(ImageBuffer, 0, (int)Length); + + Step = 7; + Serial.SendData(ImageBuffer); + } + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex, LogType.FileAndConsole, Step.ToString() + " 0x" + Offset.ToString("X8") + " 0x" + Length.ToString("X8")); + Result = false; + } + + if (Result) + LogFile.Log("Programmer loaded into phone memory", LogType.FileOnly); + + return Result; + } + + public bool Handshake() + { + bool Result = true; + + try + { + byte[] Hello = Serial.GetResponse(new byte[] { 0x01, 0x00, 0x00, 0x00 }); + + // Incoming Hello packet: + // 00000001 = Hello command id + // xxxxxxxx = Length + // xxxxxxxx = Protocol version + // xxxxxxxx = Supported version + // xxxxxxxx = Max packet length + // xxxxxxxx = Expected mode + // 6 dwords reserved space + LogFile.Log("Protocol: 0x" + ByteOperations.ReadUInt32(Hello, 0x08).ToString("X8"), LogType.FileOnly); + LogFile.Log("Supported: 0x" + ByteOperations.ReadUInt32(Hello, 0x0C).ToString("X8"), LogType.FileOnly); + LogFile.Log("MaxLength: 0x" + ByteOperations.ReadUInt32(Hello, 0x10).ToString("X8"), LogType.FileOnly); + LogFile.Log("Mode: 0x" + ByteOperations.ReadUInt32(Hello, 0x14).ToString("X8"), LogType.FileOnly); + + byte[] HelloResponse = new byte[] { + 0x02, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + byte[] Ready = Serial.SendCommand(HelloResponse, new byte[] { 0x03, 0x00, 0x00, 0x00 }); + } + catch + { + Result = false; + } + + return Result; + } + + public void ResetSahara() + { + Serial.SendCommand(new byte[] { 0x07, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 }, new byte[] { 0x08, 0x00, 0x00, 0x00 }); + } + + public bool ConnectToProgrammerInTestMode() + { + // Behaviour of old firehose: + // Takes about 20 ms to be started. + // Then PC has to start talking to the phone. + // Behaviour of new firehose: + // After 2000 ms the firehose starts talking to the PC + // + // For the duration of 2.5 seconds we will send Hello packages + // And also wait for incoming messages + // An incoming message can be a response to our outgoing Hello packet (read incoming until "response value") + // Or it can be an incoming Hello-packet from the programmer (always 2 packets, starting with "Chip serial num") + // Sending the hello-packet can succeed immediately, or it can timeout. + // When sending succeeds, an answer should be incoming immediately to complete the handshake. + // When an incoming Hello was received, the phone still expects to receive another Hello. + + byte[] HelloPacketFromPcToProgrammer = new byte[0x20C]; + ByteOperations.WriteUInt32(HelloPacketFromPcToProgrammer, 0, 0x57503730); + ByteOperations.WriteUInt32(HelloPacketFromPcToProgrammer, 0x28, 0x57503730); + ByteOperations.WriteUInt32(HelloPacketFromPcToProgrammer, 0x208, 0x57503730); + ByteOperations.WriteUInt16(HelloPacketFromPcToProgrammer, 0x48, 0x4445); + + int HelloSendCount = 0; + bool HandshakeCompleted = false; + string Incoming; + do + { + Serial.SetTimeOut(200); + HelloSendCount++; + try + { + LogFile.Log("Send Hello to programmer (" + HelloSendCount.ToString() + ")", LogType.FileOnly); + Serial.SendData(HelloPacketFromPcToProgrammer); + LogFile.Log("Hello packet accepted", LogType.FileOnly); + } + catch + { + LogFile.Log("Hello packet not accepted", LogType.FileOnly); + } + + try + { + Serial.SetTimeOut(500); + Incoming = System.Text.Encoding.ASCII.GetString(Serial.GetResponse(null)); + LogFile.Log("In: " + Incoming, LogType.FileOnly); + Serial.SetTimeOut(200); + if (Incoming.Contains("Chip serial num")) + { + Incoming = System.Text.Encoding.ASCII.GetString(Serial.GetResponse(null)); + LogFile.Log("In: " + Incoming, LogType.FileOnly); + LogFile.Log("Incoming Hello-packets received", LogType.FileOnly); + } + + while (Incoming.IndexOf("response value") < 0) + { + Incoming = System.Text.Encoding.ASCII.GetString(Serial.GetResponse(null)); + LogFile.Log("In: " + Incoming, LogType.FileOnly); + }; + + LogFile.Log("Incoming Hello-response received", LogType.FileOnly); + HandshakeCompleted = true; + } + catch { } + } + while (!HandshakeCompleted && (HelloSendCount < 6)); + + if (HandshakeCompleted) + LogFile.Log("Handshake completed with programmer in testmode", LogType.FileOnly); + else + LogFile.Log("Handshake with programmer failed", LogType.FileOnly); + + return HandshakeCompleted; + } + + public async Task Reset(string ProgrammerPath) + { + bool SendImageResult = await Task.Run(() => SendImage(ProgrammerPath)); + if (!SendImageResult) + return false; + + await Task.Run(() => StartProgrammer()); + + bool Connected = await Task.Run(() => ConnectToProgrammerInTestMode()); + if (!Connected) + return false; + + LogFile.Log("Rebooting phone", LogType.FileAndConsole); + string Command03 = ""; + LogFile.Log("Out: " + Command03, LogType.FileOnly); + Serial.SendData(System.Text.Encoding.ASCII.GetBytes(Command03)); + + string Incoming; + do + { + Incoming = System.Text.Encoding.ASCII.GetString(Serial.GetResponse(null)); + LogFile.Log("In: " + Incoming, LogType.FileOnly); + } + while (Incoming.IndexOf("response value") < 0); + + // Workaround for problem + // SerialPort is sometimes not disposed correctly when the device is already removed. + // So explicitly dispose here + Serial.Close(); + + return true; + } + + public void SwitchMode(SaharaMode Mode) + { + byte[] SwitchModeCommand = new byte[] { 0x0C, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + ByteOperations.WriteUInt32(SwitchModeCommand, 8, (UInt32)Mode); + byte[] ResponsePattern = null; + switch (Mode) + { + case SaharaMode.ImageTransferPending: + ResponsePattern = new byte[] { 0x04, 0x00, 0x00, 0x00 }; + break; + case SaharaMode.MemoryDebug: + ResponsePattern = new byte[] { 0x09, 0x00, 0x00, 0x00 }; + break; + case SaharaMode.Command: + ResponsePattern = new byte[] { 0x0B, 0x00, 0x00, 0x00 }; + break; + } + Serial.SendCommand(SwitchModeCommand, ResponsePattern); + } + + public void StartProgrammer() + { + LogFile.Log("Starting programmer", LogType.FileAndConsole); + byte[] DoneCommand = new byte[] { 0x05, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00 }; + byte[] DoneResponse = Serial.SendCommand(DoneCommand, new byte[] { 0x06, 0x00, 0x00, 0x00 }); + LogFile.Log("Programmer being launched on phone", LogType.FileOnly); + } + } +} diff --git a/Models/QualcommSerial.cs b/Models/QualcommSerial.cs new file mode 100644 index 0000000..aeca980 --- /dev/null +++ b/Models/QualcommSerial.cs @@ -0,0 +1,349 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using MadWizard.WinUSBNet; +using System; +using System.IO.Ports; + +namespace WPinternals +{ + internal class QualcommSerial: IDisposable + { + private bool Disposed = false; + private SerialPort Port = null; + private USBDevice USBDevice = null; + private CRC16 CRC16; + + public bool EncodeCommands = true; + public bool DecodeResponses = true; + + public QualcommSerial(string DevicePath) + { + CRC16 = new CRC16(0x1189, 0xFFFF, 0xFFFF); + + string[] DevicePathElements = DevicePath.Split(new char[] { '#' }); + if (string.Compare(DevicePathElements[3], "{86E0D1E0-8089-11D0-9CE4-08003E301F73}", true) == 0) + { + string PortName = (string)Microsoft.Win32.Registry.GetValue(@"HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB\" + DevicePathElements[1] + @"\" + DevicePathElements[2] + @"\Device Parameters", "PortName", null); + if (PortName != null) + { + Port = new SerialPort(PortName, 115200); + Port.ReadTimeout = 1000; + Port.WriteTimeout = 1000; + Port.Open(); + } + } + else + { + try + { + this.USBDevice = new USBDevice(DevicePath); + } + catch { } + } + } + + public void SendData(byte[] Data) + { + byte[] FormattedData; + if (EncodeCommands) + FormattedData = FormatCommand(Data); + else + FormattedData = Data; + + if (Port != null) + Port.Write(FormattedData, 0, FormattedData.Length); + if (USBDevice != null) + USBDevice.OutputPipe.Write(FormattedData); + } + + public byte[] SendCommand(byte[] Command, byte[] ResponsePattern) + { + byte[] FormattedCommand; + if (EncodeCommands) + FormattedCommand = FormatCommand(Command); + else + FormattedCommand = Command; + + if (Port != null) + Port.Write(FormattedCommand, 0, FormattedCommand.Length); + if (USBDevice != null) + USBDevice.OutputPipe.Write(FormattedCommand); + + return GetResponse(ResponsePattern); + } + + internal byte[] GetResponse(byte[] ResponsePattern) + { + byte[] ResponseBuffer = new byte[0x2000]; + int Length = 0; + bool IsIncomplete = false; + + do + { + IsIncomplete = false; + + try + { + int BytesRead = 0; + + if (Port != null) + BytesRead = Port.Read(ResponseBuffer, Length, ResponseBuffer.Length - Length); + if (USBDevice != null) + BytesRead = USBDevice.InputPipe.Read(ResponseBuffer); + + if (BytesRead == 0) + { + LogFile.Log("Emergency mode of phone is ignoring us", LogType.FileAndConsole); + throw new BadMessageException(); + } + + Length += BytesRead; + byte[] DecodedResponse; + if (DecodeResponses) + { + DecodedResponse = DecodeResponse(ResponseBuffer, (UInt32)Length); + } + else + { + DecodedResponse = new byte[Length]; + Buffer.BlockCopy(ResponseBuffer, 0, DecodedResponse, 0, Length); + } + + if (ResponsePattern != null) + { + for (int i = 0; i < ResponsePattern.Length; i++) + if (DecodedResponse[i] != ResponsePattern[i]) + { + byte[] LogResponse = new byte[DecodedResponse.Length < 0x10 ? DecodedResponse.Length: 0x10]; + LogFile.Log("Qualcomm serial response: " + Converter.ConvertHexToString(LogResponse, ""), LogType.FileOnly); + LogFile.Log("Expected: " + Converter.ConvertHexToString(ResponsePattern, ""), LogType.FileOnly); + throw new BadMessageException(); + } + } + + return DecodedResponse; + } + catch (IncompleteMessageException) + { + IsIncomplete = true; + } + catch { } // Will be rethrown as BadConnectionException + } + while (IsIncomplete); + + if (Port != null) + Port.DiscardInBuffer(); + if (USBDevice != null) + USBDevice.InputPipe.Flush(); + + throw new BadConnectionException(); + } + + private byte[] FormatCommand(byte[] Command) + { + if ((Command == null) || (Command.Length == 0)) + throw new BadMessageException(); + + byte[] Decoded = new byte[(Command.Length * 2) + 4]; + int Length = 0; + + Decoded[Length++] = 0x7E; + + for (int i = 0; i < Command.Length; i++) + { + if ((Command[i] == 0x7D) || (Command[i] == 0x7E)) + { + Decoded[Length++] = 0x7D; + Decoded[Length++] = (byte)(Command[i] ^ 0x20); + } + else + Decoded[Length++] = Command[i]; + } + + UInt16 Checksum = CRC16.CalculateChecksum(Command); + if (((byte)(Checksum & 0xFF) == 0x7D) || ((byte)(Checksum & 0xFF) == 0x7E)) + { + Decoded[Length++] = 0x7D; + Decoded[Length++] = (byte)((Checksum & 0xFF) ^ 0x20); + } + else + Decoded[Length++] = (byte)(Checksum & 0xFF); + if (((byte)(Checksum >> 8) == 0x7D) || ((byte)(Checksum >> 8) == 0x7E)) + { + Decoded[Length++] = 0x7D; + Decoded[Length++] = (byte)((Checksum >> 8) ^ 0x20); + } + else + Decoded[Length++] = (byte)(Checksum >> 8); + Decoded[Length++] = 0x7E; + + if (Length > 0) + { + byte[] Result = new byte[Length]; + Buffer.BlockCopy(Decoded, 0, Result, 0, Length); + return Result; + } + else + return null; + } + + private byte[] DecodeResponse(byte[] Response, UInt32 Length) + { + if ((Response == null) || (Response.Length == 0) || (Response[0] != 0x7E)) + throw new BadMessageException(); + + UInt32 SourceLength = Length; + Length = 0; + UInt32 SourcePos = 1; + + byte[] Message = new byte[SourceLength]; + + while (SourcePos < SourceLength) + { + if (Response[SourcePos] == 0x7E) + break; + if (Response[SourcePos] == 0x7D) + Message[Length++] = (byte)(Response[++SourcePos] ^ 0x20); + else + Message[Length++] = Response[SourcePos]; + SourcePos++; + } + + if (SourcePos == SourceLength) throw new IncompleteMessageException(); + + if (Length < 3) throw new BadMessageException(); + + byte[] TrimmedMessage = new byte[Length - 2]; + Buffer.BlockCopy(Message, 0, TrimmedMessage, 0, (int)(Length - 2)); + + UInt16 Checksum = CRC16.CalculateChecksum(TrimmedMessage); + if (((byte)(Checksum & 0xFF) != Message[Length - 2]) || ((byte)(Checksum >> 8) != Message[Length - 1])) + throw new BadMessageException(); + + return TrimmedMessage; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~QualcommSerial() + { + Dispose(false); + } + + public void Close() + { + if (Port != null) Port.Close(); + if (USBDevice != null) USBDevice.Dispose(); + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed) + return; + + if (disposing) + { + // Other disposables + } + + // Clean unmanaged resources here. + Close(); + + Disposed = true; + } + + internal void SetTimeOut(int v) + { + if (USBDevice != null) + USBDevice.ControlPipeTimeout = v; + if (Port != null) + { + Port.ReadTimeout = v; + Port.WriteTimeout = v; + } + } + } + + public class IncompleteMessageException : Exception { }; + public class BadMessageException : Exception { }; + public class BadConnectionException : Exception { }; + + public class CRC16 + { + private UInt16[] ChecksumTable = + new UInt16[] { + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, + 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, + 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, + 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, + 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, + 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, + 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, + 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, + 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, + 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, + 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, + 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, + 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, + 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, + 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, + 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, + 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, + 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, + 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, + 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, + 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, + 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, + 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, + 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, + 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, + 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, + 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, + 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78 + }; + + private UInt16 Seed, FinalXor; + + public CRC16(UInt16 Polynomial, UInt16 Seed, UInt16 FinalXor) + { + this.Seed = Seed; + this.FinalXor = FinalXor; + } + + public UInt16 CalculateChecksum(byte[] Bytes) + { + UInt16 Crc = Seed; + for (int i = 0; i < Bytes.Length; ++i) + { + Crc = (UInt16)((Crc >> 8) ^ ChecksumTable[(byte)(Crc ^ Bytes[i])]); // Qualcomm implementation + } + return (UInt16)(Crc ^ FinalXor); + } + } +} diff --git a/Models/SBL1.cs b/Models/SBL1.cs new file mode 100644 index 0000000..246c900 --- /dev/null +++ b/Models/SBL1.cs @@ -0,0 +1,113 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace WPinternals +{ + internal class SBL1: QualcommPartition + { + internal SBL1(byte[] Binary): base(Binary, 0x2800) { } + + internal byte[] GenerateExtraSector(byte[] PartitionHeader) + { + UInt32? Offset = ByteOperations.FindPattern(Binary, + new byte[] { + 0x15, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }, null, null); + if (Offset == null) + throw new BadImageFormatException(); + + UInt32 PartitionLoaderTableOffset = (UInt32)Offset; + + byte[] FoundPattern = new byte[0x10]; + Offset = ByteOperations.FindPattern(Binary, + new byte[] { + 0x04, 0x00, 0x9F, 0xE5, 0x28, 0x00, 0xD0, 0xE5, 0x1E, 0xFF, 0x2F, 0xE1, 0xFF, 0xFF, 0xFF, 0xFF + }, + new byte[] { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF + }, + FoundPattern); + if (Offset == null) + throw new BadImageFormatException(); + + UInt32 SharedMemoryAddress = ByteOperations.ReadUInt32(FoundPattern, 0x0C); + UInt32 GlobalIsSecurityEnabledAddress = SharedMemoryAddress + 0x28; + + Offset = ByteOperations.FindPattern(Binary, + new byte[] { + 0x01, 0xFF, 0xA0, 0xE3, 0xFF, 0xFF, 0xA0, 0xE1, 0x1C, 0xD0, 0x8D, 0xE2, 0xF0, 0x4F, 0xBD, 0xE8, + 0x1E, 0xFF, 0x2F, 0xE1 + }, + new byte[] { + 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + }, + null); + if (Offset == null) + throw new BadImageFormatException(); + + UInt32 ReturnAddress = (UInt32)Offset - ImageOffset + ImageAddress; + + byte[] Sector = new byte[0x200]; + Array.Clear(Sector, 0, 0x200); + + byte[] Content = new byte[] { + 0x16, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x28, 0xBD, 0x02, 0x00, + 0xD8, 0x01, 0x00, 0x00, 0xD8, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA0, 0xE3, 0x3C, 0x10, 0x9F, 0xE5, + 0x00, 0x00, 0xC1, 0xE5, 0x38, 0x00, 0x9F, 0xE5, 0x38, 0x10, 0x9F, 0xE5, 0x00, 0x00, 0x81, 0xE5, + 0x34, 0x10, 0x9F, 0xE5, 0x00, 0x00, 0x81, 0xE5, 0x30, 0x00, 0x9F, 0xE5, 0x20, 0x10, 0x9F, 0xE5, + 0x2C, 0x30, 0x9F, 0xE5, 0x00, 0x20, 0x90, 0xE5, 0x00, 0x20, 0x81, 0xE5, 0x04, 0x00, 0x80, 0xE2, + 0x04, 0x10, 0x81, 0xE2, 0x03, 0x00, 0x50, 0xE1, 0xF9, 0xFF, 0xFF, 0xBA, 0x14, 0xF0, 0x9F, 0xE5, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x02, 0x00, 0x90, 0xBF, 0x02, 0x00, 0xD0, 0xBF, 0x02, 0x00, + 0xA0, 0xBD, 0x02, 0x00, 0xA0, 0xBE, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + byte[] PartitionTypeGuid = new byte[] { + 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74, 0x74 + }; + + Buffer.BlockCopy(Content, 0, Sector, 0, Content.Length); + + // Overwrite first part of partition-header with model-specific header + Buffer.BlockCopy(PartitionHeader, 0, Sector, 0, PartitionHeader.Length); + + ByteOperations.WriteUInt32(Sector, 0x70, GlobalIsSecurityEnabledAddress); + ByteOperations.WriteUInt32(Sector, 0x88, ReturnAddress); + + Buffer.BlockCopy(Binary, (int)PartitionLoaderTableOffset, Sector, 0xA0, 0x50); + ByteOperations.WriteUInt32(Sector, 0xA0 + 0x30, 0); + + Buffer.BlockCopy(Binary, (int)PartitionLoaderTableOffset, Sector, 0xF0, 0x50); + ByteOperations.WriteUInt32(Sector, 0xF0 + 0x2C, 0); + ByteOperations.WriteUInt32(Sector, 0xF0 + 0x38, 0x210F0); + + Buffer.BlockCopy(PartitionTypeGuid, 0, Sector, 0x190, 0x10); + + ByteOperations.WriteUInt32(Sector, 0x1FC, 0x0002BD28); + + return Sector; + } + } +} diff --git a/Models/SBL2.cs b/Models/SBL2.cs new file mode 100644 index 0000000..8c4e05b --- /dev/null +++ b/Models/SBL2.cs @@ -0,0 +1,54 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace WPinternals +{ + internal class SBL2 + { + internal byte[] Binary; + + internal SBL2(byte[] Binary) + { + this.Binary = Binary; + } + + // Magic! + internal byte[] Patch() + { + UInt32? PatchOffset = ByteOperations.FindPattern(Binary, + new byte[] { + 0xFF, 0xFF, 0xFF, 0xE3, 0x01, 0x0E, 0x42, 0xE3, 0x28, 0x00, 0xD0, 0xE5, 0x1E, 0xFF, 0x2F, 0xE1 + }, + new byte[] { + 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }, + null); + + if (PatchOffset == null) + throw new BadImageFormatException(); + + Buffer.BlockCopy(new byte[] { 0x00, 0x00, 0xA0, 0xE3 }, 0, Binary, (int)PatchOffset + 8, 4); + + return Binary; + } + } +} diff --git a/Models/SBL3.cs b/Models/SBL3.cs new file mode 100644 index 0000000..5b613fe --- /dev/null +++ b/Models/SBL3.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; + +namespace WPinternals +{ + internal class SBL3 + { + internal byte[] Binary; + + internal SBL3(byte[] Binary) + { + this.Binary = Binary; + } + + internal SBL3(string FileName) + { + Binary = null; + + // First try to parse as FFU + try + { + if (FFU.IsFFU(FileName)) + { + FFU FFUFile = new FFU(FileName); + Binary = FFUFile.GetPartition("SBL3"); + } + } + catch { } + + // If not succeeded, then try to parse it as raw image + if (Binary == null) + { + byte[] SBL3Header; + byte[] SBL3Pattern = new byte[] { 0x18, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x8F, 0xFF, 0xFF, 0xFF, 0xFF }; + byte[] SBL3Mask = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF }; + + UInt32? Offset = ByteOperations.FindPatternInFile(FileName, SBL3Pattern, SBL3Mask, out SBL3Header); + + if (Offset != null) + { + UInt32 Length = ByteOperations.ReadUInt32(SBL3Header, 0x10) + 0x28; // SBL3 Image Size + Header Size + Binary = new byte[Length]; + + FileStream Stream = new FileStream(FileName, FileMode.Open, FileAccess.Read); + Stream.Seek((long)Offset, SeekOrigin.Begin); + Stream.Read(Binary, 0, (int)Length); + Stream.Close(); + } + } + } + + // Magic! + internal byte[] Patch() + { + UInt32? PatchOffset = ByteOperations.FindPattern(Binary, + new byte[] { 0x04, 0x00, 0x9F, 0xE5, 0x28, 0x00, 0xD0, 0xE5, 0x1E, 0xFF, 0x2F, 0xE1 }, + null, null); + + if (PatchOffset == null) + throw new BadImageFormatException(); + + Buffer.BlockCopy(new byte[] { 0x00, 0x00, 0xA0, 0xE3 }, 0, Binary, (int)PatchOffset + 4, 4); + + return Binary; + } + } +} diff --git a/Models/UEFI.cs b/Models/UEFI.cs new file mode 100644 index 0000000..444737f --- /dev/null +++ b/Models/UEFI.cs @@ -0,0 +1,591 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace WPinternals +{ + internal class EFI + { + internal Guid Guid; + internal string Name; + internal int Type; + internal UInt32 Size; + internal UInt32 FileOffset; + internal UInt32 SectionOffset; + internal UInt32 BinaryOffset; + } + + internal class UEFI + { + internal byte[] Binary; + private byte[] DecompressedImage; + internal List EFIs = new List(); + private byte PaddingByteValue = 0xFF; + private UInt32 DecompressedVolumeSectionHeaderOffset; + private UInt32 DecompressedVolumeHeaderOffset; + private UInt32 VolumeSize; + private UInt16 VolumeHeaderSize; + private UInt32 FileHeaderOffset; + private UInt32 SectionHeaderOffset; + private UInt32 CompressedSubImageOffset; + private UInt32 CompressedSubImageSize; + + // First 0x28 bytes are Qualcomm partition header + // Inside the attributes of the VolumeHeader, the Volume-alignment is set to 8 (on Windows Phone UEFI images) + // The Volume always starts right after the Qualcomm header at position 0x28. + // So the VolumeHeader-alignment is always complied. + private UInt32 VolumeHeaderOffset = 0x28; + + internal UEFI(byte[] UefiBinary) + { + Binary = UefiBinary; + + string VolumeHeaderMagic; + UInt32? Offset = ByteOperations.FindAscii(Binary, "_FVH"); + if (Offset == null) + throw new BadImageFormatException(); + else + VolumeHeaderOffset = (UInt32)Offset - 0x28; + + if (!VerifyVolumeChecksum(Binary, VolumeHeaderOffset)) + throw new BadImageFormatException(); + + VolumeSize = ByteOperations.ReadUInt32(Binary, VolumeHeaderOffset + 0x20); // TODO: This is actually a QWORD + VolumeHeaderSize = ByteOperations.ReadUInt16(Binary, VolumeHeaderOffset + 0x30); + PaddingByteValue = (ByteOperations.ReadUInt32(Binary, VolumeHeaderOffset + 0x2C) & 0x00000800) > 0 ? (byte)0xFF : (byte)0x00; // EFI_FVB_ERASE_POLARITY = 0x00000800 + + // In the volume look for a file of type EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE (0x0B) + + FileHeaderOffset = VolumeHeaderOffset + VolumeHeaderSize; + bool VolumeFound = false; + int FileType; + UInt32 FileSize; + do + { + if (!VerifyFileChecksum(Binary, FileHeaderOffset)) + throw new BadImageFormatException(); + + FileType = ByteOperations.ReadUInt8(Binary, FileHeaderOffset + 0x12); + FileSize = ByteOperations.ReadUInt24(Binary, FileHeaderOffset + 0x14); + + if (FileType == 0x0B) // EFI_FV_FILETYPE_FIRMWARE_VOLUME_IMAGE + { + VolumeFound = true; + } + else + { + FileHeaderOffset += FileSize; + + // FileHeaderOffset in Volume-body must be Align 8 + // In the file-header-attributes the file-alignment relative to the start of the volume is always set to 1, + // so that alignment can be ignored. + FileHeaderOffset = ByteOperations.Align(VolumeHeaderOffset + VolumeHeaderSize, FileHeaderOffset, 8); + } + } + while (!VolumeFound && (FileHeaderOffset < (VolumeHeaderOffset + VolumeSize))); + + if (!VolumeFound) + throw new BadImageFormatException(); + + // Look in file for section of type EFI_SECTION_GUID_DEFINED (0x02) + + SectionHeaderOffset = FileHeaderOffset + 0x18; + int SectionType; + UInt32 SectionSize; + UInt16 SectionHeaderSize = 0; + + bool DecompressedVolumeFound = false; + do + { + SectionType = ByteOperations.ReadUInt8(Binary, SectionHeaderOffset + 0x03); + SectionSize = ByteOperations.ReadUInt24(Binary, SectionHeaderOffset + 0x00); + + if (SectionType == 0x02) // EFI_SECTION_GUID_DEFINED + { + SectionHeaderSize = ByteOperations.ReadUInt16(Binary, SectionHeaderOffset + 0x14); + DecompressedVolumeFound = true; + } + else + { + SectionHeaderOffset += SectionSize; + + // SectionHeaderOffset in File-body must be Align 4 + SectionHeaderOffset = ByteOperations.Align(FileHeaderOffset + 0x18, SectionHeaderOffset, 4); + } + } + while (!DecompressedVolumeFound && (SectionHeaderOffset < (FileHeaderOffset + FileSize))); + + if (!DecompressedVolumeFound) + throw new BadImageFormatException(); + + // Decompress subvolume + CompressedSubImageOffset = SectionHeaderOffset + SectionHeaderSize; + CompressedSubImageSize = SectionSize - SectionHeaderSize; + + // DECOMPRESS HERE + DecompressedImage = LZMA.Decompress(Binary, CompressedSubImageOffset, CompressedSubImageSize); + + // Extracted volume contains Sections at its root level + + DecompressedVolumeSectionHeaderOffset = 0; + DecompressedVolumeFound = false; + do + { + SectionType = ByteOperations.ReadUInt8(DecompressedImage, DecompressedVolumeSectionHeaderOffset + 0x03); + SectionSize = ByteOperations.ReadUInt24(DecompressedImage, DecompressedVolumeSectionHeaderOffset + 0x00); + SectionHeaderSize = ByteOperations.ReadUInt16(DecompressedImage, DecompressedVolumeSectionHeaderOffset + 0x14); + + if (SectionType == 0x17) // EFI_SECTION_FIRMWARE_VOLUME_IMAGE + { + DecompressedVolumeFound = true; + } + else + { + DecompressedVolumeSectionHeaderOffset += SectionSize; + + // SectionHeaderOffset in File-body must be Align 4 + DecompressedVolumeSectionHeaderOffset = ByteOperations.Align(FileHeaderOffset + 0x18, DecompressedVolumeSectionHeaderOffset, 4); + } + } + while (!DecompressedVolumeFound && (DecompressedVolumeSectionHeaderOffset < DecompressedImage.Length)); + + if (!DecompressedVolumeFound) + throw new BadImageFormatException(); + + DecompressedVolumeHeaderOffset = DecompressedVolumeSectionHeaderOffset + 4; + + // PARSE COMPRESSED VOLUME + VolumeHeaderMagic = ByteOperations.ReadAsciiString(DecompressedImage, DecompressedVolumeHeaderOffset + 0x28, 0x04); + if (VolumeHeaderMagic != "_FVH") + throw new BadImageFormatException(); + + if (!VerifyVolumeChecksum(DecompressedImage, DecompressedVolumeHeaderOffset)) + throw new BadImageFormatException(); + + Int32 DecompressedVolumeSize = ByteOperations.ReadInt32(DecompressedImage, DecompressedVolumeHeaderOffset + 0x20); // TODO: This is actually a QWORD + UInt16 DecompressedVolumeHeaderSize = ByteOperations.ReadUInt16(DecompressedImage, DecompressedVolumeHeaderOffset + 0x30); + + // The files in this decompressed volume are the real EFI's. + UInt32 DecompressedFileHeaderOffset = DecompressedVolumeHeaderOffset + DecompressedVolumeHeaderSize; + EFI CurrentEFI; + do + { + if ((DecompressedFileHeaderOffset + 0x18) >= (DecompressedVolumeHeaderOffset + DecompressedVolumeSize)) + break; + + bool ContentFound = false; + for (int i = 0; i < 0x18; i++) + { + if (DecompressedImage[DecompressedFileHeaderOffset + i] != PaddingByteValue) + { + ContentFound = true; + break; + } + } + if (!ContentFound) + break; + + FileSize = ByteOperations.ReadUInt24(DecompressedImage, DecompressedFileHeaderOffset + 0x14); + + if ((DecompressedFileHeaderOffset + FileSize) >= (DecompressedVolumeHeaderOffset + DecompressedVolumeSize)) + break; + + if (!VerifyFileChecksum(DecompressedImage, DecompressedFileHeaderOffset)) + throw new BadImageFormatException(); + + CurrentEFI = new EFI(); + + CurrentEFI.Type = ByteOperations.ReadUInt8(DecompressedImage, DecompressedFileHeaderOffset + 0x12); + byte[] FileGuidBytes = new byte[0x10]; + System.Buffer.BlockCopy(DecompressedImage, (int)DecompressedFileHeaderOffset + 0x00, FileGuidBytes, 0, 0x10); + CurrentEFI.Guid = new Guid(FileGuidBytes); + + // Parse sections of the EFI + CurrentEFI.FileOffset = DecompressedFileHeaderOffset; + UInt32 DecompressedSectionHeaderOffset = DecompressedFileHeaderOffset + 0x18; + do + { + SectionType = ByteOperations.ReadUInt8(DecompressedImage, DecompressedSectionHeaderOffset + 0x03); + SectionSize = ByteOperations.ReadUInt24(DecompressedImage, DecompressedSectionHeaderOffset + 0x00); + + // SectionTypes that are relevant here: + // 0x10 = PE File + // 0x19 = RAW + // 0x15 = Description + // Not all section headers in the UEFI specs are 4 bytes long, + // but the sections that are used in Windows Phone EFI's all have a header of 4 bytes. + if (SectionType == 0x15) + { + CurrentEFI.Name = ByteOperations.ReadUnicodeString(DecompressedImage, DecompressedSectionHeaderOffset + 0x04, SectionSize - 0x04).TrimEnd(new char[] { (char)0, ' ' }); + } + else if ((SectionType == 0x10) || (SectionType == 0x19)) + { + CurrentEFI.SectionOffset = DecompressedSectionHeaderOffset; + CurrentEFI.BinaryOffset = DecompressedSectionHeaderOffset + 0x04; + CurrentEFI.Size = SectionSize - 0x04; + } + + DecompressedSectionHeaderOffset += SectionSize; + + // SectionHeaderOffset in File-body must be Align 4 + DecompressedSectionHeaderOffset = ByteOperations.Align(DecompressedFileHeaderOffset + 0x18, DecompressedSectionHeaderOffset, 4); + } + while (DecompressedSectionHeaderOffset < (DecompressedFileHeaderOffset + FileSize)); + + DecompressedFileHeaderOffset += FileSize; + + // FileHeaderOffset in Volume-body must be Align 8 + // In the file-header-attributes the file-alignment relative to the start of the volume is always set to 1, + // so that alignment can be ignored. + DecompressedFileHeaderOffset = ByteOperations.Align(DecompressedVolumeHeaderOffset + DecompressedVolumeHeaderSize, DecompressedFileHeaderOffset, 8); + + EFIs.Add(CurrentEFI); + } + while (DecompressedFileHeaderOffset < (DecompressedVolumeHeaderOffset + DecompressedVolumeSize)); + } + + internal byte[] GetFile(string Name) + { + EFI File = EFIs.Where(f => (string.Compare(Name, f.Name, true) == 0) || (string.Compare(Name, f.Guid.ToString(), true) == 0)).FirstOrDefault(); + if (File == null) + return null; + + byte[] Bytes = new byte[File.Size]; + Buffer.BlockCopy(DecompressedImage, (int)File.BinaryOffset, Bytes, 0, (int)File.Size); + + return Bytes; + } + + internal byte[] GetFile(Guid Guid) + { + EFI File = EFIs.Where(f => (Guid == f.Guid)).FirstOrDefault(); + if (File == null) + return null; + + byte[] Bytes = new byte[File.Size]; + Buffer.BlockCopy(DecompressedImage, (int)File.BinaryOffset, Bytes, 0, (int)File.Size); + + return Bytes; + } + + internal void ReplaceFile(string Name, byte[] Binary) + { + EFI File = EFIs.Where(f => (string.Compare(Name, f.Name, true) == 0) || (string.Compare(Name, f.Guid.ToString(), true) == 0)).FirstOrDefault(); + if (File == null) + throw new ArgumentOutOfRangeException(); + + UInt32 OldBinarySize = File.Size; + UInt32 NewBinarySize = (UInt32)Binary.Length; + + UInt32 OldSectionPadding = ByteOperations.Align(0, OldBinarySize, 4) - OldBinarySize; + UInt32 NewSectionPadding = ByteOperations.Align(0, NewBinarySize, 4) - NewBinarySize; + + UInt32 OldFileSize = ByteOperations.ReadUInt24(DecompressedImage, File.FileOffset + 0x14); + UInt32 NewFileSize = OldFileSize - OldBinarySize - OldSectionPadding + NewBinarySize + NewSectionPadding; + + UInt32 OldFilePadding = ByteOperations.Align(0, OldFileSize, 8) - OldFileSize; + UInt32 NewFilePadding = ByteOperations.Align(0, NewFileSize, 8) - NewFileSize; + + if ((OldBinarySize + OldSectionPadding) != (NewBinarySize + NewSectionPadding)) + { + byte[] NewImage = new byte[DecompressedImage.Length - OldFileSize - OldFilePadding + NewFileSize + NewFilePadding]; // Also preserve space for File-alignement here + + // Copy Volume-head and File-head + Buffer.BlockCopy(DecompressedImage, 0, NewImage, 0, (int)File.BinaryOffset); + + // Copy new binary + Buffer.BlockCopy(Binary, 0, NewImage, (int)File.BinaryOffset, Binary.Length); + + // Insert section-padding + for (int i = 0; i < NewSectionPadding; i++) + NewImage[File.BinaryOffset + NewBinarySize + i] = PaddingByteValue; + + // Copy file-tail + Buffer.BlockCopy( + DecompressedImage, + (int)(File.BinaryOffset + OldBinarySize + OldSectionPadding), + NewImage, + (int)(File.BinaryOffset + NewBinarySize + NewSectionPadding), + (int)(File.FileOffset + OldFileSize - File.BinaryOffset - OldBinarySize - OldSectionPadding)); + + // Insert file-padding + for (int i = 0; i < NewFilePadding; i++) + NewImage[File.BinaryOffset + NewFileSize + i] = PaddingByteValue; + + // Copy volume-tail + Buffer.BlockCopy( + DecompressedImage, + (int)(File.FileOffset + OldFileSize + OldFilePadding), + NewImage, + (int)(File.FileOffset + NewFileSize + NewFilePadding), + (int)(DecompressedImage.Length - File.FileOffset - OldFileSize - OldFilePadding)); + + Int32 NewOffset = (int)(NewFileSize + NewFilePadding) - (int)(OldFileSize - OldFilePadding); + + // Fix section-size + ByteOperations.WriteUInt24(NewImage, File.SectionOffset, (UInt32)(ByteOperations.ReadUInt24(NewImage, File.SectionOffset) + NewOffset)); + + // Fix file-size + ByteOperations.WriteUInt24(NewImage, File.FileOffset + 0x14, (UInt32)(ByteOperations.ReadUInt24(NewImage, File.FileOffset + 0x14) + NewOffset)); + + // Fix volume-size - TODO: This is actually a QWORD + ByteOperations.WriteUInt32(NewImage, DecompressedVolumeHeaderOffset + 0x20, (UInt32)(ByteOperations.ReadUInt32(NewImage, DecompressedVolumeHeaderOffset + 0x20) + NewOffset)); + + // Fix section-size + ByteOperations.WriteUInt24(NewImage, DecompressedVolumeSectionHeaderOffset, (UInt32)(ByteOperations.ReadUInt24(NewImage, DecompressedVolumeSectionHeaderOffset) + NewOffset)); + + DecompressedImage = NewImage; + + // Modify all sizes in EFI's + foreach (EFI CurrentFile in EFIs) + { + if (CurrentFile.FileOffset > File.FileOffset) + { + CurrentFile.FileOffset = (UInt32)(CurrentFile.FileOffset + NewOffset); + CurrentFile.SectionOffset = (UInt32)(CurrentFile.SectionOffset + NewOffset); + CurrentFile.BinaryOffset = (UInt32)(CurrentFile.BinaryOffset + NewOffset); + } + } + } + else + { + Buffer.BlockCopy(Binary, 0, DecompressedImage, (int)File.BinaryOffset, Binary.Length); + for (int i = 0; i < NewSectionPadding; i++) + DecompressedImage[File.BinaryOffset + Binary.Length + i] = PaddingByteValue; + } + + // Calculate File-checksum + CalculateFileChecksum(DecompressedImage, File.FileOffset); + + // Calculate Volume-checksum + CalculateVolumeChecksum(DecompressedImage, DecompressedVolumeHeaderOffset); + } + + internal byte[] Rebuild() + { + // The new binary will include the Qualcomm header, but not the signature and certificates, because they won't match anyway. + byte[] NewBinary = new byte[Binary.Length]; + Buffer.BlockCopy(Binary, 0, NewBinary, 0, (int)CompressedSubImageOffset); + + ByteOperations.WriteUInt32(NewBinary, 0x10, ByteOperations.ReadUInt32(NewBinary, 0x14)); // Complete image size - does not include signature and certs anymore + ByteOperations.WriteUInt32(NewBinary, 0x18, 0x00000000); // Address of signature + ByteOperations.WriteUInt32(NewBinary, 0x1C, 0x00000000); // Signature length + ByteOperations.WriteUInt32(NewBinary, 0x20, 0x00000000); // Address of certificate + ByteOperations.WriteUInt32(NewBinary, 0x24, 0x00000000); // Certificate length + + // Compress volume + byte[] NewCompressedImage = LZMA.Compress(DecompressedImage, 0, (UInt32)DecompressedImage.Length); + + // Replace compressed volume and add correct padding + // First copy new image + Buffer.BlockCopy(NewCompressedImage, 0, NewBinary, (int)CompressedSubImageOffset, NewCompressedImage.Length); + + // Determine padding + UInt32 OldSectionPadding = ByteOperations.Align(0, CompressedSubImageSize, 4) - CompressedSubImageSize; + UInt32 NewSectionPadding = ByteOperations.Align(0, (UInt32)NewCompressedImage.Length, 4) - (UInt32)NewCompressedImage.Length; + UInt32 OldFileSize = ByteOperations.ReadUInt24(Binary, FileHeaderOffset + 0x14); + + // Filesize includes fileheader. But it does not include the padding-bytes. Not even the padding bytes of the last section. + UInt32 NewFileSize; + if ((CompressedSubImageOffset + CompressedSubImageSize + OldSectionPadding) >= (FileHeaderOffset + OldFileSize)) + // Compressed image is the last section of this file + NewFileSize = CompressedSubImageOffset - FileHeaderOffset + (UInt32)NewCompressedImage.Length; + else + // Compressed image is NOT the last section of this file + NewFileSize = OldFileSize - CompressedSubImageSize - OldSectionPadding + (UInt32)NewCompressedImage.Length + NewSectionPadding; + + // Add section padding + for (int i = 0; i < NewSectionPadding; i++) + NewBinary[CompressedSubImageOffset + NewCompressedImage.Length + i] = PaddingByteValue; + + // If there are more bytes after the section padding of the compressed image, then copy the trailing sections + if (((Int32)FileHeaderOffset + OldFileSize - CompressedSubImageOffset - CompressedSubImageSize - OldSectionPadding) > 0) + Buffer.BlockCopy(Binary, (int)(CompressedSubImageOffset + CompressedSubImageSize + OldSectionPadding), NewBinary, + (int)(CompressedSubImageOffset + NewCompressedImage.Length + NewSectionPadding), + (int)(FileHeaderOffset + OldFileSize - CompressedSubImageOffset - CompressedSubImageSize - OldSectionPadding)); + + // Add file padding + // Filesize does not include last section padding or file padding + UInt32 OldFilePadding = ByteOperations.Align(0, OldFileSize, 8) - OldFileSize; + UInt32 NewFilePadding = ByteOperations.Align(0, NewFileSize, 8) - NewFileSize; + for (int i = 0; i < NewFilePadding; i++) + NewBinary[FileHeaderOffset + NewFileSize + i] = PaddingByteValue; + + if (NewCompressedImage.Length > CompressedSubImageSize) + { + Buffer.BlockCopy(Binary, (int)(FileHeaderOffset + OldFileSize + OldFilePadding), NewBinary, (int)(FileHeaderOffset + NewFileSize + NewFilePadding), + (int)(VolumeHeaderOffset + VolumeSize - FileHeaderOffset - NewFileSize - NewFilePadding)); + } + else + { + Buffer.BlockCopy(Binary, (int)(FileHeaderOffset + OldFileSize + OldFilePadding), NewBinary, (int)(FileHeaderOffset + NewFileSize + NewFilePadding), + (int)(VolumeHeaderOffset + VolumeSize - FileHeaderOffset - OldFileSize - OldFilePadding)); + for (int i = (int)(VolumeHeaderOffset + VolumeSize - OldFileSize - OldFilePadding + NewFileSize + NewFilePadding); i < VolumeHeaderOffset + VolumeSize; i++) + NewBinary[i] = PaddingByteValue; + } + CompressedSubImageSize = (UInt32)NewCompressedImage.Length; + + // Fix section + ByteOperations.WriteUInt24(NewBinary, SectionHeaderOffset, CompressedSubImageSize + ByteOperations.ReadUInt16(Binary, SectionHeaderOffset + 0x14)); + + // Fix file + ByteOperations.WriteUInt24(NewBinary, FileHeaderOffset + 0x14, NewFileSize); + CalculateFileChecksum(NewBinary, FileHeaderOffset); + + // Fix volume (volume size is fixed) + CalculateVolumeChecksum(NewBinary, VolumeHeaderOffset); + + Binary = NewBinary; + return Binary; + } + + private void CalculateVolumeChecksum(byte[] Image, UInt32 Offset) + { + UInt16 VolumeHeaderSize = ByteOperations.ReadUInt16(Image, Offset + 0x30); + ByteOperations.WriteUInt16(Image, Offset + 0x32, 0); // Clear checksum + UInt16 NewChecksum = ByteOperations.CalculateChecksum16(Image, Offset, VolumeHeaderSize); + ByteOperations.WriteUInt16(Image, Offset + 0x32, NewChecksum); + } + + private bool VerifyVolumeChecksum(byte[] Image, UInt32 Offset) + { + UInt16 VolumeHeaderSize = ByteOperations.ReadUInt16(Image, Offset + 0x30); + byte[] Header = new byte[VolumeHeaderSize]; + System.Buffer.BlockCopy(Image, (int)Offset, Header, 0, VolumeHeaderSize); + ByteOperations.WriteUInt16(Header, 0x32, 0); // Clear checksum + UInt16 CurrentChecksum = ByteOperations.ReadUInt16(Image, Offset + 0x32); + UInt16 NewChecksum = ByteOperations.CalculateChecksum16(Header, 0, VolumeHeaderSize); + return (CurrentChecksum == NewChecksum); + } + + private void CalculateFileChecksum(byte[] Image, UInt32 Offset) + { + UInt16 FileHeaderSize = 0x18; + UInt32 FileSize = ByteOperations.ReadUInt24(Image, Offset + 0x14); + + ByteOperations.WriteUInt16(Image, Offset + 0x10, 0); // Clear checksum + byte NewChecksum = ByteOperations.CalculateChecksum8(Image, Offset, (UInt32)FileHeaderSize - 1); + ByteOperations.WriteUInt8(Image, Offset + 0x10, NewChecksum); // File-Header checksum + + byte FileAttribs = ByteOperations.ReadUInt8(Image, Offset + 0x13); + if ((FileAttribs & 0x40) > 0) + { + // Calculate file checksum + byte CalculatedFileChecksum = ByteOperations.CalculateChecksum8(Image, Offset + FileHeaderSize, FileSize - FileHeaderSize); + ByteOperations.WriteUInt8(Image, Offset + 0x11, CalculatedFileChecksum); + } + else + { + // Fixed file checksum + ByteOperations.WriteUInt8(Image, Offset + 0x11, 0xAA); + } + } + + private bool VerifyFileChecksum(byte[] Image, UInt32 Offset) + { + // This function only checks fixed checksum-values 0x55 and 0xAA. + + UInt16 FileHeaderSize = 0x18; + UInt32 FileSize = ByteOperations.ReadUInt24(Image, Offset + 0x14); + + byte[] Header = new byte[FileHeaderSize - 1]; + System.Buffer.BlockCopy(Image, (int)Offset, Header, 0, FileHeaderSize - 1); + ByteOperations.WriteUInt16(Header, 0x10, 0); // Clear checksum + byte CurrentHeaderChecksum = ByteOperations.ReadUInt8(Image, Offset + 0x10); + byte CalculatedHeaderChecksum = ByteOperations.CalculateChecksum8(Header, 0, (UInt32)FileHeaderSize - 1); + + if (CurrentHeaderChecksum != CalculatedHeaderChecksum) + return false; + + byte FileAttribs = ByteOperations.ReadUInt8(Image, Offset + 0x13); + byte CurrentFileChecksum = ByteOperations.ReadUInt8(Image, Offset + 0x11); + if ((FileAttribs & 0x40) > 0) + { + // Calculate file checksum + byte CalculatedFileChecksum = ByteOperations.CalculateChecksum8(Image, Offset + FileHeaderSize, FileSize - FileHeaderSize); + if (CurrentFileChecksum != CalculatedFileChecksum) + return false; + } + else + { + // Fixed file checksum + if ((CurrentFileChecksum != 0xAA) && (CurrentFileChecksum != 0x55)) + return false; + } + + return true; + } + + private void ClearEfiChecksum(byte[] EfiFile) + { + UInt32 PEHeaderOffset = ByteOperations.ReadUInt32(EfiFile, 0x3C); + ByteOperations.WriteUInt32(EfiFile, PEHeaderOffset + 0x58, 0); + } + + // Magic! + internal byte[] Patch() + { + byte[] SecurityDxe = GetFile("SecurityDxe"); + ClearEfiChecksum(SecurityDxe); + + UInt32? PatchOffset = ByteOperations.FindPattern(SecurityDxe, + new byte[] { 0xF0, 0x41, 0x2D, 0xE9, 0xFF, 0xFF, 0xB0, 0xE1, 0x28, 0xD0, 0x4D, 0xE2, 0xFF, 0xFF, 0xA0, 0xE1, 0x00, 0x00, 0xFF, 0x13, 0x20, 0xFF, 0xA0, 0xE3 }, + new byte[] { 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00 }, + null); + + if (PatchOffset == null) + throw new BadImageFormatException(); + + Buffer.BlockCopy(new byte[] { 0x00, 0x00, 0xA0, 0xE3, 0x1E, 0xFF, 0x2F, 0xE1 }, 0, SecurityDxe, (int)PatchOffset, 8); + + ReplaceFile("SecurityDxe", SecurityDxe); + + byte[] SecurityServicesDxe = GetFile("SecurityServicesDxe"); + ClearEfiChecksum(SecurityServicesDxe); + + PatchOffset = ByteOperations.FindPattern(SecurityServicesDxe, + new byte[] { 0x10, 0xFF, 0xFF, 0xE5, 0x80, 0xFF, 0x10, 0xE3, 0xFF, 0xFF, 0xFF, 0x0A }, + new byte[] { 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00 }, + null); + + if (PatchOffset == null) + throw new BadImageFormatException(); + + ByteOperations.WriteUInt8(SecurityServicesDxe, (UInt32)PatchOffset + 0x0B, 0xEA); + + PatchOffset = ByteOperations.FindPattern(SecurityServicesDxe, + new byte[] { 0x11, 0xFF, 0xFF, 0xE5, 0x40, 0xFF, 0x10, 0xE3, 0xFF, 0xFF, 0xFF, 0x0A }, + new byte[] { 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00 }, + null); + + if (PatchOffset == null) + throw new BadImageFormatException(); + + ByteOperations.WriteUInt8(SecurityServicesDxe, (UInt32)PatchOffset + 0x0B, 0xEA); + + ReplaceFile("SecurityServicesDxe", SecurityServicesDxe); + + return Rebuild(); + } + } +} diff --git a/PatchDefinitions.xml b/PatchDefinitions.xml new file mode 100644 index 0000000..021e400 --- /dev/null +++ b/PatchDefinitions.xml @@ -0,0 +1,4314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1522882 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,55 @@ +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WPinternals")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WPinternals")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// 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)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("2.6.*")] +// [assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs new file mode 100644 index 0000000..1e8273e --- /dev/null +++ b/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18449 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WPinternals.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WPinternals.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/Properties/Resources.resx b/Properties/Resources.resx new file mode 100644 index 0000000..ffecec8 --- /dev/null +++ b/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs new file mode 100644 index 0000000..063ceab --- /dev/null +++ b/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.18449 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace WPinternals.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/Properties/Settings.settings b/Properties/Settings.settings new file mode 100644 index 0000000..8f2fd95 --- /dev/null +++ b/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/SB b/SB new file mode 100644 index 0000000000000000000000000000000000000000..45d915c1f4a6e1e1e28e05cadae4cf702d7902d5 GIT binary patch literal 736 zcmezGoS$1zlv-Szni7y$RFYYenV-kN$iTp$3d9TyEMQW;n*+*wdwH){jH3v{gW|g) zW|}!VO;M2?W=c(4)3}dz9PDkHyU9(2iSzW=^@73L5jQ&e9|+(0!8C1OgIJe-f|THy z$h%Jj%bfr0yLniWFZ~%``MbJ*Mn5&XI{x;o>v(38Z2vFgTHU^BAloSb4gW*7hMDc^ zQtF!XKJ`oaFNZC@xnJ)-dUWJZ#^-P6XSXiz@LKk?XX@+Q&8Hu)p11jXVR4Y8oy@7I z&(c2yf)xnuSCfsUW~x7~?%Q-V`0Ngg+BhY4==3v>t=T(q^5VOMzZazkGf__y2mSewtkbzsrB9)czRz| zj+^r5l$!H_+p1PciZAcK?LGD4&F-fQ+kY>fJ9FZi(sZ69$KU)B+-*4hcUbY;FuTR` zDu1QM-n?|~J0yLO>%$MzLb6jAukJVz|0eZIDKMtMp7EPcVG8(B{)g@M6^=)sgRRx?VoGE7rd`6-maeKCiu}NSz%FBnoa-xRd>24n}Dq&pZG98 zYuo8}4uLKcU-$MbtTT5xzvca$$Owu3rcrzUF4mej-ObNi^7QZd@p=32+J66-BeN$v zrq*=M&R5bq1uhqwPg~sc`(kJNM}>D!cZel_of+OWdDrKQ=XUw6aWjA3Suthtn(SGU z(eLZm=$pAkM|&T>zi&@nQL;*Lv*h$7Bkk} z{ZFplXn5;Zl+?d(Prpuwazc z{Nv`8XV>@MG*2$R?*2+n|NqwGVP#jX-YtA_R+)_=?^~44db`Owz~za)nUUT%pzohO ZfB5Hn-FJ!%WnlPYUB}q8QBVPt3jol{AJ_l@ literal 0 HcmV?d00001 diff --git a/Terminal.cs b/Terminal.cs new file mode 100644 index 0000000..7911d4f --- /dev/null +++ b/Terminal.cs @@ -0,0 +1,68 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; + +namespace WPinternals +{ + internal static class Terminal + { + public static TerminalResponse Parse(byte[] Buffer, int Offset) + { + TerminalResponse Response = new TerminalResponse(); + + // Get root node + if (Buffer.Length >= (Offset + 8)) + { + int NodeNumber = BitConverter.ToInt32(Buffer, Offset); + int NodeSize = BitConverter.ToInt32(Buffer, Offset + 4); + int End = NodeSize + Offset + 8; + int Index = Offset + 8; + if ((NodeNumber == 0x10000) && (End <= Buffer.Length)) + { + // Get subnodes + while (Index < End) + { + NodeNumber = BitConverter.ToInt32(Buffer, Index); + NodeSize = BitConverter.ToInt32(Buffer, Index + 4); + byte[] Raw = new byte[NodeSize]; + Array.Copy(Buffer, Index + 8, Raw, 0, NodeSize); + Response.RawEntries.Add(NodeNumber, Raw); + Index += NodeSize + 8; + } + } + } + + // Parse subnodes + Response.RawEntries.TryGetValue(3, out Response.PublicId); + Response.RawEntries.TryGetValue(7, out Response.RootKeyHash); + + return Response; + } + } + + internal class TerminalResponse + { + public Dictionary RawEntries = new Dictionary(); + public byte[] PublicId = null; + public byte[] RootKeyHash = null; + } +} diff --git a/TestCode.cs b/TestCode.cs new file mode 100644 index 0000000..bc028f0 --- /dev/null +++ b/TestCode.cs @@ -0,0 +1,81 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading.Tasks; + +namespace WPinternals +{ + internal static class TestCode + { + internal static async Task Test(System.Threading.SynchronizationContext UIContext) + { + // To avoid warnings when there is no code here. + await Task.Run(() => { }); + + // PhoneNotifierViewModel Notifier = new PhoneNotifierViewModel(); + // Notifier.Start(); + // await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_MassStorage); + // MassStorage MassStorage = (MassStorage)Notifier.CurrentModel; + } + + internal static async Task TestProgrammer(System.Threading.SynchronizationContext UIContext, string ProgrammerPath) + { + LogFile.BeginAction("TestProgrammer"); + try + { + LogFile.Log("Starting Firehose Test", LogType.FileAndConsole); + + PhoneNotifierViewModel Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + if (Notifier.CurrentInterface == PhoneInterfaces.Qualcomm_Download) + LogFile.Log("Phone found in emergency mode", LogType.FileAndConsole); + else + { + LogFile.Log("Phone needs to be switched to emergency mode.", LogType.FileAndConsole); + await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash); + PhoneInfo Info = ((NokiaFlashModel)Notifier.CurrentModel).ReadPhoneInfo(); + Info.Log(LogType.ConsoleOnly); + await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Qualcomm_Download); + if (Notifier.CurrentInterface != PhoneInterfaces.Qualcomm_Download) + throw new WPinternalsException("Switching mode failed."); + LogFile.Log("Phone is in emergency mode.", LogType.FileAndConsole); + } + + // Send and start programmer + QualcommSerial Serial = (QualcommSerial)Notifier.CurrentModel; + QualcommSahara Sahara = new QualcommSahara(Serial); + + if (await Sahara.Reset(ProgrammerPath)) + LogFile.Log("Emergency programmer test succeeded", LogType.FileAndConsole); + else + LogFile.Log("Emergency programmer test failed", LogType.FileAndConsole); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("TestProgrammer"); + } + } + } +} diff --git a/ViewModels/AboutViewModel.cs b/ViewModels/AboutViewModel.cs new file mode 100644 index 0000000..c626539 --- /dev/null +++ b/ViewModels/AboutViewModel.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +namespace WPinternals +{ + internal class AboutViewModel: ContextViewModel + { + internal AboutViewModel() : base() { } + + public int MajorVersion + { + get + { + return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Major; + } + } + + public int MinorVersion + { + get + { + return System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Minor; + } + } + } +} diff --git a/ViewModels/BackupTargetSelectionViewModel.cs b/ViewModels/BackupTargetSelectionViewModel.cs new file mode 100644 index 0000000..8364f0a --- /dev/null +++ b/ViewModels/BackupTargetSelectionViewModel.cs @@ -0,0 +1,216 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; + +namespace WPinternals +{ + internal class BackupTargetSelectionViewModel: ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + private Action BackupCallback; + private Action BackupArchiveCallback; + internal Action SwitchToUnlockBoot; + + internal BackupTargetSelectionViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToUnlockBoot, Action BackupArchiveCallback, Action BackupCallback) + : base() + { + this.PhoneNotifier = PhoneNotifier; + this.BackupCallback = BackupCallback; + this.BackupArchiveCallback = BackupArchiveCallback; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + + this.PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + this.PhoneNotifier.DeviceRemoved += DeviceRemoved; + + new Thread(() => EvaluateViewState()).Start(); + } + + private string _ArchivePath; + public string ArchivePath + { + get + { + return _ArchivePath; + } + set + { + if (value != _ArchivePath) + { + _ArchivePath = value; + OnPropertyChanged("ArchivePath"); + } + } + } + + private string _EFIESPPath; + public string EFIESPPath + { + get + { + return _EFIESPPath; + } + set + { + if (value != _EFIESPPath) + { + _EFIESPPath = value; + OnPropertyChanged("EFIESPPath"); + } + } + } + + private string _MainOSPath; + public string MainOSPath + { + get + { + return _MainOSPath; + } + set + { + if (value != _MainOSPath) + { + _MainOSPath = value; + OnPropertyChanged("MainOSPath"); + } + } + } + + private string _DataPath; + public string DataPath + { + get + { + return _DataPath; + } + set + { + if (value != _DataPath) + { + _DataPath = value; + OnPropertyChanged("DataPath"); + } + } + } + + private bool _IsPhoneDisconnected; + public bool IsPhoneDisconnected + { + get + { + return _IsPhoneDisconnected; + } + set + { + if (value != _IsPhoneDisconnected) + { + _IsPhoneDisconnected = value; + OnPropertyChanged("IsPhoneDisconnected"); + } + } + } + + private bool _IsPhoneInMassStorage; + public bool IsPhoneInMassStorage + { + get + { + return _IsPhoneInMassStorage; + } + set + { + if (value != _IsPhoneInMassStorage) + { + _IsPhoneInMassStorage = value; + OnPropertyChanged("IsPhoneInMassStorage"); + } + } + } + + private bool _IsPhoneInOtherMode; + public bool IsPhoneInOtherMode + { + get + { + return _IsPhoneInOtherMode; + } + set + { + if (value != _IsPhoneInOtherMode) + { + _IsPhoneInOtherMode = value; + OnPropertyChanged("IsPhoneInOtherMode"); + } + } + } + + private DelegateCommand _BackupArchiveCommand; + public DelegateCommand BackupArchiveCommand + { + get + { + if (_BackupArchiveCommand == null) + { + _BackupArchiveCommand = new DelegateCommand(() => { BackupArchiveCallback(ArchivePath); }, () => ((ArchivePath != null) && (PhoneNotifier.CurrentInterface != null))); + } + return _BackupArchiveCommand; + } + } + + private DelegateCommand _BackupCommand; + public DelegateCommand BackupCommand + { + get + { + if (_BackupCommand == null) + { + _BackupCommand = new DelegateCommand(() => { BackupCallback(EFIESPPath, MainOSPath, DataPath); }, () => (((EFIESPPath != null) || (MainOSPath != null) || (DataPath != null)) && (PhoneNotifier.CurrentInterface != null))); + } + return _BackupCommand; + } + } + + ~BackupTargetSelectionViewModel() + { + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + } + + void NewDeviceArrived(ArrivalEventArgs Args) + { + new Thread(() => EvaluateViewState()).Start(); + } + + void DeviceRemoved() + { + new Thread(() => EvaluateViewState()).Start(); + } + + internal override void EvaluateViewState() + { + IsPhoneDisconnected = PhoneNotifier.CurrentInterface == null; + IsPhoneInMassStorage = PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_MassStorage; + IsPhoneInOtherMode = (!IsPhoneDisconnected && !IsPhoneInMassStorage); + BackupCommand.RaiseCanExecuteChanged(); + BackupArchiveCommand.RaiseCanExecuteChanged(); + } + } +} diff --git a/ViewModels/BackupViewModel.cs b/ViewModels/BackupViewModel.cs new file mode 100644 index 0000000..cc4ae37 --- /dev/null +++ b/ViewModels/BackupViewModel.cs @@ -0,0 +1,353 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; + +namespace WPinternals +{ + internal class BackupViewModel: ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + private Action Callback; + private Action SwitchToUnlockBoot; + + internal BackupViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToUnlockBoot, Action Callback) + : base() + { + IsFlashModeOperation = true; + + this.PhoneNotifier = PhoneNotifier; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.Callback = Callback; + } + + internal override void EvaluateViewState() + { + if (!IsActive) + return; + + if (SubContextViewModel == null) + { + ActivateSubContext(new BackupTargetSelectionViewModel(PhoneNotifier, SwitchToUnlockBoot, DoBackupArchive, DoBackup)); + IsSwitchingInterface = false; + } + + if (SubContextViewModel is BackupTargetSelectionViewModel) + ((BackupTargetSelectionViewModel)SubContextViewModel).EvaluateViewState(); + } + + internal async void DoBackup(string EFIESPPath, string MainOSPath, string DataPath) + { + try + { + IsSwitchingInterface = true; + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_MassStorage, + (msg, sub) => ActivateSubContext(new BusyViewModel(msg, sub))); + BackupTask(EFIESPPath, MainOSPath, DataPath); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, Callback)); + } + } + + internal async void DoBackupArchive(string ArchivePath) + { + try + { + IsSwitchingInterface = true; + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_MassStorage, + (msg, sub) => ActivateSubContext(new BusyViewModel(msg, sub))); + BackupArchiveTask(ArchivePath); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, Callback)); + } + } + + internal void BackupTask(string EFIESPPath, string MainOSPath, string DataPath) + { + IsSwitchingInterface = false; + new Thread(() => + { + bool Result = true; + + ActivateSubContext(new BusyViewModel("Initializing backup...")); + + ulong TotalSizeSectors = 0; + int PartitionCount = 0; + + MassStorage Phone = (MassStorage)PhoneNotifier.CurrentModel; + + Phone.OpenVolume(false); + byte[] GPTBuffer = Phone.ReadSectors(1, 33); + GPT GPT = new WPinternals.GPT(GPTBuffer); + Partition Partition; + try + { + if (EFIESPPath != null) + { + Partition = GPT.Partitions.Where(p => p.Name == "EFIESP").First(); + TotalSizeSectors += Partition.SizeInSectors; + PartitionCount++; + } + + if (MainOSPath != null) + { + Partition = GPT.Partitions.Where(p => p.Name == "MainOS").First(); + TotalSizeSectors += Partition.SizeInSectors; + PartitionCount++; + } + + if (DataPath != null) + { + Partition = GPT.Partitions.Where(p => p.Name == "Data").First(); + TotalSizeSectors += Partition.SizeInSectors; + PartitionCount++; + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + + BusyViewModel Busy = new BusyViewModel("Create backup...", MaxProgressValue: TotalSizeSectors, UIContext: UIContext); + ProgressUpdater Updater = Busy.ProgressUpdater; + ActivateSubContext(Busy); + + int i = 0; + if (Result) + { + try + { + if (EFIESPPath != null) + { + i++; + Busy.Message = "Create backup of partition EFIESP (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.BackupPartition("EFIESP", EFIESPPath, Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (Result) + { + try + { + if (MainOSPath != null) + { + i++; + Busy.Message = "Create backup of partition MainOS (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.BackupPartition("MainOS", MainOSPath, Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (Result) + { + try + { + if (DataPath != null) + { + i++; + Busy.Message = "Create backup of partition Data (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.BackupPartition("Data", DataPath, Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + Phone.CloseVolume(); + + if (!Result) + { + ActivateSubContext(new MessageViewModel("Failed to create backup!", Exit)); + return; + } + + ActivateSubContext(new MessageViewModel("Successfully created a backup!", Exit)); + }).Start(); + } + + internal void BackupArchiveTask(string ArchivePath) + { + IsSwitchingInterface = false; + new Thread(() => + { + bool Result = true; + + ActivateSubContext(new BusyViewModel("Initializing backup...")); + + ulong TotalSizeSectors = 0; + int PartitionCount = 3; + + MassStorage Phone = (MassStorage)PhoneNotifier.CurrentModel; + + try + { + Phone.OpenVolume(false); + byte[] GPTBuffer = Phone.ReadSectors(1, 33); + GPT GPT = new WPinternals.GPT(GPTBuffer); + + Partition Partition; + + try + { + Partition = GPT.Partitions.Where(p => p.Name == "EFIESP").First(); + TotalSizeSectors += Partition.SizeInSectors; + + Partition = GPT.Partitions.Where(p => p.Name == "MainOS").First(); + TotalSizeSectors += Partition.SizeInSectors; + + Partition = GPT.Partitions.Where(p => p.Name == "Data").First(); + TotalSizeSectors += Partition.SizeInSectors; + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + + BusyViewModel Busy = new BusyViewModel("Create backup...", MaxProgressValue: TotalSizeSectors, UIContext: UIContext); + ProgressUpdater Updater = Busy.ProgressUpdater; + ActivateSubContext(Busy); + ZipArchiveEntry Entry; + Stream EntryStream = null; + + using (FileStream FileStream = new FileStream(ArchivePath, FileMode.Create)) + { + using (ZipArchive Archive = new ZipArchive(FileStream, ZipArchiveMode.Create)) + { + int i = 0; + + if (Result) + { + try + { + Entry = Archive.CreateEntry("EFIESP.bin", CompressionLevel.Optimal); + EntryStream = Entry.Open(); + i++; + Busy.Message = "Create backup of partition EFIESP (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.BackupPartition("EFIESP", EntryStream, Updater); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + finally + { + if (EntryStream != null) + EntryStream.Close(); + EntryStream = null; + } + } + + if (Result) + { + try + { + Entry = Archive.CreateEntry("MainOS.bin", CompressionLevel.Optimal); + EntryStream = Entry.Open(); + i++; + Busy.Message = "Create backup of partition MainOS (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.BackupPartition("MainOS", EntryStream, Updater); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + finally + { + if (EntryStream != null) + EntryStream.Close(); + EntryStream = null; + } + } + + if (Result) + { + try + { + Entry = Archive.CreateEntry("Data.bin", CompressionLevel.Optimal); + EntryStream = Entry.Open(); + i++; + Busy.Message = "Create backup of partition Data (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.BackupPartition("Data", EntryStream, Updater); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + finally + { + if (EntryStream != null) + EntryStream.Close(); + EntryStream = null; + } + } + } + } + } + catch { } + finally + { + Phone.CloseVolume(); + } + + if (!Result) + { + ActivateSubContext(new MessageViewModel("Failed to create backup!", Exit)); + return; + } + + ActivateSubContext(new MessageViewModel("Successfully created a backup!", Exit)); + }).Start(); + } + + private void Exit() + { + IsSwitchingInterface = false; + ActivateSubContext(null); + Callback(); + } + } +} diff --git a/ViewModels/BusyViewModel.cs b/ViewModels/BusyViewModel.cs new file mode 100644 index 0000000..db5760d --- /dev/null +++ b/ViewModels/BusyViewModel.cs @@ -0,0 +1,192 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; + +namespace WPinternals +{ + internal class BusyViewModel : ContextViewModel + { + private ulong MaxProgressValue = 0; + internal ProgressUpdater ProgressUpdater = null; + + // UIContext can be passed to BusyViewModel, when it needs to update progress-controls and it is created on a worker-thread. + internal BusyViewModel(string Message, string SubMessage = null, ulong? MaxProgressValue = null, SynchronizationContext UIContext = null, bool ShowAnimation = true) + { + LogFile.Log(Message); + + if (UIContext == null) + this.UIContext = SynchronizationContext.Current; + else + this.UIContext = UIContext; + + this.Message = Message; + this.SubMessage = SubMessage; + this.ShowAnimation = ShowAnimation; + if (MaxProgressValue != null) + { + ProgressPercentage = 0; + this.MaxProgressValue = (ulong)MaxProgressValue; + ProgressUpdater = new ProgressUpdater((ulong)MaxProgressValue, (p, t) => + { + if ((this.UIContext == null) || (this.UIContext == SynchronizationContext.Current)) + { + ProgressPercentage = p; + TimeRemaining = t; + } + else + { + this.UIContext.Post((s) => + { + ProgressPercentage = p; + TimeRemaining = t; + }, null); + } + }); + } + } + + internal void SetProgress(ulong Value) + { + if (ProgressUpdater != null) + UIContext.Post((s) => { ProgressUpdater.SetProgress(Value); }, null); + } + + private string _Message = null; + public string Message + { + get + { + return _Message; + } + set + { + _Message = value; + OnPropertyChanged("Message"); + } + } + + private string _SubMessage = null; + public string SubMessage + { + get + { + return _SubMessage; + } + set + { + _SubMessage = value; + OnPropertyChanged("SubMessage"); + } + } + + private int? _ProgressPercentage = null; + public int? ProgressPercentage + { + get + { + return _ProgressPercentage; + } + set + { + if (_ProgressPercentage != value) + { + _ProgressPercentage = value; + OnPropertyChanged("ProgressPercentage"); + OnPropertyChanged("ShowAnimation"); + UpdateProgressText(); + } + } + } + + private TimeSpan? _TimeRemaining = null; + public TimeSpan? TimeRemaining + { + get + { + return _TimeRemaining; + } + set + { + if (_TimeRemaining != value) + { + _TimeRemaining = value; + OnPropertyChanged("TimeRemaining"); + UpdateProgressText(); + } + } + } + + private void UpdateProgressText() + { + string NewText = null; + if (ProgressPercentage != null) + { + NewText = "Progress: " + ((int)ProgressPercentage).ToString() + "%"; + } + if (TimeRemaining != null) + { + if (NewText == null) + NewText = ""; + else + NewText += " - "; + + NewText += "Estimated time remaining: " + ((TimeSpan)TimeRemaining).ToString(@"h\:mm\:ss"); + } + ProgressText = NewText; + } + + private string _ProgressText = null; + public string ProgressText + { + get + { + return _ProgressText; + } + set + { + if (_ProgressText != value) + { + _ProgressText = value; + OnPropertyChanged("ProgressText"); + } + } + } + + private bool _ShowAnimation = true; + public bool ShowAnimation + { + get + { + return (_ProgressPercentage == null) && _ShowAnimation; + } + set + { + if (_ShowAnimation != value) + { + _ShowAnimation = value; + OnPropertyChanged("ShowAnimation"); + } + } + } + + } +} diff --git a/ViewModels/ContextViewModel.cs b/ViewModels/ContextViewModel.cs new file mode 100644 index 0000000..c7c0f47 --- /dev/null +++ b/ViewModels/ContextViewModel.cs @@ -0,0 +1,153 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.ComponentModel; +using System.Threading; + +namespace WPinternals +{ + internal class ContextViewModel : INotifyPropertyChanged + { + protected SynchronizationContext UIContext; + + public bool IsSwitchingInterface = false; + public bool IsFlashModeOperation = false; + private bool _IsActive = false; + + public event PropertyChangedEventHandler PropertyChanged = delegate { }; + + protected void OnPropertyChanged(string propertyName) + { + if ((UIContext == null) && (SynchronizationContext.Current != null)) + UIContext = SynchronizationContext.Current; + + if (this.PropertyChanged != null) + { + if (SynchronizationContext.Current == UIContext) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + else + { + UIContext.Post((s) => PropertyChanged(this, new PropertyChangedEventArgs(propertyName)), null); + } + } + } + + private ContextViewModel _SubContextViewModel; + public ContextViewModel SubContextViewModel + { + get + { + return _SubContextViewModel; + } + private set + { + if (_SubContextViewModel != null) + _SubContextViewModel.IsActive = false; + _SubContextViewModel = value; + if (_SubContextViewModel != null) + _SubContextViewModel.IsActive = IsActive; + OnPropertyChanged("SubContextViewModel"); + } + } + + internal ContextViewModel() + { + UIContext = SynchronizationContext.Current; + } + + internal ContextViewModel(MainViewModel Main): this() + { + } + + internal ContextViewModel(MainViewModel Main, ContextViewModel SubContext): this(Main) + { + SubContextViewModel = SubContext; + } + + internal bool IsActive + { + get + { + return _IsActive; + } + set + { + _IsActive = value; + } + } + + internal virtual void EvaluateViewState() + { + + } + + internal void Activate() + { + IsActive = true; + EvaluateViewState(); + if (SubContextViewModel != null) + SubContextViewModel.Activate(); + } + + internal void ActivateSubContext(ContextViewModel NewSubContext) + { + if (_SubContextViewModel != null) + _SubContextViewModel.IsActive = false; + if (NewSubContext != null) + { + if (IsActive) + NewSubContext.Activate(); + else + NewSubContext.IsActive = false; + } + SubContextViewModel = NewSubContext; + } + + internal void SetWorkingStatus(string Message, string SubMessage, ulong? MaxProgressValue, bool ShowAnimation = true, WPinternalsStatus Status = WPinternalsStatus.Undefined) + { + ActivateSubContext(new BusyViewModel(Message, SubMessage, MaxProgressValue, UIContext: UIContext, ShowAnimation: ShowAnimation)); + } + + internal void UpdateWorkingStatus(string Message, string SubMessage, ulong? CurrentProgressValue, WPinternalsStatus Status = WPinternalsStatus.Undefined) + { + if (SubContextViewModel is BusyViewModel) + { + BusyViewModel Busy = (BusyViewModel)SubContextViewModel; + if (Message != null) + { + Busy.Message = Message; + Busy.SubMessage = SubMessage; + } + if ((CurrentProgressValue != null) && (Busy.ProgressUpdater != null)) + { + try + { + Busy.ProgressUpdater.SetProgress((ulong)CurrentProgressValue); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + } + } + } + } +} diff --git a/ViewModels/DisclaimerAndNdaViewModel.cs b/ViewModels/DisclaimerAndNdaViewModel.cs new file mode 100644 index 0000000..ea7cada --- /dev/null +++ b/ViewModels/DisclaimerAndNdaViewModel.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using Microsoft.Win32; +using System; +using System.Windows; + +namespace WPinternals +{ + internal class DisclaimerAndNdaViewModel : ContextViewModel + { + Action Accepted; + + internal DisclaimerAndNdaViewModel(Action Accepted) + : base() + { + this.Accepted = Accepted; + } + + private DelegateCommand _ExitCommand = null; + public DelegateCommand ExitCommand + { + get + { + if (_ExitCommand == null) + { + _ExitCommand = new DelegateCommand(() => + { + Application.Current.Shutdown(); + }); + } + return _ExitCommand; + } + } + + private DelegateCommand _ContinueCommand = null; + public DelegateCommand ContinueCommand + { + get + { + if (_ContinueCommand == null) + { + _ContinueCommand = new DelegateCommand(() => + { + Registry.CurrentUser.OpenSubKey("Software\\WPInternals", true).SetValue("DisclaimerAccepted", 1, RegistryValueKind.DWord); + Registry.CurrentUser.OpenSubKey("Software\\WPInternals", true).SetValue("NdaAccepted", 1, RegistryValueKind.DWord); + Accepted(); + }); + } + return _ContinueCommand; + } + } + } +} diff --git a/ViewModels/DisclaimerViewModel.cs b/ViewModels/DisclaimerViewModel.cs new file mode 100644 index 0000000..317b539 --- /dev/null +++ b/ViewModels/DisclaimerViewModel.cs @@ -0,0 +1,72 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using Microsoft.Win32; +using System; +using System.Windows; + +namespace WPinternals +{ + internal delegate void DisclaimerAcceptedHandler(); + + internal class DisclaimerViewModel : ContextViewModel + { + Action Accepted; + + internal DisclaimerViewModel(Action Accepted) + : base() + { + this.Accepted = Accepted; + } + + private DelegateCommand _ExitCommand = null; + public DelegateCommand ExitCommand + { + get + { + if (_ExitCommand == null) + { + _ExitCommand = new DelegateCommand(() => + { + Application.Current.Shutdown(); + }); + } + return _ExitCommand; + } + } + + private DelegateCommand _ContinueCommand = null; + public DelegateCommand ContinueCommand + { + get + { + if (_ContinueCommand == null) + { + _ContinueCommand = new DelegateCommand(() => + { + Registry.CurrentUser.OpenSubKey("Software\\WPInternals", true).SetValue("DisclaimerAccepted", 1, RegistryValueKind.DWord); + Accepted(); + }); + } + return _ContinueCommand; + } + } + } +} diff --git a/ViewModels/DownloadsViewModel.cs b/ViewModels/DownloadsViewModel.cs new file mode 100644 index 0000000..34519a1 --- /dev/null +++ b/ViewModels/DownloadsViewModel.cs @@ -0,0 +1,871 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using Microsoft.Win32; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Windows.Data; + +namespace WPinternals +{ + internal class DownloadsViewModel: ContextViewModel + { + private PhoneNotifierViewModel Notifier; + private Timer SpeedTimer; + + internal DownloadsViewModel(PhoneNotifierViewModel Notifier) + { + IsSwitchingInterface = false; + IsFlashModeOperation = false; + this.Notifier = Notifier; + Notifier.NewDeviceArrived += Notifier_NewDeviceArrived; + + RegistryKey Key = Registry.CurrentUser.OpenSubKey(@"Software\WPInternals", true); + if (Key == null) + Key = Registry.CurrentUser.CreateSubKey(@"Software\WPInternals"); + DownloadFolder = (string)Key.GetValue("DownloadFolder", @"C:\ProgramData\WPinternals\Repository"); + Key.Close(); + + SpeedTimer = new Timer(TimerCallback, this, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); + + AddFFUCommand = new DelegateCommand(() => + { + string FFUPath = null; + + Microsoft.Win32.OpenFileDialog dlg = new Microsoft.Win32.OpenFileDialog(); + dlg.DefaultExt = ".ffu"; // Default file extension + dlg.Filter = "ROM images (.ffu)|*.ffu"; // Filter files by extension + + bool? result = dlg.ShowDialog(); + + if (result == true) + { + FFUPath = dlg.FileName; + string FFUFile = System.IO.Path.GetFileName(FFUPath); + + try + { + App.Config.AddFfuToRepository(FFUPath); + App.Config.WriteConfig(); + LastStatusText = "File \"" + FFUFile + "\" was added to the repository."; + } + catch (WPinternalsException Ex) + { + LastStatusText = "Error: " + Ex.Message + ". File \"" + FFUFile + "\" was not added."; + } + catch + { + LastStatusText = "Error: File \"" + FFUFile + "\" was not added."; + } + + } + else + { + LastStatusText = null; + } + }); + } + + private string _LastStatusText = null; + public string LastStatusText + { + get + { + return _LastStatusText; + } + set + { + _LastStatusText = value; + OnPropertyChanged("LastStatusText"); + } + } + + internal static void TimerCallback(object State) + { + foreach (DownloadEntry Entry in App.DownloadManager.DownloadList) + { + if (Entry.SpeedIndex >= 0) + { + int ArrayIndex = (int)(Entry.SpeedIndex % 10); + Entry.Speeds[ArrayIndex] = Entry.BytesReceived - Entry.LastBytesReceived; + int Count = (int)((Entry.SpeedIndex + 1) > 10 ? 10 : (Entry.SpeedIndex + 1)); + long Sum = 0; + for (int i = 0; i < Count; i++) + Sum += Entry.Speeds[i]; + Entry.Speed = Sum / Count; + if (Entry.Speed < 1000) + Entry.TimeLeft = Timeout.InfiniteTimeSpan; + else + Entry.TimeLeft = TimeSpan.FromSeconds((Entry.Size - Entry.BytesReceived) / Entry.Speed); + } + Entry.LastBytesReceived = Entry.BytesReceived; + Entry.SpeedIndex++; + } + } + + private void Notifier_NewDeviceArrived(ArrivalEventArgs Args) + { + EvaluateViewState(); + } + + internal static long GetFileLengthFromURL(string URL) + { + long Length = 0; + HttpWebRequest req = (HttpWebRequest)System.Net.HttpWebRequest.Create(URL); + req.Method = "HEAD"; + req.ServicePoint.ConnectionLimit = 10; + using (System.Net.WebResponse resp = req.GetResponse()) + { + long.TryParse(resp.Headers.Get("Content-Length"), out Length); + } + return Length; + } + + internal static string GetFileNameFromURL(string URL) + { + string FileName = System.IO.Path.GetFileName(URL); + int End = FileName.IndexOf('?'); + if (End >= 0) + FileName = FileName.Substring(0, End); + return FileName; + } + + private void Search() + { + SynchronizationContext UIContext = SynchronizationContext.Current; + SearchResultList.Clear(); + + new Thread(() => + { + string FFUURL = null; + string[] EmergencyURLs = null; + try + { + string TempProductType = ProductType.ToUpper(); + if ((TempProductType != null) && TempProductType.StartsWith("RM") && !TempProductType.StartsWith("RM-")) + TempProductType = "RM-" + TempProductType.Substring(2); + ProductType = TempProductType; + FFUURL = LumiaDownloadModel.SearchFFU(ProductType, ProductCode, OperatorCode, out TempProductType); + if (TempProductType != null) + ProductType = TempProductType; + if (ProductType != null) + EmergencyURLs = LumiaDownloadModel.SearchEmergencyFiles(ProductType); + } + catch { } + + UIContext.Post(s => + { + if (FFUURL != null) + SearchResultList.Add(new SearchResult(FFUURL, ProductType, FFUDownloaded, null)); + if (EmergencyURLs != null) + SearchResultList.Add(new SearchResult(ProductType + " emergency-files", EmergencyURLs, ProductType, EmergencyDownloaded, ProductType)); + }, null); + }).Start(); + } + + internal void Download(string URL, string Category, Action Callback, object State = null) + { + string Folder; + if (Category == null) + Folder = DownloadFolder; + else + Folder = Path.Combine(DownloadFolder, Category); + DownloadList.Add(new DownloadEntry(URL, Folder, null, Callback, State)); + } + + internal void Download(string[] URLs, string Category, Action Callback, object State = null) + { + string Folder; + if (Category == null) + Folder = DownloadFolder; + else + Folder = Path.Combine(DownloadFolder, Category); + foreach (string URL in URLs) + DownloadList.Add(new DownloadEntry(URL, Folder, URLs, Callback, State)); + } + + private void DownloadAll() + { + SynchronizationContext UIContext = SynchronizationContext.Current; + + new Thread(() => + { + string FFUURL = null; + string[] EmergencyURLs = null; + try + { + string TempProductType = ProductType.ToUpper(); + if ((TempProductType != null) && TempProductType.StartsWith("RM") && !TempProductType.StartsWith("RM-")) + TempProductType = "RM-" + TempProductType.Substring(2); + ProductType = TempProductType; + FFUURL = LumiaDownloadModel.SearchFFU(ProductType, ProductCode, OperatorCode, out TempProductType); + if (TempProductType != null) + ProductType = TempProductType; + if (ProductType != null) + EmergencyURLs = LumiaDownloadModel.SearchEmergencyFiles(ProductType); + } + catch { } + + UIContext.Post(s => + { + if (FFUURL != null) + Download(FFUURL, ProductType, FFUDownloadedAndCheckSupported, null); + if (EmergencyURLs != null) + Download(EmergencyURLs, ProductType, EmergencyDownloaded, ProductType); + }, null); + }).Start(); + } + + private void DownloadSelected() + { + IEnumerable Selection = SearchResultList.Where(r => r.IsSelected); + foreach (SearchResult Result in Selection) + App.DownloadManager.Download(Result.URLs, Result.Category, Result.Callback, Result.State); + } + + private void FFUDownloaded(string[] Files, object State) + { + App.Config.AddFfuToRepository(Files[0]); + } + + private void FFUDownloadedAndCheckSupported(string[] Files, object State) + { + App.Config.AddFfuToRepository(Files[0]); + + if (App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).Count() == 0) + { + string ProductType2 = "RM-1085"; + string URL = LumiaDownloadModel.SearchFFU(ProductType2, null, null); + Download(URL, ProductType2, FFUDownloaded, null); + } + } + + private void EmergencyDownloaded(string[] Files, object State) + { + string Type = (string)State; + string ProgrammerPath = null; + string PayloadPath = null; + + for (int i = 0; i < Files.Length; i++) + { + if (Files[i].EndsWith(".ede", StringComparison.OrdinalIgnoreCase)) + ProgrammerPath = Files[i]; + if (Files[i].EndsWith(".edp", StringComparison.OrdinalIgnoreCase)) + PayloadPath = Files[i]; + } + + if ((Type != null) && (ProgrammerPath != null) && (PayloadPath != null)) + App.Config.AddEmergencyToRepository(Type, ProgrammerPath, PayloadPath); + } + + private ObservableCollection _DownloadList = new ObservableCollection(); + public ObservableCollection DownloadList + { + get + { + return _DownloadList; + } + } + + private ObservableCollection _SearchResultList = new ObservableCollection(); + public ObservableCollection SearchResultList + { + get + { + return _SearchResultList; + } + } + + private DelegateCommand _DownloadSelectedCommand = null; + public DelegateCommand DownloadSelectedCommand + { + get + { + if (_DownloadSelectedCommand == null) + { + _DownloadSelectedCommand = new DelegateCommand(() => + { + DownloadSelected(); + }); + } + return _DownloadSelectedCommand; + } + } + + private DelegateCommand _SearchCommand = null; + public DelegateCommand SearchCommand + { + get + { + if (_SearchCommand == null) + { + _SearchCommand = new DelegateCommand(() => + { + Search(); + }); + } + return _SearchCommand; + } + } + + private DelegateCommand _DownloadAllCommand = null; + public DelegateCommand DownloadAllCommand + { + get + { + if (_DownloadAllCommand == null) + { + _DownloadAllCommand = new DelegateCommand(() => + { + DownloadAll(); + }); + } + return _DownloadAllCommand; + } + } + + private string _DownloadFolder = null; + public string DownloadFolder + { + get + { + return _DownloadFolder; + } + set + { + if (_DownloadFolder != value) + { + _DownloadFolder = value; + + try + { + Directory.CreateDirectory(_DownloadFolder); + } + catch { } + if (!Directory.Exists(_DownloadFolder)) + { + _DownloadFolder = @"C:\ProgramData\WPinternals\Repository"; + Directory.CreateDirectory(_DownloadFolder); + } + + RegistryKey Key = Registry.CurrentUser.OpenSubKey(@"Software\WPInternals", true); + + if (_DownloadFolder == null) + { + if (Key.GetValue("DownloadFolder") != null) + Key.DeleteValue("DownloadFolder"); + } + else + Key.SetValue("DownloadFolder", _DownloadFolder); + + Key.Close(); + + OnPropertyChanged("DownloadFolder"); + } + } + } + + private string _ProductCode = null; + public string ProductCode + { + get + { + return _ProductCode; + } + set + { + if (_ProductCode != value) + { + _ProductCode = value; + + OnPropertyChanged("ProductCode"); + } + } + } + + private string _ProductType = null; + public string ProductType + { + get + { + return _ProductType; + } + set + { + if (_ProductType != value) + { + _ProductType = value; + + OnPropertyChanged("ProductType"); + } + } + } + + private string _OperatorCode = null; + public string OperatorCode + { + get + { + return _OperatorCode; + } + set + { + if (_OperatorCode != value) + { + _OperatorCode = value; + + OnPropertyChanged("OperatorCode"); + } + } + } + + internal override void EvaluateViewState() + { + if (!IsActive) + return; + + if ((Notifier.CurrentInterface == PhoneInterfaces.Lumia_Flash)) + { + NokiaFlashModel LumiaFlashModel = (NokiaFlashModel)Notifier.CurrentModel; + PhoneInfo Info = LumiaFlashModel.ReadPhoneInfo(); + ProductType = Info.Type; + OperatorCode = ""; + ProductCode = Info.ProductCode; + } + else if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Normal) + { + NokiaPhoneModel LumiaNormalModel = (NokiaPhoneModel)Notifier.CurrentModel; + OperatorCode = LumiaNormalModel.ExecuteJsonMethodAsString("ReadOperatorName", "OperatorName"); // Example: 000-NL + string TempProductType = LumiaNormalModel.ExecuteJsonMethodAsString("ReadManufacturerModelName", "ManufacturerModelName"); // RM-821_eu_denmark_251 + if (TempProductType.IndexOf('_') >= 0) TempProductType = TempProductType.Substring(0, TempProductType.IndexOf('_')); + ProductType = TempProductType; + ProductCode = LumiaNormalModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); // 059Q9D7 + } + } + + private DelegateCommand _AddFFUCommand = null; + public DelegateCommand AddFFUCommand + { + get + { + return _AddFFUCommand; + } + private set + { + _AddFFUCommand = value; + } + } + } + + internal enum DownloadStatus + { + Downloading, + Ready, + Failed + }; + + internal class DownloadEntry: INotifyPropertyChanged + { + private SynchronizationContext UIContext; + public event PropertyChangedEventHandler PropertyChanged = delegate { }; + internal Action Callback; + internal object State; + internal string URL; + internal string[] URLCollection; + internal string Folder; + internal MyWebClient Client; + internal long SpeedIndex = -1; + internal long[] Speeds = new long[10]; + internal long LastBytesReceived; + internal long BytesReceived; + + internal DownloadEntry(string URL, string Folder, string[] URLCollection, Action Callback, object State) + { + UIContext = SynchronizationContext.Current; + this.URL = URL; + this.Callback = Callback; + this.State = State; + this.URLCollection = URLCollection; + this.Folder = Folder; + Directory.CreateDirectory(Folder); + Name = DownloadsViewModel.GetFileNameFromURL(URL); + Uri Uri = new Uri(URL); + Status = DownloadStatus.Downloading; + new Thread(() => + { + Size = DownloadsViewModel.GetFileLengthFromURL(URL); + + Client = new MyWebClient(); + Client.DownloadFileCompleted += Client_DownloadFileCompleted; + Client.DownloadProgressChanged += Client_DownloadProgressChanged; + Client.DownloadFileAsync(Uri, Path.Combine(Folder, DownloadsViewModel.GetFileNameFromURL(Uri.LocalPath)), null); + }).Start(); + } + + private void Client_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e) + { + + Action Finish = () => + { + Status = e.Error == null ? DownloadStatus.Ready : DownloadStatus.Failed; + App.DownloadManager.DownloadList.Remove(this); + if (Status == DownloadStatus.Ready) + { + if ((URLCollection == null) || (!URLCollection.Any(c => App.DownloadManager.DownloadList.Any(d => d.URL == c)))) // if there are no files left to download from this collection, then call the callback-function. + { + string[] Files; + if (URLCollection == null) + { + Files = new string[1]; + Files[0] = System.IO.Path.Combine(Folder, DownloadsViewModel.GetFileNameFromURL(URL)); + } + else + { + Files = new string[URLCollection.Length]; + for (int i = 0; i < URLCollection.Length; i++) + Files[i] = System.IO.Path.Combine(Folder, DownloadsViewModel.GetFileNameFromURL(URLCollection[i])); + } + + Callback(Files, State); + } + } + }; + + if (UIContext != null) + UIContext.Post(d => Finish(), null); + } + + private void Client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) + { + BytesReceived = e.BytesReceived; + Progress = e.ProgressPercentage; + } + + protected void OnPropertyChanged(string propertyName) + { + if (this.PropertyChanged != null) + { + if (SynchronizationContext.Current == UIContext) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + else + { + UIContext.Post((s) => PropertyChanged(this, new PropertyChangedEventArgs(propertyName)), null); + } + } + } + + private DownloadStatus _Status; + public DownloadStatus Status + { + get + { + return _Status; + } + set + { + if (_Status != value) + { + _Status = value; + OnPropertyChanged("Status"); + } + } + } + + private string _Name; + public string Name + { + get + { + return _Name; + } + set + { + if (_Name != value) + { + _Name = value; + OnPropertyChanged("Name"); + } + } + } + + private long _Size; + public long Size + { + get + { + return _Size; + } + set + { + if (_Size != value) + { + _Size = value; + OnPropertyChanged("Size"); + } + } + } + + private TimeSpan _TimeLeft; + public TimeSpan TimeLeft + { + get + { + return _TimeLeft; + } + set + { + if (_TimeLeft != value) + { + _TimeLeft = value; + OnPropertyChanged("TimeLeft"); + } + } + } + + private double _Speed; + public double Speed + { + get + { + return _Speed; + } + set + { + if (_Speed != value) + { + _Speed = value; + OnPropertyChanged("Speed"); + } + } + } + + private int _Progress; + public int Progress + { + get + { + return _Progress; + } + set + { + if (_Progress != value) + { + _Progress = value; + OnPropertyChanged("Progress"); + } + } + } + } + + internal class SearchResult : INotifyPropertyChanged + { + private SynchronizationContext UIContext; + public event PropertyChangedEventHandler PropertyChanged; + internal string[] URLs; + internal Action Callback; + internal object State; + internal string Category; + + internal SearchResult(string URL, string Category, Action Callback, object State) + { + UIContext = SynchronizationContext.Current; + URLs = new string[1]; + URLs[0] = URL; + Name = DownloadsViewModel.GetFileNameFromURL(URL); + this.Callback = Callback; + this.State = State; + this.Category = Category; + GetSize(); + } + + internal SearchResult(string Name, string[] URLs, string Category, Action Callback, object State) + { + UIContext = SynchronizationContext.Current; + this.URLs = URLs; + this.Name = Name; + this.Callback = Callback; + this.State = State; + this.Category = Category; + GetSize(); + } + + private void GetSize() + { + new Thread(() => + { + long CalcSize = 0; + foreach (string URL in URLs) + { + CalcSize += DownloadsViewModel.GetFileLengthFromURL(URL); + } + Size = CalcSize; + }).Start(); + } + + protected void OnPropertyChanged(string propertyName) + { + if (this.PropertyChanged != null) + { + if (SynchronizationContext.Current == UIContext) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + else + { + UIContext.Post((s) => PropertyChanged(this, new PropertyChangedEventArgs(propertyName)), null); + } + } + } + + private string _Name; + public string Name + { + get + { + return _Name; + } + set + { + if (_Name != value) + { + _Name = value; + OnPropertyChanged("Name"); + } + } + } + + private long _Size; + public long Size + { + get + { + return _Size; + } + set + { + if (_Size != value) + { + _Size = value; + OnPropertyChanged("Size"); + } + } + } + + private bool _IsSelected; + public bool IsSelected + { + get + { + return _IsSelected; + } + set + { + if (_IsSelected != value) + { + _IsSelected = value; + OnPropertyChanged("IsSelected"); + } + } + } + } + + internal class MyWebClient : WebClient + { + protected override WebRequest GetWebRequest(Uri address) + { + HttpWebRequest req = (HttpWebRequest)base.GetWebRequest(address); + req.ServicePoint.ConnectionLimit = 10; + return (WebRequest)req; + } + } + + public class DownloaderNameConvertor : IValueConverter + { + public object Convert(object value, Type targetType, + object parameter, CultureInfo culture) + { + return System.IO.Path.GetFileNameWithoutExtension((string)value); + } + + public object ConvertBack(object value, Type targetType, + object parameter, CultureInfo culture) + { + return Binding.DoNothing; + } + } + + public class DownloaderSizeConvertor : IValueConverter + { + public object Convert(object value, Type targetType, + object parameter, CultureInfo culture) + { + long? Size = value as long?; + if (Size < 1024) + return Size + " B"; + if (Size < (1024 * 1024)) + return Math.Round(((double)Size / 1024), 0) + " KB"; + return Math.Round(((double)Size / 1024 / 1024), 0) + " MB"; + } + + public object ConvertBack(object value, Type targetType, + object parameter, CultureInfo culture) + { + return Binding.DoNothing; + } + } + + public class DownloaderSpeedConvertor : IValueConverter + { + public object Convert(object value, Type targetType, + object parameter, CultureInfo culture) + { + return ((int)((double)value / 1024)).ToString() + " KB/s"; + } + + public object ConvertBack(object value, Type targetType, + object parameter, CultureInfo culture) + { + return Binding.DoNothing; + } + } + + public class DownloaderTimeRemainingConvertor : IValueConverter + { + public object Convert(object value, Type targetType, + object parameter, CultureInfo culture) + { + TimeSpan TimeLeft = (TimeSpan)value; + if (TimeLeft == Timeout.InfiniteTimeSpan) + return ""; + return TimeLeft.ToString(@"h\:mm\:ss"); + } + + public object ConvertBack(object value, Type targetType, + object parameter, CultureInfo culture) + { + return Binding.DoNothing; + } + } +} diff --git a/ViewModels/DumpRomTargetSelectionViewModel.cs b/ViewModels/DumpRomTargetSelectionViewModel.cs new file mode 100644 index 0000000..4734ebd --- /dev/null +++ b/ViewModels/DumpRomTargetSelectionViewModel.cs @@ -0,0 +1,232 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; + +namespace WPinternals +{ + internal class DumpRomTargetSelectionViewModel: ContextViewModel + { + private Action DumpCallback; + internal Action SwitchToUnlockBoot; + internal Action SwitchToUnlockRoot; + internal Action SwitchToFlashRom; + + internal DumpRomTargetSelectionViewModel(Action SwitchToUnlockBoot, Action SwitchToUnlockRoot, Action SwitchToFlashRom, Action DumpCallback) + : base() + { + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.SwitchToUnlockRoot = SwitchToUnlockRoot; + this.SwitchToFlashRom = SwitchToFlashRom; + this.DumpCallback = DumpCallback; + + new Thread(() => EvaluateViewState()).Start(); + } + + private string _FFUPath; + public string FFUPath + { + get + { + return _FFUPath; + } + set + { + if (value != _FFUPath) + { + _FFUPath = value; + OnPropertyChanged("FFUPath"); + } + } + } + + private string _EFIESPPath; + public string EFIESPPath + { + get + { + return _EFIESPPath; + } + set + { + if (value != _EFIESPPath) + { + _EFIESPPath = value; + OnPropertyChanged("EFIESPPath"); + } + } + } + + private bool _CompressEFIESP; + public bool CompressEFIESP + { + get + { + return _CompressEFIESP; + } + set + { + if (value != _CompressEFIESP) + { + _CompressEFIESP = value; + OnPropertyChanged("CompressEFIESP"); + } + } + } + + private string _MainOSPath; + public string MainOSPath + { + get + { + return _MainOSPath; + } + set + { + if (value != _MainOSPath) + { + _MainOSPath = value; + OnPropertyChanged("MainOSPath"); + } + } + } + + private bool _CompressMainOS; + public bool CompressMainOS + { + get + { + return _CompressMainOS; + } + set + { + if (value != _CompressMainOS) + { + _CompressMainOS = value; + OnPropertyChanged("CompressMainOS"); + } + } + } + + private string _DataPath; + public string DataPath + { + get + { + return _DataPath; + } + set + { + if (value != _DataPath) + { + _DataPath = value; + OnPropertyChanged("DataPath"); + } + } + } + + private bool _CompressData = true; + public bool CompressData + { + get + { + return _CompressData; + } + set + { + if (value != _CompressData) + { + _CompressData = value; + OnPropertyChanged("CompressData"); + } + } + } + + private bool _IsPhoneDisconnected; + public bool IsPhoneDisconnected + { + get + { + return _IsPhoneDisconnected; + } + set + { + if (value != _IsPhoneDisconnected) + { + _IsPhoneDisconnected = value; + OnPropertyChanged("IsPhoneDisconnected"); + } + } + } + + private bool _IsPhoneInMassStorage; + public bool IsPhoneInMassStorage + { + get + { + return _IsPhoneInMassStorage; + } + set + { + if (value != _IsPhoneInMassStorage) + { + _IsPhoneInMassStorage = value; + OnPropertyChanged("IsPhoneInMassStorage"); + } + } + } + + private bool _IsPhoneInOtherMode; + public bool IsPhoneInOtherMode + { + get + { + return _IsPhoneInOtherMode; + } + set + { + if (value != _IsPhoneInOtherMode) + { + _IsPhoneInOtherMode = value; + OnPropertyChanged("IsPhoneInOtherMode"); + } + } + } + + private DelegateCommand _DumpCommand; + public DelegateCommand DumpCommand + { + get + { + if (_DumpCommand == null) + { + _DumpCommand = new DelegateCommand(() => { DumpCallback(FFUPath, EFIESPPath, CompressEFIESP, MainOSPath, CompressMainOS, DataPath, CompressData); }, () => ((FFUPath != null) && ((EFIESPPath != null) || (MainOSPath != null) || (DataPath != null)))); + } + return _DumpCommand; + } + } + + internal override void EvaluateViewState() + { + DumpCommand.RaiseCanExecuteChanged(); + } + } +} diff --git a/ViewModels/DumpRomViewModel.cs b/ViewModels/DumpRomViewModel.cs new file mode 100644 index 0000000..5244b1c --- /dev/null +++ b/ViewModels/DumpRomViewModel.cs @@ -0,0 +1,169 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Threading; + +namespace WPinternals +{ + internal class DumpRomViewModel: ContextViewModel + { + private Action SwitchToUnlockBoot; + private Action SwitchToUnlockRoot; + private Action SwitchToFlashRom; + + internal DumpRomViewModel(Action SwitchToUnlockBoot, Action SwitchToUnlockRoot, Action SwitchToFlashRom) + : base() + { + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.SwitchToUnlockRoot = SwitchToUnlockRoot; + this.SwitchToFlashRom = SwitchToFlashRom; + } + + internal override void EvaluateViewState() + { + if (!IsActive) + return; + + if (SubContextViewModel == null) + ActivateSubContext(new DumpRomTargetSelectionViewModel(SwitchToUnlockBoot, SwitchToUnlockRoot, SwitchToFlashRom, DoDumpRom)); + } + + internal void DoDumpRom(string FFUPath, string EFIESPPath, bool CompressEFIESP, string MainOSPath, bool CompressMainOS, string DataPath, bool CompressData) + { + new Thread(() => + { + bool Result = true; + + ActivateSubContext(new BusyViewModel("Initializing ROM dump...")); + + ulong TotalSizeSectors = 0; + int PartitionCount = 0; + Partition Partition; + FFU FFU = null; + try + { + FFU = new FFU(FFUPath); + + if (EFIESPPath != null) + { + Partition = FFU.GPT.Partitions.Where(p => p.Name == "EFIESP").First(); + TotalSizeSectors += Partition.SizeInSectors; + PartitionCount++; + } + + if (MainOSPath != null) + { + Partition = FFU.GPT.Partitions.Where(p => p.Name == "MainOS").First(); + TotalSizeSectors += Partition.SizeInSectors; + PartitionCount++; + } + + if (DataPath != null) + { + Partition = FFU.GPT.Partitions.Where(p => p.Name == "Data").First(); + TotalSizeSectors += Partition.SizeInSectors; + PartitionCount++; + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + + // We are on a worker thread! + // So we must pass the SynchronizationContext of the UI thread + BusyViewModel Busy = new BusyViewModel("Dumping ROM...", MaxProgressValue: TotalSizeSectors, UIContext: UIContext); + ProgressUpdater Updater = Busy.ProgressUpdater; + ActivateSubContext(Busy); + + int i = 0; + if (Result) + { + try + { + if (EFIESPPath != null) + { + i++; + Busy.Message = "Dumping partition EFIESP (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + FFU.WritePartition("EFIESP", EFIESPPath, Updater, CompressEFIESP); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (Result) + { + try + { + if (MainOSPath != null) + { + i++; + Busy.Message = "Dumping partition MainOS (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + FFU.WritePartition("MainOS", MainOSPath, Updater, CompressMainOS); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (Result) + { + try + { + if (DataPath != null) + { + i++; + Busy.Message = "Dumping partition Data (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + FFU.WritePartition("Data", DataPath, Updater, CompressData); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (!Result) + { + ActivateSubContext(new MessageViewModel("Failed to dump ROM partitions!", Restart)); + return; + } + + ActivateSubContext(new MessageViewModel("Successfully dumped ROM partitions!", Restart)); + }).Start(); + } + + internal void Restart() + { + ActivateSubContext(new DumpRomTargetSelectionViewModel(SwitchToUnlockBoot, SwitchToUnlockRoot, SwitchToFlashRom, DoDumpRom)); + } + } +} diff --git a/ViewModels/GettingStartedViewModel.cs b/ViewModels/GettingStartedViewModel.cs new file mode 100644 index 0000000..4c872e4 --- /dev/null +++ b/ViewModels/GettingStartedViewModel.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace WPinternals +{ + internal class GettingStartedViewModel: ContextViewModel + { + internal Action ShowDisclaimer; + internal Action SwitchToUnlockBoot; + internal Action SwitchToUnlockRoot; + internal Action SwitchToBackup; + internal Action SwitchToDumpRom; + internal Action SwitchToFlashRom; + internal Action SwitchToDownload; + + internal GettingStartedViewModel(Action ShowDisclaimer, Action SwitchToUnlockBoot, Action SwitchToUnlockRoot, Action SwitchToBackup, Action SwitchToDumpRom, Action SwitchToFlashRom, Action SwitchToDownload) + : base() + { + this.ShowDisclaimer = ShowDisclaimer; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.SwitchToUnlockRoot = SwitchToUnlockRoot; + this.SwitchToBackup = SwitchToBackup; + this.SwitchToFlashRom = SwitchToFlashRom; + this.SwitchToDumpRom = SwitchToDumpRom; + this.SwitchToDownload = SwitchToDownload; + } + } +} diff --git a/ViewModels/LumiaFlashRomSourceSelectionViewModel.cs b/ViewModels/LumiaFlashRomSourceSelectionViewModel.cs new file mode 100644 index 0000000..5a9d00a --- /dev/null +++ b/ViewModels/LumiaFlashRomSourceSelectionViewModel.cs @@ -0,0 +1,262 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; + +namespace WPinternals +{ + internal class LumiaFlashRomSourceSelectionViewModel: ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + private Action FlashPartitionsCallback; + private Action FlashFFUCallback; + private Action FlashArchiveCallback; + internal Action SwitchToUnlockBoot; + internal Action SwitchToUnlockRoot; + internal Action SwitchToDumpFFU; + internal Action SwitchToBackup; + + internal LumiaFlashRomSourceSelectionViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToUnlockBoot, Action SwitchToUnlockRoot, Action SwitchToDumpFFU, Action SwitchToBackup, Action FlashPartitionsCallback, Action FlashArchiveCallback, Action FlashFFUCallback) + : base() + { + this.PhoneNotifier = PhoneNotifier; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.SwitchToUnlockRoot = SwitchToUnlockRoot; + this.SwitchToDumpFFU = SwitchToDumpFFU; + this.SwitchToBackup = SwitchToBackup; + this.FlashPartitionsCallback = FlashPartitionsCallback; + this.FlashArchiveCallback = FlashArchiveCallback; + this.FlashFFUCallback = FlashFFUCallback; + + this.PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + this.PhoneNotifier.DeviceRemoved += DeviceRemoved; + + new Thread(() => EvaluateViewState()).Start(); + } + + private string _EFIESPPath; + public string EFIESPPath + { + get + { + return _EFIESPPath; + } + set + { + if (value != _EFIESPPath) + { + _EFIESPPath = value; + OnPropertyChanged("EFIESPPath"); + } + } + } + + private string _MainOSPath; + public string MainOSPath + { + get + { + return _MainOSPath; + } + set + { + if (value != _MainOSPath) + { + _MainOSPath = value; + OnPropertyChanged("MainOSPath"); + } + } + } + + private string _DataPath; + public string DataPath + { + get + { + return _DataPath; + } + set + { + if (value != _DataPath) + { + _DataPath = value; + OnPropertyChanged("DataPath"); + } + } + } + + private string _ArchivePath; + public string ArchivePath + { + get + { + return _ArchivePath; + } + set + { + if (value != _ArchivePath) + { + _ArchivePath = value; + OnPropertyChanged("ArchivePath"); + } + } + } + + private string _FFUPath; + public string FFUPath + { + get + { + return _FFUPath; + } + set + { + if (value != _FFUPath) + { + _FFUPath = value; + OnPropertyChanged("FFUPath"); + } + } + } + + private bool _IsPhoneDisconnected; + public bool IsPhoneDisconnected + { + get + { + return _IsPhoneDisconnected; + } + set + { + if (value != _IsPhoneDisconnected) + { + _IsPhoneDisconnected = value; + OnPropertyChanged("IsPhoneDisconnected"); + } + } + } + + private bool _IsPhoneInFlashMode; + public bool IsPhoneInFlashMode + { + get + { + return _IsPhoneInFlashMode; + } + set + { + if (value != _IsPhoneInFlashMode) + { + _IsPhoneInFlashMode = value; + OnPropertyChanged("IsPhoneInFlashMode"); + } + } + } + + private bool _IsPhoneInOtherMode; + public bool IsPhoneInOtherMode + { + get + { + return _IsPhoneInOtherMode; + } + set + { + if (value != _IsPhoneInOtherMode) + { + _IsPhoneInOtherMode = value; + OnPropertyChanged("IsPhoneInOtherMode"); + } + } + } + + private DelegateCommand _FlashPartitionsCommand; + public DelegateCommand FlashPartitionsCommand + { + get + { + if (_FlashPartitionsCommand == null) + { + _FlashPartitionsCommand = new DelegateCommand(() => { FlashPartitionsCallback(EFIESPPath, MainOSPath, DataPath); }, () => (((EFIESPPath != null) || (MainOSPath != null) || (DataPath != null)) && (PhoneNotifier.CurrentInterface != null))); + } + return _FlashPartitionsCommand; + } + } + + private DelegateCommand _FlashFFUCommand; + public DelegateCommand FlashFFUCommand + { + get + { + if (_FlashFFUCommand == null) + { + _FlashFFUCommand = new DelegateCommand(() => { FlashFFUCallback(FFUPath); }, () => ((FFUPath != null) && (PhoneNotifier.CurrentInterface != null))); + } + return _FlashFFUCommand; + } + } + + private DelegateCommand _FlashArchiveCommand; + public DelegateCommand FlashArchiveCommand + { + get + { + if (_FlashArchiveCommand == null) + { + _FlashArchiveCommand = new DelegateCommand(() => { FlashArchiveCallback(ArchivePath); }, () => ((ArchivePath != null) && (PhoneNotifier.CurrentInterface != null))); + } + return _FlashArchiveCommand; + } + } + + ~LumiaFlashRomSourceSelectionViewModel() + { + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + } + + void NewDeviceArrived(ArrivalEventArgs Args) + { + new Thread(() => + { + try + { + EvaluateViewState(); + } + catch { } + }).Start(); + } + + void DeviceRemoved() + { + new Thread(() => EvaluateViewState()).Start(); + } + + internal override void EvaluateViewState() + { + IsPhoneDisconnected = PhoneNotifier.CurrentInterface == null; + IsPhoneInFlashMode = PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_Flash; + IsPhoneInOtherMode = (!IsPhoneDisconnected && !IsPhoneInFlashMode); + FlashPartitionsCommand.RaiseCanExecuteChanged(); + FlashArchiveCommand.RaiseCanExecuteChanged(); + FlashFFUCommand.RaiseCanExecuteChanged(); + } + } +} diff --git a/ViewModels/LumiaFlashRomViewModel.cs b/ViewModels/LumiaFlashRomViewModel.cs new file mode 100644 index 0000000..8e28c5f --- /dev/null +++ b/ViewModels/LumiaFlashRomViewModel.cs @@ -0,0 +1,657 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace WPinternals +{ + internal class LumiaFlashRomViewModel: ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + internal Action SwitchToUnlockBoot; + internal Action SwitchToUnlockRoot; + internal Action SwitchToDumpFFU; + internal Action SwitchToBackup; + private Action Callback; + + internal LumiaFlashRomViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToUnlockBoot, Action SwitchToUnlockRoot, Action SwitchToDumpFFU, Action SwitchToBackup, Action Callback) + : base() + { + IsSwitchingInterface = false; + IsFlashModeOperation = true; + + this.PhoneNotifier = PhoneNotifier; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.SwitchToUnlockRoot = SwitchToUnlockRoot; + this.SwitchToDumpFFU = SwitchToDumpFFU; + this.SwitchToBackup = SwitchToBackup; + this.Callback = Callback; + } + + internal override void EvaluateViewState() + { + if (!IsActive) + return; + + if (SubContextViewModel == null) + ActivateSubContext(new LumiaFlashRomSourceSelectionViewModel(PhoneNotifier, SwitchToUnlockBoot, SwitchToUnlockRoot, SwitchToDumpFFU, SwitchToBackup, FlashPartitions, FlashArchive, FlashFFU)); + } + + // Called from an event-handler. So, "async void" is valid here. + internal async void FlashPartitions(string EFIESPPath, string MainOSPath, string DataPath) + { + IsSwitchingInterface = true; // Prevents that a device is forced to Flash mode on this screen which is meant for flashing + try + { + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_Flash, + (msg, sub) => + ActivateSubContext(new BusyViewModel(msg, sub))); + if (((NokiaFlashModel)PhoneNotifier.CurrentModel).ReadPhoneInfo(ExtendedInfo: false).FlashAppProtocolVersionMajor < 2) + FlashPartitionsTask(EFIESPPath, MainOSPath, DataPath); + else + await Task.Run(async () => await LumiaV2UnlockBootViewModel.LumiaV2FlashPartitions(PhoneNotifier, EFIESPPath, MainOSPath, DataPath, SetWorkingStatus, UpdateWorkingStatus, ExitSuccess, ExitFailure)); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, Callback)); + } + } + + internal void FlashPartitionsTask(string EFIESPPath, string MainOSPath, string DataPath) + { + new Thread(() => + { + bool Result = true; + + ActivateSubContext(new BusyViewModel("Initializing flash...")); + + NokiaFlashModel Phone = (NokiaFlashModel)PhoneNotifier.CurrentModel; + + GPT GPT = Phone.ReadGPT(); + + ulong TotalSizeSectors = 0; + int PartitionCount = 0; + ulong MainOSOldSectorCount = 0; + ulong MainOSNewSectorCount = 0; + ulong DataOldSectorCount = 0; + ulong DataNewSectorCount = 0; + ulong FirstMainOSSector = 0; + + try + { + if (EFIESPPath != null) + { + using (Stream Stream = new DecompressedStream(File.Open(EFIESPPath, FileMode.Open))) + { + ulong StreamLengthInSectors = (ulong)Stream.Length / 0x200; + TotalSizeSectors += StreamLengthInSectors; + PartitionCount++; + Partition Partition = GPT.Partitions.Where(p => string.Compare(p.Name, "EFIESP", true) == 0).FirstOrDefault(); + if (StreamLengthInSectors > Partition.SizeInSectors) + { + LogFile.Log("Flash failed! Size of partition 'EFIESP' is too big."); + ExitFailure("Flash failed!", "Size of partition 'EFIESP' is too big."); + return; + } + } + } + + if (MainOSPath != null) + { + using (Stream Stream = new DecompressedStream(File.Open(MainOSPath, FileMode.Open))) + { + ulong StreamLengthInSectors = (ulong)Stream.Length / 0x200; + TotalSizeSectors += StreamLengthInSectors; + PartitionCount++; + Partition Partition = GPT.Partitions.Where(p => string.Compare(p.Name, "MainOS", true) == 0).FirstOrDefault(); + MainOSOldSectorCount = Partition.SizeInSectors; + MainOSNewSectorCount = StreamLengthInSectors; + FirstMainOSSector = Partition.FirstSector; + } + } + + if (DataPath != null) + { + using (Stream Stream = new DecompressedStream(File.Open(DataPath, FileMode.Open))) + { + ulong StreamLengthInSectors = (ulong)Stream.Length / 0x200; + TotalSizeSectors += StreamLengthInSectors; + PartitionCount++; + Partition Partition = GPT.Partitions.Where(p => string.Compare(p.Name, "Data", true) == 0).FirstOrDefault(); + DataOldSectorCount = Partition.SizeInSectors; + DataNewSectorCount = StreamLengthInSectors; + } + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + + if ((MainOSNewSectorCount > 0) && (DataNewSectorCount > 0)) + { + if ((MainOSNewSectorCount > MainOSOldSectorCount) || (DataNewSectorCount > DataOldSectorCount)) + { + UInt64 OSSpace = GPT.LastUsableSector - FirstMainOSSector + 1; + if ((MainOSNewSectorCount + DataNewSectorCount) <= OSSpace) + { + // MainOS and Data partitions need to be re-aligned! + Partition MainOSPartition = GPT.Partitions.Where(p => string.Compare(p.Name, "MainOS", true) == 0).Single(); + Partition DataPartition = GPT.Partitions.Where(p => string.Compare(p.Name, "Data", true) == 0).Single(); + MainOSPartition.LastSector = MainOSPartition.FirstSector + MainOSNewSectorCount - 1; + DataPartition.FirstSector = MainOSPartition.LastSector + 1; + DataPartition.LastSector = DataPartition.FirstSector + DataNewSectorCount - 1; + Phone.WriteGPT(GPT); + } + else + { + LogFile.Log("Flash failed! Size of partitions 'MainOS' and 'Data' together are too big."); + ExitFailure("Flash failed!", "Sizes of partitions 'MainOS' and 'Data' together are too big."); + return; + } + } + } + else if ((MainOSNewSectorCount > 0) && (MainOSNewSectorCount > MainOSOldSectorCount)) + { + LogFile.Log("Flash failed! Size of partition 'MainOS' is too big."); + ExitFailure("Flash failed!", "Size of partition 'MainOS' is too big."); + return; + } + else if ((DataNewSectorCount > 0) && (DataNewSectorCount > DataOldSectorCount)) + { + LogFile.Log("Flash failed! Size of partition 'Data' is too big."); + ExitFailure("Flash failed!", "Size of partition 'Data' together is too big."); + return; + } + + BusyViewModel Busy = new BusyViewModel("Flashing...", MaxProgressValue: TotalSizeSectors, UIContext: UIContext); + ProgressUpdater Updater = Busy.ProgressUpdater; + ActivateSubContext(Busy); + + int i = 0; + if (Result) + { + try + { + if (EFIESPPath != null) + { + i++; + Busy.Message = "Flashing partition EFIESP (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.FlashRawPartition(EFIESPPath, "EFIESP", Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (Result) + { + try + { + if (MainOSPath != null) + { + i++; + Busy.Message = "Flashing partition MainOS (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.FlashRawPartition(MainOSPath, "MainOS", Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (Result) + { + try + { + if (DataPath != null) + { + i++; + Busy.Message = "Flashing partition Data (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.FlashRawPartition(DataPath, "Data", Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (!Result) + { + ExitFailure("Flash failed!", null); + return; + } + + ExitSuccess("Flash successful! Make sure you disable Windows Update on the phone!", null); + }).Start(); + } + + // Called from an event-handler. So, "async void" is valid here. + internal async void FlashArchive(string ArchivePath) + { + IsSwitchingInterface = true; // Prevents that a device is forced to Flash mode on this screen which is meant for flashing + try + { + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_Flash, + (msg, sub) => + ActivateSubContext(new BusyViewModel(msg, sub))); + if (((NokiaFlashModel)PhoneNotifier.CurrentModel).ReadPhoneInfo(ExtendedInfo: false).FlashAppProtocolVersionMajor < 2) + FlashArchiveTask(ArchivePath); + else + await Task.Run(async () => await LumiaV2UnlockBootViewModel.LumiaV2FlashArchive(PhoneNotifier, ArchivePath, SetWorkingStatus, UpdateWorkingStatus, ExitSuccess, ExitFailure)); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, Callback)); + } + } + + internal void FlashArchiveTask(string ArchivePath) + { + new Thread(() => + { + ActivateSubContext(new BusyViewModel("Initializing flash...")); + + NokiaFlashModel Phone = (NokiaFlashModel)PhoneNotifier.CurrentModel; + + ulong TotalSizeSectors = 0; + int PartitionCount = 0; + ulong MainOSOldSectorCount = 0; + ulong MainOSNewSectorCount = 0; + ulong DataOldSectorCount = 0; + ulong DataNewSectorCount = 0; + ulong FirstMainOSSector = 0; + bool GPTChanged = false; + + try + { + GPT GPT = Phone.ReadGPT(); + + using (FileStream FileStream = new FileStream(ArchivePath, FileMode.Open)) + { + using (ZipArchive Archive = new ZipArchive(FileStream, ZipArchiveMode.Read)) + { + foreach (ZipArchiveEntry Entry in Archive.Entries) + { + // Determine if there is a partition layout present + ZipArchiveEntry PartitionEntry = Archive.GetEntry("Partitions.xml"); + if (PartitionEntry == null) + { + GPT.MergePartitions(null, false, Archive); + GPTChanged |= GPT.HasChanged; + } + else + { + using (Stream ZipStream = PartitionEntry.Open()) + { + using (StreamReader ZipReader = new StreamReader(ZipStream)) + { + string PartitionXml = ZipReader.ReadToEnd(); + GPT.MergePartitions(PartitionXml, false, Archive); + GPTChanged |= GPT.HasChanged; + } + } + } + + // First determine if we need a new GPT! + if (!Entry.FullName.Contains("/")) // No subfolders + { + string PartitionName = System.IO.Path.GetFileNameWithoutExtension(Entry.Name); + int P = PartitionName.IndexOf('.'); + if (P >= 0) + PartitionName = PartitionName.Substring(0, P); // Example: Data.bin.gz -> Data + Partition Partition = GPT.Partitions.Where(p => string.Compare(p.Name, PartitionName, true) == 0).FirstOrDefault(); + if (Partition != null) + { + DecompressedStream DecompressedStream = new DecompressedStream(Entry.Open()); + ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; + try + { + StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; + } + catch { } + + TotalSizeSectors += StreamLengthInSectors; + PartitionCount++; + + if (string.Compare(PartitionName, "MainOS", true) == 0) + { + MainOSOldSectorCount = Partition.SizeInSectors; + MainOSNewSectorCount = StreamLengthInSectors; + FirstMainOSSector = Partition.FirstSector; + } + else if (string.Compare(PartitionName, "Data", true) == 0) + { + DataOldSectorCount = Partition.SizeInSectors; + DataNewSectorCount = StreamLengthInSectors; + } + else if (StreamLengthInSectors > Partition.SizeInSectors) + { + LogFile.Log("Flash failed! Size of partition '" + PartitionName + "' is too big."); + ExitFailure("Flash failed!", "Size of partition '" + PartitionName + "' is too big."); + return; + } + } + } + } + + if ((MainOSNewSectorCount > 0) && (DataNewSectorCount > 0)) + { + if ((MainOSNewSectorCount > MainOSOldSectorCount) || (DataNewSectorCount > DataOldSectorCount)) + { + UInt64 OSSpace = GPT.LastUsableSector - FirstMainOSSector + 1; + if ((MainOSNewSectorCount + DataNewSectorCount) <= OSSpace) + { + // MainOS and Data partitions need to be re-aligned! + Partition MainOSPartition = GPT.Partitions.Where(p => string.Compare(p.Name, "MainOS", true) == 0).Single(); + Partition DataPartition = GPT.Partitions.Where(p => string.Compare(p.Name, "Data", true) == 0).Single(); + MainOSPartition.LastSector = MainOSPartition.FirstSector + MainOSNewSectorCount - 1; + DataPartition.FirstSector = MainOSPartition.LastSector + 1; + DataPartition.LastSector = DataPartition.FirstSector + DataNewSectorCount - 1; + + GPTChanged = true; + } + else + { + LogFile.Log("Flash failed! Size of partitions 'MainOS' and 'Data' together are too big."); + ExitFailure("Flash failed!", "Sizes of partitions 'MainOS' and 'Data' together are too big."); + return; + } + } + } + else if ((MainOSNewSectorCount > 0) && (MainOSNewSectorCount > MainOSOldSectorCount)) + { + LogFile.Log("Flash failed! Size of partition 'MainOS' is too big."); + ExitFailure("Flash failed!", "Size of partition 'MainOS' is too big."); + return; + } + else if ((DataNewSectorCount > 0) && (DataNewSectorCount > DataOldSectorCount)) + { + LogFile.Log("Flash failed! Size of partition 'Data' is too big."); + ExitFailure("Flash failed!", "Size of partition 'Data' is too big."); + return; + } + + if (GPTChanged) + Phone.WriteGPT(GPT); + + if (PartitionCount > 0) + { + BusyViewModel Busy = new BusyViewModel("Flashing...", MaxProgressValue: TotalSizeSectors, UIContext: UIContext); + ProgressUpdater Updater = Busy.ProgressUpdater; + ActivateSubContext(Busy); + + int i = 0; + + foreach (ZipArchiveEntry Entry in Archive.Entries) + { + // "MainOS.bin.gz" => "MainOS" + string PartitionName = Entry.Name; + int Pos = PartitionName.IndexOf('.'); + if (Pos >= 0) + PartitionName = PartitionName.Substring(0, Pos); + + Partition Partition = GPT.Partitions.Where(p => string.Compare(p.Name, PartitionName, true) == 0).FirstOrDefault(); + if (Partition != null) + { + Stream DecompressedStream = new DecompressedStream(Entry.Open()); + ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; + try + { + StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; + } + catch { } + + if (StreamLengthInSectors <= Partition.SizeInSectors) + { + i++; + Busy.Message = "Flashing partition " + Partition.Name + " (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.FlashRawPartition(DecompressedStream, Partition.Name, Updater); + } + DecompressedStream.Close(); + } + } + } + else + { + LogFile.Log("Flash failed! No valid partitions found in the archive."); + ExitFailure("Flash failed!", "No valid partitions found in the archive"); + return; + } + } + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + if (Ex is WPinternalsException) + { + ExitFailure("Flash failed!", ((WPinternalsException)Ex).SubMessage); + } + else + ExitFailure("Flash failed!", null); + return; + } + + ExitSuccess("Flash successful! Make sure you disable Windows Update on the phone!", null); + }).Start(); + } + + // Called from an event-handler. So, "async void" is valid here. + internal async void FlashFFU(string FFUPath) + { + IsSwitchingInterface = true; // Prevents that a device is forced to Flash mode on this screen which is meant for flashing + try + { + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_Flash, + (msg, sub) => + ActivateSubContext(new BusyViewModel(msg, sub))); + FlashFFUTask(FFUPath); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, Callback)); + } + } + + internal void FlashFFUTask(string FFUPath) + { + new Thread(() => + { + bool Result = true; + + NokiaFlashModel Phone = (NokiaFlashModel)PhoneNotifier.CurrentModel; + PhoneInfo Info = Phone.ReadPhoneInfo(false); + + #region Remove bootloader changes + + // If necessary remove bootloader changes + // In case the NV vars were redirected, and a stock FFU is flashed, then the IsFlashing flag will be cleared in the redirected NV vars + // And after a reboot the original NV vars are active again, but the IsFlashing flag is still set from when the bootloader was unlocked + // So we will first restore the GPT, so the original vars are active again. + // Then IsFlashing is true and the phone boots forcibly to FlashApp again. + // Then we start normal FFU flasing and at the end the IsFlashing flag is cleared in the original vars. + + if (Info.FlashAppProtocolVersionMajor >= 2) + { + byte[] GPTChunk = LumiaV2UnlockBootViewModel.GetGptChunk(Phone, 0x20000); // TODO: Get proper profile FFU and get ChunkSizeInBytes + GPT GPT = new GPT(GPTChunk); + FlashPart Part; + List FlashParts = new List(); + + Partition NvBackupPartition = GPT.GetPartition("BACKUP_BS_NV"); + if (NvBackupPartition != null) + { + // This must be a left over of a half unlocked bootloader + Partition NvPartition = GPT.GetPartition("UEFI_BS_NV"); + NvBackupPartition.Name = "UEFI_BS_NV"; + NvBackupPartition.PartitionGuid = NvPartition.PartitionGuid; + NvBackupPartition.PartitionTypeGuid = NvPartition.PartitionTypeGuid; + GPT.Partitions.Remove(NvPartition); + + GPT.Rebuild(); + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + FlashParts.Add(Part); + } + + // We should only clear NV if there was no backup NV to be restored and the current NV contains the SB unlock. + if ((NvBackupPartition == null) && !Info.UefiSecureBootEnabled) + { + // ClearNV + Part = new FlashPart(); + Partition Target = GPT.GetPartition("UEFI_BS_NV"); + Part.StartSector = (UInt32)Target.FirstSector; + Part.Stream = new MemoryStream(new byte[0x40000]); + FlashParts.Add(Part); + } + + if (FlashParts.Count > 0) + { + ActivateSubContext(new BusyViewModel("Restoring bootloader...")); + WPinternalsStatus LastStatus = WPinternalsStatus.Undefined; + LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(PhoneNotifier, FFUPath, false, false, FlashParts, true, ClearFlashingStatusAtEnd: false, + SetWorkingStatus: (m, s, v, a, st) => + { + if ((st == WPinternalsStatus.Scanning) || (st == WPinternalsStatus.WaitingForManualReset)) + SetWorkingStatus(m, s, v, a, st); + else if ((LastStatus == WPinternalsStatus.Scanning) || (LastStatus == WPinternalsStatus.WaitingForManualReset)) + SetWorkingStatus("Restoring bootloader...", null, null, Status: WPinternalsStatus.Flashing); + LastStatus = st; + }, + UpdateWorkingStatus: (m, s, v, st) => + { + if ((st == WPinternalsStatus.Scanning) || (st == WPinternalsStatus.WaitingForManualReset)) + UpdateWorkingStatus(m, s, v, st); + else if ((LastStatus == WPinternalsStatus.Scanning) || (LastStatus == WPinternalsStatus.WaitingForManualReset)) + SetWorkingStatus("Restoring bootloader...", null, null, Status: WPinternalsStatus.Flashing); + LastStatus = st; + } + ).Wait(); + + if ((PhoneNotifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader) && (PhoneNotifier.CurrentInterface != PhoneInterfaces.Lumia_Flash)) + PhoneNotifier.WaitForArrival().Wait(); + + if (PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_Bootloader) + ((NokiaFlashModel)PhoneNotifier.CurrentModel).SwitchToFlashAppContext(); + } + } + + #endregion + + Phone = (NokiaFlashModel)PhoneNotifier.CurrentModel; + + ActivateSubContext(new BusyViewModel("Initializing flash...")); + + string ErrorSubMessage = null; + + try + { + FFU FFU = new FFU(FFUPath); + BusyViewModel Busy = new BusyViewModel("Flashing original FFU...", MaxProgressValue: FFU.TotalChunkCount, UIContext: UIContext); + ActivateSubContext(Busy); + byte Options = 0; + if (!Info.SecureFfuEnabled || Info.Authenticated || Info.RdcPresent) + Options = (byte)((FlashOptions)Options | FlashOptions.SkipSignatureCheck); + Phone.FlashFFU(FFU, Busy.ProgressUpdater, true, Options); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + if (Ex is WPinternalsException) + ErrorSubMessage = ((WPinternalsException)Ex).SubMessage; + Result = false; + } + + + if (!Result) + { + ExitFailure("Flash failed!", ErrorSubMessage); + return; + } + + ExitSuccess("Flash successful!", null); + }).Start(); + } + + // Called from an event-handler. So, "async void" is valid here. + internal async void Exit() + { + try + { + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_Normal, + (msg, sub) => + ActivateSubContext(new BusyViewModel(msg, sub))); + IsSwitchingInterface = false; + Callback(); + ActivateSubContext(null); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, () => + { + IsSwitchingInterface = false; + Callback(); + ActivateSubContext(null); + })); + } + } + + internal void ExitSuccess(string Message, string SubMessage) + { + MessageViewModel SuccessMessageViewModel = new MessageViewModel(Message, () => + { + // No need to call Exit() to go to normal mode, because it already switches to normal mode automatically. + IsSwitchingInterface = false; // From here on a device will be forced to Flash mode again on this screen which is meant for flashing + Callback(); + ActivateSubContext(null); + }); + SuccessMessageViewModel.SubMessage = SubMessage; + ActivateSubContext(SuccessMessageViewModel); + } + + internal void ExitFailure(string Message, string SubMessage) + { + MessageViewModel ErrorMessageViewModel = new MessageViewModel(Message, () => + { + IsSwitchingInterface = false; + Callback(); + ActivateSubContext(null); + }); + ErrorMessageViewModel.SubMessage = SubMessage; + ActivateSubContext(ErrorMessageViewModel); + } + } +} diff --git a/ViewModels/LumiaInfoViewModel.cs b/ViewModels/LumiaInfoViewModel.cs new file mode 100644 index 0000000..a1ff095 --- /dev/null +++ b/ViewModels/LumiaInfoViewModel.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace WPinternals +{ + internal class LumiaInfoViewModel: ContextViewModel + { + internal PhoneInterfaces? CurrentInterface; + internal IDisposable CurrentModel; + internal PhoneNotifierViewModel PhoneNotifier; + private Action ModeSwitchRequestCallback; + private Action SwitchToGettingStarted; + + internal LumiaInfoViewModel(PhoneNotifierViewModel PhoneNotifier, Action ModeSwitchRequestCallback, Action SwitchToGettingStarted) + : base() + { + this.PhoneNotifier = PhoneNotifier; + this.ModeSwitchRequestCallback = ModeSwitchRequestCallback; + this.SwitchToGettingStarted = SwitchToGettingStarted; + + CurrentInterface = PhoneNotifier.CurrentInterface; + CurrentModel = PhoneNotifier.CurrentModel; + + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + PhoneNotifier.DeviceRemoved += DeviceRemoved; + } + + ~LumiaInfoViewModel() + { + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + PhoneNotifier.DeviceRemoved -= DeviceRemoved; + } + + void DeviceRemoved() + { + CurrentInterface = null; + CurrentModel = null; + ActivateSubContext(null); + } + + void NewDeviceArrived(ArrivalEventArgs Args) + { + CurrentInterface = Args.NewInterface; + CurrentModel = Args.NewModel; + + // Determine SubcontextViewModel + switch (CurrentInterface) + { + case null: + case PhoneInterfaces.Lumia_Bootloader: + ActivateSubContext(null); + break; + case PhoneInterfaces.Lumia_Normal: + ActivateSubContext(new NokiaNormalViewModel((NokiaPhoneModel)CurrentModel, ModeSwitchRequestCallback)); + break; + case PhoneInterfaces.Lumia_Flash: + ActivateSubContext(new NokiaFlashViewModel((NokiaFlashModel)CurrentModel, ModeSwitchRequestCallback, SwitchToGettingStarted)); + break; + case PhoneInterfaces.Lumia_Label: + ActivateSubContext(new NokiaLabelViewModel((NokiaPhoneModel)CurrentModel, ModeSwitchRequestCallback)); + break; + case PhoneInterfaces.Lumia_MassStorage: + ActivateSubContext(new NokiaMassStorageViewModel((MassStorage)CurrentModel)); + break; + }; + } + } +} diff --git a/ViewModels/LumiaModeViewModel.cs b/ViewModels/LumiaModeViewModel.cs new file mode 100644 index 0000000..f7ccefc --- /dev/null +++ b/ViewModels/LumiaModeViewModel.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace WPinternals +{ + internal class LumiaModeViewModel : ContextViewModel + { + internal PhoneInterfaces? CurrentInterface; + internal IDisposable CurrentModel; + internal PhoneNotifierViewModel PhoneNotifier; + private Action Callback; + + internal LumiaModeViewModel(PhoneNotifierViewModel PhoneNotifier, Action Callback) + : base() + { + this.PhoneNotifier = PhoneNotifier; + this.Callback = Callback; + + CurrentInterface = PhoneNotifier.CurrentInterface; + CurrentModel = PhoneNotifier.CurrentModel; + + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + PhoneNotifier.DeviceRemoved += DeviceRemoved; + } + + ~LumiaModeViewModel() + { + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + PhoneNotifier.DeviceRemoved -= DeviceRemoved; + } + + void DeviceRemoved() + { + CurrentInterface = null; + CurrentModel = null; + + if (!IsSwitchingInterface) + ActivateSubContext(null); + } + + void NewDeviceArrived(ArrivalEventArgs Args) + { + CurrentInterface = Args.NewInterface; + CurrentModel = Args.NewModel; + + if (!IsSwitchingInterface && IsActive) + Refresh(); + } + + internal override void EvaluateViewState() + { + if (!IsSwitchingInterface && IsActive) + Refresh(); + } + + private void Refresh() + { + // Determine SubcontextViewModel + switch (CurrentInterface) + { + case null: + case PhoneInterfaces.Lumia_Bootloader: + break; + case PhoneInterfaces.Lumia_Normal: + ActivateSubContext(new NokiaModeNormalViewModel((NokiaPhoneModel)CurrentModel, OnModeSwitchRequested)); + break; + case PhoneInterfaces.Lumia_Flash: + ActivateSubContext(new NokiaModeFlashViewModel((NokiaFlashModel)CurrentModel, OnModeSwitchRequested)); + break; + case PhoneInterfaces.Lumia_Label: + ActivateSubContext(new NokiaModeLabelViewModel((NokiaPhoneModel)CurrentModel, OnModeSwitchRequested)); + break; + case PhoneInterfaces.Lumia_MassStorage: + ActivateSubContext(new NokiaModeMassStorageViewModel(null)); + break; + }; + } + + // Called from eventhandler, so "async void" is valid here. + internal async void OnModeSwitchRequested(PhoneInterfaces? TargetInterface) + { + IsSwitchingInterface = true; + + try + { + await SwitchModeViewModel.SwitchToWithStatus(PhoneNotifier, TargetInterface, SetWorkingStatus, UpdateWorkingStatus, null); // This is a manual switch. We don't care about which volume arrives. + + IsSwitchingInterface = false; + Callback(); + Refresh(); + } + catch (Exception Ex) + { + IsSwitchingInterface = false; + ActivateSubContext(new MessageViewModel(Ex.Message, () => { + Callback(); + Refresh(); + })); + } + } + } +} diff --git a/ViewModels/LumiaUnlockBootViewModel.cs b/ViewModels/LumiaUnlockBootViewModel.cs new file mode 100644 index 0000000..b43c903 --- /dev/null +++ b/ViewModels/LumiaUnlockBootViewModel.cs @@ -0,0 +1,1652 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using Microsoft.Win32; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace WPinternals +{ + // Create this class on the UI thread, after the main-window of the application is initialized. + // It is necessary to create the object on the UI thread, because notification events to the View need to be fired on that thread. + // The Model for this ViewModel communicates over USB and for that it uses the hWnd of the main window. + // Therefore the main window must be created before the ViewModel is created. + + internal enum MachineState + { + Default, + LumiaSpecAGetGPT, + LumiaSpecBUnlockBoot + }; + + internal class LumiaUnlockBootViewModel : ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + private Action Callback; + private bool IsFlashingDone = false; + private string FFUPath; + private string LoadersPath; + private string SBL3Path; + private string ProfileFFUPath; + private string EDEPath; + private string SupportedFFUPath; + private bool IsBootLoaderUnlocked; + private byte[] RootKeyHash = null; + private List PossibleLoaders; + private GPT NewGPT; + private byte[] GPT; + private byte[] ExtraSector; + private byte[] SBL2; + private byte[] SBL3; + private byte[] UEFI; + private FFU FFU; + private Action SwitchToFlashRom; + private Action SwitchToUndoRoot; + private Action SwitchToDownload; + private bool DoUnlock; + private MachineState State; + private object EvaluateViewStateLockObject = new object(); + + internal LumiaUnlockBootViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToFlashRom, Action SwitchToUndoRoot, Action SwitchToDownload, bool DoUnlock, Action Callback) + : base() + { + IsSwitchingInterface = false; + IsFlashModeOperation = true; + + this.PhoneNotifier = PhoneNotifier; + this.SwitchToFlashRom = SwitchToFlashRom; + this.SwitchToUndoRoot = SwitchToUndoRoot; + this.SwitchToDownload = SwitchToDownload; + this.DoUnlock = DoUnlock; + this.Callback = Callback; + + State = MachineState.Default; + + this.PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + + // ViewState will be evaluated as soon as this object is set as DataContext + } + + ~LumiaUnlockBootViewModel() + { + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + } + + internal void SendLoader() + { + // Assume 9008 mode + if (!((PhoneNotifier.CurrentModel is QualcommSerial) && (PossibleLoaders != null) && (PossibleLoaders.Count > 0))) + return; + + ActivateSubContext(new BusyViewModel("Sending loader...")); + LogFile.Log("Sending loader"); + + QualcommSerial Serial = (QualcommSerial)PhoneNotifier.CurrentModel; + QualcommDownload Download = new QualcommDownload(Serial); + if (Download.IsAlive()) + { + int Attempt = 1; + bool Result = false; + foreach (QualcommPartition Loader in PossibleLoaders) + { + LogFile.Log("Attempt " + Attempt.ToString()); + + try + { + Download.SendToPhoneMemory(0x2A000000, Loader.Binary); + Download.StartBootloader(0x2A000000); + Result = true; + LogFile.Log("Loader sent successfully"); + } + catch { } + + if (Result) + break; + + Attempt++; + } + Serial.Close(); + + if (!Result) + LogFile.Log("Loader failed"); + } + else + { + LogFile.Log("Failed to communicate to Qualcomm Emergency Download mode"); + throw new BadConnectionException(); + } + } + + // Potentially blocking UI. Threadsafe. + internal override void EvaluateViewState() + { + if (!IsActive) + return; + + if ((State == MachineState.LumiaSpecAGetGPT) || (State == MachineState.LumiaSpecBUnlockBoot)) + return; + + lock (EvaluateViewStateLockObject) + { + switch (PhoneNotifier.CurrentInterface) + { + case PhoneInterfaces.Lumia_Normal: + case PhoneInterfaces.Lumia_Label: + IsSwitchingInterface = false; + if (IsFlashingDone) + { + if (DoUnlock) + { + LogFile.Log("Bootloader successfully unlocked!"); + ActivateSubContext(new MessageViewModel("Bootloader succesfully unlocked!", Exit)); + } + else + { + LogFile.Log("Bootloader successfully restored!"); + ActivateSubContext(new MessageViewModel("Bootloader succesfully restored!", Exit)); + } + } + else + { + if (DoUnlock) + { + // Display View to switch to Flash mode + LogFile.Log("Start unlock. Phone needs to switch to Flash-mode"); + ActivateSubContext(new MessageViewModel("In order to start unlocking the bootloader, the phone needs to be switched to Flash-mode.", SwitchToFlashMode, Exit)); + } + else + { + // Display View to switch to Flash mode + LogFile.Log("Start boot restore. Phone needs to switch to Flash-mode"); + ActivateSubContext(new MessageViewModel("In order to start restoring the bootloader, the phone needs to be switched to Flash-mode.", SwitchToFlashMode, Exit)); + } + } + break; + case PhoneInterfaces.Lumia_Flash: + // Display View with device info and request for resources + // Click on "Continue" will start processing all resources + // Processing may fail with error message + // Or processing will succeed and user will again be asked to continue with Bricking-procedure (to switch to emergency mode) + + // This code is not always invoked by OnArrival event. + // So this is not always in a thread from the threadpool. + // So we need to avoid UI blocking code here. + + // Flash Param "FS" is the Flash Status (4 bytes, Big Endian DWORD, values: 0 / 1) + // When flashing is done and phone is still in flash-mode, write raw dummy sector and restart phone + NokiaFlashModel FlashModel = (NokiaFlashModel)PhoneNotifier.CurrentModel; + if (IsFlashingDone && (FlashModel.ReadParam("FS")[3] > 0)) + { + if (DoUnlock) + { + IsSwitchingInterface = true; + LogFile.Log("Phone detected in Flash-in-progress-mode. Escaping from Flash-mode.;"); + ActivateSubContext(new BusyViewModel("Escaping from Flash mode...")); + + new Thread(() => + { + RecoverFromFlashMode(); + }).Start(); + } + else + { + IsSwitchingInterface = false; + ActivateSubContext(new MessageViewModel("Bootloader restored. You can now flash a stock ROM.", SwitchToFlashRom)); + } + } + else + { + IsSwitchingInterface = false; + + int TestPos = 0; + + try // In case phone reboots during the time that status is being read + { + // Some phones, like Lumia 928 verizon, do not support the Terminal interface! + // To read the RootKeyHash we use ReadParam("RRKH"), instead of GetTerminalResponse().RootKeyHash. + RootKeyHash = ((NokiaFlashModel)PhoneNotifier.CurrentModel).ReadParam("RRKH"); + + TestPos = 1; + + UefiSecurityStatusResponse SecurityStatus = ((NokiaFlashModel)PhoneNotifier.CurrentModel).ReadSecurityStatus(); + IsBootLoaderUnlocked = (SecurityStatus.AuthenticationStatus || SecurityStatus.RdcStatus || !SecurityStatus.SecureFfuEfuseStatus); + + TestPos = 2; + + PhoneInfo Info = ((NokiaFlashModel)PhoneNotifier.CurrentModel).ReadPhoneInfo(); + + TestPos = 3; + + if (Info.FlashAppProtocolVersionMajor < 2) + { + // This action is executed after the resources are selected by the user. + Action ReturnFunction = (FFUPath, LoadersPath, SBL3Path, ProfileFFUPath, EDEPath, SupportedFFUPath, DoFixBoot) => + { + // This is a callback on the UI thread + // Resources are confirmed by user + this.FFUPath = FFUPath; + this.LoadersPath = LoadersPath; + this.SBL3Path = SBL3Path; + StorePaths(); + + LogFile.Log("Processing resources:"); + LogFile.Log("FFU: " + FFUPath); + LogFile.Log("Loaders: " + LoadersPath); + if (SBL3Path == null) + LogFile.Log("No SBL3 specified"); + else + LogFile.Log("SBL3: " + SBL3Path); + + ActivateSubContext(new BusyViewModel("Processing resources...")); + + new Thread(() => + { + bool ResourcesVerified = false; + try + { + ResourcesVerified = EvaluateResources(); + if (!ResourcesVerified) + { + LogFile.Log("Processing resources failed."); + ActivateSubContext(new MessageViewModel("Invalid resources.", Abort)); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ActivateSubContext(new MessageViewModel(Ex.Message, Abort)); + } + + if (ResourcesVerified) + { + if (IsBootLoaderUnlocked) + { + CustomFlashBootLoader(); + } + else + PerformSoftBrick(); + } + }).Start(); + }; + + if (DoUnlock) + ActivateSubContext(new BootUnlockResourcesViewModel("Lumia Flash mode", RootKeyHash, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, ReturnFunction, Abort, IsBootLoaderUnlocked, false)); + else + ActivateSubContext(new BootRestoreResourcesViewModel("Lumia Flash mode", RootKeyHash, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, ReturnFunction, Abort, IsBootLoaderUnlocked, false)); + } + else + { + if (DoUnlock) + { + GPT GPT = FlashModel.ReadGPT(); + if ((GPT.GetPartition("IS_UNLOCKED") != null) || (GPT.GetPartition("BACKUP_EFIESP") != null)) + { + ExitMessage("Phone is already unlocked", null); + return; + } + } + + TestPos = 4; + + // Stop responding to device arrival here, because all connections are handled by subfunctions, not here. + IsSwitchingInterface = true; + + // This action is executed after the resources are selected by the user. + Action ReturnFunction = (FFUPath, LoadersPath, SBL3Path, ProfileFFUPath, EDEPath, SupportedFFUPath, DoFixBoot) => + { + State = MachineState.LumiaSpecBUnlockBoot; + if (DoUnlock) + { + // This is a callback on the UI thread + // Resources are confirmed by user + this.ProfileFFUPath = ProfileFFUPath; + this.EDEPath = EDEPath; + this.SupportedFFUPath = SupportedFFUPath; + StorePaths(); + + if (DoFixBoot) + LogFile.Log("Fix Boot"); + else + LogFile.Log("Unlock Bootloader"); + + LogFile.Log("Processing resources:"); + LogFile.Log("Profile FFU: " + ProfileFFUPath); + LogFile.Log("EDE file: " + EDEPath); + if (SupportedFFUPath != null) + LogFile.Log("Donor-FFU with supported OS version: " + SupportedFFUPath); + + Task.Run(async () => + { + if (DoFixBoot) + await LumiaV2UnlockBootViewModel.LumiaV2FixBoot(PhoneNotifier, SetWorkingStatus, UpdateWorkingStatus, ExitMessage, ExitMessage); + else + await LumiaV2UnlockBootViewModel.LumiaV2UnlockBootloader(PhoneNotifier, ProfileFFUPath, EDEPath, SupportedFFUPath, SetWorkingStatus, UpdateWorkingStatus, ExitMessage, ExitMessage); + }); + } + else + { + Task.Run(async () => + { + await LumiaV2UnlockBootViewModel.LumiaV2RelockPhone(PhoneNotifier, null, true, SetWorkingStatus, UpdateWorkingStatus, ExitMessage, ExitMessage); + }); + } + }; + + TestPos = 5; + + if (DoUnlock) + ActivateSubContext(new BootUnlockResourcesViewModel("Lumia Flash mode", RootKeyHash, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, ReturnFunction, Abort, IsBootLoaderUnlocked, true, Info.PlatformID, Info.Type)); + else + ActivateSubContext(new BootRestoreResourcesViewModel("Lumia Flash mode", RootKeyHash, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, ReturnFunction, Abort, IsBootLoaderUnlocked, true, Info.PlatformID, Info.Type)); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex, LogType.FileAndConsole, TestPos.ToString()); + } + } + break; + case PhoneInterfaces.Qualcomm_Download: + IsSwitchingInterface = false; + + // If resources are not confirmed yet, then display view with device info and request for resources. + QualcommDownload Download = new QualcommDownload((QualcommSerial)PhoneNotifier.CurrentModel); + byte[] QualcommRootKeyHash = Download.GetRKH(); + if (RootKeyHash == null) + RootKeyHash = QualcommRootKeyHash; + else if (!StructuralComparisons.StructuralEqualityComparer.Equals(RootKeyHash, QualcommRootKeyHash)) + { + LogFile.Log("Error: Root Key Hash in Qualcomm Emergency mode does not match!"); + ActivateSubContext(new MessageViewModel("Error: Root Key Hash in Qualcomm Emergency mode does not match!", Callback)); + return; + } + + if ((this.FFUPath == null) || (this.PossibleLoaders == null) || (this.PossibleLoaders.Count == 0)) + { + // This action is executed after the user selected the resources. + Action ReturnFunction = (FFUPath, LoadersPath, SBL3Path, ProfileFFUPath, EDEPath, SupportedFFUPath, DoFixBoot) => + { + // This is a callback on the UI thread + // Resources are confirmed by user + this.FFUPath = FFUPath; + this.LoadersPath = LoadersPath; + this.SBL3Path = SBL3Path; + StorePaths(); + + LogFile.Log("Processing resources:"); + LogFile.Log("FFU: " + FFUPath); + LogFile.Log("Loaders: " + LoadersPath); + if (SBL3Path == null) + LogFile.Log("No SBL3 specified"); + else + LogFile.Log("SBL3: " + SBL3Path); + + ActivateSubContext(new BusyViewModel("Processing resources...")); + + new Thread(() => + { + bool ResourcesVerified = false; + try + { + ResourcesVerified = EvaluateResources(); + if (!ResourcesVerified) + { + LogFile.Log("Processing resources failed."); + ActivateSubContext(new MessageViewModel("Invalid resources.", Abort)); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ActivateSubContext(new MessageViewModel(Ex.Message, Abort)); + } + + if (ResourcesVerified) + SendLoader(); + }).Start(); + }; + + if (DoUnlock) + ActivateSubContext(new BootUnlockResourcesViewModel("Qualcomm Emergency Download mode", RootKeyHash, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, ReturnFunction, Abort, IsBootLoaderUnlocked, false)); + else + ActivateSubContext(new BootRestoreResourcesViewModel("Qualcomm Emergency Download mode", RootKeyHash, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, ReturnFunction, Abort, IsBootLoaderUnlocked, false)); + } + else + new Thread(() => + { + SendLoader(); + }).Start(); + break; + case PhoneInterfaces.Qualcomm_Flash: + new Thread(() => + { + FlashBootLoader(); + }).Start(); + break; + case PhoneInterfaces.Lumia_Bootloader: + IsSwitchingInterface = true; + if (IsFlashingDone) + { + LogFile.Log("Booting phone"); + ActivateSubContext(new BusyViewModel("Booting phone...")); + } + break; + default: + // Show View "Waiting for connection" + IsSwitchingInterface = false; + ActivateSubContext(null); + break; + } + } + } + + private void SwitchToFlashMode() + { + // SwitchModeViewModel must be created on the UI thread + IsSwitchingInterface = true; + UIContext.Post(async (t) => + { + LogFile.Log("Switching to Flash-mode"); + + try + { + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_Flash, + (msg, sub) => + ActivateSubContext(new BusyViewModel(msg, sub))); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, Callback)); + } + }, null); + } + + private void Abort() + { + // SwitchModeViewModel must be created on the UI thread + IsSwitchingInterface = false; + UIContext.Post((t) => + { + StorePaths(); + LogFile.Log("Aborting."); + Exit(); + }, null); + } + + private void StorePaths() + { + RegistryKey Key = Registry.CurrentUser.OpenSubKey(@"Software\WPInternals", true); + if (Key == null) + Key = Registry.CurrentUser.CreateSubKey(@"Software\WPInternals"); + + if (FFUPath == null) + { + if (Key.GetValue("FFUPath") != null) + Key.DeleteValue("FFUPath"); + } + else + Key.SetValue("FFUPath", FFUPath); + + if (LoadersPath == null) + { + if (Key.GetValue("LoadersPath") != null) + Key.DeleteValue("LoadersPath"); + } + else + Key.SetValue("LoadersPath", LoadersPath); + + if (DoUnlock) + { + if (SBL3Path == null) + { + if (Key.GetValue("SBL3Path") != null) + Key.DeleteValue("SBL3Path"); + } + else + Key.SetValue("SBL3Path", SBL3Path); + } + + NokiaFlashModel Model = (NokiaFlashModel)PhoneNotifier.CurrentModel; + PhoneInfo Info = Model.ReadPhoneInfo(); + + if (ProfileFFUPath == null) + { + if (Key.GetValue("ProfileFFUPath") != null) + Key.DeleteValue("ProfileFFUPath"); + } + else + { + Key.SetValue("ProfileFFUPath", ProfileFFUPath); + + App.Config.AddFfuToRepository(ProfileFFUPath); + } + + if (EDEPath == null) + { + if (Key.GetValue("EDEPath") != null) + Key.DeleteValue("EDEPath"); + } + else + { + Key.SetValue("EDEPath", EDEPath); + + App.Config.AddEmergencyToRepository(Info.Type, EDEPath, null); + } + + if (SupportedFFUPath != null) + { + Key.SetValue("SupportedFFUPath", SupportedFFUPath); + + App.Config.AddFfuToRepository(SupportedFFUPath); + } + + Key.Close(); + } + + private void Exit() + { + IsSwitchingInterface = false; // From here on a device will be forced to Flash mode again on this screen which is meant for flashing + IsFlashingDone = false; + + FFUPath = null; + LoadersPath = null; + PossibleLoaders = null; + SBL3Path = null; + + Callback(); + ActivateSubContext(null); + } + + internal void ExitMessage(string Message, string SubMessage) + { + // SecureBoot Unlock v2 is done. Reactivate phone arrival events. + MessageViewModel SuccessMessageViewModel = new MessageViewModel(Message, () => { + State = MachineState.Default; + Exit(); + }); + SuccessMessageViewModel.SubMessage = SubMessage; + ActivateSubContext(SuccessMessageViewModel); + } + + // Potentially blocking UI. Threadsafe. + private bool EvaluateResources(bool Emergency = false) + { + bool Result = true; + + bool DumpPartitions = false; + string DumpFilePrefix = Environment.ExpandEnvironmentVariables("%ALLUSERSPROFILE%\\WPInternals\\") + DateTime.Now.ToString("yyyy-MM-dd hh.mm.ss") + " - "; + + if ((FFUPath == null) || (FFUPath.Length == 0)) + throw new Exception("Error: Path for FFU-file is mandatory."); + + if (DoUnlock && ((LoadersPath == null) || (LoadersPath.Length == 0))) + throw new Exception("Error: Path for Loaders is mandatory."); + + if (PhoneNotifier.CurrentModel is NokiaFlashModel) + { + FlashVersion FlashVersion = ((NokiaFlashModel)PhoneNotifier.CurrentModel).GetFlashVersion(); + if (FlashVersion == null) + throw new Exception("Error: The version of the Flash Application on the phone could not be determined."); + if ((FlashVersion.ApplicationMajor < 1) || ((FlashVersion.ApplicationMajor == 1) && (FlashVersion.ApplicationMinor < 28))) + throw new Exception("Error: The version of the Flash Application on the phone is too old. Update your phone using Windows Updates or flash a newer ROM to your phone. Then try again."); + } + + try + { + FFU = new FFU(FFUPath); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Parsing FFU-file failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "01.bin", FFU.GetSectors(0, 34)); // Original GPT + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + + State = MachineState.LumiaSpecAGetGPT; // Stop handling arrival notifications in this screen + IsSwitchingInterface = true; // Stop handling arrival notifications in MainViewModel + SwitchModeViewModel.SwitchTo(PhoneNotifier, PhoneInterfaces.Lumia_Bootloader).Wait(); + NewGPT = ((NokiaFlashModel)PhoneNotifier.CurrentModel).ReadGPT(); + SwitchModeViewModel.SwitchTo(PhoneNotifier, PhoneInterfaces.Lumia_Flash).Wait(); + IsSwitchingInterface = true; + State = MachineState.Default; + + // Make sure all partitions are in range of the emergency flasher. + NewGPT.RestoreBackupPartitions(); + + // Magic! + // SecureBoot hack for Bootloader Spec A + if (DoUnlock) + { + try + { + this.GPT = NewGPT.InsertHack(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Parsing partitions failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "02.bin", this.GPT); // Patched GPT + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + } + else + { + NewGPT.RemoveHack(); + this.GPT = NewGPT.Rebuild(); + } + + SBL1 SBL1 = null; + try + { + SBL1 = new SBL1(FFU.GetPartition("SBL1")); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Parsing SBL1 failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "03.bin", SBL1.Binary); // Original SBL1 + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + + if (!Emergency) + { + if (RootKeyHash == null) + { + Result = false; + throw new Exception("Error: Root Key Hash could not be retrieved from the phone."); + } + if (SBL1.RootKeyHash == null) + { + Result = false; + throw new Exception("Error: Root Key Hash could not be retrieved from FFU file."); + } + if (!StructuralComparisons.StructuralEqualityComparer.Equals(RootKeyHash, SBL1.RootKeyHash)) + { + LogFile.Log("Phone: " + Converter.ConvertHexToString(RootKeyHash, "")); + LogFile.Log("SBL1: " + Converter.ConvertHexToString(SBL1.RootKeyHash, "")); + Result = false; + throw new Exception("Error: Root Key Hash from phone and from FFU file do not match!"); + } + } + + SBL2 SBL2 = null; + try + { + SBL2 = new SBL2(FFU.GetPartition("SBL2")); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Parsing SBL2 failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "05.bin", SBL2.Binary); // Original SBL2 + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + + if (DoUnlock) + { + try + { + this.SBL2 = SBL2.Patch(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Patching SBL2 failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "06.bin", SBL2.Binary); // Patched SBL2 + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + } + else + { + this.SBL2 = SBL2.Binary; + } + + this.ExtraSector = null; + if (DoUnlock) + { + try + { + byte[] PartitionHeader = new byte[0x0C]; + Buffer.BlockCopy(SBL2.Binary, 0, PartitionHeader, 0, 0x0C); + this.ExtraSector = SBL1.GenerateExtraSector(PartitionHeader); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Code generation failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "04.bin", this.ExtraSector); // Extra sector + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + } + + SBL3 SBL3; + SBL3 OriginalSBL3; + + try + { + OriginalSBL3 = new SBL3(FFU.GetPartition("SBL3")); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Parsing SBL3 from FFU failed."); + } + + if (SBL3Path == null) + { + SBL3 = OriginalSBL3; + LogFile.Log("Taking SBL3 from FFU"); + } + else + { + SBL3 = null; + try + { + SBL3 = new SBL3(SBL3Path); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Parsing external SBL3 failed."); + } + + if (SBL3.Binary.Length > OriginalSBL3.Binary.Length) + { + throw new Exception("Error: Selected SBL3 is too large."); + } + LogFile.Log("Taking selected SBL3"); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "07.bin", SBL3.Binary); // Original SBL3 + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + + if (DoUnlock) + { + try + { + this.SBL3 = SBL3.Patch(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Patching SBL3 failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "08.bin", SBL3.Binary); // Patched SBL3 + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + } + else + { + this.SBL3 = SBL3.Binary; + } + + UEFI UEFI = null; + try + { + UEFI = new UEFI(FFU.GetPartition("UEFI")); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Parsing UEFI failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "09.bin", UEFI.Binary); // Original UEFI + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + + if (DoUnlock) + { + try + { + this.UEFI = UEFI.Patch(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Patching UEFI failed."); + } + + if (DumpPartitions) + { + try + { + File.WriteAllBytes(DumpFilePrefix + "0A.bin", UEFI.Binary); // Patched UEFI + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + throw new Exception("Error: Writing binary for logging failed."); + } + } + } + else + { + this.UEFI = UEFI.Binary; + } + + if (!IsBootLoaderUnlocked) + { + try + { + PossibleLoaders = QualcommLoaders.GetPossibleLoadersForRootKeyHash(LoadersPath, this.RootKeyHash); + if (PossibleLoaders.Count == 0) + { + Result = false; + throw new Exception("Error: No matching loaders found for RootKeyHash."); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + throw new Exception("Error: Unexpected error during scanning for loaders."); + } + } + + return Result; + } + + // TODO: Add logging + private void PerformSoftBrick() + { + IsSwitchingInterface = true; + ActivateSubContext(new BusyViewModel("Switching to Emergency Download mode...")); + + // Send FFU headers + UInt64 CombinedFFUHeaderSize = this.FFU.HeaderSize; + byte[] FfuHeader = new byte[CombinedFFUHeaderSize]; + System.IO.FileStream FfuFile = new System.IO.FileStream(FFUPath, System.IO.FileMode.Open, System.IO.FileAccess.Read); + FfuFile.Read(FfuHeader, 0, (int)CombinedFFUHeaderSize); + FfuFile.Close(); + try + { + ((NokiaFlashModel)PhoneNotifier.CurrentModel).SendFfuHeaderV1(FfuHeader); + } + catch (Exception Ex) + { + IsSwitchingInterface = false; + LogFile.LogException(Ex); + ActivateSubContext(new MessageViewModel("Error using FFU. Try an FFU image which matches the phone.", Abort)); + return; + } + + // Send 1 empty chunk (according to layout in FFU headers, it will be written to first and last chunk) + byte[] EmptyChunk = new byte[0x20000]; + Array.Clear(EmptyChunk, 0, 0x20000); + ((NokiaFlashModel)PhoneNotifier.CurrentModel).SendFfuPayloadV1(EmptyChunk); + + // Reboot to Qualcomm Emergency mode + byte[] RebootCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x52 }; // NOKR + ((NokiaFlashModel)PhoneNotifier.CurrentModel).ExecuteRawVoidMethod(RebootCommand); + } + + void NewDeviceArrived(ArrivalEventArgs Args) + { + // Do not start on a new thread, because EvaluateViewState will also create new ViewModels and those should be created on the UI thread. + EvaluateViewState(); + } + + private void RecoverFromFlashMode() + { + IsSwitchingInterface = true; + LogFile.Log("Recover from Flash-mode"); + + // Flash dummy sector (only allowed when phone is authenticated) + byte[] EmptySector = new byte[0x200]; + Array.Clear(EmptySector, 0, 0x200); + ((NokiaFlashModel)PhoneNotifier.CurrentModel).FlashSectors(0x22, EmptySector); + + // Reboot to Qualcomm Emergency mode + byte[] RebootCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x52 }; // NOKR + ((NokiaFlashModel)PhoneNotifier.CurrentModel).ExecuteRawVoidMethod(RebootCommand); + } + + // Expected to be launched on worker-thread. + internal void FlashBootLoader() + { + IsSwitchingInterface = true; + LogFile.Log("Start flashing in Qualcomm Emergency Flash mode"); + + if (this.FFU == null) + { + ActivateSubContext(new BusyViewModel("Recovering resources...")); + + LogFile.Log("Phone was unexpectedly detected in this mode while resources were not loaded yet."); + LogFile.Log("WPInternals tool probably crashed in previous session."); + LogFile.Log("Trying to recover resources from the registry."); + + // In case tool was terminated + FFUPath = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "FFUPath", null); + LoadersPath = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "LoadersPath", null); + if (DoUnlock) + SBL3Path = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "SBL3Path", null); + else + SBL3Path = null; + + try + { + EvaluateResources(Emergency:true); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ActivateSubContext(new MessageViewModel(Ex.Message, Abort)); + } + } + + QualcommSerial Serial = (QualcommSerial)PhoneNotifier.CurrentModel; + Serial.EncodeCommands = false; + + QualcommFlasher Flasher = new QualcommFlasher(Serial); + + UInt64 TotalSectorCount = (UInt64)1 + 0x21 + 1 + + (UInt64)(SBL2.Length / 0x200) + + (UInt64)(SBL3.Length / 0x200) + + (UInt64)(UEFI.Length / 0x200) + + NewGPT.GetPartition("SBL1").SizeInSectors - 1 + + NewGPT.GetPartition("TZ").SizeInSectors + + NewGPT.GetPartition("RPM").SizeInSectors + + NewGPT.GetPartition("WINSECAPP").SizeInSectors; + + if (DoUnlock) + ActivateSubContext(new BusyViewModel("Flashing unlocked bootloader...", MaxProgressValue: TotalSectorCount, UIContext: UIContext)); + else + ActivateSubContext(new BusyViewModel("Flashing original bootloader...", MaxProgressValue: TotalSectorCount, UIContext: UIContext)); + + ProgressUpdater Progress = ((BusyViewModel)SubContextViewModel).ProgressUpdater; + + Flasher.Hello(); + Flasher.SetSecurityMode(0); + Flasher.OpenPartition(0x21); + + LogFile.Log("Partition opened."); + + byte[] MBR = FFU.GetSectors(0, 1); + + if (ExtraSector != null) + { + LogFile.Log("Flash EXT at 0x" + ((UInt32)NewGPT.GetPartition("HACK").FirstSector * 0x200).ToString("X8")); + Flasher.Flash((uint)NewGPT.GetPartition("HACK").FirstSector * 0x200, ExtraSector, Progress); + } + + LogFile.Log("Flash MBR at 0x" + ((UInt32)0).ToString("X8")); + Flasher.Flash(0, MBR, Progress, 0, 0x200); + + LogFile.Log("Flash GPT at 0x" + ((UInt32)0x200).ToString("X8")); + Flasher.Flash(0x200, GPT, Progress, 0, 0x41FF); // Bad bounds-check in the flash-loader prohibits to write the last byte. + + LogFile.Log("Flash SBL2 at 0x" + ((UInt32)NewGPT.GetPartition("SBL2").FirstSector * 0x200).ToString("X8")); + Flasher.Flash((uint)NewGPT.GetPartition("SBL2").FirstSector * 0x200, SBL2, Progress); + LogFile.Log("Flash SBL3 at 0x" + ((UInt32)NewGPT.GetPartition("SBL3").FirstSector * 0x200).ToString("X8")); + Flasher.Flash((uint)NewGPT.GetPartition("SBL3").FirstSector * 0x200, SBL3, Progress); + LogFile.Log("Flash UEFI at 0x" + ((UInt32)NewGPT.GetPartition("UEFI").FirstSector * 0x200).ToString("X8")); + Flasher.Flash((uint)NewGPT.GetPartition("UEFI").FirstSector * 0x200, UEFI, Progress); + + // To minimize risk of brick also flash these partitions: + LogFile.Log("Flash SBL1 at 0x" + ((UInt32)NewGPT.GetPartition("SBL1").FirstSector * 0x200).ToString("X8")); + Flasher.Flash((uint)NewGPT.GetPartition("SBL1").FirstSector * 0x200, FFU.GetPartition("SBL1"), Progress, 0, ((UInt32)NewGPT.GetPartition("SBL1").SizeInSectors - 1) * 0x200); + LogFile.Log("Flash TZ at 0x" + ((UInt32)NewGPT.GetPartition("TZ").FirstSector * 0x200).ToString("X8")); + Flasher.Flash((uint)NewGPT.GetPartition("TZ").FirstSector * 0x200, FFU.GetPartition("TZ"), Progress); + LogFile.Log("Flash RPM at 0x" + ((UInt32)NewGPT.GetPartition("RPM").FirstSector * 0x200).ToString("X8")); + Flasher.Flash((uint)NewGPT.GetPartition("RPM").FirstSector * 0x200, FFU.GetPartition("RPM"), Progress); + + // Workaround for bad bounds-check in flash-loader + UInt32 Length = (UInt32)FFU.GetPartition("WINSECAPP").Length; + UInt32 Start = (UInt32)NewGPT.GetPartition("WINSECAPP").FirstSector * 0x200; + if ((Start + Length) > 0x1E7FE00) + Length = 0x1E7FE00 - Start; + LogFile.Log("Flash WINSECAPP at 0x" + ((UInt32)NewGPT.GetPartition("WINSECAPP").FirstSector * 0x200).ToString("X8")); + Flasher.Flash(Start, FFU.GetPartition("WINSECAPP"), Progress, 0, Length); + + Flasher.ClosePartition(); + + IsFlashingDone = true; + LogFile.Log("Partition closed. Flashing ready. Rebooting."); + + ActivateSubContext(new BusyViewModel("Flashing done. Rebooting...")); + Flasher.Reboot(); + + Flasher.CloseSerial(); + } + + // Expected to be launched on worker-thread. + internal void CustomFlashBootLoader() + { + IsSwitchingInterface = true; + LogFile.Log("Start flashing in Custom Flash mode"); + + NokiaFlashModel CurrentModel = (NokiaFlashModel)PhoneNotifier.CurrentModel; + + UInt64 TotalSectorCount = (UInt64)0x21 + 1 + + (UInt64)(SBL2.Length / 0x200) + + (UInt64)(SBL3.Length / 0x200) + + (UInt64)(UEFI.Length / 0x200); + + if (DoUnlock) + ActivateSubContext(new BusyViewModel("Flashing unlocked bootloader...", MaxProgressValue: TotalSectorCount, UIContext: UIContext)); + else + ActivateSubContext(new BusyViewModel("Flashing original bootloader...", MaxProgressValue: TotalSectorCount, UIContext: UIContext)); + + ProgressUpdater Progress = ((BusyViewModel)SubContextViewModel).ProgressUpdater; + + LogFile.Log("Flash GPT at 0x" + ((UInt32)0x200).ToString("X8")); + CurrentModel.FlashSectors(1, GPT, 0); + Progress.SetProgress(0x21); + + if (ExtraSector != null) + { + LogFile.Log("Flash EXT at 0x" + ((UInt32)NewGPT.GetPartition("HACK").FirstSector * 0x200).ToString("X8")); + CurrentModel.FlashRawPartition(ExtraSector, "HACK", Progress); + } + + LogFile.Log("Flash SBL2 at 0x" + ((UInt32)NewGPT.GetPartition("SBL2").FirstSector * 0x200).ToString("X8")); + CurrentModel.FlashRawPartition(SBL2, "SBL2", Progress); + LogFile.Log("Flash SBL3 at 0x" + ((UInt32)NewGPT.GetPartition("SBL3").FirstSector * 0x200).ToString("X8")); + CurrentModel.FlashRawPartition(SBL3, "SBL3", Progress); + LogFile.Log("Flash UEFI at 0x" + ((UInt32)NewGPT.GetPartition("UEFI").FirstSector * 0x200).ToString("X8")); + CurrentModel.FlashRawPartition(UEFI, "UEFI", Progress); + + IsFlashingDone = true; + + ActivateSubContext(new BusyViewModel("Flashing done. Rebooting...")); + byte[] RebootCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x52 }; // NOKR + CurrentModel.ExecuteRawVoidMethod(RebootCommand); + } + } + + internal class BootUnlockResourcesViewModel : FlashResourcesViewModel + { + internal BootUnlockResourcesViewModel(string CurrentMode, byte[] RootKeyHash, Action SwitchToFlashRom, Action SwitchToUndoRoot, Action SwitchToDownload, Action Result, Action Abort, bool IsBootLoaderUnlocked, bool TargetHasNewFlashProtocol, string PlatformID = null, string ProductType = null) + : base(CurrentMode, RootKeyHash, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, Result, Abort, IsBootLoaderUnlocked, TargetHasNewFlashProtocol, PlatformID, ProductType) + { + SBL3Path = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "SBL3Path", null); + } + } + + internal class BootRestoreResourcesViewModel : FlashResourcesViewModel + { + internal BootRestoreResourcesViewModel(string CurrentMode, byte[] RootKeyHash, Action SwitchToFlashRom, Action SwitchToUndoRoot, Action SwitchToDownload, Action Result, Action Abort, bool IsBootLoaderUnlocked, bool TargetHasNewFlashProtocol, string PlatformID = null, string ProductType = null) + : base(CurrentMode, RootKeyHash, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, Result, Abort, IsBootLoaderUnlocked, TargetHasNewFlashProtocol, PlatformID, ProductType) + { + SBL3Path = null; + } + } + + internal class FlashResourcesViewModel: ContextViewModel + { + internal Action SwitchToFlashRom; + internal Action SwitchToUndoRoot; + internal Action SwitchToDownload; + private string PlatformID; + private string ProductType; + private string ValidatedSupportedFfuPath = null; + + internal FlashResourcesViewModel(string CurrentMode, byte[] RootKeyHash, Action SwitchToFlashRom, Action SwitchToUndoRoot, Action SwitchToDownload, Action Result, Action Abort, bool IsBootLoaderUnlocked, bool TargetHasNewFlashProtocol, string PlatformID = null, string ProductType = null): base() + { + IsSwitchingInterface = true; + this.CurrentMode = CurrentMode; + this.RootKeyHash = RootKeyHash; + this.PlatformID = PlatformID; + this.ProductType = ProductType; + this.SwitchToFlashRom = SwitchToFlashRom; + this.SwitchToUndoRoot = SwitchToUndoRoot; + this.SwitchToDownload = SwitchToDownload; + this.IsBootLoaderUnlocked = IsBootLoaderUnlocked; + OkCommand = new DelegateCommand(() => Result(FFUPath, LoadersPath, SBL3Path, ProfileFFUPath, EDEPath, IsSupportedFfuNeeded ? SupportedFFUPath : null, false), + () => (!TargetHasNewFlashProtocol || ((ProfileFFUPath != null) && (!IsSupportedFfuNeeded || (IsSupportedFfuValid && (SupportedFFUPath != null)))))); + FixCommand = new DelegateCommand(() => Result(null, null, null, null, null, null, true)); + CancelCommand = new DelegateCommand(Abort); + this.TargetHasNewFlashProtocol = TargetHasNewFlashProtocol; + + if (TargetHasNewFlashProtocol) + { + SetProfileFFUPath(); + SetEDEPath(); + } + else + { + FFUPath = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "FFUPath", null); + LoadersPath = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "LoadersPath", null); + } + } + + private void SetProfileFFUPath() + { + FFU ProfileFFU; + + try + { + if (_ProfileFFUPath != null) + { + ProfileFFU = new FFU(_ProfileFFUPath); + if (PlatformID.StartsWith(ProfileFFU.PlatformID, StringComparison.OrdinalIgnoreCase)) + { + IsProfileFfuValid = true; + return; + } + } + } + catch { } + + try + { + string TempProfileFFUPath = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "ProfileFFUPath", null); + if (TempProfileFFUPath != null) + { + ProfileFFU = new FFU(TempProfileFFUPath); + if (PlatformID.StartsWith(ProfileFFU.PlatformID, StringComparison.OrdinalIgnoreCase)) + { + ProfileFFUPath = TempProfileFFUPath; + IsProfileFfuValid = true; + return; + } + } + } + catch { } + + List FFUs = App.Config.FFURepository.Where(e => (PlatformID.StartsWith(e.PlatformID, StringComparison.OrdinalIgnoreCase) && e.Exists())).ToList(); + if (FFUs.Count() > 0) + { + IsProfileFfuValid = true; + ProfileFFUPath = FFUs[0].Path; + } + else + IsProfileFfuValid = false; + } + + private void SetEDEPath() + { + QualcommPartition Programmer; + string TempEDEPath; + + try + { + if (_EDEPath != null) + { + Programmer = new QualcommPartition(_EDEPath); + if (ByteOperations.Compare(Programmer.RootKeyHash, RootKeyHash)) + return; + } + } + catch { } + + try + { + TempEDEPath = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "EDEPath", null); + if (TempEDEPath != null) + { + Programmer = new QualcommPartition(TempEDEPath); + if (ByteOperations.Compare(Programmer.RootKeyHash, RootKeyHash)) + { + EDEPath = TempEDEPath; + return; + } + } + } + catch { } + + TempEDEPath = LumiaV2UnlockBootViewModel.GetProgrammerPath(RootKeyHash, ProductType); + if (TempEDEPath != null) + { + Programmer = new QualcommPartition(TempEDEPath); + if (ByteOperations.Compare(Programmer.RootKeyHash, RootKeyHash)) + { + EDEPath = TempEDEPath; + return; + } + } + } + + private void SetSupportedFFUPath() + { + if ((this is BootRestoreResourcesViewModel) || (_ProfileFFUPath == null)) + { + IsSupportedFfuNeeded = false; + return; + } + + try + { + FFU ProfileFFU = new FFU(_ProfileFFUPath); + IsSupportedFfuNeeded = !(App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == ProfileFFU.GetOSVersion())); + } + catch + { + IsSupportedFfuNeeded = false; + return; + } + + if (IsSupportedFfuNeeded) + { + FFU SupportedFFU; + + try + { + if (_SupportedFFUPath != null) + { + SupportedFFU = new FFU(_SupportedFFUPath); + if (App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == SupportedFFU.GetOSVersion())) + return; + } + } + catch { } + + try + { + string TempSupportedFFUPath = (string)Registry.GetValue(@"HKEY_CURRENT_USER\Software\WPInternals", "SupportedFFUPath", null); + if (TempSupportedFFUPath != null) + { + SupportedFFU = new FFU(TempSupportedFFUPath); + if (App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == SupportedFFU.GetOSVersion())) + { + ValidatedSupportedFfuPath = TempSupportedFFUPath; + SupportedFFUPath = TempSupportedFFUPath; + IsSupportedFfuValid = true; + return; + } + } + } + catch { } + + List FFUs = App.Config.FFURepository.Where(e => App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion)).ToList(); + if (FFUs.Count() > 0) + { + ValidatedSupportedFfuPath = FFUs[0].Path; + SupportedFFUPath = FFUs[0].Path; + IsSupportedFfuValid = true; + } + } + } + + private void ValidateSupportedFfuPath() + { + if (IsSupportedFfuNeeded) + { + if (SupportedFFUPath == null) + { + IsSupportedFfuValid = true; // No visible warning when there is no SupportedFFU selected yet. + } + else + { + if (App.Config.FFURepository.Any(e => ((e.Path == SupportedFFUPath) && (App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == e.OSVersion))))) + { + IsSupportedFfuValid = true; + } + else + { + FFU SupportedFFU = new FFU(SupportedFFUPath); + if (App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == SupportedFFU.GetOSVersion())) + { + IsSupportedFfuValid = true; + } + else + { + IsSupportedFfuValid = false; + } + } + } + } + else + { + IsSupportedFfuValid = true; + } + + if (IsSupportedFfuValid && (SupportedFFUPath != null)) + ValidatedSupportedFfuPath = SupportedFFUPath; + } + + private bool _IsSupportedFfuNeeded = false; + public bool IsSupportedFfuNeeded + { + get + { + return _IsSupportedFfuNeeded; + } + set + { + _IsSupportedFfuNeeded = value; + OnPropertyChanged("IsSupportedFfuNeeded"); + OkCommand.RaiseCanExecuteChanged(); + } + } + + private bool _IsSupportedFfuValid = true; + public bool IsSupportedFfuValid + { + get + { + return _IsSupportedFfuValid; + } + set + { + _IsSupportedFfuValid = value; + OnPropertyChanged("IsSupportedFfuValid"); + OkCommand.RaiseCanExecuteChanged(); + } + } + + private bool _IsProfileFfuValid = true; + public bool IsProfileFfuValid + { + get + { + return _IsProfileFfuValid; + } + set + { + _IsProfileFfuValid = value; + OnPropertyChanged("IsProfileFfuValid"); + } + } + + private bool _TargetHasNewFlashProtocol = false; + public bool TargetHasNewFlashProtocol + { + get + { + return _TargetHasNewFlashProtocol; + } + set + { + _TargetHasNewFlashProtocol = value; + OnPropertyChanged("TargetHasNewFlashProtocol"); + OkCommand.RaiseCanExecuteChanged(); + } + } + + private bool _IsBootLoaderUnlocked = false; + public bool IsBootLoaderUnlocked + { + get + { + return _IsBootLoaderUnlocked; + } + set + { + _IsBootLoaderUnlocked = value; + OnPropertyChanged("IsBootLoaderUnlocked"); + } + } + + private string _FFUPath = null; + public string FFUPath + { + get + { + return _FFUPath; + } + set + { + _FFUPath = value; + OnPropertyChanged("FFUPath"); + } + } + + private string _ProfileFFUPath = null; + public string ProfileFFUPath + { + get + { + return _ProfileFFUPath; + } + set + { + if (_ProfileFFUPath != value) + { + _ProfileFFUPath = value; + OnPropertyChanged("ProfileFFUPath"); + SetSupportedFFUPath(); + OkCommand.RaiseCanExecuteChanged(); + } + } + } + + private string _SupportedFFUPath = null; + public string SupportedFFUPath + { + get + { + return _SupportedFFUPath; + } + set + { + if (_SupportedFFUPath != value) + { + _SupportedFFUPath = value; + OnPropertyChanged("SupportedFFUPath"); + + if (value != ValidatedSupportedFfuPath) + ValidateSupportedFfuPath(); + } + } + } + + private string _LoadersPath = null; + public string LoadersPath + { + get + { + return _LoadersPath; + } + set + { + _LoadersPath = value; + OnPropertyChanged("LoadersPath"); + } + } + + private string _EDEPath = null; + public string EDEPath + { + get + { + return _EDEPath; + } + set + { + _EDEPath = value; + OnPropertyChanged("EDEPath"); + } + } + + private string _SBL3Path = null; + public string SBL3Path + { + get + { + return _SBL3Path; + } + set + { + _SBL3Path = value; + OnPropertyChanged("SBL3Path"); + } + } + + private string _CurrentMode = null; + public string CurrentMode + { + get + { + return _CurrentMode; + } + set + { + _CurrentMode = value; + OnPropertyChanged("CurrentMode"); + } + } + + private byte[] _RootKeyHash = null; + public byte[] RootKeyHash + { + get + { + return _RootKeyHash; + } + set + { + _RootKeyHash = value; + OnPropertyChanged("RootKeyHash"); + } + } + + private DelegateCommand _OkCommand = null; + public DelegateCommand OkCommand + { + get + { + return _OkCommand; + } + private set + { + _OkCommand = value; + } + } + + private DelegateCommand _CancelCommand = null; + public DelegateCommand CancelCommand + { + get + { + return _CancelCommand; + } + private set + { + _CancelCommand = value; + } + } + + private DelegateCommand _FixCommand = null; + public DelegateCommand FixCommand + { + get + { + return _FixCommand; + } + private set + { + _FixCommand = value; + } + } + } +} diff --git a/ViewModels/LumiaUnlockRootTargetSelectionViewModel.cs b/ViewModels/LumiaUnlockRootTargetSelectionViewModel.cs new file mode 100644 index 0000000..fdcd6a2 --- /dev/null +++ b/ViewModels/LumiaUnlockRootTargetSelectionViewModel.cs @@ -0,0 +1,198 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; + +namespace WPinternals +{ + internal class LumiaUnlockRootTargetSelectionViewModel: LumiaRootAccessTargetSelectionViewModel + { + public LumiaUnlockRootTargetSelectionViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToUnlockBoot, Action SwitchToDumpRom, Action SwitchToFlashRom, Action UnlockPhoneCallback, Action UnlockImageCallback) + : base(PhoneNotifier, SwitchToUnlockBoot, SwitchToDumpRom, SwitchToFlashRom, UnlockPhoneCallback, UnlockImageCallback) { } + } + + internal class LumiaUndoRootTargetSelectionViewModel: LumiaRootAccessTargetSelectionViewModel + { + public LumiaUndoRootTargetSelectionViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToUnlockBoot, Action SwitchToDumpRom, Action SwitchToFlashRom, Action UnlockPhoneCallback, Action UnlockImageCallback) + : base(PhoneNotifier, SwitchToUnlockBoot, SwitchToDumpRom, SwitchToFlashRom, UnlockPhoneCallback, UnlockImageCallback) { } + } + + internal class LumiaRootAccessTargetSelectionViewModel: ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + internal Action SwitchToUnlockBoot; + internal Action SwitchToDumpRom; + internal Action SwitchToFlashRom; + private Action UnlockPhoneCallback; + private Action UnlockImageCallback; + + internal LumiaRootAccessTargetSelectionViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToUnlockBoot, Action SwitchToDumpRom, Action SwitchToFlashRom, Action UnlockPhoneCallback, Action UnlockImageCallback) + : base() + { + this.PhoneNotifier = PhoneNotifier; + this.SwitchToDumpRom = SwitchToDumpRom; + this.SwitchToFlashRom = SwitchToFlashRom; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.UnlockPhoneCallback = UnlockPhoneCallback; + this.UnlockImageCallback = UnlockImageCallback; + + this.PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + this.PhoneNotifier.DeviceRemoved += DeviceRemoved; + + new Thread(() => EvaluateViewState()).Start(); + } + + private string _EFIESPMountPoint; + public string EFIESPMountPoint + { + get + { + return _EFIESPMountPoint; + } + set + { + if (value != _EFIESPMountPoint) + { + _EFIESPMountPoint = value; + OnPropertyChanged("EFIESPMountPoint"); + } + } + } + + private string _MainOSMountPoint; + public string MainOSMountPoint + { + get + { + return _MainOSMountPoint; + } + set + { + if (value != _MainOSMountPoint) + { + _MainOSMountPoint = value; + OnPropertyChanged("MainOSMountPoint"); + } + } + } + + private bool _IsPhoneDisconnected; + public bool IsPhoneDisconnected + { + get + { + return _IsPhoneDisconnected; + } + set + { + if (value != _IsPhoneDisconnected) + { + _IsPhoneDisconnected = value; + OnPropertyChanged("IsPhoneDisconnected"); + } + } + } + + private bool _IsPhoneInMassStorage; + public bool IsPhoneInMassStorage + { + get + { + return _IsPhoneInMassStorage; + } + set + { + if (value != _IsPhoneInMassStorage) + { + _IsPhoneInMassStorage = value; + OnPropertyChanged("IsPhoneInMassStorage"); + } + } + } + + private bool _IsPhoneInOtherMode; + public bool IsPhoneInOtherMode + { + get + { + return _IsPhoneInOtherMode; + } + set + { + if (value != _IsPhoneInOtherMode) + { + _IsPhoneInOtherMode = value; + OnPropertyChanged("IsPhoneInOtherMode"); + } + } + } + + private DelegateCommand _UnlockPhoneCommand; + public DelegateCommand UnlockPhoneCommand + { + get + { + if (_UnlockPhoneCommand == null) + { + _UnlockPhoneCommand = new DelegateCommand(() => { UnlockPhoneCallback(); }, () => !IsPhoneDisconnected); + } + return _UnlockPhoneCommand; + } + } + + private DelegateCommand _UnlockImageCommand; + public DelegateCommand UnlockImageCommand + { + get + { + if (_UnlockImageCommand == null) + { + _UnlockImageCommand = new DelegateCommand(() => { UnlockImageCallback(EFIESPMountPoint, MainOSMountPoint); }, () => ((EFIESPMountPoint != null) || (MainOSMountPoint != null))); + } + return _UnlockImageCommand; + } + } + + ~LumiaRootAccessTargetSelectionViewModel() + { + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + } + + void NewDeviceArrived(ArrivalEventArgs Args) + { + new Thread(() => EvaluateViewState()).Start(); + } + + void DeviceRemoved() + { + new Thread(() => EvaluateViewState()).Start(); + } + + internal override void EvaluateViewState() + { + IsPhoneDisconnected = PhoneNotifier.CurrentInterface == null; + IsPhoneInMassStorage = PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_MassStorage; + IsPhoneInOtherMode = (!IsPhoneDisconnected && !IsPhoneInMassStorage); + UnlockPhoneCommand.RaiseCanExecuteChanged(); + UnlockImageCommand.RaiseCanExecuteChanged(); + } + } +} diff --git a/ViewModels/LumiaUnlockRootViewModel.cs b/ViewModels/LumiaUnlockRootViewModel.cs new file mode 100644 index 0000000..64989c2 --- /dev/null +++ b/ViewModels/LumiaUnlockRootViewModel.cs @@ -0,0 +1,243 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Linq; +using System.Threading; + +namespace WPinternals +{ + internal class LumiaUnlockRootViewModel: ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + private Action SwitchToUnlockBoot; + private Action SwitchToDumpRom; + private Action SwitchToFlashRom; + private Action Callback; + private bool DoUnlock; + + internal LumiaUnlockRootViewModel(PhoneNotifierViewModel PhoneNotifier, Action SwitchToUnlockBoot, Action SwitchToDumpRom, Action SwitchToFlashRom, bool DoUnlock, Action Callback) + : base() + { + IsSwitchingInterface = false; + IsFlashModeOperation = true; + + this.PhoneNotifier = PhoneNotifier; + this.SwitchToDumpRom = SwitchToDumpRom; + this.SwitchToFlashRom = SwitchToFlashRom; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.DoUnlock = DoUnlock; + this.Callback = Callback; + } + + internal override void EvaluateViewState() + { + if (!IsActive) + return; + + if (DoUnlock) + { + if ((SubContextViewModel == null) || (SubContextViewModel is LumiaUndoRootTargetSelectionViewModel)) + ActivateSubContext(new LumiaUnlockRootTargetSelectionViewModel(PhoneNotifier, SwitchToUnlockBoot, SwitchToDumpRom, SwitchToFlashRom, DoUnlockPhone, DoUnlockImage)); + } + else + { + if ((SubContextViewModel == null) || (SubContextViewModel is LumiaUnlockRootTargetSelectionViewModel)) + ActivateSubContext(new LumiaUndoRootTargetSelectionViewModel(PhoneNotifier, SwitchToUnlockBoot, SwitchToDumpRom, SwitchToFlashRom, DoUnlockPhone, DoUnlockImage)); + } + } + + internal async void DoUnlockPhone() + { + try + { + IsSwitchingInterface = true; + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_MassStorage, + (msg, sub) => + ActivateSubContext(new BusyViewModel(msg, sub))); + bool HasNewBootloader = HasNewBootloaderFromMassStorage(); + string EFIESPPath = HasNewBootloader ? null : ((MassStorage)PhoneNotifier.CurrentModel).Drive + @"\EFIESP\"; + string MainOSPath = ((MassStorage)PhoneNotifier.CurrentModel).Drive + @"\"; + StartPatch(EFIESPPath, MainOSPath, HasNewBootloader); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, () => { + Callback(); + ActivateSubContext(null); + })); + } + } + + internal void DoUnlockImage(string EFIESPMountPoint, string MainOSMountPoint) + { + StartPatch(EFIESPMountPoint, MainOSMountPoint, false); // Unlock image is only supported for Lumia's with bootloader Spec A. Due to complexity of Spec B bootloader hack, it cannot be applied on a mounted image. + } + + // Magic! + // Apply patches for Root Access + private void StartPatch(string EFIESP, string MainOS, bool HasNewBootloader) + { + IsSwitchingInterface = false; + new Thread(() => + { + if (DoUnlock) + LogFile.BeginAction("EnableRootAccess"); + else + LogFile.BeginAction("DisableRootAccess"); + + bool Result = false; + + if (EFIESP != null) + { + if (DoUnlock) + ActivateSubContext(new BusyViewModel("Enable Root Access on EFIESP...")); + else + ActivateSubContext(new BusyViewModel("Disable Root Access on EFIESP...")); + + try + { + App.PatchEngine.TargetPath = EFIESP; + if (DoUnlock) + { + Result = App.PatchEngine.Patch("SecureBootHack-V1-EFIESP"); + + if (!Result) + { + ActivateSubContext(new MessageViewModel("Failed to enable Root Access on EFIESP! Check the OS version on the phone and verify the compatibility-list in the \"Getting started\" section.", Exit)); + return; + } + } + else + { + App.PatchEngine.Restore("SecureBootHack-V1-EFIESP"); + Result = true; + } + } + catch (UnauthorizedAccessException Ex) + { + LogFile.LogException(Ex); + ActivateSubContext(new MessageViewModel("Failed to enable Root Access on EFIESP! Not enough privileges to perform action. Try to logon to Windows with an administrator account.", Exit)); + return; + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + + if (!Result) + { + ActivateSubContext(new MessageViewModel("Failed to enable Root Access on EFIESP!", Exit)); + return; + } + } + + if (MainOS != null) + { + if (DoUnlock) + ActivateSubContext(new BusyViewModel("Enable Root Access on MainOS...")); + else + ActivateSubContext(new BusyViewModel("Disable Root Access on MainOS...")); + + try + { + App.PatchEngine.TargetPath = MainOS; + if (DoUnlock) + { + Result = App.PatchEngine.Patch("RootAccess-MainOS"); + + if (Result) + Result = App.PatchEngine.Patch("SecureBootHack-MainOS"); + + if (!Result) + { + ActivateSubContext(new MessageViewModel("Failed to enable Root Access on MainOS! Check the OS version on the phone and verify the compatibility-list in the \"Getting started\" section.", Exit)); + return; + } + } + else + { + App.PatchEngine.Restore("RootAccess-MainOS"); + + if (!HasNewBootloader) + App.PatchEngine.Restore("SecureBootHack-MainOS"); + + Result = true; + } + } + catch (UnauthorizedAccessException Ex) + { + LogFile.LogException(Ex); + ActivateSubContext(new MessageViewModel("Failed to enable Root Access on MainOS! Not enough privileges to perform action. Try to logon to Windows with an administrator account.", Exit)); + Result = false; + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + + if (!Result) + { + ActivateSubContext(new MessageViewModel("Failed to enable Root Access on MainOS!", Exit)); + } + else + { + if (DoUnlock) + ActivateSubContext(new MessageViewModel("Root Access successfully enabled!", Exit)); + else + ActivateSubContext(new MessageViewModel("Root Access successfully disabled!", Exit)); + } + } + + if (DoUnlock) + LogFile.EndAction("EnableRootAccess"); + else + LogFile.EndAction("DisableRootAccess"); + }).Start(); + } + + private void Exit() + { + IsSwitchingInterface = false; + Callback(); + ActivateSubContext(null); + } + + private bool HasNewBootloaderFromMassStorage() + { + bool Result = false; + MassStorage Phone = (MassStorage)PhoneNotifier.CurrentModel; + Phone.OpenVolume(false); + byte[] GPTBuffer = Phone.ReadSectors(1, 33); + GPT GPT = new WPinternals.GPT(GPTBuffer); + Partition Partition = GPT.GetPartition("UEFI"); + byte[] UefiBuffer = Phone.ReadSectors(Partition.FirstSector, Partition.LastSector - Partition.FirstSector + 1); + UEFI UEFI = new UEFI(UefiBuffer); + string BootMgrName = UEFI.EFIs.Where(efi => ((efi.Name != null) && ((efi.Name.Contains("BootMgrApp")) || (efi.Name.Contains("FlashApp"))))).First().Name; + byte[] BootMgr = UEFI.GetFile(BootMgrName); + // "Header V2" + Result = (ByteOperations.FindAscii(BootMgr, "Header V2") != null); + Phone.CloseVolume(); + return Result; + } + } +} diff --git a/ViewModels/LumiaV2UnlockBootViewModel.cs b/ViewModels/LumiaV2UnlockBootViewModel.cs new file mode 100644 index 0000000..dddc158 --- /dev/null +++ b/ViewModels/LumiaV2UnlockBootViewModel.cs @@ -0,0 +1,2988 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace WPinternals +{ + internal class LumiaV2UnlockBootViewModel : ContextViewModel + { + internal static async Task LumiaV2FindFlashingProfile(PhoneNotifierViewModel Notifier, string FFUPath, bool DoResetFirst = true, bool Experimental = false, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null, ExitSuccess ExitSuccess = null, ExitFailure ExitFailure = null) + { + if (SetWorkingStatus == null) SetWorkingStatus = (m, s, v, a, st) => { }; + if (UpdateWorkingStatus == null) UpdateWorkingStatus = (m, s, v, st) => { }; + if (ExitSuccess == null) ExitSuccess = (m, s) => { }; + if (ExitFailure == null) ExitFailure = (m, s) => { }; + + LogFile.BeginAction("FindFlashingProfile"); + try + { + LogFile.Log("Find Flashing Profile", LogType.FileAndConsole); + + NokiaFlashModel FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + + PhoneInfo Info; + if (DoResetFirst) + { + // The phone will be reset before flashing, so we have the opportunity to get some more info from the phone + Info = FlashModel.ReadPhoneInfo(); + Info.Log(LogType.ConsoleOnly); + + string FfuFirmware = null; + if (FFUPath != null) + { + FFU FFU = new FFU(FFUPath); + FfuFirmware = FFU.GetFirmwareVersion(); + } + FlashProfile Profile = App.Config.GetProfile(Info.PlatformID, Info.Firmware, FfuFirmware); + + if (Profile != null) + { + LogFile.Log("Flashing Profile already present for this phone", LogType.FileAndConsole); + return; + } + } + else + { + Info = FlashModel.ReadPhoneInfo(ExtendedInfo: false); + } + + SetWorkingStatus("Scanning for flashing-profile", "Your phone may appear to be in a reboot-loop. This is expected behavior. Don't interfere this process.", null, Status: WPinternalsStatus.Scanning); + + await LumiaV2CustomFlash(Notifier, FFUPath, false, !Info.SecureFfuEnabled || Info.RdcPresent || Info.Authenticated, null, DoResetFirst, Experimental: Experimental, SetWorkingStatus: + (m, s, v, a, st) => + { + if (st == WPinternalsStatus.SwitchingMode) + SetWorkingStatus(m, s, v, a, st); + }, + UpdateWorkingStatus: + (m, s, v, st) => + { + if (st == WPinternalsStatus.SwitchingMode) + UpdateWorkingStatus(m, s, v, st); + }, + ExitSuccess: ExitSuccess, ExitFailure: ExitFailure); + + LogFile.Log("Flashing profile found!", LogType.FileAndConsole); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("FindFlashingProfile"); + } + } + + internal static byte[] GetGptChunk(NokiaFlashModel FlashModel, UInt32 Size) + { + // This function is also used to generate a dummy chunk to flash for testing. + // The dummy chunk will contain the GPT, so it can be flashed to the first sectors for testing. + byte[] GPTChunk = new byte[Size]; + + PhoneInfo Info = FlashModel.ReadPhoneInfo(ExtendedInfo: false); + FlashAppType OriginalAppType = Info.App; + bool Switch = ((Info.App != FlashAppType.BootManager) && Info.SecureFfuEnabled && !Info.Authenticated && !Info.RdcPresent); + if (Switch) + FlashModel.SwitchToBootManagerContext(); + + byte[] Request = new byte[0x04]; + const string Header = "NOKT"; + + System.Buffer.BlockCopy(System.Text.Encoding.ASCII.GetBytes(Header), 0, Request, 0, Header.Length); + + byte[] Buffer = FlashModel.ExecuteRawMethod(Request); + if ((Buffer == null) || (Buffer.Length < 0x4408)) + throw new InvalidOperationException("Unable to read GPT!"); + + UInt16 Error = (UInt16)((Buffer[6] << 8) + Buffer[7]); + if (Error > 0) + throw new NotSupportedException("ReadGPT: Error 0x" + Error.ToString("X4")); + + System.Buffer.BlockCopy(Buffer, 8, GPTChunk, 0, 0x4400); + + if (Switch) + { + if (OriginalAppType == FlashAppType.FlashApp) + FlashModel.SwitchToFlashAppContext(); + else + FlashModel.SwitchToPhoneInfoAppContext(); + } + + return GPTChunk; + } + + internal static async Task LumiaV2EnableTestSigning(System.Threading.SynchronizationContext UIContext, string FFUPath, bool DoResetFirst = true) + { + LogFile.BeginAction("EnableTestSigning"); + try + { + LogFile.Log("Command: Enable testsigning", LogType.FileAndConsole); + PhoneNotifierViewModel Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + NokiaFlashModel FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + + List Parts = new List(); + FlashPart Part; + + // Use GetGptChunk() here instead of ReadGPT(), because ReadGPT() skips the first sector. + // We need the fist sector if we want to write back the GPT. + byte[] GPTChunk = GetGptChunk(FlashModel, 0x20000); + GPT GPT = new GPT(GPTChunk); + bool GPTChanged = false; + + Partition BACKUP_BS_NV = GPT.GetPartition("BACKUP_BS_NV"); + Partition UEFI_BS_NV; + if (BACKUP_BS_NV == null) + { + BACKUP_BS_NV = GPT.GetPartition("UEFI_BS_NV"); + Guid OriginalPartitionTypeGuid = BACKUP_BS_NV.PartitionTypeGuid; + Guid OriginalPartitionGuid = BACKUP_BS_NV.PartitionGuid; + BACKUP_BS_NV.Name = "BACKUP_BS_NV"; + BACKUP_BS_NV.PartitionGuid = Guid.NewGuid(); + BACKUP_BS_NV.PartitionTypeGuid = Guid.NewGuid(); + UEFI_BS_NV = new Partition(); + UEFI_BS_NV.Name = "UEFI_BS_NV"; + UEFI_BS_NV.Attributes = BACKUP_BS_NV.Attributes; + UEFI_BS_NV.PartitionGuid = OriginalPartitionGuid; + UEFI_BS_NV.PartitionTypeGuid = OriginalPartitionTypeGuid; + UEFI_BS_NV.FirstSector = BACKUP_BS_NV.LastSector + 1; + UEFI_BS_NV.LastSector = UEFI_BS_NV.FirstSector + BACKUP_BS_NV.LastSector - BACKUP_BS_NV.FirstSector; + GPT.Partitions.Add(UEFI_BS_NV); + GPTChanged = true; + } + if (GPTChanged) + { + GPT.Rebuild(); + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + Parts.Add(Part); + } + + // This code was used to compress the partition to an embedded resource: + // + // byte[] sbpart = System.IO.File.ReadAllBytes(@"C:\Windows Phone 8\Sources\WPInternals\SB.Original.bin"); + // System.IO.FileStream s = new System.IO.FileStream(@"C:\Windows Phone 8\Sources\WPInternals\SB", System.IO.FileMode.Create, System.IO.FileAccess.Write); + // CompressedStream Out = new CompressedStream(s, (ulong)sbpart.Length); + // Out.Write(sbpart, 0, sbpart.Length); + // Out.Close(); + // s.Close(); + + Part = new FlashPart(); + Partition TargetPartition = GPT.GetPartition("UEFI_BS_NV"); + Part.StartSector = (UInt32)TargetPartition.FirstSector; // GPT is prepared for 64-bit sector-offset, but flash app isn't. + Part.Stream = new SeekableStream(() => + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + + // Magic! + // The SB resource is a compressed version of a raw NV-variable-partition. + // In this partition the SecureBoot variable is disabled. + // It overwrites the variable in a different NV-partition than where this variable is stored usually. + // This normally leads to endless-loops when the NV-variables are enumerated. + // But the partition contains an extra hack to break out the endless loops. + var stream = assembly.GetManifestResourceStream("WPinternals.SB"); + + return new DecompressedStream(stream); + }); + Parts.Add(Part); + + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(Notifier, FFUPath, false, false, Parts, DoResetFirst, ClearFlashingStatusAtEnd: false); + + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("EnableTestSigning"); + } + } + + internal static async Task LumiaV2SwitchToMassStorageMode(PhoneNotifierViewModel Notifier, string FFUPath, bool DoResetFirst = true) + { + // If there is no phone connected yet, we wait here for the phone to connect. + // Because it could be connecting in mass storage mode. + // So we dont want to let SwitchTo() wait for it, because it might already be trying to switch to flash mode, which is not possible and not necessary at that point. + if (Notifier.CurrentInterface == null) + { + LogFile.Log("Waiting for phone to connect...", LogType.FileAndConsole); + await Notifier.WaitForArrival(); + } + + if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_MassStorage) + { + LogFile.Log("Phone is already in Mass Storage Mode", LogType.FileAndConsole); + return ((MassStorage)Notifier.CurrentModel).Drive; + } + + NokiaFlashModel FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + if (DoResetFirst) + { + // The phone will be reset before flashing, so we have the opportunity to get some more info from the phone + PhoneInfo Info = FlashModel.ReadPhoneInfo(); + Info.Log(LogType.ConsoleOnly); + } + + await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_MassStorage); + + MassStorage Storage = null; + if (Notifier.CurrentModel is MassStorage) + Storage = (MassStorage)Notifier.CurrentModel; + + if (Storage == null) + throw new WPinternalsException("Failed to switch to Mass Storage Mode"); + string Drive = Storage.Drive; + + return Drive; + } + + internal static async Task LumiaV2RelockPhone(PhoneNotifierViewModel Notifier, string FFUPath = null, bool DoResetFirst = true, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null, ExitSuccess ExitSuccess = null, ExitFailure ExitFailure = null) + { + if (SetWorkingStatus == null) SetWorkingStatus = (m, s, v, a, st) => { }; + if (UpdateWorkingStatus == null) UpdateWorkingStatus = (m, s, v, st) => { }; + if (ExitSuccess == null) ExitSuccess = (m, s) => { }; + if (ExitFailure == null) ExitFailure = (m, s) => { }; + + LogFile.BeginAction("RelockPhone"); + try + { + GPT GPT = null; + Partition Target = null; + NokiaFlashModel FlashModel = null; + + LogFile.Log("Command: Relock phone", LogType.FileAndConsole); + + if (Notifier.CurrentInterface == null) + await Notifier.WaitForArrival(); + + byte[] EFIESPBackup = null; + + try + { + if (Notifier.CurrentInterface != PhoneInterfaces.Lumia_MassStorage) + { + await SwitchModeViewModel.SwitchToWithStatus(Notifier, PhoneInterfaces.Lumia_MassStorage, SetWorkingStatus, UpdateWorkingStatus); + } + + if (!(Notifier.CurrentModel is MassStorage)) + throw new WPinternalsException("Failed to switch to Mass Storage mode"); + + SetWorkingStatus("Patching...", null, null, Status: WPinternalsStatus.Patching); + + // Now relock the phone + MassStorage Storage = (MassStorage)Notifier.CurrentModel; + + App.PatchEngine.TargetPath = Storage.Drive + "\\EFIESP\\"; + App.PatchEngine.Restore("SecureBootHack-V2-EFIESP"); + + App.PatchEngine.TargetPath = Storage.Drive + "\\"; + App.PatchEngine.Restore("SecureBootHack-MainOS"); + App.PatchEngine.Restore("RootAccess-MainOS"); + + // Edit BCD + LogFile.Log("Edit BCD"); + using (Stream BCDFileStream = new System.IO.FileStream(Storage.Drive + @"\EFIESP\efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite)) + { + using (DiscUtils.Registry.RegistryHive BCDHive = new DiscUtils.Registry.RegistryHive(BCDFileStream)) + { + DiscUtils.BootConfig.Store BCDStore = new DiscUtils.BootConfig.Store(BCDHive.Root); + DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); + DiscUtils.BootConfig.Element NoCodeIntegrityElement = MobileStartupObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + MobileStartupObject.RemoveElement(0x16000048); + + DiscUtils.BootConfig.BcdObject WinLoadObject = BCDStore.GetObject(new Guid("{7619dcc9-fafe-11d9-b411-000476eba25f}")); + NoCodeIntegrityElement = WinLoadObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + WinLoadObject.RemoveElement(0x16000048); + } + } + + byte[] GPTBuffer = Storage.ReadSectors(0, 0x22); + GPT = new GPT(GPTBuffer); + Partition EFIESPPartition = GPT.GetPartition("EFIESP"); + byte[] EFIESP = Storage.ReadSectors(EFIESPPartition.FirstSector, EFIESPPartition.SizeInSectors); + UInt32 EfiespSizeInSectors = (UInt32)EFIESPPartition.SizeInSectors; + if ((ByteOperations.ReadUInt32(EFIESP, 0x20) == (EfiespSizeInSectors / 2)) && (ByteOperations.ReadAsciiString(EFIESP, (UInt32)(EFIESP.Length / 2) + 3, 8)) == "MSDOS5.0") + { + EFIESPBackup = new byte[EfiespSizeInSectors * 0x200 / 2]; + Buffer.BlockCopy(EFIESP, (Int32)EfiespSizeInSectors * 0x200 / 2, EFIESPBackup, 0, (Int32)EfiespSizeInSectors * 0x200 / 2); + } + + LogFile.Log("The phone is currently in Mass Storage Mode", LogType.ConsoleOnly); + LogFile.Log("To continue the relock-sequence, the phone needs to be rebooted", LogType.ConsoleOnly); + LogFile.Log("Keep the phone connected to the PC", LogType.ConsoleOnly); + LogFile.Log("Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates", LogType.ConsoleOnly); + LogFile.Log("The relock-sequence will resume automatically", LogType.ConsoleOnly); + LogFile.Log("Waiting for manual reset of the phone...", LogType.ConsoleOnly); + + SetWorkingStatus("You need to manually reset your phone now!", "The phone is currently in Mass Storage Mode. To continue the relock-sequence, the phone needs to be rebooted. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. The relock-sequence will resume automatically.", null, false, WPinternalsStatus.WaitingForManualReset); + + await Notifier.WaitForRemoval(); + + SetWorkingStatus("Rebooting phone..."); + + await Notifier.WaitForArrival(); + } + catch + { + // If switching to mass storage mode failed, then we just skip that part. This might be a half unlocked phone. + LogFile.Log("Skipping Mass Storage mode", LogType.FileAndConsole); + } + + // Phone can also be in normal mode if switching to Mass Storage Mode had failed. + if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Normal) + await SwitchModeViewModel.SwitchToWithStatus(Notifier, PhoneInterfaces.Lumia_Flash, SetWorkingStatus, UpdateWorkingStatus); + + if ((Notifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader) && (Notifier.CurrentInterface != PhoneInterfaces.Lumia_Flash)) + await Notifier.WaitForArrival(); + + SetWorkingStatus("Flashing...", "The phone may reboot a couple of times. Just wait for it.", null, Status: WPinternalsStatus.Flashing); + + ((NokiaFlashModel)Notifier.CurrentModel).SwitchToFlashAppContext(); + + List FlashParts = new List(); + FlashPart Part; + + FlashModel = (NokiaFlashModel)Notifier.CurrentModel; + + // Remove IS_UNLOCKED flag in GPT + byte[] GPTChunk = GetGptChunk(FlashModel, 0x20000); // TODO: Get proper profile FFU and get ChunkSizeInBytes + GPT = new GPT(GPTChunk); + bool GPTChanged = false; + + Partition IsUnlockedPartition = GPT.GetPartition("IS_UNLOCKED"); + if (IsUnlockedPartition != null) + { + GPT.Partitions.Remove(IsUnlockedPartition); + GPTChanged = true; + } + + Partition EfiEspBackupPartition = GPT.GetPartition("BACKUP_EFIESP"); + if (EfiEspBackupPartition != null) + { + // This must be a left over of a half unlocked bootloader + Partition EfiEspPartition = GPT.GetPartition("EFIESP"); + EfiEspBackupPartition.Name = "EFIESP"; + EfiEspBackupPartition.LastSector = EfiEspPartition.LastSector; + EfiEspBackupPartition.PartitionGuid = EfiEspPartition.PartitionGuid; + EfiEspBackupPartition.PartitionTypeGuid = EfiEspPartition.PartitionTypeGuid; + GPT.Partitions.Remove(EfiEspPartition); + GPTChanged = true; + } + + Partition NvBackupPartition = GPT.GetPartition("BACKUP_BS_NV"); + if (NvBackupPartition != null) + { + // This must be a left over of a half unlocked bootloader + Partition NvPartition = GPT.GetPartition("UEFI_BS_NV"); + NvBackupPartition.Name = "UEFI_BS_NV"; + NvBackupPartition.PartitionGuid = NvPartition.PartitionGuid; + NvBackupPartition.PartitionTypeGuid = NvPartition.PartitionTypeGuid; + GPT.Partitions.Remove(NvPartition); + GPTChanged = true; + } + + if (GPTChanged) + { + GPT.Rebuild(); + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + FlashParts.Add(Part); + } + + if (EFIESPBackup != null) + { + Part = new FlashPart(); + Target = GPT.GetPartition("EFIESP"); + Part.StartSector = (UInt32)Target.FirstSector; + Part.Stream = new MemoryStream(EFIESPBackup); + FlashParts.Add(Part); + } + + // We should only clear NV if there was no backup NV to be restored and the current NV contains the SB unlock. + bool NvCleared = false; + PhoneInfo Info = ((NokiaFlashModel)Notifier.CurrentModel).ReadPhoneInfo(); + if ((NvBackupPartition == null) && !Info.UefiSecureBootEnabled) + { + // ClearNV + Part = new FlashPart(); + Target = GPT.GetPartition("UEFI_BS_NV"); + Part.StartSector = (UInt32)Target.FirstSector; + Part.Stream = new MemoryStream(new byte[0x40000]); + FlashParts.Add(Part); + NvCleared = true; + } + + WPinternalsStatus LastStatus = WPinternalsStatus.Undefined; + ulong? MaxProgressValue = null; + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(Notifier, FFUPath, false, false, FlashParts, DoResetFirst, ClearFlashingStatusAtEnd: !NvCleared, + SetWorkingStatus: (m, s, v, a, st) => + { + if (SetWorkingStatus != null) + { + if ((st == WPinternalsStatus.Scanning) || (st == WPinternalsStatus.WaitingForManualReset)) + SetWorkingStatus(m, s, v, a, st); + else if ((LastStatus == WPinternalsStatus.Scanning) || (LastStatus == WPinternalsStatus.WaitingForManualReset) || (LastStatus == WPinternalsStatus.Undefined)) + { + MaxProgressValue = v; + SetWorkingStatus("Flashing...", "The phone may reboot a couple of times. Just wait for it.", v, Status: WPinternalsStatus.Flashing); + } + LastStatus = st; + } + }, + UpdateWorkingStatus: (m, s, v, st) => + { + if (UpdateWorkingStatus != null) + { + if ((st == WPinternalsStatus.Scanning) || (st == WPinternalsStatus.WaitingForManualReset)) + UpdateWorkingStatus(m, s, v, st); + else if ((LastStatus == WPinternalsStatus.Scanning) || (LastStatus == WPinternalsStatus.WaitingForManualReset)) + SetWorkingStatus("Flashing...", "The phone may reboot a couple of times. Just wait for it.", MaxProgressValue, Status: WPinternalsStatus.Flashing); + else + UpdateWorkingStatus("Flashing...", "The phone may reboot a couple of times. Just wait for it.", v, Status: WPinternalsStatus.Flashing); + LastStatus = st; + } + }); + + if (NvBackupPartition != null) + { + // An old NV backup was restored and it possibly contained the IsFlashing flag. + // Can't clear it immeadiately, so we need another flash. + + SetWorkingStatus("Flashing...", "The phone may reboot a couple of times. Just wait for it.", null, Status: WPinternalsStatus.Flashing); + + // If last flash was a normal flash, with no forced crash at the end (!NvCleared), then we have to wait for device arrival, because it could still be detected as Flash-mode from previous flash. + // When phone was forcably crashed, it can be in emergency mode, or still rebooting. Then also wait for device arrival. + // But it is also possible that it is already in bootmgr mode after being crashed (Lumia 950 / 950XL). In that case don't wait for arrival. + if (!NvCleared || ((Notifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader) && (Notifier.CurrentInterface != PhoneInterfaces.Lumia_Flash))) + await Notifier.WaitForArrival(); + + if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Bootloader) + ((NokiaFlashModel)Notifier.CurrentModel).SwitchToFlashAppContext(); + + if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Flash) + { + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(Notifier, FFUPath, false, false, null, DoResetFirst, ClearFlashingStatusAtEnd: true, ShowProgress: false); + } + } + + LogFile.Log("Phone is relocked", LogType.FileAndConsole); + ExitSuccess("The phone is relocked", "NOTE: Make sure the phone properly boots and shuts down at least once before you unlock it again"); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ExitFailure("Error: " + Ex.Message, null); + } + finally + { + LogFile.EndAction("RelockPhone"); + } + } + + internal static async Task LumiaV2ClearNV(System.Threading.SynchronizationContext UIContext, string FFUPath, bool DoResetFirst = true) + { + LogFile.BeginAction("ClearNV"); + try + { + LogFile.Log("Command: Clear NV", LogType.FileAndConsole); + PhoneNotifierViewModel Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + NokiaFlashModel FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + List Parts = new List(); + + // Use GetGptChunk() here instead of ReadGPT(), because ReadGPT() skips the first sector. + // We need the fist sector if we want to write back the GPT. + byte[] GPTChunk = GetGptChunk(FlashModel, 0x20000); + GPT GPT = new GPT(GPTChunk); + bool GPTChanged = false; + Partition BACKUP_BS_NV = GPT.GetPartition("BACKUP_BS_NV"); + Partition UEFI_BS_NV; + if (BACKUP_BS_NV == null) + { + BACKUP_BS_NV = GPT.GetPartition("UEFI_BS_NV"); + Guid OriginalPartitionTypeGuid = BACKUP_BS_NV.PartitionTypeGuid; + Guid OriginalPartitionGuid = BACKUP_BS_NV.PartitionGuid; + BACKUP_BS_NV.Name = "BACKUP_BS_NV"; + BACKUP_BS_NV.PartitionGuid = Guid.NewGuid(); + BACKUP_BS_NV.PartitionTypeGuid = Guid.NewGuid(); + UEFI_BS_NV = new Partition(); + UEFI_BS_NV.Name = "UEFI_BS_NV"; + UEFI_BS_NV.Attributes = BACKUP_BS_NV.Attributes; + UEFI_BS_NV.PartitionGuid = OriginalPartitionGuid; + UEFI_BS_NV.PartitionTypeGuid = OriginalPartitionTypeGuid; + UEFI_BS_NV.FirstSector = BACKUP_BS_NV.LastSector + 1; + UEFI_BS_NV.LastSector = UEFI_BS_NV.FirstSector + BACKUP_BS_NV.LastSector - BACKUP_BS_NV.FirstSector; + GPT.Partitions.Add(UEFI_BS_NV); + GPTChanged = true; + } + if (GPTChanged) + { + GPT.Rebuild(); + FlashPart Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + Parts.Add(Part); + } + + using (MemoryStream Space = new MemoryStream(new byte[0x40000])) + { + Partition Target = GPT.GetPartition("UEFI_BS_NV"); + Parts.Add(new FlashPart() { StartSector = (uint)Target.FirstSector, Stream = Space }); + + await LumiaV2CustomFlash(Notifier, FFUPath, false, false, Parts, DoResetFirst, ClearFlashingStatusAtEnd: false); + } + + LogFile.Log("NV successfully cleared!", LogType.FileAndConsole); + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("ClearNV"); + } + } + + internal static async Task LumiaV2FlashPartition(System.Threading.SynchronizationContext UIContext, string FFUPath, string PartitionName, string PartitionPath, bool DoResetFirst = true) + { + LogFile.BeginAction("FlashPartition"); + try + { + LogFile.Log("Command: Flash Partition", LogType.FileAndConsole); + LogFile.Log("Partition name: " + PartitionName, LogType.FileAndConsole); + LogFile.Log("Partition file: " + PartitionPath, LogType.FileAndConsole); + if (FFUPath != null) + LogFile.Log("Profile FFU file: " + FFUPath, LogType.FileAndConsole); + + PhoneNotifierViewModel Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + + NokiaFlashModel FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + + PhoneInfo Info = FlashModel.ReadPhoneInfo(); + + // Use GetGptChunk() here instead of ReadGPT(), because ReadGPT() skips the first sector. + // We need the fist sector if we want to write back the GPT. + byte[] GPTChunk = GetGptChunk(FlashModel, 0x20000); + GPT GPT = new GPT(GPTChunk); + + Partition TargetPartition = GPT.GetPartition(PartitionName); + if (TargetPartition == null) + throw new WPinternalsException("Target partition not found!"); + LogFile.Log("Target-partition found at sector: 0x" + TargetPartition.FirstSector.ToString("X8") + " - 0x" + TargetPartition.LastSector.ToString("X8"), LogType.FileAndConsole); + + bool IsUnlocked = false; + bool GPTChanged = false; + List Parts = new List(); + FlashPart Part; + if (string.Compare(PartitionName, "EFIESP", true) == 0) + { + byte[] EfiespBinary = File.ReadAllBytes(PartitionPath); + IsUnlocked = ((ByteOperations.ReadUInt32(EfiespBinary, 0x20) == (EfiespBinary.Length / 0x200 / 2)) && (ByteOperations.ReadAsciiString(EfiespBinary, (UInt32)(EfiespBinary.Length / 2) + 3, 8)) == "MSDOS5.0"); + + if (IsUnlocked) + { + Partition IsUnlockedFlag = GPT.GetPartition("IS_UNLOCKED"); + if (IsUnlockedFlag == null) + { + IsUnlockedFlag = new Partition(); + IsUnlockedFlag.Name = "IS_UNLOCKED"; + IsUnlockedFlag.Attributes = 0; + IsUnlockedFlag.PartitionGuid = Guid.NewGuid(); + IsUnlockedFlag.PartitionTypeGuid = Guid.NewGuid(); + IsUnlockedFlag.FirstSector = 0x40; + IsUnlockedFlag.LastSector = 0x40; + GPT.Partitions.Add(IsUnlockedFlag); + GPTChanged = true; + } + } + else + { + Partition IsUnlockedFlag = GPT.GetPartition("IS_UNLOCKED"); + if (IsUnlockedFlag != null) + { + GPT.Partitions.Remove(IsUnlockedFlag); + GPTChanged = true; + } + } + + if (GPTChanged) + { + GPT.Rebuild(); + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + Parts.Add(Part); + } + } + + using (FileStream Stream = new FileStream(PartitionPath, FileMode.Open)) + { + if ((UInt64)Stream.Length != (TargetPartition.SizeInSectors * 0x200)) + throw new WPinternalsException("Raw partition has wrong size. Size = 0x" + Stream.Length.ToString("X8") + ". Expected size = 0x" + (TargetPartition.SizeInSectors * 0x200).ToString("X8")); + + Part = new FlashPart(); + Part.StartSector = (UInt32)TargetPartition.FirstSector; + Part.Stream = Stream; + Parts.Add(Part); + await LumiaV2CustomFlash(Notifier, FFUPath, false, false, Parts, DoResetFirst, string.Compare(PartitionName, "UEFI_BS_NV", true) != 0); + } + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("FlashPartition"); + } + } + + internal static async Task LumiaV2FlashRaw(System.Threading.SynchronizationContext UIContext, UInt64 StartSector, string DataPath, string FFUPath, bool DoResetFirst = true) + { + LogFile.BeginAction("FlashRaw"); + try + { + LogFile.Log("Command: Flash Raw", LogType.FileAndConsole); + LogFile.Log("Start sector: 0x" + StartSector.ToString("X16"), LogType.FileAndConsole); + LogFile.Log("Data file: " + DataPath, LogType.FileAndConsole); + if (FFUPath != null) + LogFile.Log("FFU file: " + FFUPath, LogType.FileAndConsole); + + PhoneNotifierViewModel Notifier = new PhoneNotifierViewModel(); + UIContext.Send(s => Notifier.Start(), null); + + NokiaFlashModel FlashModel = (NokiaFlashModel)(await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash)); + + PhoneInfo Info = FlashModel.ReadPhoneInfo(); + + byte[] Data = System.IO.File.ReadAllBytes(DataPath); + + await LumiaV2CustomFlash(Notifier, FFUPath, false, false, (UInt32)StartSector, Data, DoResetFirst); + Notifier.Stop(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + finally + { + LogFile.EndAction("FlashRaw"); + } + } + + internal async static Task LumiaV2CustomFlash(PhoneNotifierViewModel Notifier, string FFUPath, bool PerformFullFlashFirst, bool SkipWrite, UInt32 StartSector, byte[] Data, bool DoResetFirst = true, bool ClearFlashingStatusAtEnd = true, bool CheckSectorAlignment = true, bool ShowProgress = true, bool Experimental = false) //, string LoaderPath = null) + { + using (MemoryStream Stream = new MemoryStream(Data)) + { + FlashPart Part = new FlashPart() { StartSector = StartSector, Stream = Stream }; + List Parts = new List(); + Parts.Add(Part); + await LumiaV2CustomFlash(Notifier, FFUPath, PerformFullFlashFirst, SkipWrite, Parts, DoResetFirst, ClearFlashingStatusAtEnd, CheckSectorAlignment, ShowProgress, Experimental); + } + } + + internal async static Task LumiaV2CustomFlash(PhoneNotifierViewModel Notifier, string FFUPath, bool PerformFullFlashFirst, bool SkipWrite, UInt32 StartSector, Stream Data, bool DoResetFirst = true, bool ClearFlashingStatusAtEnd = true, bool CheckSectorAlignment = true, bool ShowProgress = true, bool Experimental = false) //, string LoaderPath = null) + { + FlashPart Part = new FlashPart() { StartSector = StartSector, Stream = Data }; + List Parts = new List(); + Parts.Add(Part); + await LumiaV2CustomFlash(Notifier, FFUPath, PerformFullFlashFirst, SkipWrite, Parts, DoResetFirst, ClearFlashingStatusAtEnd, CheckSectorAlignment, ShowProgress, Experimental); + } + + // Magic! + internal async static Task LumiaV2CustomFlash(PhoneNotifierViewModel Notifier, string FFUPath, bool PerformFullFlashFirst, bool SkipWrite, List FlashParts, bool DoResetFirst = true, bool ClearFlashingStatusAtEnd = true, bool CheckSectorAlignment = true, bool ShowProgress = true, bool Experimental = false, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null, ExitSuccess ExitSuccess = null, ExitFailure ExitFailure = null, string ProgrammerPath = null) //, string LoaderPath = null) + { + // Both SecurityHeader and StoreHeader need to be modified. + // Those should both not fall in a memory-gap to allow modification. + // The partial FFU header must be allocated in front of those headers, so the size of the partial header must be at least the size of the the SecurityHeader. + // Hashes take more space than descriptors, so the SecurityHeader will always be the biggest. + + bool AutoEmergencyReset = true; + bool Timeout; + + if (SetWorkingStatus == null) SetWorkingStatus = (m, s, v, a, st) => { }; + if (UpdateWorkingStatus == null) UpdateWorkingStatus = (m, s, v, st) => { }; + if (ExitSuccess == null) ExitSuccess = (m, s) => { }; + if (ExitFailure == null) ExitFailure = (m, s) => { }; + + NokiaFlashModel Model = (NokiaFlashModel)Notifier.CurrentModel; + PhoneInfo Info = Model.ReadPhoneInfo(); + + string Type = Info.Type; + if (ProgrammerPath == null) + { + ProgrammerPath = GetProgrammerPath(Info.RKH, Type); + if (ProgrammerPath == null) + LogFile.Log("WARNING: No emergency programmer file found. Finding flash profile and rebooting phone may take a long time!", LogType.FileAndConsole); + } + List FFUs = null; + FlashProfile Profile; + if (FFUPath == null) + { + // Try to find an FFU from the repository for which there is also a known flashing profile + FFUs = App.Config.FFURepository.Where(e => (Info.PlatformID.StartsWith(e.PlatformID, StringComparison.OrdinalIgnoreCase) && e.Exists())).ToList(); + foreach (FFUEntry CurrentEntry in FFUs) + { + Profile = App.Config.GetProfile(Info.PlatformID, Info.Firmware, CurrentEntry.FirmwareVersion); + if (Profile != null) + { + FFUPath = CurrentEntry.Path; + break; + } + } + } + + if (FFUPath == null) + { + // Try to find any FFU with matching PlatformID in the repository + if (FFUs.Count > 0) + { + FFUPath = FFUs[0].Path; + } + } + + if (FFUPath == null) + throw new WPinternalsException("No valid profile FFU found in repository", "You can download necessary files in the "Download" section"); + + FFU FFU = new FFU(FFUPath); + UInt32 UpdateType = ByteOperations.ReadUInt32(FFU.StoreHeader, 0); + if (UpdateType != 0) + throw new WPinternalsException("Only Full Flash images supported"); + + UInt32 ChunkCount = 1; // Always flash one extra chunk on the GPT (for purpose of testing and for making sure that first chunk does not contain all zero's). + if (FlashParts != null) + { + foreach (FlashPart Part in FlashParts) + { + if (Part.Stream == null) + throw new ArgumentException("Stream is null"); + if (!Part.Stream.CanSeek) + throw new ArgumentException("Streams must be seekable"); + if (((Part.StartSector * 0x200) % FFU.ChunkSize) != 0) + throw new ArgumentException("Invalid StartSector alignment"); + if (CheckSectorAlignment) + { + if ((Part.Stream.Length % FFU.ChunkSize) != 0) + throw new ArgumentException("Invalid Data length"); + } + + ChunkCount += (UInt32)(Part.Stream.Length / FFU.ChunkSize); + } + } + + if ((Info.SecureFfuSupportedProtocolMask & ((ushort)FfuProtocol.ProtocolSyncV2)) == 0) // Exploit needs protocol v2 -> This check is not conclusive, because old phones also report support for this protocol, although it is really not supported. + throw new WPinternalsException("Flash failed!", "Protocols not supported"); + if (Info.FlashAppProtocolVersionMajor < 2) // Old phones do not support the hack. These phones have Flash protocol 1.x. + throw new WPinternalsException("Flash failed!", "Protocols not supported"); + UEFI UEFI = new UEFI(FFU.GetPartition("UEFI")); + string BootMgrName = UEFI.EFIs.Where(efi => ((efi.Name != null) && (efi.Name.Contains("BootMgrApp")))).First().Name; + UInt32 EstimatedSizeOfMemGap = (UInt32)UEFI.GetFile(BootMgrName).Length; + byte Options = 0; + if (SkipWrite) + Options = (byte)FlashOptions.SkipWrite; + if (!Info.SecureFfuEnabled || Info.Authenticated || Info.RdcPresent) + Options = (byte)((FlashOptions)Options | FlashOptions.SkipSignatureCheck); + + // Gap fill calculation: + // About 0x18000 of the gap is used for other purposes. + // Then round down to fill up the rest of the space, to make sure the memory for the headers is not allocated in a gap. + UInt32 EstimatedGapFill = FFU.RoundDownToChunks(EstimatedSizeOfMemGap - 0x18000); + + UInt32 MaximumGapFill; + int MaximumAttempts; + + if (!Experimental) + { + MaximumGapFill = FFU.RoundUpToChunks(2 * EstimatedSizeOfMemGap); + MaximumAttempts = (int)(((MaximumGapFill / FFU.ChunkSize) + 1) * 4); + } + else + { + MaximumGapFill = FFU.RoundUpToChunks(4 * EstimatedSizeOfMemGap); + MaximumAttempts = (int)(((MaximumGapFill / FFU.ChunkSize) + 1) * 8); + } + + byte[] GPTChunk = GetGptChunk(Model, (UInt32)FFU.ChunkSize); + + // Start with a reset + if (DoResetFirst) + { + SetWorkingStatus("Initializing flash...", "Rebooting phone", null, Status: WPinternalsStatus.Initializing); + + // When in flash mode, it is not possible to reboot straight to flash. + // Reboot and catch the phone in bootloader mode and then switch to flash context + Model.ResetPhone(); + + #region Properly recover from reset - many phones respond differently + + Timeout = false; + try + { + await Notifier.WaitForArrival().TimeoutAfter(TimeSpan.FromSeconds(40)); + } + catch (TimeoutException) + { + Timeout = true; + } + if ((Notifier.CurrentInterface == null) && (!AutoEmergencyReset || Timeout)) + { + AutoEmergencyReset = false; + + if (Timeout) + { + LogFile.Log("The phone is not responding", LogType.ConsoleOnly); + LogFile.Log("It might be in emergency mode, while you have no matching driver installed", LogType.ConsoleOnly); + } + LogFile.Log("To continue the unlock-sequence, the phone needs to be rebooted", LogType.ConsoleOnly); + LogFile.Log("Keep the phone connected to the PC", LogType.ConsoleOnly); + LogFile.Log("Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates", LogType.ConsoleOnly); + LogFile.Log("The unlock-sequence will resume automatically", LogType.ConsoleOnly); + LogFile.Log("Waiting for manual reset of the phone...", LogType.ConsoleOnly); + + SetWorkingStatus("You need to manually reset your phone now!", (Timeout ? "The phone is not responding. It might be in emergency mode, while you have no matching driver installed. " : "") + "To continue the unlock-sequence, the phone needs to be rebooted. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. The unlock-sequence will resume automatically.", null, false, WPinternalsStatus.WaitingForManualReset); + + await Notifier.WaitForRemoval(); + + UpdateWorkingStatus("Initializing flash...", null, null); + + await Notifier.WaitForArrival(); + } + if (Notifier.CurrentInterface == PhoneInterfaces.Qualcomm_Download) + { + if (ProgrammerPath != null) + { + QualcommSahara Sahara = new QualcommSahara((QualcommSerial)Notifier.CurrentModel); + await Sahara.Reset(ProgrammerPath); + await Notifier.WaitForArrival(); + } + else + { + ((QualcommSerial)Notifier.CurrentModel).Close(); // Prevent "Resource in use"; + + Timeout = false; + if (AutoEmergencyReset) + { + try + { + await Notifier.WaitForArrival().TimeoutAfter(TimeSpan.FromSeconds(40)); + } + catch (TimeoutException) + { + Timeout = true; + } + } + if (!AutoEmergencyReset || Timeout) + { + AutoEmergencyReset = false; + + LogFile.Log("The phone is in emergency mode and you didn't provide an emergency programmer", LogType.ConsoleOnly); + LogFile.Log("This phone also doesn't seem to reboot after a timeout, so you got to help a bit", LogType.ConsoleOnly); + LogFile.Log("Keep the phone connected to the PC", LogType.ConsoleOnly); + LogFile.Log("Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates", LogType.ConsoleOnly); + LogFile.Log("The unlock-sequence will resume automatically", LogType.ConsoleOnly); + LogFile.Log("To prevent this, provide an emergency programmer next time you will unlock a bootloader", LogType.ConsoleOnly); + LogFile.Log("Waiting for manual reset of the phone...", LogType.ConsoleOnly); + + SetWorkingStatus("You need to manually reset your phone now!", "The phone is in emergency mode and you didn't provide an emergency programmer. This phone also doesn't seem to reboot after a timeout, so you got to help a bit. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. The unlock-sequence will resume automatically. To prevent this, provide an emergency programmer next time you will unlock a bootloader.", null, false, WPinternalsStatus.WaitingForManualReset); + + await Notifier.WaitForRemoval(); + + UpdateWorkingStatus("Initializing flash...", null, null); + + await Notifier.WaitForArrival(); + } + } + } + + #endregion + + if ((Notifier.CurrentInterface != PhoneInterfaces.Lumia_Flash) && (Notifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader)) + throw new WPinternalsException("Phone is in wrong mode"); + Model = (NokiaFlashModel)Notifier.CurrentModel; + UpdateWorkingStatus("Initializing flash...", null, null); + } + + try + { + // This will succeed on new models + Model.SwitchToFlashAppContext(); + Model.DisableRebootTimeOut(); + } + catch + { + // This will succeed on old models + Model.ResetPhoneToFlashMode(); + await Notifier.WaitForArrival(); + Model = (NokiaFlashModel)Notifier.CurrentModel; + } + + bool AssumeImageHeaderFallsInGap = true; + bool AllocateAsyncBuffersOnPhone = true; + bool AllocateBackupBuffersOnPhone = false; + UInt32 CurrentGapFill = EstimatedGapFill; + UInt32 OldGapFill; + bool Success = false; + bool Abort = false; + bool PhoneNeedsReset = false; + bool WaitForReset = false; + int AttemptCount = 0; + UInt32 ExploitHeaderAllocationSize = 0; + UInt32 LastHeaderV2Size = 0; + byte[] PartialHeader; + byte[] FfuHeader; + UInt64 CombinedFFUHeaderSize; + Allocation SecurityHeaderAllocation = null; + Allocation ImageHeaderAllocation = null; + Allocation StoreHeaderAllocation = null; + Allocation PartialHeaderAllocation = null; + UInt32 HeaderOffset = 0; + bool Scanning = false; + bool ResetScanning = false; + + Profile = App.Config.GetProfile(Info.PlatformID, Info.Firmware, FFU.GetFirmwareVersion()); + if (Profile == null) + LogFile.Log("No flashing profile found", LogType.FileAndConsole); + else + { + if (ShowProgress) + LogFile.Log("Flashing profile loaded", LogType.FileAndConsole); + CurrentGapFill = Profile.FillSize; + ExploitHeaderAllocationSize = Profile.HeaderSize; + AllocateAsyncBuffersOnPhone = Profile.AllocateAsyncBuffersOnPhone; + AssumeImageHeaderFallsInGap = Profile.AssumeImageHeaderFallsInGap; + } + + do + { + AttemptCount++; + if ((Profile == null) || (AttemptCount > 1)) + { + LogFile.Log("Custom flash attempt: " + AttemptCount + " of " + MaximumAttempts, LogType.FileAndConsole); + + if (!Scanning) + SetWorkingStatus("Scanning for flashing-profile - attempt " + AttemptCount.ToString() + " of " + MaximumAttempts.ToString(), "Your phone may appear to be in a reboot-loop. This is expected behavior. Don't interfere this process.", (uint)MaximumAttempts, Status: WPinternalsStatus.Scanning); + Scanning = true; + UpdateWorkingStatus("Scanning for flashing-profile - attempt " + AttemptCount.ToString() + " of " + MaximumAttempts.ToString(), "Your phone may appear to be in a reboot-loop. This is expected behavior. Don't interfere this process.", (uint)AttemptCount, Status: WPinternalsStatus.Scanning); + + ExploitHeaderAllocationSize = CurrentGapFill + (UInt32)FFU.ChunkSize; + } + + // Initialize flash attempts + // Make sure async buffers are allocated on the phone before overflow attempts, + // or else failed attempts may cause more memory-gaps and allocation becomes more unpredictable. + // The phone is rebooted after each attempt (to avoid memory-corruption). + // And it seems that that normally all allocations are in a big memory-gap, which was created before BootMgr was loaded. + // And there is still memory allocated in a lower range. + // And by allocating USB buffers, it could cause more memory-scattering. + // On Lumia 950 2 USB buffers are allocated and on Lumia 930 there is only one USB buffer allocated. + // StartAsyncFlash() is needed on 950 and 640. But not needed on 930! + // In any case, the allocation of the async-buffers should not be simulated in the Uefi Memory Simulator, + // because that would create a gap in the Simulator, instead of avoiding a gap on the phone. + // + if (AllocateAsyncBuffersOnPhone) + { + Model.StartAsyncFlash(); + Model.EndAsyncFlash(); // Ending Async flashing is not necessary for Lumia 950, but it is necessary for Lumia 640! + } + + if (AllocateBackupBuffersOnPhone) + { + Model.BackupPartitionToRam("MODEM_FSG"); + Model.BackupPartitionToRam("MODEM_FS1"); + Model.BackupPartitionToRam("MODEM_FS2"); + Model.BackupPartitionToRam("SSD"); + Model.BackupPartitionToRam("DPP"); + } + + HeaderOffset = 0; + SecurityHeaderAllocation = null; + ImageHeaderAllocation = null; + StoreHeaderAllocation = null; + PartialHeaderAllocation = null; + UInt32 DestinationChunkIndex = 0; + UInt32 DestinationChunkOffset = 0; + + // Create memory map + UefiMemorySim.Reset(); + SecurityHeaderAllocation = UefiMemorySim.AllocatePool((uint)FFU.SecurityHeader.Length); + SecurityHeaderAllocation.CopyToThisAllocation(FFU.SecurityHeader, 0, (uint)FFU.SecurityHeader.Length, 0); + if (!AssumeImageHeaderFallsInGap) + { + ImageHeaderAllocation = UefiMemorySim.AllocatePool((uint)FFU.ImageHeader.Length); + ImageHeaderAllocation.CopyToThisAllocation(FFU.ImageHeader, 0, (uint)FFU.ImageHeader.Length, 0); + } + StoreHeaderAllocation = UefiMemorySim.AllocatePool((uint)FFU.StoreHeader.Length); + StoreHeaderAllocation.CopyToThisAllocation(FFU.StoreHeader, 0, (uint)FFU.StoreHeader.Length, 0); + + // Simulate sending partial header + PartialHeaderAllocation = UefiMemorySim.AllocatePool(ExploitHeaderAllocationSize); + + CombinedFFUHeaderSize = FFU.HeaderSize; + FfuHeader = new byte[CombinedFFUHeaderSize]; + UInt32 TotalChunkCount = ChunkCount; + bool HeadersFull; + int FlashingPhase = 0; + int FlashingPhaseStartStreamIndex = -1; + long FlashingPhaseStartStreamPosition = 0; + UInt32 FlashingPhaseStartChunkIndex = 0; + UInt32 FlashingPhaseChunkCount; + bool FlashInProgress = false; + byte[] Buffer = new byte[FFU.ChunkSize]; + LastHeaderV2Size = 0; + do + { + HeadersFull = false; + + // On every flashing phase we must fill the memory gap first, before sending the header. + if (CurrentGapFill > UefiMemorySim.PageSize) + { + if (FlashingPhase > 0) + { + // Avoid Error 0x0010 "Invalid sub block length". + // Headersize must increase compared to last time a header was sent, to avoid processing the header. + // Offset must be 0, to reset buffers. + // Previous data + new data must fit in new headersize. + // We can send an extra byte, because last memory buffer was sent including the tail. + // And there is always extra space in the memoryspace after the tail. + Model.SendFfuHeaderV2(LastHeaderV2Size + 1, 0, new byte[1], Options); + } + + // CurrentGapFill is the amount of data we want to be allocated on the phone + // But we send less data, so the header won't be processed yet. + PartialHeader = new byte[UefiMemorySim.PageSize]; + Model.SendFfuHeaderV2(CurrentGapFill, 0, PartialHeader, Options); // Fill memory gap -> This will fail on phones with Flash Protocol v1.x !! On Lumia 640 this will hang on receiving the response when EndAsyncFlash was not called. + } + + using (System.IO.FileStream FfuFile = new System.IO.FileStream(FFU.Path, System.IO.FileMode.Open, System.IO.FileAccess.Read)) + { + // On every flashing phase we need to send the full header again to reset all the counters. + FfuFile.Read(FfuHeader, 0, (int)CombinedFFUHeaderSize); + Model.SendFfuHeaderV1(FfuHeader, Options); + + if (PerformFullFlashFirst && (FlashingPhase == 0)) + { + // If we flash the stock ROM at this point, the header is in memory is not overwritten yet. + // This means that after the last chunk was written, the flashing-status is written to NV (and also flushed). + // But this doesn't matter, because even when we want to overwrite NV, this happens later. + // When the header is successfully overwritten in memory, the next chunk of custom data will be allowed to be written. + + LogFile.Log("Starting custom flash attempt by doing a full flash.", LogType.FileAndConsole); + + UInt64 Position = CombinedFFUHeaderSize; + byte[] FlashPayload; + int ChunkIndex = 0; + TotalChunkCount += (UInt32)FFU.TotalChunkCount; + + // Protocol v2 + FlashPayload = new byte[Info.WriteBufferSize]; + + while (Position < (UInt64)FfuFile.Length) + { + UInt32 CommonFlashPayloadSize = Info.WriteBufferSize; + if (((UInt64)FfuFile.Length - Position) < CommonFlashPayloadSize) + { + CommonFlashPayloadSize = (UInt32)((UInt64)FfuFile.Length - Position); + FlashPayload = new byte[CommonFlashPayloadSize]; + } + + FfuFile.Read(FlashPayload, 0, (int)CommonFlashPayloadSize); + ChunkIndex += (int)(CommonFlashPayloadSize / FFU.ChunkSize); + Model.SendFfuPayloadV2(FlashPayload, ShowProgress ? (int)((double)(ChunkIndex + 1) * 100 / TotalChunkCount) : 0, 0); + Position += CommonFlashPayloadSize; + } + } + } + + UInt32 NewWriteDescriptorOffset = 0xF8; + UInt32 CatalogSize = ByteOperations.ReadUInt32(UefiMemorySim.Buffer, SecurityHeaderAllocation.ContentStart + 0x18); + UInt32 NewHashOffset = 0x20 + CatalogSize; + + UInt32 WriteDescriptorLength = 0; + UInt32 WriteDescriptorCount = 0; + UInt32 HashTableSize = 0; + + if (PerformFullFlashFirst && (FlashingPhase == 0)) + { + // Set offset for new descriptor to end of descriptor-table + WriteDescriptorLength = ByteOperations.ReadUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + 0xD4); + NewWriteDescriptorOffset += WriteDescriptorLength; + + // Get descriptor-count of the FFU + WriteDescriptorCount = ByteOperations.ReadUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + 0xD0); + + // Set offset for new hash to end of hash-table + HashTableSize = ByteOperations.ReadUInt32(UefiMemorySim.Buffer, SecurityHeaderAllocation.ContentStart + 0x1C); + NewHashOffset += HashTableSize; // Skip to the end of the original hash-table. + } + else + { + // From start of hash-table skip the first hashes for Image- and StoreHeaders. + HashTableSize = (UInt32)(((FFU.ImageHeader.Length + FFU.StoreHeader.Length) / FFU.ChunkSize) * 0x20); + NewHashOffset += HashTableSize; + } + + // Determine number of chunks for this phase + UInt32 HashSpace = (UInt32)(FFU.SecurityHeader.Length - NewHashOffset); + UInt32 FreeHashCount = HashSpace / 0x20; // Round down automatically + UInt32 DescriptorSpace = (UInt32)(FFU.StoreHeader.Length - NewWriteDescriptorOffset); + UInt32 FreeDescriptorCount = DescriptorSpace / 0x10; + FlashingPhaseChunkCount = FreeHashCount < FreeDescriptorCount ? FreeHashCount : FreeDescriptorCount; + if ((ChunkCount - FlashingPhaseStartChunkIndex) <= FlashingPhaseChunkCount) + FlashingPhaseChunkCount = ChunkCount - FlashingPhaseStartChunkIndex; + else + HeadersFull = true; + + HashTableSize += (FlashingPhaseChunkCount * 0x20); + WriteDescriptorCount += FlashingPhaseChunkCount; + WriteDescriptorLength += (FlashingPhaseChunkCount * 0x10); + + if (!ClearFlashingStatusAtEnd || HeadersFull) + WriteDescriptorCount++; + + // Write back new header values. + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + 0xD4, WriteDescriptorLength); + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + 0xD0, WriteDescriptorCount); + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, SecurityHeaderAllocation.ContentStart + 0x1C, HashTableSize); + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + 0xEC, 0); // FlashOnlyTableLength - Make flash progress bar white immediately. + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + 0xE8, 1); // FlashOnlyTableCount + + UInt32 CustomChunkCount = FlashingPhaseChunkCount; + + // Write new descriptors + // First write descriptor and hash for the first GPT chunk + if (!FlashInProgress && (FlashingPhaseChunkCount > 0)) + { + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + NewWriteDescriptorOffset + 0x00, 0x00000001); // Location count + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + NewWriteDescriptorOffset + 0x04, 0x00000001); // Chunk count + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + NewWriteDescriptorOffset + 0x08, 0x00000000); // Disk access method (0 = Begin, 2 = End) + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + NewWriteDescriptorOffset + 0x0C, 0x00000000); // Chunk index = GPT + NewWriteDescriptorOffset += 0x10; + byte[] GPTHashValue = System.Security.Cryptography.SHA256.Create().ComputeHash(GPTChunk, 0, FFU.ChunkSize); // Hash is 0x20 bytes + System.Buffer.BlockCopy(GPTHashValue, 0, UefiMemorySim.Buffer, (int)(SecurityHeaderAllocation.ContentStart + NewHashOffset), 0x20); + NewHashOffset += 0x20; + CustomChunkCount--; + } + + // TODO: Optimize: make multiple locations for chunks with same content. + Stream CurrentStream = null; + int StreamIndex = FlashingPhaseStartStreamIndex; + if (StreamIndex >= 0) + { + CurrentStream = FlashParts[StreamIndex].Stream; + CurrentStream.Seek(FlashingPhaseStartStreamPosition, SeekOrigin.Begin); + } + byte[] PayloadBuffer = new byte[FFU.ChunkSize]; + for (int i = 0; i < CustomChunkCount; i++) + { + if ((CurrentStream == null) || (CurrentStream.Position == CurrentStream.Length)) + { + StreamIndex++; + CurrentStream = FlashParts[StreamIndex].Stream; + CurrentStream.Seek(0, SeekOrigin.Begin); + DestinationChunkOffset = (UInt32)((Int64)FlashParts[StreamIndex].StartSector * 0x200 / FFU.ChunkSize); + } + + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + NewWriteDescriptorOffset + 0x00, 0x00000001); // Location count + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + NewWriteDescriptorOffset + 0x04, 0x00000001); // Chunk count + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + NewWriteDescriptorOffset + 0x08, 0x00000000); // Disk access method (0 = Begin, 2 = End) + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.ContentStart + NewWriteDescriptorOffset + 0x0C, DestinationChunkOffset); // Chunk index + NewWriteDescriptorOffset += 0x10; + + // Write new hash + if ((CurrentStream.Length - CurrentStream.Position) < PayloadBuffer.Length) + Array.Clear(PayloadBuffer, 0, PayloadBuffer.Length); + CurrentStream.Read(PayloadBuffer, 0, FFU.ChunkSize); + byte[] HashValue = System.Security.Cryptography.SHA256.Create().ComputeHash(PayloadBuffer, 0, FFU.ChunkSize); // Hash is 0x20 bytes + System.Buffer.BlockCopy(HashValue, 0, UefiMemorySim.Buffer, (int)(SecurityHeaderAllocation.ContentStart + NewHashOffset), 0x20); + NewHashOffset += 0x20; + + DestinationChunkOffset++; + } + + int Step = 0; + try + { + // Send a small portion of header v2 at offset 0 + // The payload is smaller than the total headersize, so that it won't start processing the header now. + // This will allocate new memory at the bottom of the memory-pool, but it will not reset the previously imported ffu header. + Step = 1; + PartialHeader = new byte[UefiMemorySim.PageSize]; + Model.SendFfuHeaderV2(ExploitHeaderAllocationSize, 0, PartialHeader, Options); // SkipWrite = 1 (only works on engineering phones) + + // Now we will send the rest of the exploit header, but we will increase the total size even higher, so that it still won't start processing the headers. + // We've send only a small first part of the header. The allocated header was bigger: ExploitHeaderAllocationSize. + // But we HAVE to send the whole header. We can't skip a part. + Step = 2; + UInt32 ExploitHeaderRemaining = SecurityHeaderAllocation.TailEnd + 1 - PartialHeaderAllocation.ContentStart - (UInt32)PartialHeader.Length; + HeaderOffset = (UInt32)PartialHeader.Length; + while (ExploitHeaderRemaining > 0) + { + UInt32 CurrentFill = ExploitHeaderRemaining; + if (CurrentFill > Info.WriteBufferSize) + CurrentFill = Info.WriteBufferSize; + PartialHeader = new byte[CurrentFill]; + PartialHeaderAllocation.CopyFromThisAllocation(HeaderOffset, CurrentFill, PartialHeader, 0); + Model.SendFfuHeaderV2(HeaderOffset + CurrentFill + 1, HeaderOffset, PartialHeader, Options); // Phone may crash here. USB write is done. USB read might fail due to crash. Happens on my own Lumia 650. + LastHeaderV2Size = HeaderOffset + CurrentFill + 1; + ExploitHeaderRemaining -= CurrentFill; + HeaderOffset += CurrentFill; + } + + // Send custom payload + // TODO: Optimize to send multiple chunks at once + Step = 3; + DestinationChunkIndex = (PerformFullFlashFirst && (FlashingPhase == 0)) ? (UInt32)FFU.TotalChunkCount : 0; + + CurrentStream = null; + StreamIndex = FlashingPhaseStartStreamIndex; + if (StreamIndex >= 0) + { + CurrentStream = FlashParts[StreamIndex].Stream; + CurrentStream.Seek(FlashingPhaseStartStreamPosition, SeekOrigin.Begin); + } + + for (int i = 0; i < FlashingPhaseChunkCount; i++) + { + string NewProgressText = null; + if (!FlashInProgress) + { + // First send the GPT chunk + Step = 4; + System.Buffer.BlockCopy(GPTChunk, 0, Buffer, 0, (int)FFU.ChunkSize); + } + else + { + Step = 5; + if ((CurrentStream == null) || (CurrentStream.Position == CurrentStream.Length)) + { + StreamIndex++; + CurrentStream = FlashParts[StreamIndex].Stream; + CurrentStream.Seek(0, SeekOrigin.Begin); + NewProgressText = FlashParts[StreamIndex].ProgressText; + } + + Step = 6; + if ((CurrentStream.Length - CurrentStream.Position) < Buffer.Length) + Array.Clear(Buffer, 0, Buffer.Length); + + Step = 7; + CurrentStream.Read(Buffer, 0, FFU.ChunkSize); + } + + Step = 8; + // This may fail. Normally with WPinternalsException for Invalid Hash or Data not aligned. + // Or it may fail with a BadConnectionException when the phone crashes and drops the connection. + Model.SendFfuPayloadV1(Buffer, ShowProgress ? (int)((FlashingPhaseStartChunkIndex + DestinationChunkIndex + 1) * 100 / TotalChunkCount) : 0); + if (!FlashInProgress) + { + Step = 9; + if (ShowProgress) + LogFile.Log("Flashing in progress!", LogType.FileAndConsole); + FlashInProgress = true; + Scanning = false; + SetWorkingStatus(null, null, TotalChunkCount, Status: WPinternalsStatus.Flashing); + } + UpdateWorkingStatus(NewProgressText, null, FlashingPhaseStartChunkIndex + DestinationChunkIndex + 1, WPinternalsStatus.Flashing); + NewProgressText = null; + DestinationChunkIndex++; + } + + Step = 10; + FlashingPhaseStartChunkIndex += FlashingPhaseChunkCount; + FlashingPhaseStartStreamIndex = StreamIndex; + if (StreamIndex >= 0) + FlashingPhaseStartStreamPosition = CurrentStream.Position; + + Step = 11; + if (!HeadersFull) + { + Step = 12; + App.Config.SetProfile(Info.Type, Info.PlatformID, Info.ProductCode, Info.Firmware, FFU.GetFirmwareVersion(), CurrentGapFill, ExploitHeaderAllocationSize, AssumeImageHeaderFallsInGap, AllocateAsyncBuffersOnPhone); + if (ShowProgress) + LogFile.Log("Custom flash succeeded!", LogType.FileAndConsole); + Success = true; + } + } + catch (BadConnectionException) + { + LogFile.Log("Connection to phone is lost - " + + Step.ToString() + " " + + StreamIndex.ToString() + " " + + (CurrentStream == null ? "0" : CurrentStream.Position.ToString()) + " " + + FlashingPhase.ToString() + " " + + FlashingPhaseStartChunkIndex.ToString() + " " + + DestinationChunkIndex.ToString()); + LogFile.Log("Expect phone to reboot", LogType.FileAndConsole); + WaitForReset = true; + } + catch (Exception Ex) + { + if (FlashInProgress) + { + // Normally, when we end up here, we were not in process of flashing yet. + // It would be a flash attempt which failed. + // But if we were already flashing, then something else is wrong. + // We need more info and stop flashing. + LogFile.Log("Custom flash failed", LogType.FileAndConsole); + LogFile.LogException(Ex, LogType.FileOnly, + Step.ToString() + " " + + StreamIndex.ToString() + " " + + (CurrentStream == null ? "0" : CurrentStream.Position.ToString()) + " " + + FlashingPhase.ToString() + " " + + FlashingPhaseStartChunkIndex.ToString() + " " + + DestinationChunkIndex.ToString()); + Abort = true; + } + else + { + LogFile.Log("Custom flash attempt failed", LogType.FileAndConsole); + LogFile.LogException(Ex, LogType.FileOnly, + Step.ToString() + " " + + StreamIndex.ToString() + " " + + (CurrentStream == null ? "0" : CurrentStream.Position.ToString()) + " " + + FlashingPhase.ToString() + " " + + FlashingPhaseStartChunkIndex.ToString() + " " + + DestinationChunkIndex.ToString()); + } + + PhoneNeedsReset = true; + } + + if (FlashInProgress) + FlashingPhase++; + } + while (HeadersFull && FlashInProgress && !Abort); + + if (!Success) + { + if ((Profile != null) && !Abort) + { + LogFile.Log("Flashing profile was loaded, but it is not working", LogType.FileAndConsole); + LogFile.Log("Attempting to find a working profile", LogType.FileAndConsole); + ResetScanning = true; + } + + if (PhoneNeedsReset) + { + Model.ResetPhone(); + WaitForReset = true; + } + + if (WaitForReset) + { + #region Properly recover from reset between flash attempts - many phones respond differently + + Timeout = false; + try + { + await Notifier.WaitForArrival().TimeoutAfter(TimeSpan.FromSeconds(40)); + } + catch (TimeoutException) + { + Timeout = true; + } + if ((Notifier.CurrentInterface == null) && (!AutoEmergencyReset || Timeout)) + { + AutoEmergencyReset = false; + + if (Timeout) + { + LogFile.Log("The phone is not responding", LogType.ConsoleOnly); + LogFile.Log("It might be in emergency mode, while you have no matching driver installed", LogType.ConsoleOnly); + } + LogFile.Log("To continue the unlock-sequence, the phone needs to be rebooted", LogType.ConsoleOnly); + LogFile.Log("Keep the phone connected to the PC", LogType.ConsoleOnly); + LogFile.Log("Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates", LogType.ConsoleOnly); + LogFile.Log("The unlock-sequence will resume automatically", LogType.ConsoleOnly); + LogFile.Log("Waiting for manual reset of the phone...", LogType.ConsoleOnly); + + UpdateWorkingStatus("You need to manually reset your phone now!", (Timeout ? "The phone is not responding. It might be in emergency mode, while you have no matching driver installed. " : "") + "To continue the unlock-sequence, the phone needs to be rebooted. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. The unlock-sequence will resume automatically.", null, WPinternalsStatus.WaitingForManualReset); + + await Notifier.WaitForRemoval(); + + UpdateWorkingStatus("Scanning for flashing-profile - attempt " + AttemptCount.ToString() + " of " + MaximumAttempts.ToString(), "Your phone may appear to be in a reboot-loop. This is expected behavior. Don't interfere this process.", (uint)AttemptCount, Status: WPinternalsStatus.Scanning); + + await Notifier.WaitForArrival(); + } + if (Notifier.CurrentInterface == PhoneInterfaces.Qualcomm_Download) + { + if (ProgrammerPath != null) + { + QualcommSahara Sahara = new QualcommSahara((QualcommSerial)Notifier.CurrentModel); + await Sahara.Reset(ProgrammerPath); + await Notifier.WaitForArrival(); + } + else + { + ((QualcommSerial)Notifier.CurrentModel).Close(); // Prevent "Resource in use"; + + Timeout = false; + if (AutoEmergencyReset) + { + try + { + await Notifier.WaitForArrival().TimeoutAfter(TimeSpan.FromSeconds(40)); + } + catch (TimeoutException) + { + Timeout = true; + } + } + if (!AutoEmergencyReset || Timeout) + { + AutoEmergencyReset = false; + + LogFile.Log("The phone is in emergency mode and you didn't provide an emergency programmer", LogType.ConsoleOnly); + LogFile.Log("This phone also doesn't seem to reboot after a timeout, so you got to help a bit", LogType.ConsoleOnly); + LogFile.Log("Keep the phone connected to the PC", LogType.ConsoleOnly); + LogFile.Log("Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates", LogType.ConsoleOnly); + LogFile.Log("The unlock-sequence will resume automatically", LogType.ConsoleOnly); + LogFile.Log("To prevent this, provide an emergency programmer next time you will unlock a bootloader", LogType.ConsoleOnly); + LogFile.Log("Waiting for manual reset of the phone...", LogType.ConsoleOnly); + + UpdateWorkingStatus("You need to manually reset your phone now!", "The phone is in emergency mode and you didn't provide an emergency programmer. This phone also doesn't seem to reboot after a timeout, so you got to help a bit. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. The unlock-sequence will resume automatically. To prevent this, provide an emergency programmer next time you will unlock a bootloader.", null, WPinternalsStatus.WaitingForManualReset); + + await Notifier.WaitForRemoval(); + + UpdateWorkingStatus("Scanning for flashing-profile - attempt " + AttemptCount.ToString() + " of " + MaximumAttempts.ToString(), "Your phone may appear to be in a reboot-loop. This is expected behavior. Don't interfere this process.", (uint)AttemptCount, Status: WPinternalsStatus.Scanning); + + await Notifier.WaitForArrival(); + } + } + } + + #endregion + + // Sanity check: must be in flash mode + if ((Notifier.CurrentInterface != PhoneInterfaces.Lumia_Flash) && (Notifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader)) + break; + + Model = (NokiaFlashModel)Notifier.CurrentModel; + + // In case we are on an Engineering phone which isn't stuck in flashmode and booted to BootMgrApp + Model.SwitchToFlashAppContext(); + Model.DisableRebootTimeOut(); + } + + PhoneNeedsReset = false; + WaitForReset = false; + + // Calculate variables for next attempt + if (ResetScanning) + { + AssumeImageHeaderFallsInGap = true; + AllocateAsyncBuffersOnPhone = true; + AllocateBackupBuffersOnPhone = false; + CurrentGapFill = EstimatedGapFill; + Profile = null; + ResetScanning = false; + } + else if (Experimental && !AllocateBackupBuffersOnPhone) + { + AllocateBackupBuffersOnPhone = true; + } + else + { + AllocateBackupBuffersOnPhone = false; + if (AllocateAsyncBuffersOnPhone) + AllocateAsyncBuffersOnPhone = false; + else + { + AllocateAsyncBuffersOnPhone = true; + OldGapFill = CurrentGapFill; + if (OldGapFill <= EstimatedGapFill) + { + CurrentGapFill = EstimatedGapFill + (EstimatedGapFill - OldGapFill) + (UInt32)FFU.ChunkSize; + if (CurrentGapFill > MaximumGapFill) + { + if (OldGapFill > 0) + { + CurrentGapFill = OldGapFill - (UInt32)FFU.ChunkSize; + } + else if (AssumeImageHeaderFallsInGap) + { + AssumeImageHeaderFallsInGap = false; + CurrentGapFill = EstimatedGapFill; + } + else + { + break; + } + } + } + else + { + if (OldGapFill <= (EstimatedGapFill * 2)) + { + CurrentGapFill = EstimatedGapFill - (OldGapFill - EstimatedGapFill); + } + else + { + CurrentGapFill = OldGapFill + (UInt32)FFU.ChunkSize; + if (CurrentGapFill > MaximumGapFill) + { + if (AssumeImageHeaderFallsInGap) + { + AssumeImageHeaderFallsInGap = false; + CurrentGapFill = EstimatedGapFill; + } + else + { + break; + } + } + } + } + } + } + } + } + while (!Success && !Abort); + + // Now we will first try to create a memory corruption in the phone, before we reset the phone. + // The memory corruption will cause the phone to abort the shutdown-sequence and switch to emergency mode immediately. + // We already avoided that the FlashingStatus was written to NV. + // But we also need to avoid that the BootFlag is written to NV when the phone is properly shut down, because that will overwrite the NV vars we wrote earlier. + if (Success && !ClearFlashingStatusAtEnd) + { + // Make the phone crash here! + // This will actually make the phone crash when it frees memory during shutdown or reboot of the phone + + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, SecurityHeaderAllocation.HeadStart + 4, 0); // Set allocation size to 0 in allocationhead + ByteOperations.WriteUInt32(UefiMemorySim.Buffer, StoreHeaderAllocation.HeadStart + 4, 0); // Set allocation size to 0 in allocationhead + if (CurrentGapFill > UefiMemorySim.PageSize) + { + Model.SendFfuHeaderV2(LastHeaderV2Size + 1, 0, new byte[1], Options); + PartialHeader = new byte[UefiMemorySim.PageSize]; + Model.SendFfuHeaderV2(CurrentGapFill, 0, PartialHeader, Options); // Fill memory gap + } + using (System.IO.FileStream FfuFile = new System.IO.FileStream(FFU.Path, System.IO.FileMode.Open, System.IO.FileAccess.Read)) + { + // On every flashing phase we need to send the full header again, because this triggers ffu_import_invalidate(), which is necessary to reset all the counters. + FfuFile.Read(FfuHeader, 0, (int)CombinedFFUHeaderSize); + Model.SendFfuHeaderV1(FfuHeader, Options); + } + PartialHeader = new byte[UefiMemorySim.PageSize]; + Model.SendFfuHeaderV2(ExploitHeaderAllocationSize, 0, PartialHeader, Options); // SkipWrite = 1 (only works on engineering phones) + UInt32 ExploitHeaderRemaining = SecurityHeaderAllocation.TailEnd + 1 - PartialHeaderAllocation.ContentStart - (UInt32)PartialHeader.Length; + HeaderOffset = (UInt32)PartialHeader.Length; + while (ExploitHeaderRemaining > 0) + { + UInt32 CurrentFill = ExploitHeaderRemaining; + if (CurrentFill > Info.WriteBufferSize) + CurrentFill = Info.WriteBufferSize; + PartialHeader = new byte[CurrentFill]; + PartialHeaderAllocation.CopyFromThisAllocation(HeaderOffset, CurrentFill, PartialHeader, 0); + Model.SendFfuHeaderV2(HeaderOffset + CurrentFill + 1, HeaderOffset, PartialHeader, Options); + LastHeaderV2Size = HeaderOffset + CurrentFill + 1; + ExploitHeaderRemaining -= CurrentFill; + HeaderOffset += CurrentFill; + } + + // Do the actual reset, which will result in a crash while cleaning up memory + ((NokiaFlashModel)Notifier.CurrentModel).ResetPhone(); + + LogFile.Log("Phone performs hard exit", LogType.FileAndConsole); + + #region Properly recover from reset at the end of custom flash - many phones respond differently + + // Wait for the phone to boot to emergency mode + // Emergency mode is always triggered on purpose to avoid writing NV vars + // We also wait for emergency mode when no valid programmer is present, or else caller-code will not know at which stage the phone is rebooting + + // Possibilities here: + // - Lumia 950 or 950 XL which does not crash to emergency mode, switching to mass storage mode, but not assigning a drive-letter -> Bootmgr, Nothing + // - Lumia 950 or 950 XL which does not crash to emergency mode, switching to mass storage mode, and assigning a drive-letter -> Bootmgr, MSM + // - Other Lumia's, switching to mass storage mode, but not assigning a drive-letter -> Bootmgr, Nothing + + Timeout = false; + try + { + await Notifier.WaitForArrival().TimeoutAfter(TimeSpan.FromSeconds(40)); + } + catch (TimeoutException) + { + Timeout = true; + } + if ((Notifier.CurrentInterface == null) && (!AutoEmergencyReset || Timeout)) + { + AutoEmergencyReset = false; + + if (Timeout) + { + LogFile.Log("The phone is not responding", LogType.ConsoleOnly); + LogFile.Log("It might be in emergency mode, while you have no matching driver installed", LogType.ConsoleOnly); + } + LogFile.Log("To continue the unlock-sequence, the phone needs to be rebooted", LogType.ConsoleOnly); + LogFile.Log("Keep the phone connected to the PC", LogType.ConsoleOnly); + LogFile.Log("Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates", LogType.ConsoleOnly); + LogFile.Log("The unlock-sequence will resume automatically", LogType.ConsoleOnly); + LogFile.Log("Waiting for manual reset of the phone...", LogType.ConsoleOnly); + + SetWorkingStatus("You need to manually reset your phone now!", (Timeout ? "The phone is not responding. It might be in emergency mode, while you have no matching driver installed. " : "") + "To continue the unlock-sequence, the phone needs to be rebooted. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. The unlock-sequence will resume automatically.", null, false, WPinternalsStatus.WaitingForManualReset); + + await Notifier.WaitForRemoval(); + + SetWorkingStatus("Rebooting phone..."); + } + if (Notifier.CurrentInterface == PhoneInterfaces.Qualcomm_Download) + { + if (ProgrammerPath != null) + { + QualcommSahara Sahara = new QualcommSahara((QualcommSerial)Notifier.CurrentModel); + await Sahara.Reset(ProgrammerPath); + } + else + { + ((QualcommSerial)Notifier.CurrentModel).Close(); // Prevent "Resource in use"; + + Timeout = false; + if (AutoEmergencyReset) + { + try + { + await Notifier.WaitForArrival().TimeoutAfter(TimeSpan.FromSeconds(40)); + } + catch (TimeoutException) + { + Timeout = true; + } + } + if (!AutoEmergencyReset || Timeout) + { + AutoEmergencyReset = false; + + LogFile.Log("The phone is in emergency mode and you didn't provide an emergency programmer", LogType.ConsoleOnly); + LogFile.Log("This phone also doesn't seem to reboot after a timeout, so you got to help a bit", LogType.ConsoleOnly); + LogFile.Log("Keep the phone connected to the PC", LogType.ConsoleOnly); + LogFile.Log("Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates", LogType.ConsoleOnly); + LogFile.Log("The unlock-sequence will resume automatically", LogType.ConsoleOnly); + LogFile.Log("To prevent this, provide an emergency programmer next time you will unlock a bootloader", LogType.ConsoleOnly); + LogFile.Log("Waiting for manual reset of the phone...", LogType.ConsoleOnly); + + SetWorkingStatus("You need to manually reset your phone now!", "The phone is in emergency mode and you didn't provide an emergency programmer. This phone also doesn't seem to reboot after a timeout, so you got to help a bit. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. The unlock-sequence will resume automatically. To prevent this, provide an emergency programmer next time you will unlock a bootloader.", null, false, WPinternalsStatus.WaitingForManualReset); + + await Notifier.WaitForRemoval(); + + SetWorkingStatus("Rebooting phone..."); + + // await Notifier.WaitForArrival(); // Function will exit while phone is rebooting + } + } + } + + #endregion + } + else + { + // If we didn't do a hard exit, we need to do a normal reboot + ((NokiaFlashModel)Notifier.CurrentModel).ResetPhone(); + } + + if (Success) + ExitSuccess("Flash succeeded!", null); + else + throw new WPinternalsException("Custom flash failed"); + } + + internal static string GetProgrammerPath(byte[] RKH, string Type) + { + IEnumerable RKHEntries = App.Config.EmergencyRepository.Where(e => (StructuralComparisons.StructuralEqualityComparer.Equals(e.RKH, RKH) && e.ProgrammerExists())); + if (RKHEntries.Count() > 0) + { + if (RKHEntries.Count() == 1) + return RKHEntries.First().ProgrammerPath; + else + { + EmergencyFileEntry RKHEntry = RKHEntries.Where(e => string.Compare(e.Type, Type, false) == 0).FirstOrDefault(); + if (RKHEntry != null) + return RKHEntry.ProgrammerPath; + else + return RKHEntries.First().ProgrammerPath; // Cannot be sure this is the right one!! + } + } + else + { + EmergencyFileEntry TypeEntry = App.Config.EmergencyRepository.Where(e => ((string.Compare(e.Type, Type, false) == 0) && e.ProgrammerExists())).FirstOrDefault(); + if (TypeEntry != null) + return TypeEntry.ProgrammerPath; + else + return null; + } + + } + + // Assumes phone with Flash protocol v2 + // Assumes phone is in flash mode + internal async static Task LumiaV2FlashArchive(PhoneNotifierViewModel Notifier, string ArchivePath, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null, ExitSuccess ExitSuccess = null, ExitFailure ExitFailure = null) + { + LogFile.BeginAction("FlashCustomROM"); + + NokiaFlashModel FlashModel = (NokiaFlashModel)Notifier.CurrentModel; + + // Use GetGptChunk() here instead of ReadGPT(), because ReadGPT() skips the first sector. + // We need the fist sector if we want to write back the GPT. + byte[] GPTChunk = GetGptChunk(FlashModel, 0x20000); + GPT GPT = new GPT(GPTChunk); + + Partition Target; + FlashPart Part; + List Parts = new List(); + ulong MainOSOldSectorCount = 0; + ulong MainOSNewSectorCount = 0; + ulong DataOldSectorCount = 0; + ulong DataNewSectorCount = 0; + ulong FirstMainOSSector = 0; + int PartitionCount = 0; + ulong TotalSizeSectors = 0; + bool IsUnlocked = false; + bool GPTChanged = false; + + if (SetWorkingStatus == null) SetWorkingStatus = (m, s, v, a, st) => { }; + if (UpdateWorkingStatus == null) UpdateWorkingStatus = (m, s, v, st) => { }; + if (ExitSuccess == null) ExitSuccess = (m, s) => { }; + if (ExitSuccess == null) ExitSuccess = (m, s) => { }; + + SetWorkingStatus("Initializing flash...", null, null, Status: WPinternalsStatus.Initializing); + + try + { + using (FileStream FileStream = new FileStream(ArchivePath, FileMode.Open)) + { + using (ZipArchive Archive = new ZipArchive(FileStream, ZipArchiveMode.Read)) + { + // Determine if there is a partition layout present + ZipArchiveEntry PartitionEntry = Archive.GetEntry("Partitions.xml"); + if (PartitionEntry == null) + { + GPT.MergePartitions(null, true, Archive); + GPTChanged |= GPT.HasChanged; + } + else + { + using (Stream ZipStream = PartitionEntry.Open()) + { + using (StreamReader ZipReader = new StreamReader(ZipStream)) + { + string PartitionXml = ZipReader.ReadToEnd(); + GPT.MergePartitions(PartitionXml, true, Archive); + GPTChanged |= GPT.HasChanged; + } + } + } + + // First determine if we need a new GPT! + foreach (ZipArchiveEntry Entry in Archive.Entries) + { + if (!Entry.FullName.Contains("/")) // No subfolders + { + string PartitionName = Entry.Name; + int Pos = PartitionName.IndexOf('.'); + if (Pos >= 0) + PartitionName = PartitionName.Substring(0, Pos); + + Partition Partition = GPT.Partitions.Where(p => string.Compare(p.Name, PartitionName, true) == 0).FirstOrDefault(); + if (Partition != null) + { + using (DecompressedStream DecompressedStream = new DecompressedStream(Entry.Open())) + { + ulong StreamLengthInSectors = (ulong)Entry.Length / 0x200; + try + { + StreamLengthInSectors = (ulong)DecompressedStream.Length / 0x200; + } + catch { } + + TotalSizeSectors += StreamLengthInSectors; + PartitionCount++; + + if (string.Compare(PartitionName, "MainOS", true) == 0) + { + MainOSOldSectorCount = Partition.SizeInSectors; + MainOSNewSectorCount = StreamLengthInSectors; + FirstMainOSSector = Partition.FirstSector; + } + else if (string.Compare(PartitionName, "Data", true) == 0) + { + DataOldSectorCount = Partition.SizeInSectors; + DataNewSectorCount = StreamLengthInSectors; + } + else if (StreamLengthInSectors > Partition.SizeInSectors) + { + LogFile.Log("Flash failed! Size of partition '" + PartitionName + "' is too big.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "Size of partition '" + PartitionName + "' is too big."); + return; + } + else if (string.Compare(PartitionName, "EFIESP", true) == 0) + { + ulong EfiespLength = StreamLengthInSectors * 0x200; + byte[] EfiespBinary = new byte[EfiespLength]; + DecompressedStream.Read(EfiespBinary, 0, (int)EfiespLength); + IsUnlocked = ((ByteOperations.ReadUInt32(EfiespBinary, 0x20) == (EfiespBinary.Length / 0x200 / 2)) && (ByteOperations.ReadAsciiString(EfiespBinary, (UInt32)(EfiespBinary.Length / 2) + 3, 8)) == "MSDOS5.0"); + if (IsUnlocked) + { + Partition IsUnlockedFlag = GPT.GetPartition("IS_UNLOCKED"); + if (IsUnlockedFlag == null) + { + IsUnlockedFlag = new Partition(); + IsUnlockedFlag.Name = "IS_UNLOCKED"; + IsUnlockedFlag.Attributes = 0; + IsUnlockedFlag.PartitionGuid = Guid.NewGuid(); + IsUnlockedFlag.PartitionTypeGuid = Guid.NewGuid(); + IsUnlockedFlag.FirstSector = 0x40; + IsUnlockedFlag.LastSector = 0x40; + GPT.Partitions.Add(IsUnlockedFlag); + GPTChanged = true; + } + } + else + { + Partition IsUnlockedFlag = GPT.GetPartition("IS_UNLOCKED"); + if (IsUnlockedFlag != null) + { + GPT.Partitions.Remove(IsUnlockedFlag); + GPTChanged = true; + } + } + } + } + } + } + } + + if ((MainOSNewSectorCount > 0) && (DataNewSectorCount > 0)) + { + if ((MainOSNewSectorCount > MainOSOldSectorCount) || (DataNewSectorCount > DataOldSectorCount)) + { + UInt64 OSSpace = GPT.LastUsableSector - FirstMainOSSector + 1; + if ((MainOSNewSectorCount + DataNewSectorCount) <= OSSpace) + { + // MainOS and Data partitions need to be re-aligned! + Partition MainOSPartition = GPT.Partitions.Where(p => string.Compare(p.Name, "MainOS", true) == 0).Single(); + Partition DataPartition = GPT.Partitions.Where(p => string.Compare(p.Name, "Data", true) == 0).Single(); + MainOSPartition.LastSector = MainOSPartition.FirstSector + MainOSNewSectorCount - 1; + DataPartition.FirstSector = MainOSPartition.LastSector + 1; + if ((DataPartition.FirstSector % 0x100) > 0) + DataPartition.FirstSector = ((UInt64)((DataPartition.FirstSector + 0x100) / 0x100)) * 0x100; + DataPartition.LastSector = DataPartition.FirstSector + DataNewSectorCount - 1; + + GPTChanged = true; + } + else + { + LogFile.Log("Flash failed! Sizes of partitions 'MainOS' and 'Data' together are too big.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "Sizes of partitions 'MainOS' and 'Data' together are too big."); + return; + } + } + } + else if ((MainOSNewSectorCount > 0) && (MainOSNewSectorCount > MainOSOldSectorCount)) + { + LogFile.Log("Flash failed! Size of partition 'MainOS' is too big.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "Size of partition 'MainOS' is too big."); + return; + } + else if ((DataNewSectorCount > 0) && (DataNewSectorCount > DataOldSectorCount)) + { + LogFile.Log("Flash failed! Size of partition 'Data' is too big.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "Size of partition 'Data' is too big."); + return; + } + + // Now add NV partition + Partition BACKUP_BS_NV = GPT.GetPartition("BACKUP_BS_NV"); + Partition UEFI_BS_NV; + if (BACKUP_BS_NV == null) + { + BACKUP_BS_NV = GPT.GetPartition("UEFI_BS_NV"); + Guid OriginalPartitionTypeGuid = BACKUP_BS_NV.PartitionTypeGuid; + Guid OriginalPartitionGuid = BACKUP_BS_NV.PartitionGuid; + BACKUP_BS_NV.Name = "BACKUP_BS_NV"; + BACKUP_BS_NV.PartitionGuid = Guid.NewGuid(); + BACKUP_BS_NV.PartitionTypeGuid = Guid.NewGuid(); + UEFI_BS_NV = new Partition(); + UEFI_BS_NV.Name = "UEFI_BS_NV"; + UEFI_BS_NV.Attributes = BACKUP_BS_NV.Attributes; + UEFI_BS_NV.PartitionGuid = OriginalPartitionGuid; + UEFI_BS_NV.PartitionTypeGuid = OriginalPartitionTypeGuid; + UEFI_BS_NV.FirstSector = BACKUP_BS_NV.LastSector + 1; + UEFI_BS_NV.LastSector = UEFI_BS_NV.FirstSector + BACKUP_BS_NV.LastSector - BACKUP_BS_NV.FirstSector; + GPT.Partitions.Add(UEFI_BS_NV); + GPTChanged = true; + } + Part = new FlashPart(); + Target = GPT.GetPartition("UEFI_BS_NV"); + Part.StartSector = (UInt32)Target.FirstSector; // GPT is prepared for 64-bit sector-offset, but flash app isn't. + Part.Stream = new SeekableStream(() => + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + + // Magic! + // The SB resource is a compressed version of a raw NV-variable-partition. + // In this partition the SecureBoot variable is disabled. + // It overwrites the variable in a different NV-partition than where this variable is stored usually. + // This normally leads to endless-loops when the NV-variables are enumerated. + // But the partition contains an extra hack to break out the endless loops. + var stream = assembly.GetManifestResourceStream("WPinternals.SB"); + + return new DecompressedStream(stream); + }); + Parts.Add(Part); + + if (GPTChanged) + { + GPT.Rebuild(); + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + Parts.Add(Part); + } + + // And then add the partitions from the archive + if (PartitionCount > 0) + { + foreach (ZipArchiveEntry Entry in Archive.Entries) + { + if (!Entry.FullName.Contains("/")) // No subfolders + { + // "MainOS.bin.gz" => "MainOS" + string PartitionName = Entry.Name; + int Pos = PartitionName.IndexOf('.'); + if (Pos >= 0) + PartitionName = PartitionName.Substring(0, Pos); + + Target = GPT.Partitions.Where(p => string.Compare(p.Name, PartitionName, true) == 0).FirstOrDefault(); + if (Target != null) + { + Part = new FlashPart(); + Part.StartSector = (UInt32)Target.FirstSector; + Part.Stream = new SeekableStream(() => new DecompressedStream(Entry.Open()), Entry.Length); + Part.ProgressText = "Flashing partition " + Target.Name; + Parts.Add(Part); + LogFile.Log("Partition name=" + PartitionName + ", startsector=0x" + Target.FirstSector.ToString("X8") + ", sectorcount = 0x" + (Entry.Length / 0x200).ToString("X8"), LogType.FileOnly); + } + } + } + + Parts = Parts.OrderBy(p => p.StartSector).ToList(); + int Count = 1; + Parts.Where(p => ((p.ProgressText != null) && p.ProgressText.StartsWith("Flashing partition "))).ToList().ForEach((p) => + { + p.ProgressText += " (" + Count.ToString() + "/" + PartitionCount.ToString() + ")"; + Count++; + }); + + // Do actual flashing! + await LumiaV2CustomFlash(Notifier, null, false, false, Parts, true, false, false, true, false, SetWorkingStatus, UpdateWorkingStatus, ExitSuccess, ExitFailure); + } + else + { + LogFile.Log("Flash failed! No valid partitions found in the archive.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "No valid partitions found in the archive"); + return; + } + } + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ExitFailure(Ex.Message, Ex is WPinternalsException ? ((WPinternalsException)Ex).SubMessage : null); + } + + LogFile.EndAction("FlashCustomROM"); + } + + internal async static Task LumiaV2FixBoot(PhoneNotifierViewModel Notifier, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null, ExitSuccess ExitSuccess = null, ExitFailure ExitFailure = null) + { + LogFile.BeginAction("FixBoot"); + + LogFile.Log("Command: Fix boot after unlocking bootloader", LogType.FileAndConsole); + NokiaFlashModel FlashModel = (NokiaFlashModel)Notifier.CurrentModel; + + if (SetWorkingStatus == null) SetWorkingStatus = (m, s, v, a, st) => { }; + if (UpdateWorkingStatus == null) UpdateWorkingStatus = (m, s, v, st) => { }; + if (ExitSuccess == null) ExitSuccess = (m, s) => { }; + if (ExitFailure == null) ExitFailure = (m, s) => { }; + + try + { + await SwitchModeViewModel.SwitchToWithProgress(Notifier, PhoneInterfaces.Lumia_MassStorage, (msg, sub) => SetWorkingStatus(msg, sub, null, Status: WPinternalsStatus.SwitchingMode)); + SetWorkingStatus("Applying patches...", null, null, Status: WPinternalsStatus.Patching); + App.PatchEngine.TargetPath = ((MassStorage)Notifier.CurrentModel).Drive + "\\"; + bool PatchResult = App.PatchEngine.Patch("SecureBootHack-MainOS"); + if (!PatchResult) + throw new WPinternalsException("Patch failed"); + LogFile.Log("Fixed bootloader", LogType.FileAndConsole); + LogFile.Log("The phone is left in Mass Storage mode", LogType.FileAndConsole); + LogFile.Log("Press and hold the power-button of the phone for at least 10 seconds to reset the phone", LogType.FileAndConsole); + ExitSuccess("Fixed bootloader!", "The phone is left in Mass Storage mode. Press and hold the power-button of the phone for at least 10 seconds to reset the phone."); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ExitFailure(Ex.Message, Ex is WPinternalsException ? ((WPinternalsException)Ex).SubMessage : null); + } + + LogFile.EndAction("FixBoot"); + } + + // Magic! + // Assumes phone with Flash protocol v2 + // Assumes phone is in flash mode + internal async static Task LumiaV2UnlockBootloader(PhoneNotifierViewModel Notifier, string ProfileFFUPath, string EDEPath, string SupportedFFUPath, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null, ExitSuccess ExitSuccess = null, ExitFailure ExitFailure = null) + { + LogFile.BeginAction("UnlockBootloader"); + NokiaFlashModel FlashModel = (NokiaFlashModel)Notifier.CurrentModel; + + if (SetWorkingStatus == null) SetWorkingStatus = (m, s, v, a, st) => { }; + if (UpdateWorkingStatus == null) UpdateWorkingStatus = (m, s, v, st) => { }; + if (ExitSuccess == null) ExitSuccess = (m, s) => { }; + if (ExitFailure == null) ExitFailure = (m, s) => { }; + + try + { + PhoneInfo Info = FlashModel.ReadPhoneInfo(); + bool IsBootLoaderSecure = !Info.Authenticated && !Info.RdcPresent && Info.SecureFfuEnabled; + + if (ProfileFFUPath == null) + throw new ArgumentNullException("Profile FFU path is missing"); + + FFU ProfileFFU = new FFU(ProfileFFUPath); + + if (IsBootLoaderSecure) + { + if (!Info.PlatformID.StartsWith(ProfileFFU.PlatformID, StringComparison.OrdinalIgnoreCase)) + throw new ArgumentNullException("Profile FFU has wrong Platform ID for connected phone"); + } + + FFU SupportedFFU = null; + if (App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == ProfileFFU.GetOSVersion())) + SupportedFFU = ProfileFFU; + else if (SupportedFFUPath == null) + throw new ArgumentNullException("Donor-FFU with supported OS version was not provided"); + else + { + SupportedFFU = new FFU(SupportedFFUPath); + if (!App.PatchEngine.PatchDefinitions.Where(p => p.Name == "SecureBootHack-V2-EFIESP").First().TargetVersions.Any(v => v.Description == SupportedFFU.GetOSVersion())) + throw new ArgumentNullException("Donor-FFU with supported OS version was not provided"); + } + + // TODO: Check EDE file + + LogFile.Log("Assembling data for unlock", LogType.FileAndConsole); + SetWorkingStatus("Assembling data for unlock", null, null); + byte[] UnlockedEFIESP = ProfileFFU.GetPartition("EFIESP"); + + DiscUtils.Fat.FatFileSystem UnlockedEFIESPFileSystem = new DiscUtils.Fat.FatFileSystem(new MemoryStream(UnlockedEFIESP)); + + if (SupportedFFU.Path != ProfileFFU.Path) + { + LogFile.Log("Donor-FFU: " + SupportedFFU.Path); + byte[] SupportedEFIESP = SupportedFFU.GetPartition("EFIESP"); + DiscUtils.Fat.FatFileSystem SupportedEFIESPFileSystem = new DiscUtils.Fat.FatFileSystem(new MemoryStream(SupportedEFIESP)); + DiscUtils.SparseStream SupportedMobileStartupStream = SupportedEFIESPFileSystem.OpenFile(@"\Windows\System32\Boot\mobilestartup.efi", FileMode.Open); + MemoryStream SupportedMobileStartupMemStream = new MemoryStream(); + SupportedMobileStartupStream.CopyTo(SupportedMobileStartupMemStream); + byte[] SupportedMobileStartup = SupportedMobileStartupMemStream.ToArray(); + SupportedMobileStartupMemStream.Close(); + SupportedMobileStartupStream.Close(); + + // Save supported mobilestartup.efi + LogFile.Log("Taking mobilestartup.efi from donor-FFU"); + Stream MobileStartupStream = UnlockedEFIESPFileSystem.OpenFile(@"Windows\System32\Boot\mobilestartup.efi", FileMode.Create, FileAccess.Write); + MobileStartupStream.Write(SupportedMobileStartup, 0, SupportedMobileStartup.Length); + MobileStartupStream.Close(); + } + + // Magic! + // This patch contains multiple hacks to disable SecureBoot, disable Bootpolicies and allow Mass Storage Mode on retail phones + App.PatchEngine.TargetImage = UnlockedEFIESPFileSystem; + bool PatchResult = App.PatchEngine.Patch("SecureBootHack-V2-EFIESP"); + if (!PatchResult) + throw new WPinternalsException("Failed to patch bootloader"); + + // Edit BCD + LogFile.Log("Edit BCD"); + using (Stream BCDFileStream = UnlockedEFIESPFileSystem.OpenFile(@"efi\Microsoft\Boot\BCD", FileMode.Open, FileAccess.ReadWrite)) + { + using (DiscUtils.Registry.RegistryHive BCDHive = new DiscUtils.Registry.RegistryHive(BCDFileStream)) + { + DiscUtils.BootConfig.Store BCDStore = new DiscUtils.BootConfig.Store(BCDHive.Root); + DiscUtils.BootConfig.BcdObject MobileStartupObject = BCDStore.GetObject(new Guid("{01de5a27-8705-40db-bad6-96fa5187d4a6}")); + DiscUtils.BootConfig.Element NoCodeIntegrityElement = MobileStartupObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + MobileStartupObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + + DiscUtils.BootConfig.BcdObject WinLoadObject = BCDStore.GetObject(new Guid("{7619dcc9-fafe-11d9-b411-000476eba25f}")); + NoCodeIntegrityElement = WinLoadObject.GetElement(0x16000048); + if (NoCodeIntegrityElement != null) + NoCodeIntegrityElement.Value = DiscUtils.BootConfig.ElementValue.ForBoolean(true); + else + WinLoadObject.AddElement(0x16000048, DiscUtils.BootConfig.ElementValue.ForBoolean(true)); + } + } + + UnlockedEFIESPFileSystem.Dispose(); + + List Parts = new List(); + FlashPart Part; + GPT GPT = FlashModel.ReadGPT(); + + // Create backup-partition for EFIESP + byte[] GPTChunk = GetGptChunk(FlashModel, (UInt32)ProfileFFU.ChunkSize); + byte[] GPTChunkBackup = new byte[GPTChunk.Length]; + Buffer.BlockCopy(GPTChunk, 0, GPTChunkBackup, 0, GPTChunk.Length); + GPT = new GPT(GPTChunk); + bool GPTChanged = false; + Partition BACKUP_EFIESP = GPT.GetPartition("BACKUP_EFIESP"); + Partition EFIESP; + UInt32 OriginalEfiespSizeInSectors = (UInt32)GPT.GetPartition("EFIESP").SizeInSectors; + if (BACKUP_EFIESP == null) + { + BACKUP_EFIESP = GPT.GetPartition("EFIESP"); + Guid OriginalPartitionTypeGuid = BACKUP_EFIESP.PartitionTypeGuid; + Guid OriginalPartitionGuid = BACKUP_EFIESP.PartitionGuid; + BACKUP_EFIESP.Name = "BACKUP_EFIESP"; + BACKUP_EFIESP.LastSector = BACKUP_EFIESP.FirstSector + ((OriginalEfiespSizeInSectors) / 2) - 1; // Original is 0x10000 + BACKUP_EFIESP.PartitionGuid = Guid.NewGuid(); + BACKUP_EFIESP.PartitionTypeGuid = Guid.NewGuid(); + EFIESP = new Partition(); + EFIESP.Name = "EFIESP"; + EFIESP.Attributes = BACKUP_EFIESP.Attributes; + EFIESP.PartitionGuid = OriginalPartitionGuid; + EFIESP.PartitionTypeGuid = OriginalPartitionTypeGuid; + EFIESP.FirstSector = BACKUP_EFIESP.LastSector + 1; + EFIESP.LastSector = EFIESP.FirstSector + ((OriginalEfiespSizeInSectors) / 2) - 1; // Original is 0x10000 + GPT.Partitions.Add(EFIESP); + GPTChanged = true; + } + EFIESP = GPT.GetPartition("EFIESP"); + if ((UInt64)UnlockedEFIESP.Length > (EFIESP.SizeInSectors * 0x200)) + { + byte[] HalfEFIESP = new byte[EFIESP.SizeInSectors * 0x200]; + Buffer.BlockCopy(UnlockedEFIESP, 0, HalfEFIESP, 0, HalfEFIESP.Length); + UnlockedEFIESP = HalfEFIESP; + ByteOperations.WriteUInt32(UnlockedEFIESP, 0x20, (UInt32)EFIESP.SizeInSectors); // Correction of partitionsize + } + + Partition TargetPartition = GPT.GetPartition("EFIESP"); + if (TargetPartition == null) + throw new WPinternalsException("EFIESP partition not found!"); + + if ((UInt64)UnlockedEFIESP.Length != (TargetPartition.SizeInSectors * 0x200)) + throw new WPinternalsException("New EFIESP partition has wrong size. Size = 0x" + UnlockedEFIESP.Length.ToString("X8") + ". Expected size = 0x" + (TargetPartition.SizeInSectors * 0x200).ToString("X8")); + + Part = new FlashPart(); + Part.StartSector = (UInt32)TargetPartition.FirstSector; // GPT is prepared for 64-bit sector-offset, but flash app isn't. + Part.Stream = new MemoryStream(UnlockedEFIESP); + Part.ProgressText = "Flashing unlocked bootloader (part 1)..."; + Parts.Add(Part); + + // Now add NV partition + Partition BACKUP_BS_NV = GPT.GetPartition("BACKUP_BS_NV"); + Partition UEFI_BS_NV; + if (BACKUP_BS_NV == null) + { + BACKUP_BS_NV = GPT.GetPartition("UEFI_BS_NV"); + Guid OriginalPartitionTypeGuid = BACKUP_BS_NV.PartitionTypeGuid; + Guid OriginalPartitionGuid = BACKUP_BS_NV.PartitionGuid; + BACKUP_BS_NV.Name = "BACKUP_BS_NV"; + BACKUP_BS_NV.PartitionGuid = Guid.NewGuid(); + BACKUP_BS_NV.PartitionTypeGuid = Guid.NewGuid(); + UEFI_BS_NV = new Partition(); + UEFI_BS_NV.Name = "UEFI_BS_NV"; + UEFI_BS_NV.Attributes = BACKUP_BS_NV.Attributes; + UEFI_BS_NV.PartitionGuid = OriginalPartitionGuid; + UEFI_BS_NV.PartitionTypeGuid = OriginalPartitionTypeGuid; + UEFI_BS_NV.FirstSector = BACKUP_BS_NV.LastSector + 1; + UEFI_BS_NV.LastSector = UEFI_BS_NV.FirstSector + BACKUP_BS_NV.LastSector - BACKUP_BS_NV.FirstSector; + GPT.Partitions.Add(UEFI_BS_NV); + GPTChanged = true; + } + Part = new FlashPart(); + TargetPartition = GPT.GetPartition("UEFI_BS_NV"); + Part.StartSector = (UInt32)TargetPartition.FirstSector; // GPT is prepared for 64-bit sector-offset, but flash app isn't. + Part.Stream = new SeekableStream(() => + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + + // Magic! + // The SB resource is a compressed version of a raw NV-variable-partition. + // In this partition the SecureBoot variable is disabled. + // It overwrites the variable in a different NV-partition than where this variable is stored usually. + // This normally leads to endless-loops when the NV-variables are enumerated. + // But the partition contains an extra hack to break out the endless loops. + var stream = assembly.GetManifestResourceStream("WPinternals.SB"); + + return new DecompressedStream(stream); + }); + Parts.Add(Part); + + if (GPTChanged) + { + GPT.Rebuild(); + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + Parts.Add(Part); + } + + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(Notifier, ProfileFFU.Path, false, false, Parts, true, false, true, true, false, SetWorkingStatus, UpdateWorkingStatus, null, null, EDEPath); + + if ((Notifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader) && (Notifier.CurrentInterface != PhoneInterfaces.Lumia_Flash)) + await Notifier.WaitForArrival(); + + if ((Notifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader) && (Notifier.CurrentInterface != PhoneInterfaces.Lumia_Flash)) + throw new WPinternalsException("Error: Phone is in wrong mode"); + + // Not going to retry in a loop because a second attempt will result in gears due to changed BootOrder. + // Just inform user of problem and revert. + // User can try again after revert. + bool IsPhoneInBadMassStorageMode = false; + string ErrorMessage = null; + try + { + await SwitchModeViewModel.SwitchToWithStatus(Notifier, PhoneInterfaces.Lumia_MassStorage, SetWorkingStatus, UpdateWorkingStatus); + } + catch (WPinternalsException Ex) + { + ErrorMessage = "Error: " + Ex.Message; + LogFile.LogException(Ex); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + } + if (Notifier.CurrentInterface == PhoneInterfaces.Lumia_BadMassStorage) + { + SetWorkingStatus("You need to manually reset your phone now!", "The phone is currently in Mass Storage Mode, but the driver of the PC failed to start. Unfortunately this happens sometimes. You need to manually reset the phone now. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. Windows Phone Internals will automatically start to revert the changes. After the phone is fully booted again, you can retry to unlock the bootloader.", null, false, WPinternalsStatus.WaitingForManualReset); + await Notifier.WaitForArrival(); // Should be detected in Bootmanager mode + if (Notifier.CurrentInterface != PhoneInterfaces.Lumia_MassStorage) + IsPhoneInBadMassStorageMode = true; + } + + if (Notifier.CurrentInterface != PhoneInterfaces.Lumia_MassStorage) + { + // Probably the "BootOrder" prevents to boot to MobileStartup. Mass Storage mode depends on MobileStartup. + // In this case Bootarm boots straight to Winload. But Winload can't handle the change of the EFIESP partition. That will cause a bootloop. + + SetWorkingStatus("Problem detected, rolling back...", ErrorMessage); + await SwitchModeViewModel.SwitchTo(Notifier, PhoneInterfaces.Lumia_Flash); + Parts = new List(); + + // Restore original GPT, which will also reference the original NV. + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunkBackup); + Parts.Add(Part); + + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(Notifier, ProfileFFU.Path, false, false, Parts, true, false, true, true, false, SetWorkingStatus, UpdateWorkingStatus, null, null, EDEPath); + + // An old NV backup was restored and it possibly contained the IsFlashing flag. + // Can't clear it immeadiately, so we need another flash. + if ((Notifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader) && (Notifier.CurrentInterface != PhoneInterfaces.Lumia_Flash)) + await Notifier.WaitForArrival(); + + if ((Notifier.CurrentInterface == PhoneInterfaces.Lumia_Bootloader) || (Notifier.CurrentInterface == PhoneInterfaces.Lumia_Flash)) + { + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(Notifier, ProfileFFU.Path, false, false, null, true, true, true, true, false, SetWorkingStatus, UpdateWorkingStatus, null, null, EDEPath); + } + + if (IsPhoneInBadMassStorageMode) + ExitFailure("Failed to unlock the bootloader due to misbahaving driver. Wait for phone to boot to Windows and then try again.", "The Mass Storage driver of the PC failed to start. Unfortunately this happens sometimes. After the phone is fully booted again, you can retry to unlock the bootloader."); + else + ExitFailure("Failed to unlock the bootloader", "It is not possible to unlock the bootloader straight after flashing. NOTE: Fully reboot the phone and then properly shutdown the phone, before you can try to unlock again!"); + + return; + } + + SetWorkingStatus("Create backup partition...", null, null); + + MassStorage MassStorage = (MassStorage)Notifier.CurrentModel; + GPTChunk = MassStorage.ReadSectors(0, 0x100); + GPT = new GPT(GPTChunk); + BACKUP_EFIESP = GPT.GetPartition("BACKUP_EFIESP"); + byte[] BackupEFIESP = MassStorage.ReadSectors(BACKUP_EFIESP.FirstSector, BACKUP_EFIESP.SizeInSectors); + + SetWorkingStatus("Boot optimization...", null, null); + + App.PatchEngine.TargetPath = MassStorage.Drive + "\\"; + App.PatchEngine.Patch("SecureBootHack-MainOS"); // Don't care about result here. Some phones do not need this. + + LogFile.Log("The phone is currently in Mass Storage Mode", LogType.ConsoleOnly); + LogFile.Log("To continue the unlock-sequence, the phone needs to be rebooted", LogType.ConsoleOnly); + LogFile.Log("Keep the phone connected to the PC", LogType.ConsoleOnly); + LogFile.Log("Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates", LogType.ConsoleOnly); + LogFile.Log("The unlock-sequence will resume automatically", LogType.ConsoleOnly); + LogFile.Log("Waiting for manual reset of the phone...", LogType.ConsoleOnly); + + SetWorkingStatus("You need to manually reset your phone now!", "The phone is currently in Mass Storage Mode. To continue the unlock-sequence, the phone needs to be rebooted. Keep the phone connected to the PC. Reboot the phone manually by pressing and holding the power-button of the phone for about 10 seconds until it vibrates. The unlock-sequence will resume automatically.", null, false, WPinternalsStatus.WaitingForManualReset); + + await Notifier.WaitForRemoval(); + + SetWorkingStatus("Rebooting phone..."); + + await Notifier.WaitForArrival(); + if (Notifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader) + throw new WPinternalsException("Phone is in wrong mode"); + + ((NokiaFlashModel)Notifier.CurrentModel).SwitchToFlashAppContext(); + + // EFIESP is appended at the end of the GPT + // BACKUP_EFIESP is at original location in GPT + EFIESP = GPT.GetPartition("EFIESP"); + UInt32 OriginalEfiespFirstSector = (UInt32)BACKUP_EFIESP.FirstSector; + BACKUP_EFIESP.Name = "EFIESP"; + BACKUP_EFIESP.LastSector = BACKUP_EFIESP.FirstSector + 0xFFFF; + BACKUP_EFIESP.PartitionGuid = EFIESP.PartitionGuid; + BACKUP_EFIESP.PartitionTypeGuid = EFIESP.PartitionTypeGuid; + GPT.Partitions.Remove(EFIESP); + + Partition IsUnlockedFlag = GPT.GetPartition("IS_UNLOCKED"); + if (IsUnlockedFlag == null) + { + IsUnlockedFlag = new Partition(); + IsUnlockedFlag.Name = "IS_UNLOCKED"; + IsUnlockedFlag.Attributes = 0; + IsUnlockedFlag.PartitionGuid = Guid.NewGuid(); + IsUnlockedFlag.PartitionTypeGuid = Guid.NewGuid(); + IsUnlockedFlag.FirstSector = 0x40; + IsUnlockedFlag.LastSector = 0x40; + GPT.Partitions.Add(IsUnlockedFlag); + } + + Parts = new List(); + GPT.Rebuild(); + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + Part.ProgressText = "Flashing unlocked bootloader (part 2)..."; + Parts.Add(Part); + Part = new FlashPart(); + Part.StartSector = OriginalEfiespFirstSector; + Part.Stream = new MemoryStream(UnlockedEFIESP); + Parts.Add(Part); + Part = new FlashPart(); + Part.StartSector = OriginalEfiespFirstSector + ((OriginalEfiespSizeInSectors) / 2); + Part.Stream = new MemoryStream(BackupEFIESP); + Parts.Add(Part); + + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(Notifier, ProfileFFU.Path, false, false, Parts, true, true, true, true, false, SetWorkingStatus, UpdateWorkingStatus, null, null, EDEPath); + + LogFile.Log("Bootloader unlocked!", LogType.FileAndConsole); + ExitSuccess("Bootloader unlocked successfully!", null); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ExitFailure(Ex.Message, Ex is WPinternalsException ? ((WPinternalsException)Ex).SubMessage : null); + } + LogFile.EndAction("UnlockBootloader"); + } + + // Assumes phone with Flash protocol v2 + // Assumes phone is in flash mode + internal async static Task LumiaV2FlashPartitions(PhoneNotifierViewModel Notifier, string EFIESPPath, string MainOSPath, string DataPath, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null, ExitSuccess ExitSuccess = null, ExitFailure ExitFailure = null) + { + NokiaFlashModel FlashModel = (NokiaFlashModel)Notifier.CurrentModel; + + // Use GetGptChunk() here instead of ReadGPT(), because ReadGPT() skips the first sector. + // We need the fist sector if we want to write back the GPT. + byte[] GPTChunk = GetGptChunk(FlashModel, 0x20000); + GPT GPT = new GPT(GPTChunk); + + Partition Target; + FlashPart Part; + List Parts = new List(); + ulong MainOSOldSectorCount = 0; + ulong MainOSNewSectorCount = 0; + ulong DataOldSectorCount = 0; + ulong DataNewSectorCount = 0; + ulong FirstMainOSSector = 0; + int PartitionCount = 0; + ulong LengthInSectors; + FileInfo FileInfo; + Partition Partition; + bool IsUnlocked = false; + bool GPTChanged = false; + + if (SetWorkingStatus == null) SetWorkingStatus = (m, s, v, a, st) => { }; + if (UpdateWorkingStatus == null) UpdateWorkingStatus = (m, s, v, st) => { }; + if (ExitSuccess == null) ExitSuccess = (m, s) => { }; + if (ExitFailure == null) ExitFailure = (m, s) => { }; + + SetWorkingStatus("Initializing flash...", null, null, Status: WPinternalsStatus.Initializing); + + try + { + if (EFIESPPath != null) + { + FileInfo = new FileInfo(EFIESPPath); + LengthInSectors = (ulong)FileInfo.Length / 0x200; + Partition = GPT.GetPartition("EFIESP"); + if (Partition.SizeInSectors < LengthInSectors) + { + LogFile.Log("Flash failed! Size of partition 'EFIESP' is too big.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "Size of partition 'EFIESP' is too big."); + return; + } + PartitionCount++; + + byte[] EfiespBinary = File.ReadAllBytes(EFIESPPath); + IsUnlocked = ((ByteOperations.ReadUInt32(EfiespBinary, 0x20) == (EfiespBinary.Length / 0x200 / 2)) && (ByteOperations.ReadAsciiString(EfiespBinary, (UInt32)(EfiespBinary.Length / 2) + 3, 8)) == "MSDOS5.0"); + if (IsUnlocked) + { + Partition IsUnlockedFlag = GPT.GetPartition("IS_UNLOCKED"); + if (IsUnlockedFlag == null) + { + IsUnlockedFlag = new Partition(); + IsUnlockedFlag.Name = "IS_UNLOCKED"; + IsUnlockedFlag.Attributes = 0; + IsUnlockedFlag.PartitionGuid = Guid.NewGuid(); + IsUnlockedFlag.PartitionTypeGuid = Guid.NewGuid(); + IsUnlockedFlag.FirstSector = 0x40; + IsUnlockedFlag.LastSector = 0x40; + GPT.Partitions.Add(IsUnlockedFlag); + GPTChanged = true; + } + } + else + { + Partition IsUnlockedFlag = GPT.GetPartition("IS_UNLOCKED"); + if (IsUnlockedFlag != null) + { + GPT.Partitions.Remove(IsUnlockedFlag); + GPTChanged = true; + } + } + } + + if (MainOSPath != null) + { + FileInfo = new FileInfo(MainOSPath); + LengthInSectors = (ulong)FileInfo.Length / 0x200; + Partition = GPT.GetPartition("MainOS"); + MainOSOldSectorCount = Partition.SizeInSectors; + MainOSNewSectorCount = LengthInSectors; + FirstMainOSSector = Partition.FirstSector; + PartitionCount++; + } + + if (DataPath != null) + { + FileInfo = new FileInfo(DataPath); + LengthInSectors = (ulong)FileInfo.Length / 0x200; + Partition = GPT.GetPartition("Data"); + DataOldSectorCount = Partition.SizeInSectors; + DataNewSectorCount = LengthInSectors; + PartitionCount++; + } + + if (PartitionCount > 0) + { + if ((MainOSNewSectorCount > 0) && (DataNewSectorCount > 0)) + { + if ((MainOSNewSectorCount > MainOSOldSectorCount) || (DataNewSectorCount > DataOldSectorCount)) + { + UInt64 OSSpace = GPT.LastUsableSector - FirstMainOSSector + 1; + if ((MainOSNewSectorCount + DataNewSectorCount) <= OSSpace) + { + // MainOS and Data partitions need to be re-aligned! + Partition MainOSPartition = GPT.Partitions.Where(p => string.Compare(p.Name, "MainOS", true) == 0).Single(); + Partition DataPartition = GPT.Partitions.Where(p => string.Compare(p.Name, "Data", true) == 0).Single(); + MainOSPartition.LastSector = MainOSPartition.FirstSector + MainOSNewSectorCount - 1; + DataPartition.FirstSector = MainOSPartition.LastSector + 1; + if ((DataPartition.FirstSector % 0x100) > 0) + DataPartition.FirstSector = ((UInt64)((DataPartition.FirstSector + 0x100) / 0x100)) * 0x100; + DataPartition.LastSector = DataPartition.FirstSector + DataNewSectorCount - 1; + + GPTChanged = true; + } + else + { + LogFile.Log("Flash failed! Sizes of partitions 'MainOS' and 'Data' together are too big.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "Sizes of partitions 'MainOS' and 'Data' together are too big."); + return; + } + } + } + else if ((MainOSNewSectorCount > 0) && (MainOSNewSectorCount > MainOSOldSectorCount)) + { + LogFile.Log("Flash failed! Size of partition 'MainOS' is too big.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "Size of partition 'MainOS' is too big."); + return; + } + else if ((DataNewSectorCount > 0) && (DataNewSectorCount > DataOldSectorCount)) + { + LogFile.Log("Flash failed! Size of partition 'Data' is too big.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "Size of partition 'Data' is too big."); + return; + } + + // Now add NV partition + Partition BACKUP_BS_NV = GPT.GetPartition("BACKUP_BS_NV"); + Partition UEFI_BS_NV; + if (BACKUP_BS_NV == null) + { + BACKUP_BS_NV = GPT.GetPartition("UEFI_BS_NV"); + Guid OriginalPartitionTypeGuid = BACKUP_BS_NV.PartitionTypeGuid; + Guid OriginalPartitionGuid = BACKUP_BS_NV.PartitionGuid; + BACKUP_BS_NV.Name = "BACKUP_BS_NV"; + BACKUP_BS_NV.PartitionGuid = Guid.NewGuid(); + BACKUP_BS_NV.PartitionTypeGuid = Guid.NewGuid(); + UEFI_BS_NV = new Partition(); + UEFI_BS_NV.Name = "UEFI_BS_NV"; + UEFI_BS_NV.Attributes = BACKUP_BS_NV.Attributes; + UEFI_BS_NV.PartitionGuid = OriginalPartitionGuid; + UEFI_BS_NV.PartitionTypeGuid = OriginalPartitionTypeGuid; + UEFI_BS_NV.FirstSector = BACKUP_BS_NV.LastSector + 1; + UEFI_BS_NV.LastSector = UEFI_BS_NV.FirstSector + BACKUP_BS_NV.LastSector - BACKUP_BS_NV.FirstSector; + GPT.Partitions.Add(UEFI_BS_NV); + GPTChanged = true; + } + Part = new FlashPart(); + Target = GPT.GetPartition("UEFI_BS_NV"); + Part.StartSector = (UInt32)Target.FirstSector; // GPT is prepared for 64-bit sector-offset, but flash app isn't. + Part.Stream = new SeekableStream(() => + { + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + + // Magic! + // The SB resource is a compressed version of a raw NV-variable-partition. + // In this partition the SecureBoot variable is disabled. + // It overwrites the variable in a different NV-partition than where this variable is stored usually. + // This normally leads to endless-loops when the NV-variables are enumerated. + // But the partition contains an extra hack to break out the endless loops. + var stream = assembly.GetManifestResourceStream("WPinternals.SB"); + + return new DecompressedStream(stream); + }); + Parts.Add(Part); + + if (GPTChanged) + { + GPT.Rebuild(); + Part = new FlashPart(); + Part.StartSector = 0; + Part.Stream = new MemoryStream(GPTChunk); + Parts.Add(Part); + } + + int Count = 0; + + Target = GPT.Partitions.Where(p => string.Compare(p.Name, "EFIESP", true) == 0).FirstOrDefault(); + if ((EFIESPPath != null) && (Target != null)) + { + Count++; + Part = new FlashPart(); + Part.StartSector = (UInt32)Target.FirstSector; + Part.Stream = new FileStream(EFIESPPath, FileMode.Open); + Part.ProgressText = "Flashing partition EFIESP (" + Count.ToString() + " / " + PartitionCount.ToString() + ")"; + Parts.Add(Part); + LogFile.Log("Partition name=EFIESP, startsector=0x" + Target.FirstSector.ToString("X8") + ", sectorcount = 0x" + (Part.Stream.Length / 0x200).ToString("X8"), LogType.FileOnly); + } + + Target = GPT.Partitions.Where(p => string.Compare(p.Name, "MainOS", true) == 0).FirstOrDefault(); + if ((MainOSPath != null) && (Target != null)) + { + Count++; + Part = new FlashPart(); + Part.StartSector = (UInt32)Target.FirstSector; + Part.Stream = new FileStream(MainOSPath, FileMode.Open); + Part.ProgressText = "Flashing partition MainOS (" + Count.ToString() + " / " + PartitionCount.ToString() + ")"; + Parts.Add(Part); + LogFile.Log("Partition name=MainOS, startsector=0x" + Target.FirstSector.ToString("X8") + ", sectorcount = 0x" + (Part.Stream.Length / 0x200).ToString("X8"), LogType.FileOnly); + } + + Target = GPT.Partitions.Where(p => string.Compare(p.Name, "Data", true) == 0).FirstOrDefault(); + if ((DataPath != null) && (Target != null)) + { + Count++; + Part = new FlashPart(); + Part.StartSector = (UInt32)Target.FirstSector; + Part.Stream = new FileStream(DataPath, FileMode.Open); + Part.ProgressText = "Flashing partition Data (" + Count.ToString() + " / " + PartitionCount.ToString() + ")"; + Parts.Add(Part); + LogFile.Log("Partition name=Data, startsector=0x" + Target.FirstSector.ToString("X8") + ", sectorcount = 0x" + (Part.Stream.Length / 0x200).ToString("X8"), LogType.FileOnly); + } + + // Do actual flashing! + await LumiaV2CustomFlash(Notifier, null, false, false, Parts, true, false, false, true, false, SetWorkingStatus, UpdateWorkingStatus, ExitSuccess, ExitFailure); + } + else + { + LogFile.Log("Flash failed! No valid partitions found in the archive.", LogType.FileAndConsole); + ExitFailure("Flash failed!", "No valid partitions found in the archive"); + return; + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ExitFailure(Ex.Message, Ex is WPinternalsException ? ((WPinternalsException)Ex).SubMessage : null); + return; + } + } + } + + internal static class UefiMemorySim + { + internal const UInt32 PageSize = 0x1000; + + private static UInt32 CurrentBufferSize = 0; + public static byte[] Buffer; + private static List Allocations = new List(); + private static List FreeMemRanges = new List(); + + public static UInt32 RoundUpToPages(UInt32 Size) + { + UInt32 Result = Size + 0x18; + if ((Size % PageSize) != 0) + Size = ((Size / PageSize) + 1) * PageSize; + return Result; + } + + public static void Reset() + { + CurrentBufferSize = 0; + Buffer = null; + Allocations.Clear(); + FreeMemRanges.Clear(); + } + + private static void ExtendBuffer(UInt32 Size) + { + byte[] NewBuffer = new byte[CurrentBufferSize + Size]; + if (CurrentBufferSize > 0) + System.Buffer.BlockCopy(Buffer, 0, NewBuffer, (int)Size, (int)CurrentBufferSize); + foreach (Allocation CurrentAllocation in Allocations) + { + CurrentAllocation.TotalStart += Size; + CurrentAllocation.TotalEnd += Size; + CurrentAllocation.HeadStart += Size; + CurrentAllocation.HeadEnd += Size; + CurrentAllocation.ContentStart += Size; + CurrentAllocation.ContentEnd += Size; + CurrentAllocation.TailStart += Size; + CurrentAllocation.TailEnd += Size; + } + foreach (FreeMemRange CurrentRange in FreeMemRanges) + { + CurrentRange.Start += Size; + CurrentRange.End += Size; + } + CurrentBufferSize += Size; + Buffer = NewBuffer; + } + + internal static Allocation AllocatePages(UInt32 Size) + { + Allocation NewAllocation = null; + + UInt32 TotalSize = Size; + + if ((TotalSize % PageSize) != 0) + { + throw new NotSupportedException("Wrong allocation size"); + } + else + { + for (int i = FreeMemRanges.Count() - 1; i >= 0; i--) + { + if (FreeMemRanges[i].Size >= TotalSize) + { + NewAllocation = new Allocation(); + NewAllocation.TotalStart = FreeMemRanges[i].End - TotalSize + 1; + + if (FreeMemRanges[i].Size == TotalSize) + FreeMemRanges.RemoveAt(i); + else + FreeMemRanges[i].End -= TotalSize; + + break; + } + } + + if (NewAllocation == null) + { + UInt32 FreeBuffer; + + if (Allocations.Count() > 0) + FreeBuffer = Allocations[0].TotalStart; + else + FreeBuffer = CurrentBufferSize; + + if (FreeBuffer < TotalSize) + ExtendBuffer(TotalSize - FreeBuffer); + + NewAllocation = new Allocation(); + + if (Allocations.Count() > 0) + NewAllocation.TotalStart = Allocations[0].TotalStart - TotalSize; + else + FreeBuffer = CurrentBufferSize - TotalSize; + } + + bool Added = false; + for (int i = 0; i < Allocations.Count(); i++) + { + if (NewAllocation.TotalStart < Allocations[i].TotalStart) + { + Allocations.Insert(i, NewAllocation); + Added = true; + break; + } + } + if (!Added) + Allocations.Add(NewAllocation); + } + + return NewAllocation; + } + + internal static Allocation AllocatePool(UInt32 Size) + { + Allocation NewAllocation = null; + + UInt32 TotalSize = Size + 24; + + if (TotalSize < PageSize) + { + throw new NotSupportedException("Allocation of small memory area's is not supported by this UEFI memory management simulator"); + } + else + { + if ((TotalSize % PageSize) != 0) + TotalSize = ((TotalSize / PageSize) + 1) * PageSize; + + for (int i = FreeMemRanges.Count() - 1; i >= 0; i--) + { + if (FreeMemRanges[i].Size >= TotalSize) + { + NewAllocation = new Allocation(); + NewAllocation.TotalStart = FreeMemRanges[i].End - TotalSize + 1; + + if (FreeMemRanges[i].Size == TotalSize) + FreeMemRanges.RemoveAt(i); + else + FreeMemRanges[i].End -= TotalSize; + + break; + } + } + + if (NewAllocation == null) + { + UInt32 FreeBuffer; + + if (Allocations.Count() > 0) + FreeBuffer = Allocations[0].TotalStart; + else + FreeBuffer = CurrentBufferSize; + + if (FreeBuffer < TotalSize) + ExtendBuffer(TotalSize - FreeBuffer); + + NewAllocation = new Allocation(); + + if (Allocations.Count() > 0) + NewAllocation.TotalStart = Allocations[0].TotalStart - TotalSize; + else + FreeBuffer = CurrentBufferSize - TotalSize; + } + + NewAllocation.TotalEnd = NewAllocation.TotalStart + TotalSize - 1; + NewAllocation.HeadStart = NewAllocation.TotalStart; + NewAllocation.HeadEnd = NewAllocation.HeadStart + 16 - 1; + NewAllocation.ContentStart = NewAllocation.HeadEnd + 1; + NewAllocation.ContentEnd = NewAllocation.ContentStart + Size - 1; + NewAllocation.TailStart = NewAllocation.ContentEnd + 1; + NewAllocation.TailEnd = NewAllocation.TailStart + 8 - 1; + + ByteOperations.WriteAsciiString(Buffer, NewAllocation.HeadStart + 0x00, "phd0"); + + // Correct value here is: Size + 24 + // Wrong value is: TotalSize + // Having correct value avoids memory errors and phone can reboot normally, but NV vars might be written (and that will overwrite the NV vars we wrote ourselves). + // Wrong value will make phone reboot to emergency boot and it makes the phone crash when you want to flash in multiple phases, but it will avoid writing NV vars. + ByteOperations.WriteUInt32(Buffer, NewAllocation.HeadStart + 0x04, Size + 24); + + ByteOperations.WriteUInt32(Buffer, NewAllocation.HeadStart + 0x08, 0x04); // EfiBootServicesData = 0x04 + ByteOperations.WriteUInt32(Buffer, NewAllocation.HeadStart + 0x0C, 0x00); // Reserved + + ByteOperations.WriteAsciiString(Buffer, NewAllocation.TailStart + 0x00, "ptal"); + + // Correct value here is: Size + 24 + // Wrong value is: TotalSize + // Having correct value avoids memory errors and phone can reboot normally, but NV vars might be written (and that will overwrite the NV vars we wrote ourselves). + // Wrong value will make phone reboot to emergency boot and it makes the phone crash when you want to flash in multiple phases, but it will avoid writing NV vars. + ByteOperations.WriteUInt32(Buffer, NewAllocation.TailStart + 0x04, Size + 24); + + bool Added = false; + for (int i = 0; i < Allocations.Count(); i++) + { + if (NewAllocation.TotalStart < Allocations[i].TotalStart) + { + Allocations.Insert(i, NewAllocation); + Added = true; + break; + } + } + if (!Added) + Allocations.Add(NewAllocation); + } + + return NewAllocation; + } + + internal static void FreePool(Allocation Allocation) + { + if (Allocations.Contains(Allocation)) + { + Allocations.Remove(Allocation); + + if (Allocations.Count() == 0) + { + FreeMemRanges.Clear(); + } + else + { + FreeMemRange NewFreeRange = new FreeMemRange(); + NewFreeRange.Start = Allocation.TotalStart; + NewFreeRange.End = Allocation.TotalEnd; + + bool Added = false; + int i; + for (i = 0; i < FreeMemRanges.Count(); i++) + { + if (NewFreeRange.Start < FreeMemRanges[i].Start) + { + FreeMemRanges.Insert(i, NewFreeRange); + Added = true; + break; + } + } + if (!Added) + { + FreeMemRanges.Add(NewFreeRange); + i = FreeMemRanges.Count(); + } + + if ((i > 0) && (FreeMemRanges[i].Start == (FreeMemRanges[i - 1].End + 1))) + { + FreeMemRanges[i - 1].End = FreeMemRanges[i].End; + FreeMemRanges.RemoveAt(i); + i--; + } + + if ((i < (FreeMemRanges.Count() - 1)) && (FreeMemRanges[i].End == (FreeMemRanges[i - 1].Start - 1))) + { + FreeMemRanges[i].End = FreeMemRanges[i + 1].End; + FreeMemRanges.RemoveAt(i + 1); + } + + if ((Allocations.Count() > 0) && (FreeMemRanges[i].Start < Allocations[0].TotalStart)) + FreeMemRanges.RemoveAt(i); + } + } + } + } + + internal class Allocation + { + public UInt32 TotalStart; + public UInt32 TotalEnd; + public UInt32 ContentStart; + public UInt32 ContentEnd; + public UInt32 HeadStart; + public UInt32 HeadEnd; + public UInt32 TailStart; + public UInt32 TailEnd; + + public UInt32 TotalSize + { + get + { + return TotalEnd - TotalStart + 1; + } + } + + public UInt32 ContentSize + { + get + { + return ContentEnd - ContentStart + 1; + } + } + + public void CopyFromThisAllocation(UInt32 ContentOffset, UInt32 Size, byte[] Destination, UInt32 DestinationOffset) + { + Buffer.BlockCopy(UefiMemorySim.Buffer, (int)(ContentStart + ContentOffset), Destination, (int)DestinationOffset, (int)Size); + } + + public void CopyToThisAllocation(byte[] Source, UInt32 SourceOffset, UInt32 Size, UInt32 ContentOffset) + { + Buffer.BlockCopy(Source, (int)SourceOffset, UefiMemorySim.Buffer, (int)(ContentStart + ContentOffset), (int)Size); + } + } + + internal class FreeMemRange + { + public UInt32 Start; + public UInt32 End; + + public UInt32 Size + { + get + { + return End - Start + 1; + } + } + } + + internal class FlashPart + { + public string ProgressText; + public UInt32 StartSector; + public Stream Stream; + } +} diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..b8042fa --- /dev/null +++ b/ViewModels/MainViewModel.cs @@ -0,0 +1,580 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using Microsoft.Win32; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace WPinternals +{ + public enum NavigationSubject + { + Info, + Mode, + Install, + About + }; + + public enum PhoneInterfaces + { + Lumia_Normal, + Lumia_Flash, + Lumia_Label, + Lumia_MassStorage, + Lumia_Bootloader, + Qualcomm_Download, + Qualcomm_Flash, + Lumia_BadMassStorage + }; + + // Create this class on the UI thread, after the main-window of the application is initialized. + // It is necessary to create the object on the UI thread, because notification events to the View need to be fired on that thread. + // The Model for this ViewModel communicates over USB and for that it uses the hWnd of the main window. + // Therefore the main window must be created before the ViewModel is created. + internal class MainViewModel : INotifyPropertyChanged + { + public PhoneInterfaces? CurrentInterface = null; + public PhoneInterfaces? LastInterface = null; + public NokiaPhoneModel CurrentModel = null; + public PhoneNotifierViewModel PhoneNotifier; + public LumiaInfoViewModel InfoViewModel; + public LumiaModeViewModel ModeViewModel; + public LumiaUnlockBootViewModel BootUnlockViewModel; + public LumiaUnlockBootViewModel BootRestoreViewModel; + public LumiaUnlockRootViewModel RootUnlockViewModel; + public LumiaUnlockRootViewModel RootRestoreViewModel; + public BackupViewModel BackupViewModel; + public RestoreViewModel RestoreViewModel; + public LumiaFlashRomViewModel LumiaFlashRomViewModel; + public DumpRomViewModel DumpRomViewModel; + public DownloadsViewModel DownloadsViewModel; + private GettingStartedViewModel _GettingStartedViewModel = null; + + public event PropertyChangedEventHandler PropertyChanged; + private void OnPropertyChanged(string propertyName) + { + if (this.PropertyChanged != null) + { + if (MainSyncContext == SynchronizationContext.Current) + PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); + else + MainSyncContext.Post(s => PropertyChanged(this, new PropertyChangedEventArgs(propertyName)), null); + } + } + + private ContextViewModel _ContextViewModel; + public ContextViewModel ContextViewModel + { + get + { + return _ContextViewModel; + } + set + { + if (_ContextViewModel != value) + { + if (_ContextViewModel != null) + _ContextViewModel.IsActive = false; + _ContextViewModel = value; + if (_ContextViewModel != null) + { + _ContextViewModel.Activate(); + } + OnPropertyChanged("ContextViewModel"); + } + } + } + + private SynchronizationContext MainSyncContext; + + public MainViewModel() + { + MainSyncContext = SynchronizationContext.Current; + + LogFile.LogApplicationVersion(); + + // Set global callback for cases where Dependency Injection is not possible. + App.NavigateToGettingStarted = () => { GettingStartedCommand.Execute(null); }; + App.NavigateToUnlockBoot = () => { BootUnlockCommand.Execute(null); }; + + if (Registry.CurrentUser.OpenSubKey("Software\\WPInternals") == null) + Registry.CurrentUser.OpenSubKey("Software", true).CreateSubKey("WPInternals"); + + if ((Registration.IsPrerelease) && (Registry.CurrentUser.OpenSubKey("Software\\WPInternals").GetValue("NdaAccepted") == null)) + { + this.ContextViewModel = new DisclaimerAndNdaViewModel(Disclaimer_Accepted); + } + else if (Registry.CurrentUser.OpenSubKey("Software\\WPInternals").GetValue("DisclaimerAccepted") == null) + { + this.ContextViewModel = new DisclaimerViewModel(Disclaimer_Accepted); + } + else if ((Registration.IsPrerelease) && !Registration.IsRegistered()) + { + ContextViewModel = new RegistrationViewModel(Registration_Completed, Registration_Failed); + } + else + StartOperation(); + } + + void Disclaimer_Accepted() + { + ContextViewModel = null; + + if ((Registration.IsPrerelease) && !Registration.IsRegistered()) + { + ContextViewModel = new RegistrationViewModel(Registration_Completed, Registration_Failed); + } + else + StartOperation(); + } + + void Registration_Completed() + { + ContextViewModel = null; + StartOperation(); + } + + void Registration_Failed() + { + ContextViewModel = new MessageViewModel("Registration failed", () => Environment.Exit(0)); + ((MessageViewModel)ContextViewModel).SubMessage = "Check your filewall settings"; + } + + public void StartOperation() + { + IsMenuEnabled = true; + + _GettingStartedViewModel = new GettingStartedViewModel( + () => + { + ContextViewModel = new DisclaimerViewModel( + () => ContextViewModel = _GettingStartedViewModel + ); + }, + SwitchToUnlockBoot, + SwitchToUnlockRoot, + SwitchToBackup, + SwitchToDumpFFU, + SwitchToFlashRom, + SwitchToDownload + ); + this.ContextViewModel = _GettingStartedViewModel; + + PhoneNotifier = new PhoneNotifierViewModel(); + PhoneNotifier.NewDeviceArrived += PhoneNotifier_NewDeviceArrived; + PhoneNotifier.DeviceRemoved += PhoneNotifier_DeviceRemoved; + + InfoViewModel = new LumiaInfoViewModel(PhoneNotifier, (TargetInterface) => + { + ModeViewModel.OnModeSwitchRequested(TargetInterface); + ContextViewModel = ModeViewModel; + }, + () => + { + ContextViewModel = _GettingStartedViewModel; + }); + InfoViewModel.ActivateSubContext(null); + + ModeViewModel = new LumiaModeViewModel(PhoneNotifier, SwitchToInfoViewModel); + ModeViewModel.ActivateSubContext(null); + + BootUnlockViewModel = new LumiaUnlockBootViewModel(PhoneNotifier, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, true, SwitchToInfoViewModel); + BootUnlockViewModel.ActivateSubContext(null); + + BootRestoreViewModel = new LumiaUnlockBootViewModel(PhoneNotifier, SwitchToFlashRom, SwitchToUndoRoot, SwitchToDownload, false, SwitchToInfoViewModel); + BootRestoreViewModel.ActivateSubContext(null); + + RootUnlockViewModel = new LumiaUnlockRootViewModel(PhoneNotifier, SwitchToUnlockBoot, SwitchToDumpFFU, SwitchToFlashRom, true, SwitchToInfoViewModel); + RootUnlockViewModel.ActivateSubContext(null); + + RootRestoreViewModel = new LumiaUnlockRootViewModel(PhoneNotifier, SwitchToUnlockBoot, SwitchToDumpFFU, SwitchToFlashRom, false, SwitchToInfoViewModel); + RootRestoreViewModel.ActivateSubContext(null); + + BackupViewModel = new BackupViewModel(PhoneNotifier, SwitchToUnlockBoot, SwitchToInfoViewModel); + BackupViewModel.ActivateSubContext(null); + + RestoreViewModel = new RestoreViewModel(PhoneNotifier, SwitchToDifferentInterface, SwitchToUnlockBoot, SwitchToFlashRom, SwitchToInfoViewModel); + RestoreViewModel.ActivateSubContext(null); + + LumiaFlashRomViewModel = new LumiaFlashRomViewModel(PhoneNotifier, SwitchToUnlockBoot, SwitchToUnlockRoot, SwitchToDumpFFU, SwitchToBackup, SwitchToInfoViewModel); + LumiaFlashRomViewModel.ActivateSubContext(null); + + DumpRomViewModel = new DumpRomViewModel(SwitchToUnlockBoot, SwitchToUnlockRoot, SwitchToFlashRom); + DumpRomViewModel.ActivateSubContext(null); + + DownloadsViewModel = new DownloadsViewModel(PhoneNotifier); + App.DownloadManager = DownloadsViewModel; + + PhoneNotifier.Start(); + } + + internal void SwitchToInfoViewModel() + { + ContextViewModel = InfoViewModel; + } + + internal void SwitchToUnlockBoot() + { + ContextViewModel = BootUnlockViewModel; + } + + internal void SwitchToRestoreBoot() + { + ContextViewModel = BootRestoreViewModel; + } + + internal void SwitchToUnlockRoot() + { + ContextViewModel = RootUnlockViewModel; + } + + internal void SwitchToUndoRoot() + { + ContextViewModel = RootRestoreViewModel; + } + + internal void SwitchToBackup() + { + ContextViewModel = BackupViewModel; + } + + internal void SwitchToFlashRom() + { + ContextViewModel = LumiaFlashRomViewModel; + } + + internal void SwitchToDumpFFU() + { + ContextViewModel = DumpRomViewModel; + } + + internal void SwitchToDownload() + { + ContextViewModel = DownloadsViewModel; + } + + internal void SwitchToDifferentInterface(PhoneInterfaces TargetInterface) + { + ModeViewModel.OnModeSwitchRequested(TargetInterface); + ContextViewModel = ModeViewModel; + } + + void PhoneNotifier_DeviceRemoved() + { + InfoViewModel.ActivateSubContext(null); + } + + void PhoneNotifier_NewDeviceArrived(ArrivalEventArgs Args) + { + PhoneInterfaces? PreviousInterface = LastInterface; + LastInterface = Args.NewInterface; + + if (App.InterruptBoot && (Args.NewInterface == PhoneInterfaces.Lumia_Bootloader)) + { + App.InterruptBoot = false; + LogFile.Log("Found Lumia BootMgr and user forced to interrupt the boot process. Force to Flash-mode."); + Task.Run(() => SwitchModeViewModel.SwitchTo(PhoneNotifier, PhoneInterfaces.Lumia_Flash)); + } + else + { + if (Args.NewInterface != PhoneInterfaces.Qualcomm_Download) + App.InterruptBoot = false; + + if (ContextViewModel == null) + ContextViewModel = InfoViewModel; + else if (ContextViewModel.IsFlashModeOperation) + { + if ((!ContextViewModel.IsSwitchingInterface) && (Args.NewInterface == PhoneInterfaces.Lumia_Bootloader)) + { + // The current screen is marked as "Flash operation". + // When the bootloader is detected at this stage, it means a phone is booting and + // it is possible that the phone is in a non-booting stage (not possible to boot past UEFI). + // We will try to boot straight to Flash-mode, so that it will be possible to flash a new ROM. + LogFile.Log("Found Lumia BootMgr while mode is not being switched. Screen is marked as Flash Operation. Force to Flash-mode."); + Task.Run(() => SwitchModeViewModel.SwitchTo(PhoneNotifier, PhoneInterfaces.Lumia_Flash)); + } + } + else + { + if ((!ContextViewModel.IsSwitchingInterface) && (Args.NewInterface != PhoneInterfaces.Lumia_Bootloader)) + ContextViewModel = InfoViewModel; + } + } + } + + private ICommand _InfoCommand = null; + public ICommand InfoCommand + { + get + { + if (_InfoCommand == null) + { + _InfoCommand = new DelegateCommand(() => + { + ContextViewModel = InfoViewModel; + }); + } + return _InfoCommand; + } + } + + private ICommand _ModeCommand = null; + public ICommand ModeCommand + { + get + { + if (_ModeCommand == null) + { + _ModeCommand = new DelegateCommand(() => + { + ContextViewModel = ModeViewModel; + }); + } + return _ModeCommand; + } + } + + private ICommand _BootUnlockCommand = null; + public ICommand BootUnlockCommand + { + get + { + if (_BootUnlockCommand == null) + { + _BootUnlockCommand = new DelegateCommand(() => + { + ContextViewModel = BootUnlockViewModel; + }); + } + return _BootUnlockCommand; + } + } + + private ICommand _BootRestoreCommand = null; + public ICommand BootRestoreCommand + { + get + { + if (_BootRestoreCommand == null) + { + _BootRestoreCommand = new DelegateCommand(() => + { + ContextViewModel = BootRestoreViewModel; + }); + } + return _BootRestoreCommand; + } + } + + private ICommand _RootUnlockCommand = null; + public ICommand RootUnlockCommand + { + get + { + if (_RootUnlockCommand == null) + { + _RootUnlockCommand = new DelegateCommand(() => + { + ContextViewModel = RootUnlockViewModel; + }); + } + return _RootUnlockCommand; + } + } + + private ICommand _RootUndoCommand = null; + public ICommand RootUndoCommand + { + get + { + if (_RootUndoCommand == null) + { + _RootUndoCommand = new DelegateCommand(() => + { + ContextViewModel = RootRestoreViewModel; + }); + } + return _RootUndoCommand; + } + } + + private ICommand _BackupCommand = null; + public ICommand BackupCommand + { + get + { + if (_BackupCommand == null) + { + _BackupCommand = new DelegateCommand(() => + { + ContextViewModel = BackupViewModel; + }); + } + return _BackupCommand; + } + } + + private ICommand _RestoreCommand = null; + public ICommand RestoreCommand + { + get + { + if (_RestoreCommand == null) + { + _RestoreCommand = new DelegateCommand(() => + { + ContextViewModel = RestoreViewModel; + }); + } + return _RestoreCommand; + } + } + + private ICommand _LumiaFlashRomCommand = null; + public ICommand LumiaFlashRomCommand + { + get + { + if (_LumiaFlashRomCommand == null) + { + _LumiaFlashRomCommand = new DelegateCommand(() => + { + ContextViewModel = LumiaFlashRomViewModel; + }); + } + return _LumiaFlashRomCommand; + } + } + + private ICommand _DumpRomCommand = null; + public ICommand DumpRomCommand + { + get + { + if (_DumpRomCommand == null) + { + _DumpRomCommand = new DelegateCommand(() => + { + ContextViewModel = DumpRomViewModel; + }); + } + return _DumpRomCommand; + } + } + + private ICommand _AboutCommand = null; + public ICommand AboutCommand + { + get + { + if (_AboutCommand == null) + { + _AboutCommand = new DelegateCommand(() => + { + ContextViewModel = new AboutViewModel(); + }); + } + return _AboutCommand; + } + } + + private ICommand _OpenWebSiteCommand = null; + public ICommand OpenWebSiteCommand + { + get + { + if (_OpenWebSiteCommand == null) + { + _OpenWebSiteCommand = new DelegateCommand(() => + { + Process.Start("www.wpinternals.net"); + }); + } + return _OpenWebSiteCommand; + } + } + + private ICommand _DonateCommand = null; + public ICommand DonateCommand + { + get + { + if (_DonateCommand == null) + { + _DonateCommand = new DelegateCommand(() => + { + Process.Start("https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VY8N7BCBT9CS4"); + }); + } + return _DonateCommand; + } + } + + private ICommand _GettingStartedCommand = null; + public ICommand GettingStartedCommand + { + get + { + if (_GettingStartedCommand == null) + { + _GettingStartedCommand = new DelegateCommand(() => + { + ContextViewModel = _GettingStartedViewModel; + }); + } + return _GettingStartedCommand; + } + } + + private ICommand _DownloadCommand = null; + public ICommand DownloadCommand + { + get + { + if (_DownloadCommand == null) + { + _DownloadCommand = new DelegateCommand(() => + { + ContextViewModel = DownloadsViewModel; + }); + } + return _DownloadCommand; + } + } + + private bool _IsMenuEnabled = false; + public bool IsMenuEnabled + { + get + { + return _IsMenuEnabled; + } + set + { + _IsMenuEnabled = value; + OnPropertyChanged("IsMenuEnabled"); + } + } + } +} diff --git a/ViewModels/MessageViewModel.cs b/ViewModels/MessageViewModel.cs new file mode 100644 index 0000000..520443d --- /dev/null +++ b/ViewModels/MessageViewModel.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace WPinternals +{ + internal class MessageViewModel: ContextViewModel + { + internal MessageViewModel(string Message, Action OkAction = null, Action CancelAction = null) + : base() + { + LogFile.Log(Message); + + if (OkAction != null) + this.OkCommand = new DelegateCommand(OkAction); + if (CancelAction != null) + this.CancelCommand = new DelegateCommand(CancelAction); + this.Message = Message; + } + + private string _Message = null; + public string Message + { + get + { + return _Message; + } + set + { + _Message = value; + OnPropertyChanged("Message"); + } + } + + private string _SubMessage = null; + public string SubMessage + { + get + { + return _SubMessage; + } + set + { + _SubMessage = value; + OnPropertyChanged("SubMessage"); + } + } + + private DelegateCommand _OkCommand = null; + public DelegateCommand OkCommand + { + get + { + return _OkCommand; + } + private set + { + _OkCommand = value; + } + } + + private DelegateCommand _CancelCommand = null; + public DelegateCommand CancelCommand + { + get + { + return _CancelCommand; + } + private set + { + _CancelCommand = value; + } + } + } +} diff --git a/ViewModels/NokiaFlashViewModel.cs b/ViewModels/NokiaFlashViewModel.cs new file mode 100644 index 0000000..fa9d258 --- /dev/null +++ b/ViewModels/NokiaFlashViewModel.cs @@ -0,0 +1,440 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; + +namespace WPinternals +{ + // Create this class on the UI thread, after the main-window of the application is initialized. + // It is necessary to create the object on the UI thread, because notification events to the View need to be fired on that thread. + // The Model for this ViewModel communicates over USB and for that it uses the hWnd of the main window. + // Therefore the main window must be created before the ViewModel is created. + + internal class NokiaFlashViewModel : ContextViewModel + { + private NokiaFlashModel CurrentModel; + private Action RequestModeSwitch; + internal Action SwitchToGettingStarted; + private object LockDeviceInfo = new object(); + bool DeviceInfoLoaded = false; + + internal NokiaFlashViewModel(NokiaPhoneModel CurrentModel, Action RequestModeSwitch, Action SwitchToGettingStarted) + : base() + { + this.CurrentModel = (NokiaFlashModel)CurrentModel; + this.RequestModeSwitch = RequestModeSwitch; + this.SwitchToGettingStarted = SwitchToGettingStarted; + } + + // Device info should be loaded only one time and only when the ViewModel is active + internal override void EvaluateViewState() + { + if (IsActive) + new Thread(() => StartLoadDeviceInfo()).Start(); + } + + private void StartLoadDeviceInfo() + { + lock (LockDeviceInfo) + { + if (!DeviceInfoLoaded) + { + try + { + //byte[] Imsi = CurrentModel.ExecuteJsonMethodAsBytes("ReadImsi", "Imsi"); // 9 bytes: 08 29 40 40 ... + //string BatteryLevel = CurrentModel.ExecuteJsonMethodAsString("ReadBatteryLevel", "BatteryLevel"); + //string SystemAsicVersion = CurrentModel.ExecuteJsonMethodAsString("ReadSystemAsicVersion", "SystemAsicVersion"); // 8960 -> Chip SOC version + //string OperatorName = CurrentModel.ExecuteJsonMethodAsString("ReadOperatorName", "OperatorName"); // 000-DK + //string ManufacturerModelName = CurrentModel.ExecuteJsonMethodAsString("ReadManufacturerModelName", "ManufacturerModelName"); // RM-821_eu_denmark_251 + //string AkVersion = CurrentModel.ExecuteJsonMethodAsString("ReadAkVersion", "AkVersion"); // 9200.10521 + //string BspVersion = CurrentModel.ExecuteJsonMethodAsString("ReadBspVersion", "BspVersion"); // 3051.40000 + //string ProductCode = CurrentModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); // 059Q9D7 + //string SecurityMode = CurrentModel.ExecuteJsonMethodAsString("GetSecurityMode", "SecMode"); // Restricted + //string SerialNumber = CurrentModel.ExecuteJsonMethodAsString("ReadSerialNumber", "SerialNumber"); // 356355051883955 = IMEI + //string SwVersion = CurrentModel.ExecuteJsonMethodAsString("ReadSwVersion", "SwVersion"); // 3051.40000.1349.0007 + //string ModuleCode = CurrentModel.ExecuteJsonMethodAsString("ReadModuleCode", "ModuleCode"); // 0205137 + //byte[] PublicId = CurrentModel.ExecuteJsonMethodAsBytes("ReadPublicId", "PublicId"); // 0x14 bytes: a5 e5 ... + //string Psn = CurrentModel.ExecuteJsonMethodAsString("ReadPsn", "Psn"); // CEP737370 + //string HwVersion = CurrentModel.ExecuteJsonMethodAsString("ReadHwVersion", "HWVersion"); // 6504 = 6.5.0.4 + //byte[] BtId = CurrentModel.ExecuteJsonMethodAsBytes("ReadBtId", "BtId"); // 6 bytes: bc c6 ... + //byte[] WlanMacAddress1 = CurrentModel.ExecuteJsonMethodAsBytes("ReadWlanMacAddress", "WlanMacAddress1"); // 6 bytes + //byte[] WlanMacAddress2 = CurrentModel.ExecuteJsonMethodAsBytes("ReadWlanMacAddress", "WlanMacAddress2"); // same + //byte[] WlanMacAddress3 = CurrentModel.ExecuteJsonMethodAsBytes("ReadWlanMacAddress", "WlanMacAddress3"); // same + //bool SimlockActive = CurrentModel.ExecuteJsonMethodAsBoolean("ReadSimlockActive", "SimLockActive"); // false + //string ServiceTag = CurrentModel.ExecuteJsonMethodAsString("ReadServiceTag", "ServiceTag"); // error + //byte[] RfChipsetVersion = CurrentModel.ExecuteJsonMethodAsBytes("ReadRfChipsetVersion", "RfChipsetVersion"); // error + //byte[] Meid = CurrentModel.ExecuteJsonMethodAsBytes("ReadMeid", "Meid"); // error + //string Test = CurrentModel.ExecuteJsonMethodAsString("ReadManufacturingData", ""); -> This method is only possible in Label-mode. + + UefiSecurityStatusResponse SecurityStatus = CurrentModel.ReadSecurityStatus(); + + UInt32? FlagsResult = CurrentModel.ReadSecurityFlags(); + UInt32 SecurityFlags = 0; + if (FlagsResult != null) + { + SecurityFlags = (UInt32)CurrentModel.ReadSecurityFlags(); + LogFile.Log("Security flags: 0x" + SecurityFlags.ToString("X8")); + } + else + LogFile.Log("Security flags could not be read"); + + PlatformName = CurrentModel.ReadStringParam("DPI"); + LogFile.Log("Platform Name: " + PlatformName); + + // Some phones do not support the Terminal interface! (928 verizon) + // Instead read param RRKH to get the RKH. + PublicID = null; + byte[] RawPublicID = CurrentModel.ReadParam("PID"); + if ((RawPublicID != null) && (RawPublicID.Length > 4)) + { + PublicID = new byte[RawPublicID.Length - 4]; + Array.Copy(RawPublicID, 4, PublicID, 0, RawPublicID.Length - 4); + LogFile.Log("Public ID: " + Converter.ConvertHexToString(PublicID, " ")); + } + RootKeyHash = CurrentModel.ReadParam("RRKH"); + if (RootKeyHash != null) + LogFile.Log("Root Key Hash: " + Converter.ConvertHexToString(RootKeyHash, " ")); + + if (SecurityStatus != null) + { + PlatformSecureBootStatus = SecurityStatus.PlatformSecureBootStatus; + LogFile.Log("Platform Secure Boot Status: " + PlatformSecureBootStatus.ToString()); + UefiSecureBootStatus = SecurityStatus.UefiSecureBootStatus; + LogFile.Log("Uefi Secure Boot Status: " + UefiSecureBootStatus.ToString()); + EffectiveSecureBootStatus = SecurityStatus.PlatformSecureBootStatus && SecurityStatus.UefiSecureBootStatus; + LogFile.Log("Effective Secure Boot Status: " + EffectiveSecureBootStatus.ToString()); + + BootloaderSecurityQfuseStatus = SecurityStatus.SecureFfuEfuseStatus; + LogFile.Log("Bootloader Security Qfuse Status: " + BootloaderSecurityQfuseStatus.ToString()); + BootloaderSecurityAuthenticationStatus = SecurityStatus.AuthenticationStatus; + LogFile.Log("Bootloader Security Authentication Status: " + BootloaderSecurityAuthenticationStatus.ToString()); + BootloaderSecurityRdcStatus = SecurityStatus.RdcStatus; + LogFile.Log("Bootloader Security Rdc Status: " + BootloaderSecurityRdcStatus.ToString()); + EffectiveBootloaderSecurityStatus = SecurityStatus.SecureFfuEfuseStatus && !SecurityStatus.AuthenticationStatus && !SecurityStatus.RdcStatus; + LogFile.Log("Effective Bootloader Security Status: " + EffectiveBootloaderSecurityStatus.ToString()); + + NativeDebugStatus = !SecurityStatus.DebugStatus; + LogFile.Log("Native Debug Status: " + NativeDebugStatus.ToString()); + } + + byte[] CID = CurrentModel.ReadParam("CID"); + byte[] EMS = CurrentModel.ReadParam("EMS"); + UInt16 MID = (UInt16)(((UInt16)CID[0] << 8) + CID[1]); + UInt64 MemSize = (UInt64)(((UInt32)EMS[0] << 24) + ((UInt32)EMS[1] << 16) + ((UInt32)EMS[2] << 8) + EMS[3]) * 0x200; + double MemSizeDouble = (double)MemSize / 1024 / 1024 / 1024; + MemSizeDouble = (double)(int)(MemSizeDouble * 10) / 10; + string Manufacturer = null; + switch (MID) + { + case 0x0002: + case 0x0045: + Manufacturer = "SanDisk"; + break; + case 0x0011: + Manufacturer = "Toshiba"; + break; + case 0x0013: + Manufacturer = "Micron"; + break; + case 0x0015: + Manufacturer = "Samsung"; + break; + case 0x0090: + Manufacturer = "Hynix"; + break; + case 0x0070: + Manufacturer = "Kingston"; + break; + case 0x00EC: + Manufacturer = "GigaDevice"; + break; + } + if (Manufacturer == null) + eMMC = MemSizeDouble.ToString() + " GB"; + else + eMMC = Manufacturer + " " + MemSizeDouble.ToString() + " GB"; + SamsungWarningVisible = (MID == 0x0015); + + PhoneInfo Info = CurrentModel.ReadPhoneInfo(true); + if (Info.FlashAppProtocolVersionMajor < 2) + BootloaderDescription = "Lumia Bootloader Spec A"; + else + BootloaderDescription = "Lumia Bootloader Spec B"; + LogFile.Log("Bootloader: " + BootloaderDescription); + + ProductCode = Info.ProductCode; + LogFile.Log("ProductCode: " + ProductCode); + + ProductType = Info.Type; + LogFile.Log("ProductType: " + ProductType); + } + catch + { + LogFile.Log("Reading status from Flash interface was aborted."); + } + DeviceInfoLoaded = true; + } + } + } + + private byte[] _PublicID = null; + public byte[] PublicID + { + get + { + return _PublicID; + } + set + { + _PublicID = value; + OnPropertyChanged("PublicID"); + } + } + + private byte[] _RootKeyHash = null; + public byte[] RootKeyHash + { + get + { + return _RootKeyHash; + } + set + { + _RootKeyHash = value; + OnPropertyChanged("RootKeyHash"); + } + } + + private string _PlatformName = null; + public string PlatformName + { + get + { + return _PlatformName; + } + set + { + _PlatformName = value; + OnPropertyChanged("PlatformName"); + } + } + + private string _ProductType = null; + public string ProductType + { + get + { + return _ProductType; + } + set + { + _ProductType = value; + OnPropertyChanged("ProductType"); + } + } + + private string _ProductCode = null; + public string ProductCode + { + get + { + return _ProductCode; + } + set + { + _ProductCode = value; + OnPropertyChanged("ProductCode"); + } + } + + private string _eMMC = null; + public string eMMC + { + get + { + return _eMMC; + } + set + { + _eMMC = value; + OnPropertyChanged("eMMC"); + } + } + + private string _BootloaderDescription = null; + public string BootloaderDescription + { + get + { + return _BootloaderDescription; + } + set + { + _BootloaderDescription = value; + OnPropertyChanged("BootloaderDescription"); + } + } + + private bool _SamsungWarningVisible = false; + public bool SamsungWarningVisible + { + get + { + return _SamsungWarningVisible; + } + set + { + _SamsungWarningVisible = value; + OnPropertyChanged("SamsungWarningVisible"); + } + } + + private bool? _PlatformSecureBootStatus = null; + public bool? PlatformSecureBootStatus + { + get + { + return _PlatformSecureBootStatus; + } + set + { + _PlatformSecureBootStatus = value; + OnPropertyChanged("PlatformSecureBootStatus"); + } + } + + private bool? _BootloaderSecurityQfuseStatus = null; + public bool? BootloaderSecurityQfuseStatus + { + get + { + return _BootloaderSecurityQfuseStatus; + } + set + { + _BootloaderSecurityQfuseStatus = value; + OnPropertyChanged("BootloaderSecurityQfuseStatus"); + } + } + + private bool? _BootloaderSecurityRdcStatus = null; + public bool? BootloaderSecurityRdcStatus + { + get + { + return _BootloaderSecurityRdcStatus; + } + set + { + _BootloaderSecurityRdcStatus = value; + OnPropertyChanged("BootloaderSecurityRdcStatus"); + } + } + + private bool? _BootloaderSecurityAuthenticationStatus = null; + public bool? BootloaderSecurityAuthenticationStatus + { + get + { + return _BootloaderSecurityAuthenticationStatus; + } + set + { + _BootloaderSecurityAuthenticationStatus = value; + OnPropertyChanged("BootloaderSecurityAuthenticationStatus"); + } + } + + private bool? _UefiSecureBootStatus = null; + public bool? UefiSecureBootStatus + { + get + { + return _UefiSecureBootStatus; + } + set + { + _UefiSecureBootStatus = value; + OnPropertyChanged("UefiSecureBootStatus"); + } + } + + private bool? _EffectiveSecureBootStatus = null; + public bool? EffectiveSecureBootStatus + { + get + { + return _EffectiveSecureBootStatus; + } + set + { + _EffectiveSecureBootStatus = value; + OnPropertyChanged("EffectiveSecureBootStatus"); + } + } + + private bool? _EffectiveBootloaderSecurityStatus = null; + public bool? EffectiveBootloaderSecurityStatus + { + get + { + return _EffectiveBootloaderSecurityStatus; + } + set + { + _EffectiveBootloaderSecurityStatus = value; + OnPropertyChanged("EffectiveBootloaderSecurityStatus"); + } + } + + private bool? _NativeDebugStatus = null; + public bool? NativeDebugStatus + { + get + { + return _NativeDebugStatus; + } + set + { + _NativeDebugStatus = value; + OnPropertyChanged("NativeDebugStatus"); + } + } + + internal void RebootTo(string Mode) + { + switch (Mode) + { + case "Normal": + RequestModeSwitch(PhoneInterfaces.Lumia_Normal); + break; + case "Label": + RequestModeSwitch(PhoneInterfaces.Lumia_Label); + break; + case "MassStorage": + RequestModeSwitch(PhoneInterfaces.Lumia_MassStorage); + break; + default: + return; + } + } + } +} diff --git a/ViewModels/NokiaLabelViewModel.cs b/ViewModels/NokiaLabelViewModel.cs new file mode 100644 index 0000000..e1229a7 --- /dev/null +++ b/ViewModels/NokiaLabelViewModel.cs @@ -0,0 +1,290 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading; + +namespace WPinternals +{ + // Create this class on the UI thread, after the main-window of the application is initialized. + // It is necessary to create the object on the UI thread, because notification events to the View need to be fired on that thread. + // The Model for this ViewModel communicates over USB and for that it uses the hWnd of the main window. + // Therefore the main window must be created before the ViewModel is created. + + internal class NokiaLabelViewModel : ContextViewModel + { + private NokiaPhoneModel CurrentModel; + private Action RequestModeSwitch; + + internal NokiaLabelViewModel(NokiaPhoneModel CurrentModel, Action RequestModeSwitch) + : base() + { + this.RequestModeSwitch = RequestModeSwitch; + + this.CurrentModel = CurrentModel; + + new Thread(() => StartLoadDeviceInfo()).Start(); + } + + private void StartLoadDeviceInfo() + { + //byte[] Imsi = CurrentModel.ExecuteJsonMethodAsBytes("ReadImsi", "Imsi"); // 9 bytes: 08 29 40 40 ... + //string BatteryLevel = CurrentModel.ExecuteJsonMethodAsString("ReadBatteryLevel", "BatteryLevel"); + //string SystemAsicVersion = CurrentModel.ExecuteJsonMethodAsString("ReadSystemAsicVersion", "SystemAsicVersion"); // 8960 -> Chip SOC version + //string OperatorName = CurrentModel.ExecuteJsonMethodAsString("ReadOperatorName", "OperatorName"); // 000-DK + //string ManufacturerModelName = CurrentModel.ExecuteJsonMethodAsString("ReadManufacturerModelName", "ManufacturerModelName"); // RM-821_eu_denmark_251 + //string AkVersion = CurrentModel.ExecuteJsonMethodAsString("ReadAkVersion", "AkVersion"); // 9200.10521 + //string BspVersion = CurrentModel.ExecuteJsonMethodAsString("ReadBspVersion", "BspVersion"); // 3051.40000 + //string ProductCode = CurrentModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); // 059Q9D7 + //string SecurityMode = CurrentModel.ExecuteJsonMethodAsString("GetSecurityMode", "SecMode"); // Restricted + //string SerialNumber = CurrentModel.ExecuteJsonMethodAsString("ReadSerialNumber", "SerialNumber"); // 356355051883955 = IMEI + //string SwVersion = CurrentModel.ExecuteJsonMethodAsString("ReadSwVersion", "SwVersion"); // 3051.40000.1349.0007 + //string ModuleCode = CurrentModel.ExecuteJsonMethodAsString("ReadModuleCode", "ModuleCode"); // 0205137 + //byte[] PublicId = CurrentModel.ExecuteJsonMethodAsBytes("ReadPublicId", "PublicId"); // 0x14 bytes: a5 e5 ... + //string Psn = CurrentModel.ExecuteJsonMethodAsString("ReadPsn", "Psn"); // CEP737370 + //string HwVersion = CurrentModel.ExecuteJsonMethodAsString("ReadHwVersion", "HWVersion"); // 6504 = 6.5.0.4 + //byte[] BtId = CurrentModel.ExecuteJsonMethodAsBytes("ReadBtId", "BtId"); // 6 bytes: bc c6 ... + //byte[] WlanMacAddress1 = CurrentModel.ExecuteJsonMethodAsBytes("ReadWlanMacAddress", "WlanMacAddress1"); // 6 bytes + //byte[] WlanMacAddress2 = CurrentModel.ExecuteJsonMethodAsBytes("ReadWlanMacAddress", "WlanMacAddress2"); // same + //byte[] WlanMacAddress3 = CurrentModel.ExecuteJsonMethodAsBytes("ReadWlanMacAddress", "WlanMacAddress3"); // same + //bool SimlockActive = CurrentModel.ExecuteJsonMethodAsBoolean("ReadSimlockActive", "SimLockActive"); // false + //string Version = CurrentModel.ExecuteJsonMethodAsString("GetVersion", "HelloString"); // Resultvars: HelloString Version BuildDate BuildType + //string ServiceTag = CurrentModel.ExecuteJsonMethodAsString("ReadServiceTag", "ServiceTag"); // error + //byte[] RfChipsetVersion = CurrentModel.ExecuteJsonMethodAsBytes("ReadRfChipsetVersion", "RfChipsetVersion"); // error + //byte[] Meid = CurrentModel.ExecuteJsonMethodAsBytes("ReadMeid", "Meid"); // error + //string Test = CurrentModel.ExecuteJsonMethodAsString("ReadManufacturingData", ""); -> This method is only possible in Label-mode. + + byte[] AsskMask = new byte[0x10] { 1, 0, 16, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64 }; + byte[] Challenge = new byte[0x88]; + Dictionary Params = new Dictionary(); + Params.Add("AsskMask", AsskMask); + Params.Add("Challenge", Challenge); + Params.Add("AsicIndex", 0); + byte[] TerminalResponseBytes = CurrentModel.ExecuteJsonMethodAsBytes("TerminalChallenge", Params, "TerminalResponse"); + TerminalResponse TerminalResponse = Terminal.Parse(TerminalResponseBytes, 0); + if (TerminalResponse != null) + { + PublicID = TerminalResponse.PublicId; + LogFile.Log("Public ID: " + Converter.ConvertHexToString(PublicID, " ")); + RootKeyHash = TerminalResponse.RootKeyHash; + LogFile.Log("RootKeyHash: " + Converter.ConvertHexToString(RootKeyHash, " ")); + } + + ManufacturerModelName = CurrentModel.ExecuteJsonMethodAsString("ReadManufacturerModelName", "ManufacturerModelName"); // RM-821_eu_denmark_251 + LogFile.Log("Manufacturer Model Name: " + ManufacturerModelName); + ProductCode = CurrentModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); // 059Q9D7 + LogFile.Log("Product Code: " + ProductCode); + Firmware = CurrentModel.ExecuteJsonMethodAsString("ReadSwVersion", "SwVersion"); // 3051.40000.1349.0007 + LogFile.Log("Firmware: " + Firmware); + + IMEI = CurrentModel.ExecuteJsonMethodAsString("ReadSerialNumber", "SerialNumber"); // IMEI + LogFile.Log("IMEI: " + IMEI); + BluetoothMac = CurrentModel.ExecuteJsonMethodAsBytes("ReadBtId", "BtId"); // 6 bytes: bc c6 ... + LogFile.Log("Bluetooth MAC: " + Converter.ConvertHexToString(BluetoothMac, " ")); + WlanMac = CurrentModel.ExecuteJsonMethodAsBytes("ReadWlanMacAddress", "WlanMacAddress1"); // 6 bytes + LogFile.Log("WLAN MAC: " + Converter.ConvertHexToString(WlanMac, " ")); + + IsBootloaderSecurityEnabled = (bool)CurrentModel.ExecuteJsonMethodAsBoolean("ReadProductionDoneState", "ProductionDone"); + LogFile.Log("Bootloader Security: " + ((bool)IsBootloaderSecurityEnabled ? "Enabled" : "Disabled")); + + Params = new Dictionary(); + Params.Add("ID", 3534); + Params.Add("NVData", new byte[] { 0 }); + CurrentModel.ExecuteJsonMethod("WriteNVData", Params); // Error: 150 + + Params = new Dictionary(); + Params.Add("ID", 3534); + byte[] NV3534 = CurrentModel.ExecuteJsonMethodAsBytes("ReadNVData", Params, "NVData"); // Error: value not written + } + + private string _ProductCode = null; + public string ProductCode + { + get + { + return _ProductCode; + } + set + { + _ProductCode = value; + OnPropertyChanged("ProductCode"); + } + } + + private string _ManufacturerModelName = null; + public string ManufacturerModelName + { + get + { + return _ManufacturerModelName; + } + set + { + _ManufacturerModelName = value; + OnPropertyChanged("ManufacturerModelName"); + } + } + + private string _Operator = null; + public string Operator + { + get + { + return _Operator; + } + set + { + _Operator = value; + OnPropertyChanged("Operator"); + } + } + + private string _Firmware = null; + public string Firmware + { + get + { + return _Firmware; + } + set + { + _Firmware = value; + OnPropertyChanged("Firmware"); + } + } + + private string _IMEI = null; + public string IMEI + { + get + { + return _IMEI; + } + set + { + _IMEI = value; + OnPropertyChanged("IMEI"); + } + } + + private byte[] _PublicID = null; + public byte[] PublicID + { + get + { + return _PublicID; + } + set + { + _PublicID = value; + OnPropertyChanged("PublicID"); + } + } + + private byte[] _RootKeyHash = null; + public byte[] RootKeyHash + { + get + { + return _RootKeyHash; + } + set + { + _RootKeyHash = value; + OnPropertyChanged("RootKeyHash"); + } + } + + private byte[] _WlanMac = null; + public byte[] WlanMac + { + get + { + return _WlanMac; + } + set + { + _WlanMac = value; + OnPropertyChanged("WlanMac"); + } + } + + private byte[] _BluetoothMac = null; + public byte[] BluetoothMac + { + get + { + return _BluetoothMac; + } + set + { + _BluetoothMac = value; + OnPropertyChanged("BluetoothMac"); + } + } + + private bool? _IsBootloaderSecurityEnabled = null; + public bool? IsBootloaderSecurityEnabled + { + get + { + return _IsBootloaderSecurityEnabled; + } + set + { + _IsBootloaderSecurityEnabled = value; + OnPropertyChanged("IsBootloaderSecurityEnabled"); + } + } + + private bool? _IsSimLocked = null; + public bool? IsSimLocked + { + get + { + return _IsSimLocked; + } + set + { + _IsSimLocked = value; + OnPropertyChanged("IsSimLocked"); + } + } + + public void RebootTo(string Mode) + { + switch (Mode) + { + case "Flash": + RequestModeSwitch(PhoneInterfaces.Lumia_Flash); + break; + case "Label": + RequestModeSwitch(PhoneInterfaces.Lumia_Label); + break; + case "MassStorage": + RequestModeSwitch(PhoneInterfaces.Lumia_MassStorage); + break; + default: + return; + } + } + } +} diff --git a/ViewModels/NokiaMassStorageViewModel.cs b/ViewModels/NokiaMassStorageViewModel.cs new file mode 100644 index 0000000..52ae572 --- /dev/null +++ b/ViewModels/NokiaMassStorageViewModel.cs @@ -0,0 +1,45 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Threading; + +namespace WPinternals +{ + internal class NokiaMassStorageViewModel: ContextViewModel + { + private MassStorage CurrentModel; + + internal NokiaMassStorageViewModel(MassStorage CurrentModel) + : base() + { + this.CurrentModel = CurrentModel; + _Drive = CurrentModel.Drive; + } + + private string _Drive = null; + public string Drive + { + get + { + return _Drive; + } + } + } +} diff --git a/ViewModels/NokiaModeFlashViewModel.cs b/ViewModels/NokiaModeFlashViewModel.cs new file mode 100644 index 0000000..3e7314d --- /dev/null +++ b/ViewModels/NokiaModeFlashViewModel.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace WPinternals +{ + internal class NokiaModeFlashViewModel : ContextViewModel + { + private NokiaFlashModel CurrentModel; + Action RequestModeSwitch; + private object LockDeviceInfo = new object(); + bool DeviceInfoLoaded = false; + + internal NokiaModeFlashViewModel(NokiaPhoneModel CurrentModel, Action RequestModeSwitch) + : base() + { + this.CurrentModel = (NokiaFlashModel)CurrentModel; + this.RequestModeSwitch = RequestModeSwitch; + } + + internal override void EvaluateViewState() + { + if (IsActive) + new Thread(() => StartLoadDeviceInfo()).Start(); + } + + private bool? _EffectiveBootloaderSecurityStatus = null; + public bool? EffectiveBootloaderSecurityStatus + { + get + { + return _EffectiveBootloaderSecurityStatus; + } + set + { + _EffectiveBootloaderSecurityStatus = value; + OnPropertyChanged("EffectiveBootloaderSecurityStatus"); + } + } + + internal void StartLoadDeviceInfo() + { + lock (LockDeviceInfo) + { + if (!DeviceInfoLoaded) + { + try + { + PhoneInfo Info = CurrentModel.ReadPhoneInfo(); + + if (Info.FlashAppProtocolVersionMajor < 2) + { + UefiSecurityStatusResponse SecurityStatus = CurrentModel.ReadSecurityStatus(); + + if (SecurityStatus != null) + { + EffectiveBootloaderSecurityStatus = SecurityStatus.SecureFfuEfuseStatus && !SecurityStatus.AuthenticationStatus && !SecurityStatus.RdcStatus; + } + } + else + { + EffectiveBootloaderSecurityStatus = Info.UefiSecureBootEnabled; + } + + LogFile.Log("Effective Bootloader Security Status: " + EffectiveBootloaderSecurityStatus.ToString()); + } + catch + { + LogFile.Log("Reading status from Flash interface was aborted."); + } + DeviceInfoLoaded = true; + } + } + } + + internal void RebootTo(string Mode) + { + switch (Mode) + { + case "Normal": + RequestModeSwitch(PhoneInterfaces.Lumia_Normal); + break; + case "Flash": + RequestModeSwitch(PhoneInterfaces.Lumia_Flash); + break; + case "Label": + RequestModeSwitch(PhoneInterfaces.Lumia_Label); + break; + case "MassStorage": + RequestModeSwitch(PhoneInterfaces.Lumia_MassStorage); + break; + case "Shutdown": + RequestModeSwitch(null); + break; + default: + return; + } + } + } +} diff --git a/ViewModels/NokiaModeLabelViewModel.cs b/ViewModels/NokiaModeLabelViewModel.cs new file mode 100644 index 0000000..7ee9a7f --- /dev/null +++ b/ViewModels/NokiaModeLabelViewModel.cs @@ -0,0 +1,58 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace WPinternals +{ + internal class NokiaModeLabelViewModel : ContextViewModel + { + private NokiaPhoneModel CurrentModel; + private Action RequestModeSwitch; + + internal NokiaModeLabelViewModel(NokiaPhoneModel CurrentModel, Action RequestModeSwitch) + : base() + { + this.CurrentModel = CurrentModel; + this.RequestModeSwitch = RequestModeSwitch; + } + + public void RebootTo(string Mode) + { + switch (Mode) + { + case "Normal": + RequestModeSwitch(PhoneInterfaces.Lumia_Normal); + break; + case "Flash": + RequestModeSwitch(PhoneInterfaces.Lumia_Flash); + break; + case "Label": + RequestModeSwitch(PhoneInterfaces.Lumia_Label); + break; + case "MassStorage": + RequestModeSwitch(PhoneInterfaces.Lumia_MassStorage); + break; + default: + return; + } + } + } +} diff --git a/ViewModels/NokiaModeMassStorageViewModel.cs b/ViewModels/NokiaModeMassStorageViewModel.cs new file mode 100644 index 0000000..d155e2d --- /dev/null +++ b/ViewModels/NokiaModeMassStorageViewModel.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Collections.Generic; + +namespace WPinternals +{ + internal class NokiaModeMassStorageViewModel : ContextViewModel + { + private NokiaPhoneModel CurrentModel; + + internal NokiaModeMassStorageViewModel(NokiaPhoneModel CurrentModel) + : base() + { + this.CurrentModel = CurrentModel; + } + + internal void RebootTo(string Mode) + { + string DeviceMode; + + switch (Mode) + { + case "Flash": + DeviceMode = "Flash"; + LogFile.Log("Reboot to Flash"); + break; + case "Label": + DeviceMode = "Test"; + LogFile.Log("Reboot to Label"); + break; + case "MassStorage": + DeviceMode = "Flash"; // TODO: implement folow-up + LogFile.Log("Reboot to Mass Storage"); + break; + default: + return; + } + + Dictionary Params = new Dictionary(); + Params.Add("DeviceMode", DeviceMode); + Params.Add("ResetMethod", "HwReset"); + CurrentModel.ExecuteJsonMethodAsync("SetDeviceMode", Params); + } + } +} diff --git a/ViewModels/NokiaModeNormalViewModel.cs b/ViewModels/NokiaModeNormalViewModel.cs new file mode 100644 index 0000000..08ee6d3 --- /dev/null +++ b/ViewModels/NokiaModeNormalViewModel.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; + +namespace WPinternals +{ + internal class NokiaModeNormalViewModel: ContextViewModel + { + private NokiaPhoneModel CurrentModel; + private Action RequestModeSwitch; + + internal NokiaModeNormalViewModel(NokiaPhoneModel CurrentModel, Action RequestModeSwitch) + : base() + { + this.CurrentModel = CurrentModel; + this.RequestModeSwitch = RequestModeSwitch; + } + + public void RebootTo(string Mode) + { + switch (Mode) + { + case "Flash": + RequestModeSwitch(PhoneInterfaces.Lumia_Flash); + break; + case "Label": + RequestModeSwitch(PhoneInterfaces.Lumia_Label); + break; + case "MassStorage": + RequestModeSwitch(PhoneInterfaces.Lumia_MassStorage); + break; + default: + return; + } + } + } +} diff --git a/ViewModels/NokiaNormalViewModel.cs b/ViewModels/NokiaNormalViewModel.cs new file mode 100644 index 0000000..f6829cf --- /dev/null +++ b/ViewModels/NokiaNormalViewModel.cs @@ -0,0 +1,237 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; + +namespace WPinternals +{ + // Create this class on the UI thread, after the main-window of the application is initialized. + // It is necessary to create the object on the UI thread, because notification events to the View need to be fired on that thread. + // The Model for this ViewModel communicates over USB and for that it uses the hWnd of the main window. + // Therefore the main window must be created before the ViewModel is created. + + internal class NokiaNormalViewModel : ContextViewModel + { + private NokiaPhoneModel CurrentModel; + private Action RequestModeSwitch; + + internal NokiaNormalViewModel(NokiaPhoneModel CurrentModel, Action RequestModeSwitch) + : base() + { + this.CurrentModel = CurrentModel; + this.RequestModeSwitch = RequestModeSwitch; + + new Thread(() => StartLoadDeviceInfo()).Start(); + } + + private void StartLoadDeviceInfo() + { + try + { + Operator = CurrentModel.ExecuteJsonMethodAsString("ReadOperatorName", "OperatorName"); // 000-NL + LogFile.Log("Operator: " + Operator); + ManufacturerModelName = CurrentModel.ExecuteJsonMethodAsString("ReadManufacturerModelName", "ManufacturerModelName"); // RM-821_eu_denmark_251 + LogFile.Log("Manufacturer Model Name: " + ManufacturerModelName); + ProductCode = CurrentModel.ExecuteJsonMethodAsString("ReadProductCode", "ProductCode"); // 059Q9D7 + LogFile.Log("Product Code: " + ProductCode); + Firmware = CurrentModel.ExecuteJsonMethodAsString("ReadSwVersion", "SwVersion"); // 3051.40000.1349.0007 + LogFile.Log("Firmware: " + Firmware); + + IMEI = CurrentModel.ExecuteJsonMethodAsString("ReadSerialNumber", "SerialNumber"); // IMEI + LogFile.Log("IMEI: " + IMEI); + PublicID = CurrentModel.ExecuteJsonMethodAsBytes("ReadPublicId", "PublicId"); // 0x14 bytes: a5 e5 ... + LogFile.Log("Public ID: " + Converter.ConvertHexToString(PublicID, " ")); + BluetoothMac = CurrentModel.ExecuteJsonMethodAsBytes("ReadBtId", "BtId"); // 6 bytes: bc c6 ... + LogFile.Log("Bluetooth MAC: " + Converter.ConvertHexToString(BluetoothMac, " ")); + WlanMac = CurrentModel.ExecuteJsonMethodAsBytes("ReadWlanMacAddress", "WlanMacAddress1"); // 6 bytes + LogFile.Log("WLAN MAC: " + Converter.ConvertHexToString(WlanMac, " ")); + + bool? ProductionDone = CurrentModel.ExecuteJsonMethodAsBoolean("ReadProductionDoneState", "ProductionDone"); + if (ProductionDone == null) + IsBootloaderSecurityEnabled = (CurrentModel.ExecuteJsonMethodAsString("GetSecurityMode", "SecMode").IndexOf("Restricted", StringComparison.OrdinalIgnoreCase) >= 0); + else + IsBootloaderSecurityEnabled = ((CurrentModel.ExecuteJsonMethodAsString("GetSecurityMode", "SecMode").IndexOf("Restricted", StringComparison.OrdinalIgnoreCase) >= 0) && (bool)CurrentModel.ExecuteJsonMethodAsBoolean("ReadProductionDoneState", "ProductionDone")); + LogFile.Log("Bootloader Security: " + ((bool)IsBootloaderSecurityEnabled ? "Enabled" : "Disabled")); + IsSimLocked = CurrentModel.ExecuteJsonMethodAsBoolean("ReadSimlockActive", "SimLockActive"); + LogFile.Log("Simlock: " + ((bool)IsSimLocked ? "Active" : "Unlocked")); + } + catch { } + } + + private string _ProductCode = null; + public string ProductCode + { + get + { + return _ProductCode; + } + set + { + _ProductCode = value; + OnPropertyChanged("ProductCode"); + } + } + + private string _ManufacturerModelName = null; + public string ManufacturerModelName + { + get + { + return _ManufacturerModelName; + } + set + { + _ManufacturerModelName = value; + OnPropertyChanged("ManufacturerModelName"); + } + } + + private string _Operator = null; + public string Operator + { + get + { + return _Operator; + } + set + { + _Operator = value; + OnPropertyChanged("Operator"); + } + } + + private string _Firmware = null; + public string Firmware + { + get + { + return _Firmware; + } + set + { + _Firmware = value; + OnPropertyChanged("Firmware"); + } + } + + private string _IMEI = null; + public string IMEI + { + get + { + return _IMEI; + } + set + { + _IMEI = value; + OnPropertyChanged("IMEI"); + } + } + + private byte[] _PublicID = null; + public byte[] PublicID + { + get + { + return _PublicID; + } + set + { + _PublicID = value; + OnPropertyChanged("PublicID"); + } + } + + private byte[] _WlanMac = null; + public byte[] WlanMac + { + get + { + return _WlanMac; + } + set + { + _WlanMac = value; + OnPropertyChanged("WlanMac"); + } + } + + private byte[] _BluetoothMac = null; + public byte[] BluetoothMac + { + get + { + return _BluetoothMac; + } + set + { + _BluetoothMac = value; + OnPropertyChanged("BluetoothMac"); + } + } + + private bool? _IsBootloaderSecurityEnabled = null; + public bool? IsBootloaderSecurityEnabled + { + get + { + return _IsBootloaderSecurityEnabled; + } + set + { + _IsBootloaderSecurityEnabled = value; + OnPropertyChanged("IsBootloaderSecurityEnabled"); + } + } + + private bool? _IsSimLocked = null; + public bool? IsSimLocked + { + get + { + return _IsSimLocked; + } + set + { + _IsSimLocked = value; + OnPropertyChanged("IsSimLocked"); + } + } + + public void RebootTo(string Mode) + { + switch (Mode) + { + case "Flash": + RequestModeSwitch(PhoneInterfaces.Lumia_Flash); + break; + case "Label": + RequestModeSwitch(PhoneInterfaces.Lumia_Label); + break; + case "MassStorage": + RequestModeSwitch(PhoneInterfaces.Lumia_MassStorage); + break; + default: + return; + } + } + } +} diff --git a/ViewModels/NotImplementedViewModel.cs b/ViewModels/NotImplementedViewModel.cs new file mode 100644 index 0000000..b272a04 --- /dev/null +++ b/ViewModels/NotImplementedViewModel.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +namespace WPinternals +{ + internal class NotImplementedViewModel: ContextViewModel + { + internal string Message { get; set; } + + internal NotImplementedViewModel(MainViewModel Main) + : base(Main) + { + Message = "Not implemented yet..."; + } + + internal NotImplementedViewModel(MainViewModel Main, string Message) + : base(Main) + { + this.Message = Message; + } + } +} diff --git a/ViewModels/PhoneNotifierViewModel.cs b/ViewModels/PhoneNotifierViewModel.cs new file mode 100644 index 0000000..bb3aa76 --- /dev/null +++ b/ViewModels/PhoneNotifierViewModel.cs @@ -0,0 +1,424 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using MadWizard.WinUSBNet; +using System; +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace WPinternals +{ + internal delegate void NewDeviceArrivedEvent(ArrivalEventArgs Args); + internal delegate void DeviceRemovedEvent(); + + internal class PhoneNotifierViewModel + { + private USBNotifier LumiaOldCombiNotifier; + private USBNotifier LumiaNewCombiNotifier; + private USBNotifier LumiaNormalNotifier; + private USBNotifier LumiaFlashNotifier; + private USBNotifier StorageNotifier; + private USBNotifier ComPortNotifier; + private USBNotifier LumiaEmergencyNotifier; + + public PhoneInterfaces? CurrentInterface = null; + private PhoneInterfaces? LastInterface = null; + public IDisposable CurrentModel = null; + + public event NewDeviceArrivedEvent NewDeviceArrived = delegate { }; + public event DeviceRemovedEvent DeviceRemoved = delegate { }; + + private Guid OldCombiInterfaceGuid = new Guid("{0FD3B15C-D457-45d8-A779-C2B2C9F9D0FD}"); + private Guid NewCombiInterfaceGuid = new Guid("{7eaff726-34cc-4204-b09d-f95471b873cf}"); + private Guid MassStorageInterfaceGuid = new Guid("{53F56307-B6BF-11D0-94F2-00A0C91EFB8B}"); + private Guid LumiaNormalInterfaceGuid = new Guid("{08324F9C-B621-435C-859B-AE4652481B7C}"); + private Guid LumiaFlashInterfaceGuid = new Guid("{9e3bd5f7-9690-4fcc-8810-3e2650cd6ecc}"); + private Guid ComPortInterfaceGuid = new Guid("{86E0D1E0-8089-11D0-9CE4-08003E301F73}"); + private Guid LumiaEmergencyInterfaceGuid = new Guid("{71DE994D-8B7C-43DB-A27E-2AE7CD579A0C}"); + + private object ModelLock = new object(); + + private EventWaitHandle NewInterfaceWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset); + + private EventLogWatcher LogWatcher; + + internal void Start() + { + LumiaOldCombiNotifier = new USBNotifier(OldCombiInterfaceGuid); + LumiaOldCombiNotifier.Arrival += LumiaNotifier_Arrival; + LumiaOldCombiNotifier.Removal += LumiaNotifier_Removal; + + LumiaNewCombiNotifier = new USBNotifier(NewCombiInterfaceGuid); + LumiaNewCombiNotifier.Arrival += LumiaNotifier_Arrival; + LumiaNewCombiNotifier.Removal += LumiaNotifier_Removal; + + LumiaNormalNotifier = new USBNotifier(LumiaNormalInterfaceGuid); + LumiaNormalNotifier.Arrival += LumiaNotifier_Arrival; + LumiaNormalNotifier.Removal += LumiaNotifier_Removal; + + LumiaFlashNotifier = new USBNotifier(LumiaFlashInterfaceGuid); + LumiaFlashNotifier.Arrival += LumiaNotifier_Arrival; + LumiaFlashNotifier.Removal += LumiaNotifier_Removal; + + StorageNotifier = new USBNotifier(MassStorageInterfaceGuid); + StorageNotifier.Arrival += LumiaNotifier_Arrival; + StorageNotifier.Removal += LumiaNotifier_Removal; + + ComPortNotifier = new USBNotifier(ComPortInterfaceGuid); + ComPortNotifier.Arrival += LumiaNotifier_Arrival; + ComPortNotifier.Removal += LumiaNotifier_Removal; + + LumiaEmergencyNotifier = new USBNotifier(LumiaEmergencyInterfaceGuid); + LumiaEmergencyNotifier.Arrival += LumiaNotifier_Arrival; + LumiaEmergencyNotifier.Removal += LumiaNotifier_Removal; + + try + { + EventLogQuery LogQuery = new EventLogQuery("Microsoft-Windows-Kernel-PnP/Configuration", PathType.LogName, "*[System[(EventID = 411)]]"); + LogWatcher = new EventLogWatcher(LogQuery); + LogWatcher.EventRecordWritten += new EventHandler(PnPEventWritten); + LogWatcher.Enabled = true; + App.IsPnPEventLogMissing = false; + } + catch { } + } + + private void PnPEventWritten(Object obj, EventRecordWrittenEventArgs arg) + { + string Description = arg.EventRecord.FormatDescription(); + if (Description.IndexOf("VID_045E&PID_9006", 0, StringComparison.OrdinalIgnoreCase) >= 0) + { + LogFile.Log("Event " + arg.EventRecord.Id.ToString() + ": " + Description, LogType.FileOnly); + LogFile.Log("Phone switched to Mass Storage mode, but the driver on the PC did not start correctly", LogType.FileAndConsole); + CurrentInterface = PhoneInterfaces.Lumia_BadMassStorage; + CurrentModel = null; + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + } + + internal void Stop() + { + LumiaOldCombiNotifier.Dispose(); + LumiaNewCombiNotifier.Dispose(); + LumiaNormalNotifier.Dispose(); + LumiaFlashNotifier.Dispose(); + StorageNotifier.Dispose(); + ComPortNotifier.Dispose(); + LumiaEmergencyNotifier.Dispose(); + LogWatcher.Dispose(); + } + + internal async Task WaitForNextNodeChange() + { + // Node change events are on all USBnotifiers, so we just pick one + await LumiaEmergencyNotifier.WaitForNextNodeChange(); + } + + internal void NotifyArrival() + { + if (CurrentInterface != null) + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + + void LumiaNotifier_Arrival(object sender, USBEvent e) + { + try + { + if (e.DevicePath.IndexOf("VID_0421&PID_0660&MI_04", StringComparison.OrdinalIgnoreCase) >= 0) + { + CurrentInterface = PhoneInterfaces.Lumia_Label; + CurrentModel = new NokiaPhoneModel(e.DevicePath); + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + LogFile.Log("Mode: Label", LogType.FileAndConsole); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + else if ((e.DevicePath.IndexOf("VID_0421&PID_0661", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("VID_0421&PID_06FC", StringComparison.OrdinalIgnoreCase) >= 0) || // VID_0421&PID_06FC is for Lumia 930 + (e.DevicePath.IndexOf("vid_045e&pid_0a00", StringComparison.OrdinalIgnoreCase) >= 0)) // vid_045e & pid_0a00 & mi_03 = Lumia 950 XL normal mode + { + if (((USBNotifier)sender).Guid == OldCombiInterfaceGuid) + { + NewInterfaceWaitHandle.Reset(); + if (USBDevice.GetDevices(NewCombiInterfaceGuid).Count() > 0) + return; + else + { + // Old combi-interface was detected, but new combi-interface was not detected. + // This could mean 2 things: + // - It is a WP80 phone, which has only this old combi-interface to talk to. + // - It is a WP81 / W10M phone, which has an unresponsive old combi-interface and we need to wait for the new combi-interface to arrive. + // We will wait maximum 1 sec for the new interface. If it doesn't arrive we will start talking on this old interface. + // We will start a new thread, because if this thread is blocked, no new devices will arrive. + string DevicePath = e.DevicePath; + ThreadPool.QueueUserWorkItem(s => + { + if (!NewInterfaceWaitHandle.WaitOne(1000)) + { + // Waithandle not set. + // So new interface did not arrive. + // So we assume we need to talk to this old interface. + + CurrentInterface = PhoneInterfaces.Lumia_Normal; + CurrentModel = new NokiaPhoneModel(DevicePath); + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + LogFile.Log("Mode: Normal", LogType.FileAndConsole); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + }); + } + } + else + { + NewInterfaceWaitHandle.Set(); + + CurrentInterface = PhoneInterfaces.Lumia_Normal; + CurrentModel = new NokiaPhoneModel(e.DevicePath); + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + LogFile.Log("Mode: Normal", LogType.FileAndConsole); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + } + else if ((e.DevicePath.IndexOf("VID_0421&PID_066E", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("VID_0421&PID_0714", StringComparison.OrdinalIgnoreCase) >= 0) || // VID_0421&PID_0714 is for Lumia 930 + (e.DevicePath.IndexOf("VID_045E&PID_0A02", StringComparison.OrdinalIgnoreCase) >= 0)) // VID_045E&PID_0A02 is for Lumia 950 + { + CurrentModel = new NokiaFlashModel(e.DevicePath); + ((NokiaFlashModel)CurrentModel).InterfaceChanged += InterfaceChanged; + + // Attempt to request a param. + // When it succeeds we have full Flash mode. + // When it fails we are in a limited Flash mode, like Bootmanager or Hardreset-screen. + // Limited Flash mode only supports boot-commands; not querying info. + byte[] QueryResult = ((NokiaFlashModel)CurrentModel).ReadParam("SS"); + if (QueryResult == null) + { + CurrentInterface = PhoneInterfaces.Lumia_Bootloader; + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + LogFile.Log("Mode: Bootloader", LogType.FileAndConsole); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + else + { + ((NokiaFlashModel)CurrentModel).DisableRebootTimeOut(); + CurrentInterface = PhoneInterfaces.Lumia_Flash; + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + LogFile.Log("Mode: Flash", LogType.FileAndConsole); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + } + else if ((e.DevicePath.IndexOf(@"disk&ven_qualcomm&prod_mmc_storage", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf(@"DISK&VEN_MSFT&PROD_PHONE_MMC_STOR", StringComparison.OrdinalIgnoreCase) >= 0)) + { +#if DEBUG + LogFile.Log("Mass storage arrived: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Start new thread for getting metadata.", LogType.FileOnly); +#endif + + // This function is possibly called by an USB notification WndProc. + // It is not possible to invoke COM objects from a WndProc. + // MassStorage uses ManagementObjectSearcher, which is a COM object. + // Therefore we use a new thread. + ThreadPool.QueueUserWorkItem(s => { + lock (ModelLock) + { + if (!(CurrentModel is MassStorage)) + { + MassStorage NewModel = new MassStorage(e.DevicePath); + if (NewModel.Drive != null) // When logical drive is already known, we use this model. Or else we wait for the logical drive to arrive. + { + CurrentInterface = PhoneInterfaces.Lumia_MassStorage; + CurrentModel = NewModel; + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + LogFile.Log("Mode: Mass storage mode", LogType.FileAndConsole); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + } + } + }); + } + else if ((e.DevicePath.Length == @"\\.\E:".Length) && (e.DevicePath.StartsWith(@"\\.\")) && (e.DevicePath.EndsWith(":"))) + { +#if DEBUG + LogFile.Log("Mass storage arrived: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Start new thread for getting metadata.", LogType.FileOnly); +#endif + // This function is possibly called by an USB notification WndProc. + // It is not possible to invoke COM objects from a WndProc. + // MassStorage uses ManagementObjectSearcher, which is a COM object. + // Therefore we use a new thread. + ThreadPool.QueueUserWorkItem(s => + { + lock (ModelLock) + { + if (!(CurrentModel is MassStorage)) + { + MassStorage NewModel = new MassStorage(e.DevicePath); + if (NewModel.Drive != null) // When logical drive is already known, we use this model. Or else we wait for the logical drive to arrive. + { + CurrentInterface = PhoneInterfaces.Lumia_MassStorage; + CurrentModel = NewModel; + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + LogFile.Log("Mode: Mass storage mode", LogType.FileAndConsole); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + } + } + } + }); + } + else if (e.DevicePath.IndexOf("VID_05C6&PID_9008", StringComparison.OrdinalIgnoreCase) >= 0) + { + USBDeviceInfo DeviceInfo = USBDevice.GetDevices(((USBNotifier)sender).Guid).Where((d) => string.Compare(d.DevicePath, e.DevicePath, true) == 0).FirstOrDefault(); + + if ((DeviceInfo.BusName == "QHSUSB_DLOAD") || (DeviceInfo.BusName == "QHSUSB__BULK") || ((DeviceInfo.BusName == "") && (LastInterface != PhoneInterfaces.Qualcomm_Download))) // TODO: Separate for Sahara! + { + CurrentInterface = PhoneInterfaces.Qualcomm_Download; + CurrentModel = new QualcommSerial(e.DevicePath); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + if (DeviceInfo.BusName == "") + LogFile.Log("Driver does not show busname, assume mode: Qualcomm Emergency Download 9008", LogType.FileAndConsole); + else + LogFile.Log("Mode: Qualcomm Emergency Download 9008", LogType.FileAndConsole); + } + else if ((DeviceInfo.BusName == "QHSUSB_ARMPRG") || ((DeviceInfo.BusName == "") && (LastInterface == PhoneInterfaces.Qualcomm_Download))) + { + CurrentInterface = PhoneInterfaces.Qualcomm_Flash; + CurrentModel = new QualcommSerial(e.DevicePath); + NewDeviceArrived(new ArrivalEventArgs((PhoneInterfaces)CurrentInterface, CurrentModel)); + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + if (DeviceInfo.BusName == "") + LogFile.Log("Driver does not show busname, assume mode: Qualcomm Emergency Flash 9008", LogType.FileAndConsole); + else + LogFile.Log("Mode: Qualcomm Emergency Flash 9008", LogType.FileAndConsole); + } + } + else if (e.DevicePath.IndexOf("VID_05C6&PID_9006", StringComparison.OrdinalIgnoreCase) >= 0) + { + // This is part of the Mass Storage inteface. + // It is a slightly different version of the Qualcomm Emergency interface, which is implemented in SBL3. + // One important difference is that the base address for sending a loader is not 0x2A000000, but it is 0x82F00000. + + LogFile.Log("Found device on interface: " + ((USBNotifier)sender).Guid.ToString(), LogType.FileOnly); + LogFile.Log("Device path: " + e.DevicePath, LogType.FileOnly); + LogFile.Log("Connected device: Lumia", LogType.FileAndConsole); + LogFile.Log("Mode: Qualcomm Emergency 9006", LogType.FileAndConsole); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + CurrentModel = null; + CurrentInterface = null; + } + } + + private void InterfaceChanged(PhoneInterfaces NewInterface) + { + CurrentInterface = NewInterface; + } + + void LumiaNotifier_Removal(object sender, USBEvent e) + { + if ( + (e.DevicePath.IndexOf("VID_0421&PID_0660&MI_04", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("VID_0421&PID_0661", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("VID_0421&PID_06FC", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("VID_0421&PID_066E", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("VID_0421&PID_0714", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("vid_045e&pid_0a00", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("VID_045E&PID_0A02", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf("VID_05C6&PID_9008", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf(@"disk&ven_qualcomm&prod_mmc_storage", StringComparison.OrdinalIgnoreCase) >= 0) || + (e.DevicePath.IndexOf(@"DISK&VEN_MSFT&PROD_PHONE_MMC_STOR", StringComparison.OrdinalIgnoreCase) >= 0) + ) { + if (CurrentInterface != null) + LastInterface = CurrentInterface; + CurrentInterface = null; + if (CurrentModel != null) + { + CurrentModel.Dispose(); + CurrentModel = null; + LogFile.Log("Lumia disconnected", LogType.FileAndConsole); + } + DeviceRemoved(); + } + } + + internal async Task WaitForArrival() + { + IDisposable Result = null; + + if (CurrentInterface == null) + LogFile.Log("Waiting for phone to connect...", LogType.FileOnly); + + await Task.Run(() => + { + System.Threading.AutoResetEvent e = new System.Threading.AutoResetEvent(false); + NewDeviceArrivedEvent Arrived = (a) => + { + e.Set(); + Result = a.NewModel; + }; + NewDeviceArrived += Arrived; + e.WaitOne(); + NewDeviceArrived -= Arrived; + }); + + return Result; + } + + internal async Task WaitForRemoval() + { + LogFile.Log("Waiting for phone to disconnect...", LogType.FileOnly); + + await Task.Run(() => + { + System.Threading.AutoResetEvent e = new System.Threading.AutoResetEvent(false); + DeviceRemovedEvent Removed = () => + { + e.Set(); + }; + DeviceRemoved += Removed; + e.WaitOne(); + DeviceRemoved -= Removed; + }); + } + } +} diff --git a/ViewModels/RegistrationViewModel.cs b/ViewModels/RegistrationViewModel.cs new file mode 100644 index 0000000..65de77f --- /dev/null +++ b/ViewModels/RegistrationViewModel.cs @@ -0,0 +1,154 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Windows; + +namespace WPinternals +{ + internal class RegistrationViewModel : ContextViewModel + { + Action Completed; + Action Failed; + + internal RegistrationViewModel(Action Completed, Action Failed) + : base() + { + this.Completed = Completed; + this.Failed = Failed; + } + + private DelegateCommand _ExitCommand = null; + public DelegateCommand ExitCommand + { + get + { + if (_ExitCommand == null) + { + _ExitCommand = new DelegateCommand(() => + { + Application.Current.Shutdown(); + }); + } + return _ExitCommand; + } + } + + private DelegateCommand _ContinueCommand = null; + public DelegateCommand ContinueCommand + { + get + { + if (_ContinueCommand == null) + { + _ContinueCommand = new DelegateCommand(() => + { + try + { + App.Config.RegistrationName = _Name; + App.Config.RegistrationEmail = _Email; + App.Config.RegistrationSkypeID = _SkypeID; + App.Config.RegistrationTelegramID = _TelegramID; + App.Config.RegistrationKey = Registration.CalcRegKey(); + + LogFile.BeginAction("Registration"); + LogFile.EndAction("Registration"); + + App.Config.WriteConfig(); + + Completed(); + } + catch + { + Failed(); + } + }); + } + return _ContinueCommand; + } + } + + public bool IsRegistrationComplete + { + get + { + return ((_Name != null) && (_Name.Length >= 2) && (_Email != null) && (_Email.Length >= 5)); + } + } + + private string _Name = null; + public string Name + { + get + { + return _Name; + } + set + { + _Name = value; + OnPropertyChanged("Name"); + OnPropertyChanged("IsRegistrationComplete"); + } + } + + private string _Email = null; + public string Email + { + get + { + return _Email; + } + set + { + _Email = value; + OnPropertyChanged("Email"); + OnPropertyChanged("IsRegistrationComplete"); + } + } + + private string _SkypeID = null; + public string SkypeID + { + get + { + return _SkypeID; + } + set + { + _SkypeID = value; + OnPropertyChanged("SkypeID"); + } + } + + private string _TelegramID = null; + public string TelegramID + { + get + { + return _TelegramID; + } + set + { + _TelegramID = value; + OnPropertyChanged("TelegramID"); + } + } + } +} diff --git a/ViewModels/RestoreSourceSelectionViewModel.cs b/ViewModels/RestoreSourceSelectionViewModel.cs new file mode 100644 index 0000000..f8b6538 --- /dev/null +++ b/ViewModels/RestoreSourceSelectionViewModel.cs @@ -0,0 +1,208 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Threading; + +namespace WPinternals +{ + internal class RestoreSourceSelectionViewModel: ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + private Action RestoreCallback; + private Action RequestModeSwitch; + internal Action SwitchToUnlockBoot; + internal Action SwitchToFlashRom; + + internal RestoreSourceSelectionViewModel(PhoneNotifierViewModel PhoneNotifier, Action RequestModeSwitch, Action SwitchToUnlockBoot, Action SwitchToFlashRom, Action RestoreCallback) + : base() + { + this.PhoneNotifier = PhoneNotifier; + this.RestoreCallback = RestoreCallback; + this.RequestModeSwitch = RequestModeSwitch; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.SwitchToFlashRom = SwitchToFlashRom; + + this.PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + this.PhoneNotifier.DeviceRemoved += DeviceRemoved; + + new Thread(() => EvaluateViewState()).Start(); + } + + private string _EFIESPPath; + public string EFIESPPath + { + get + { + return _EFIESPPath; + } + set + { + if (value != _EFIESPPath) + { + _EFIESPPath = value; + OnPropertyChanged("EFIESPPath"); + } + } + } + + private string _MainOSPath; + public string MainOSPath + { + get + { + return _MainOSPath; + } + set + { + if (value != _MainOSPath) + { + _MainOSPath = value; + OnPropertyChanged("MainOSPath"); + } + } + } + + private string _DataPath; + public string DataPath + { + get + { + return _DataPath; + } + set + { + if (value != _DataPath) + { + _DataPath = value; + OnPropertyChanged("DataPath"); + } + } + } + + private bool _IsPhoneDisconnected; + public bool IsPhoneDisconnected + { + get + { + return _IsPhoneDisconnected; + } + set + { + if (value != _IsPhoneDisconnected) + { + _IsPhoneDisconnected = value; + OnPropertyChanged("IsPhoneDisconnected"); + } + } + } + + private bool _IsPhoneInFlashMode; + public bool IsPhoneInFlashMode + { + get + { + return _IsPhoneInFlashMode; + } + set + { + if (value != _IsPhoneInFlashMode) + { + _IsPhoneInFlashMode = value; + OnPropertyChanged("IsPhoneInFlashMode"); + } + } + } + + private bool _IsPhoneInOtherMode; + public bool IsPhoneInOtherMode + { + get + { + return _IsPhoneInOtherMode; + } + set + { + if (value != _IsPhoneInOtherMode) + { + _IsPhoneInOtherMode = value; + OnPropertyChanged("IsPhoneInOtherMode"); + } + } + } + + private DelegateCommand _RestoreCommand; + public DelegateCommand RestoreCommand + { + get + { + if (_RestoreCommand == null) + { + _RestoreCommand = new DelegateCommand(() => { RestoreCallback(EFIESPPath, MainOSPath, DataPath); }, () => (((EFIESPPath != null) || (MainOSPath != null) || (DataPath != null)) && (PhoneNotifier.CurrentInterface != null))); + } + return _RestoreCommand; + } + } + + ~RestoreSourceSelectionViewModel() + { + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + } + + void NewDeviceArrived(ArrivalEventArgs Args) + { + new Thread(() => EvaluateViewState()).Start(); + } + + void DeviceRemoved() + { + new Thread(() => EvaluateViewState()).Start(); + } + + internal override void EvaluateViewState() + { + IsPhoneDisconnected = PhoneNotifier.CurrentInterface == null; + IsPhoneInFlashMode = PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_Flash; + IsPhoneInOtherMode = (!IsPhoneDisconnected && !IsPhoneInFlashMode); + RestoreCommand.RaiseCanExecuteChanged(); + } + + internal void RebootTo(string Mode) + { + switch (Mode) + { + case "Normal": + RequestModeSwitch(PhoneInterfaces.Lumia_Normal); + break; + case "Flash": + RequestModeSwitch(PhoneInterfaces.Lumia_Flash); + break; + case "Label": + RequestModeSwitch(PhoneInterfaces.Lumia_Label); + break; + case "MassStorage": + RequestModeSwitch(PhoneInterfaces.Lumia_MassStorage); + break; + default: + return; + } + } + } +} diff --git a/ViewModels/RestoreViewModel.cs b/ViewModels/RestoreViewModel.cs new file mode 100644 index 0000000..9d133e5 --- /dev/null +++ b/ViewModels/RestoreViewModel.cs @@ -0,0 +1,198 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.IO; +using System.Threading; + +namespace WPinternals +{ + internal class RestoreViewModel: ContextViewModel + { + private PhoneNotifierViewModel PhoneNotifier; + private Action Callback; + private Action RequestModeSwitch; + private Action SwitchToUnlockBoot; + private Action SwitchToFlashRom; + + internal RestoreViewModel(PhoneNotifierViewModel PhoneNotifier, Action RequestModeSwitch, Action SwitchToUnlockBoot, Action SwitchToFlashRom, Action Callback) + : base() + { + IsSwitchingInterface = true; + + this.PhoneNotifier = PhoneNotifier; + this.Callback = Callback; + this.RequestModeSwitch = RequestModeSwitch; + this.SwitchToUnlockBoot = SwitchToUnlockBoot; + this.SwitchToFlashRom = SwitchToFlashRom; + } + + internal override void EvaluateViewState() + { + if (!IsActive) + return; + + if (SubContextViewModel == null) + ActivateSubContext(new RestoreSourceSelectionViewModel(PhoneNotifier, RequestModeSwitch, SwitchToUnlockBoot, SwitchToFlashRom, DoRestore)); + + if (SubContextViewModel is RestoreSourceSelectionViewModel) + ((RestoreSourceSelectionViewModel)SubContextViewModel).EvaluateViewState(); + } + + internal async void DoRestore(string EFIESPPath, string MainOSPath, string DataPath) + { + try + { + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_Flash, + (msg, sub) => + ActivateSubContext(new BusyViewModel(msg, sub))); + RestoreTask(EFIESPPath, MainOSPath, DataPath); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, Callback)); + } + } + + internal void RestoreTask(string EFIESPPath, string MainOSPath, string DataPath) + { + new Thread(() => + { + bool Result = true; + + ActivateSubContext(new BusyViewModel("Initializing restore...")); + + ulong TotalSizeSectors = 0; + int PartitionCount = 0; + try + { + if (EFIESPPath != null) + { + TotalSizeSectors += (ulong)new FileInfo(EFIESPPath).Length / 0x200; + PartitionCount++; + } + + if (MainOSPath != null) + { + TotalSizeSectors += (ulong)new FileInfo(MainOSPath).Length / 0x200; + PartitionCount++; + } + + if (DataPath != null) + { + TotalSizeSectors += (ulong)new FileInfo(DataPath).Length / 0x200; + PartitionCount++; + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + + NokiaFlashModel Phone = (NokiaFlashModel)PhoneNotifier.CurrentModel; + + BusyViewModel Busy = new BusyViewModel("Restoring...", MaxProgressValue: TotalSizeSectors, UIContext: UIContext); + ProgressUpdater Updater = Busy.ProgressUpdater; + ActivateSubContext(Busy); + + int i = 0; + if (Result) + { + try + { + if (EFIESPPath != null) + { + i++; + Busy.Message = "Restoring partition EFIESP (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.FlashRawPartition(EFIESPPath, "EFIESP", Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (Result) + { + try + { + if (MainOSPath != null) + { + i++; + Busy.Message = "Restoring partition MainOS (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.FlashRawPartition(EFIESPPath, "MainOS", Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (Result) + { + try + { + if (DataPath != null) + { + i++; + Busy.Message = "Restoring partition Data (" + i.ToString() + @"/" + PartitionCount.ToString() + ")"; + Phone.FlashRawPartition(EFIESPPath, "Data", Updater); + } + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + Result = false; + } + } + + if (!Result) + { + ActivateSubContext(new MessageViewModel("Failed to restore!", Exit)); + return; + } + + ActivateSubContext(new MessageViewModel("Successfully restored!", Exit)); + }).Start(); + } + + internal async void Exit() + { + IsSwitchingInterface = false; + try + { + await SwitchModeViewModel.SwitchToWithProgress(PhoneNotifier, PhoneInterfaces.Lumia_Normal, + (msg, sub) => + ActivateSubContext(new BusyViewModel(msg, sub))); + Callback(); + ActivateSubContext(null); + } + catch (Exception Ex) + { + ActivateSubContext(new MessageViewModel(Ex.Message, Callback)); + } + } + } +} diff --git a/ViewModels/SwitchModeViewModel.cs b/ViewModels/SwitchModeViewModel.cs new file mode 100644 index 0000000..b83ff24 --- /dev/null +++ b/ViewModels/SwitchModeViewModel.cs @@ -0,0 +1,669 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace WPinternals +{ + internal delegate void ModeSwitchProgressHandler(string Message, string SubMessage); + internal delegate void ModeSwitchErrorHandler(string Message); + internal delegate void ModeSwitchSuccessHandler(IDisposable NewModel, PhoneInterfaces NewInterface); + + internal class SwitchModeViewModel: ContextViewModel + { + protected PhoneNotifierViewModel PhoneNotifier; + protected IDisposable CurrentModel; + protected PhoneInterfaces? CurrentMode; + protected PhoneInterfaces? TargetMode; + protected bool IsSwitching = false; + internal event ModeSwitchProgressHandler ModeSwitchProgress = delegate { }; + internal event ModeSwitchErrorHandler ModeSwitchError = delegate { }; + internal event ModeSwitchSuccessHandler ModeSwitchSuccess = delegate { }; + internal new SetWorkingStatus SetWorkingStatus = (m, s, v, a, st) => { }; + internal new UpdateWorkingStatus UpdateWorkingStatus = (m, s, v, st) => { }; + private string MassStorageWarning = null; + + internal SwitchModeViewModel(PhoneNotifierViewModel PhoneNotifier, PhoneInterfaces? TargetMode) + : base() + { + if ((PhoneNotifier.CurrentInterface != PhoneInterfaces.Lumia_Flash) && (PhoneNotifier.CurrentInterface != PhoneInterfaces.Lumia_Bootloader) && (PhoneNotifier.CurrentInterface != PhoneInterfaces.Lumia_Label) && (PhoneNotifier.CurrentInterface != PhoneInterfaces.Lumia_Normal)) + throw new ArgumentException(); + + this.PhoneNotifier = PhoneNotifier; + this.CurrentModel = (NokiaPhoneModel)PhoneNotifier.CurrentModel; + this.CurrentMode = PhoneNotifier.CurrentInterface; + this.TargetMode = TargetMode; + + if (this.CurrentMode == null) + { + LogFile.Log("Waiting for phone to connect...", LogType.FileAndConsole); + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + } + else + { + // Make sure this ViewModel has its View loaded before we continue, + // or else loading of Views can get mixed up. + if (SynchronizationContext.Current == null) + StartSwitch(); + else + { + SynchronizationContext.Current.Post((s) => + { + ((SwitchModeViewModel)s).StartSwitch(); + }, this); + } + } + } + + internal SwitchModeViewModel(PhoneNotifierViewModel PhoneNotifier, PhoneInterfaces? TargetMode, ModeSwitchProgressHandler ModeSwitchProgress, ModeSwitchErrorHandler ModeSwitchError, ModeSwitchSuccessHandler ModeSwitchSuccess, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null) + : base() + { + if ((PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_Bootloader) && (TargetMode == PhoneInterfaces.Lumia_Flash)) + { + PhoneInfo Info = ((NokiaFlashModel)PhoneNotifier.CurrentModel).ReadPhoneInfo(false); + if (Info.BootManagerProtocolVersionMajor >= 2) + { + try + { + // The implementation of SwitchToFlashAppContext() is improved + // SwitchToFlashAppContext() should only be used with BootMgr v2 + // For switching from BootMgr to FlashApp, it will use NOKS + // That will switch to a charging state, whereas a normal context switch will not start charging + // The implementation of NOKS in BootMgr mode has changed in BootMgr v2 + // It does not disconnect / reconnect anymore and the apptype is changed immediately + // NOKS still doesnt return a status + // BootMgr v1 uses normal NOKS and waits for arrival of FlashApp + ((NokiaFlashModel)PhoneNotifier.CurrentModel).SwitchToFlashAppContext(); + + // But this was called as a real switch, so we will raise an arrival event. + PhoneNotifier.CurrentInterface = PhoneInterfaces.Lumia_Flash; + PhoneNotifier.NotifyArrival(); + } + catch { } + } + } + + if (PhoneNotifier.CurrentInterface == TargetMode) + ModeSwitchSuccess(PhoneNotifier.CurrentModel, (PhoneInterfaces)PhoneNotifier.CurrentInterface); + else + { + this.PhoneNotifier = PhoneNotifier; + this.CurrentModel = (NokiaPhoneModel)PhoneNotifier.CurrentModel; + this.CurrentMode = PhoneNotifier.CurrentInterface; + this.TargetMode = TargetMode; + if (ModeSwitchProgress != null) this.ModeSwitchProgress += ModeSwitchProgress; + if (ModeSwitchError != null) this.ModeSwitchError += ModeSwitchError; + if (ModeSwitchSuccess != null) this.ModeSwitchSuccess += ModeSwitchSuccess; + if (SetWorkingStatus != null) this.SetWorkingStatus = SetWorkingStatus; + if (UpdateWorkingStatus != null) this.UpdateWorkingStatus = UpdateWorkingStatus; + + if (this.CurrentMode == null) + { + LogFile.Log("Waiting for phone to connect...", LogType.FileAndConsole); + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + } + else + { + // Make sure this ViewModel has its View loaded before we continue, + // or else loading of Views can get mixed up. + if (SynchronizationContext.Current == null) + StartSwitch(); + else + { + SynchronizationContext.Current.Post((s) => + { + ((SwitchModeViewModel)s).StartSwitch(); + }, this); + } + } + } + } + + private void ModeSwitchProgressWrapper(string Message, string SubMessage) + { + if ((UIContext == null) || (SynchronizationContext.Current == UIContext)) + { + ModeSwitchProgress(Message, SubMessage); + SetWorkingStatus(Message, SubMessage); + } + else + { + UIContext.Post(s => { + ModeSwitchProgress(Message, SubMessage); + SetWorkingStatus(Message, SubMessage); + }, null); + } + } + + private void ModeSwitchErrorWrapper(string Message) + { + IsSwitching = false; + if ((UIContext == null) || (SynchronizationContext.Current == UIContext)) + ModeSwitchError(Message); + else + UIContext.Post(s => { ModeSwitchError(Message); }, null); + } + + private void ModeSwitchSuccessWrapper() + { + IsSwitching = false; + if ((UIContext == null) || (SynchronizationContext.Current == UIContext)) + ModeSwitchSuccess((IDisposable)PhoneNotifier.CurrentModel, (PhoneInterfaces)PhoneNotifier.CurrentInterface); + else + UIContext.Post(s => { ModeSwitchSuccess((IDisposable)PhoneNotifier.CurrentModel, (PhoneInterfaces)PhoneNotifier.CurrentInterface); }, null); + } + + ~SwitchModeViewModel() + { + if (PhoneNotifier != null) + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + } + + internal void StartSwitch() + { + IsSwitching = true; + + // Make switch and set message or navigate to error + switch (CurrentMode) + { + case PhoneInterfaces.Lumia_Normal: + case PhoneInterfaces.Lumia_Label: + string DeviceMode; + + switch (TargetMode) + { + case PhoneInterfaces.Lumia_Normal: + DeviceMode = "Normal"; + IsSwitchingInterface = true; + ModeSwitchProgressWrapper("Rebooting phone to Normal mode...", null); + LogFile.Log("Rebooting phone to Normal mode", LogType.FileAndConsole); + break; + case PhoneInterfaces.Lumia_Bootloader: + DeviceMode = "Normal"; + IsSwitchingInterface = true; + ModeSwitchProgressWrapper("Rebooting phone to Bootloader mode...", null); + LogFile.Log("Rebooting phone to Bootloader mode", LogType.FileAndConsole); + break; + case PhoneInterfaces.Lumia_Flash: + DeviceMode = "Flash"; + IsSwitchingInterface = true; + ModeSwitchProgressWrapper("Rebooting phone to Flash mode...", null); + LogFile.Log("Rebooting phone to Flash mode", LogType.FileAndConsole); + break; + case PhoneInterfaces.Lumia_Label: + DeviceMode = "Test"; + IsSwitchingInterface = true; + ModeSwitchProgressWrapper("Rebooting phone to Label mode...", null); + LogFile.Log("Rebooting phone to Label mode", LogType.FileAndConsole); + break; + case PhoneInterfaces.Lumia_MassStorage: + DeviceMode = "Flash"; + IsSwitchingInterface = true; + ModeSwitchProgressWrapper("First rebooting phone to Flash mode...", null); + LogFile.Log("First rebooting phone to Flash mode (then attempt Mass Storage mode)", LogType.FileAndConsole); + break; + case PhoneInterfaces.Qualcomm_Download: // Only available in new Lumia's + DeviceMode = "Flash"; + IsSwitchingInterface = true; + ModeSwitchProgressWrapper("First rebooting phone to Flash mode...", null); + LogFile.Log("First rebooting phone to Flash mode (then attempt Qualcomm Download mode", LogType.FileAndConsole); + break; + default: + return; + } + + Dictionary Params = new Dictionary(); + Params.Add("DeviceMode", DeviceMode); + Params.Add("ResetMethod", "HwReset"); + try + { + ((NokiaPhoneModel)CurrentModel).ExecuteJsonMethodAsync("SetDeviceMode", Params); + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ModeSwitchErrorWrapper("Failed to switch to Qualcomm Download mode"); + IsSwitchingInterface = false; + } + break; + case PhoneInterfaces.Lumia_Flash: + case PhoneInterfaces.Lumia_Bootloader: + byte[] BootModeFlagCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x58, 0x46, 0x57, 0x00, 0x55, 0x42, 0x46, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 }; // NOKFW UBF + byte[] RebootCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x52 }; // NOKR + byte[] RebootCommandResult; + IsSwitchingInterface = true; + switch (TargetMode) + { + case null: + ((NokiaFlashModel)CurrentModel).Shutdown(); + ModeSwitchProgressWrapper("Shutting down phone...", null); + LogFile.Log("Shutting down phone", LogType.FileAndConsole); + PhoneNotifier.WaitForRemoval().Wait(); + ModeSwitchSuccessWrapper(); + break; + case PhoneInterfaces.Lumia_Normal: + ((NokiaPhoneModel)CurrentModel).ExecuteRawVoidMethod(RebootCommand); + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + ModeSwitchProgressWrapper("Rebooting phone to Normal mode...", null); + LogFile.Log("Rebooting phone to Normal mode", LogType.FileAndConsole); + break; + case PhoneInterfaces.Lumia_Bootloader: + ((NokiaPhoneModel)CurrentModel).ExecuteRawVoidMethod(RebootCommand); + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + ModeSwitchProgressWrapper("Rebooting phone to Bootloader mode...", null); + LogFile.Log("Rebooting phone to Bootloader mode", LogType.FileAndConsole); + break; + case PhoneInterfaces.Lumia_Label: + BootModeFlagCommand[0x0F] = 0x59; + ((NokiaPhoneModel)CurrentModel).ExecuteRawMethod(BootModeFlagCommand); + ((NokiaPhoneModel)CurrentModel).ExecuteRawVoidMethod(RebootCommand); + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + ModeSwitchProgressWrapper("Rebooting phone to Label mode...", null); + LogFile.Log("Rebooting phone to Label mode", LogType.FileAndConsole); + break; + case PhoneInterfaces.Lumia_Flash: // attempt to boot from limited flash to full flash + byte[] RebootToFlashCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x53 }; // NOKS + ((NokiaPhoneModel)CurrentModel).ExecuteRawVoidMethod(RebootToFlashCommand); + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + ModeSwitchProgressWrapper("Rebooting phone to Flash mode...", null); + LogFile.Log("Rebooting phone to Flash mode", LogType.FileAndConsole); + break; + case PhoneInterfaces.Lumia_MassStorage: + SwitchFromFlashToMassStorageMode(); + break; + case PhoneInterfaces.Qualcomm_Download: + byte[] RebootToQualcommDownloadCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x58, 0x43, 0x42, 0x45 }; // NOKXCBE + RebootCommandResult = ((NokiaPhoneModel)CurrentModel).ExecuteRawMethod(RebootToQualcommDownloadCommand); + if ((RebootCommandResult != null) && (RebootCommandResult.Length == 4)) // This means fail: NOKU (unknow command) + { + IsSwitchingInterface = false; + ModeSwitchErrorWrapper("Failed to switch to Qualcomm Download mode"); + } + else + { + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + ModeSwitchProgressWrapper("Rebooting phone to Qualcomm Download mode...", null); + LogFile.Log("Rebooting phone to Qualcomm Download mode", LogType.FileAndConsole); + } + break; + default: + return; + } + break; + case PhoneInterfaces.Lumia_MassStorage: + // TODO: don't know how to switch from Mass Storage to other mode + break; + case PhoneInterfaces.Qualcomm_Download: + // TODO: don't know how to switch from Qualcomm Download mode to other mode + break; + } + } + + private void NewDeviceArrived(ArrivalEventArgs Args) + { + PhoneNotifier.NewDeviceArrived -= NewDeviceArrived; + + CurrentModel = (IDisposable)Args.NewModel; + CurrentMode = Args.NewInterface; + + if ((CurrentMode == PhoneInterfaces.Lumia_Bootloader) && (TargetMode == PhoneInterfaces.Lumia_Flash)) + { + try + { + // Going from BootMgr to FlashApp + // SwitchToFlashAppContext() will only switch context. Phone will not charge. + // ResetPhoneToFlashMode() reboots to real flash app. Phone will charge. Works when in BootMgrApp, not when already in FlashApp. + + ((NokiaFlashModel)CurrentModel).ResetPhoneToFlashMode(); + CurrentMode = PhoneInterfaces.Lumia_Flash; + PhoneNotifier.NotifyArrival(); + } + catch { } + } + + if (CurrentMode == TargetMode) + { + if (TargetMode == PhoneInterfaces.Lumia_Bootloader) + ((NokiaFlashModel)CurrentModel).DisableRebootTimeOut(); + + ModeSwitchSuccessWrapper(); + } + else if (!IsSwitching) + { + StartSwitch(); + } + else if ((CurrentMode == PhoneInterfaces.Lumia_Bootloader) && (TargetMode == PhoneInterfaces.Lumia_Normal)) + { + // Do nothing, because booting to Normal shortly goes into Flash mode too. + // Just wait to arrive in Normal mode; + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + } + else if ((CurrentMode == PhoneInterfaces.Lumia_Flash) && (TargetMode == PhoneInterfaces.Lumia_MassStorage)) + { + SwitchFromFlashToMassStorageMode(Continuation: true); + } + else if ((CurrentMode == PhoneInterfaces.Lumia_Flash) && (TargetMode == PhoneInterfaces.Qualcomm_Download)) + { + byte[] RebootCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x52 }; + byte[] RebootToQualcommDownloadCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x58, 0x43, 0x42, 0x45 }; // NOKXCBE + IsSwitchingInterface = true; + LogFile.Log("Sending command for rebooting to Emergency Download mode"); + byte[] RebootCommandResult = ((NokiaPhoneModel)CurrentModel).ExecuteRawMethod(RebootToQualcommDownloadCommand); + if ((RebootCommandResult != null) && (RebootCommandResult.Length >= 8)) + { + int ResultCode = (RebootCommandResult[6] << 8) + RebootCommandResult[7]; + LogFile.Log("Resultcode: 0x" + ResultCode.ToString("X4")); + } + if ((RebootCommandResult != null) && (RebootCommandResult.Length == 4)) // This means fail: NOKU (unknow command) + { + ModeSwitchErrorWrapper("Failed to switch to Qualcomm Download mode"); + IsSwitchingInterface = false; + } + else + { + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + ModeSwitchProgressWrapper("And now rebooting phone to Qualcomm Download mode...", null); + LogFile.Log("Rebooting phone to Qualcomm Download mode"); + } + } + else + { + switch (TargetMode) + { + case PhoneInterfaces.Lumia_Normal: + ModeSwitchErrorWrapper("Failed to switch to Normal mode"); + break; + case PhoneInterfaces.Lumia_Flash: + ModeSwitchErrorWrapper("Failed to switch to Flash mode"); + break; + case PhoneInterfaces.Lumia_Label: + ModeSwitchErrorWrapper("Failed to switch to Label mode"); + break; + case PhoneInterfaces.Lumia_MassStorage: + ModeSwitchErrorWrapper("Failed to switch to Mass Storage mode"); + break; + } + } + } + + private void SwitchFromFlashToMassStorageMode(bool Continuation = false) + { + string ProgressText; + if (Continuation) + ProgressText = "And now rebooting phone to Mass Storage mode..."; + else + ProgressText = "Rebooting phone to Mass Storage mode..."; + + NokiaFlashModel FlashModel = (NokiaFlashModel)CurrentModel; + if (CurrentMode == PhoneInterfaces.Lumia_Bootloader) + { + try + { + FlashModel.SwitchToFlashAppContext(); + } + catch { } + } + PhoneInfo Info = FlashModel.ReadPhoneInfo(ExtendedInfo: false); + + MassStorageWarning = null; + if (Info.FlashAppProtocolVersionMajor < 2) + MassStorageWarning = "Switching to Mass Storage mode should take about 10 seconds. The phone should be unlocked using an Engineering SBL3 to enable Mass Storage mode. When you unlocked the bootloader, but you did not use an Engineering SBL3, an attempt to boot to Mass Storage mode may result in an unresponsive state. Installing drivers for this interface may also cause to hang the PC. So when this switch is taking too long, you should reboot both the PC and the phone. To reboot the phone, you have to perform a soft-reset. Press and hold the volume-down-button and the power-button at the same time for at least 10 seconds. This will trigger a power-cycle and the phone will reboot. Once fully booted, the phone may show strange behavior, like complaining about mail-accounts, showing old text-messages, inability to load https-websites, etc. This is expected behavior, because the time-settings of the phone are incorrect. Just wait a few seconds for the phone to get a data-connection and have the date/time synced. After that the strange behavior will stop automatically and normal operation is resumed."; + else + { + MassStorageWarning = "When the screen of the phone is black for a while, it could be that the phone is already in Mass Storage Mode, but there is no drive-letter assigned. To resolve this issue, open Device Manager and manually assign a drive-letter to the MainOS partition of your phone, or open a command-prompt and type: diskpart automount enable."; + if (App.IsPnPEventLogMissing) + MassStorageWarning += " It is also possible that the phone is in Mass Storage mode, but the Mass Storage driver on this PC failed. Your PC does not have an eventlog to detect this misbehaviour. But in this case you will see a device with an exclamation mark in Device Manager and then you need to manually reset the phone by pressing and holding the power-button for at least 10 seconds until it vibrates and reboots. After that Windows Phone Internals will revert the changes. After the phone has rebooted to the OS, you can retry to unlock the bootloader."; + } + + bool IsOldLumia = (Info.FlashAppProtocolVersionMajor < 2); + bool IsNewLumia = (Info.FlashAppProtocolVersionMajor >= 2); + bool IsUnlockedNew = false; + if (IsNewLumia) + { + GPT GPT = FlashModel.ReadGPT(); + IsUnlockedNew = ((GPT.GetPartition("IS_UNLOCKED") != null) || (GPT.GetPartition("BACKUP_EFIESP") != null)); + } + bool IsOriginalEngineeringLumia = ((!Info.SecureFfuEnabled || Info.Authenticated || Info.RdcPresent) && !IsUnlockedNew); + + if (IsOldLumia || IsOriginalEngineeringLumia) + { + byte[] BootModeFlagCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x58, 0x46, 0x57, 0x00, 0x55, 0x42, 0x46, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00 }; // NOKFW UBF + byte[] RebootCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x52 }; + byte[] RebootToMassStorageCommand = new byte[] { 0x4E, 0x4F, 0x4B, 0x4D }; // NOKM + IsSwitchingInterface = true; + byte[] RebootCommandResult = ((NokiaPhoneModel)CurrentModel).ExecuteRawMethod(RebootToMassStorageCommand); + if ((RebootCommandResult != null) && (RebootCommandResult.Length == 4)) // This means fail: NOKU (unknow command) + { + BootModeFlagCommand[0x0F] = 0x4D; + byte[] BootFlagResult = ((NokiaPhoneModel)CurrentModel).ExecuteRawMethod(BootModeFlagCommand); + UInt16 ResultCode = BitConverter.ToUInt16(BootFlagResult, 6); + if (ResultCode == 0) + { + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + ((NokiaPhoneModel)CurrentModel).ExecuteRawVoidMethod(RebootCommand); + ModeSwitchProgressWrapper(ProgressText, MassStorageWarning); + LogFile.Log("Rebooting phone to Mass Storage mode"); + } + else + { + ModeSwitchErrorWrapper("Failed to switch to Mass Storage mode"); + IsSwitchingInterface = false; + } + } + else + { + PhoneNotifier.NewDeviceArrived += NewDeviceArrived; + ModeSwitchProgressWrapper(ProgressText, MassStorageWarning); + LogFile.Log("Rebooting phone to Mass Storage mode"); + } + } + else if (IsUnlockedNew) + { + new Thread(async () => + { + LogFile.BeginAction("SwitchToMassStorageMode"); + + try + { + // Implementation of writing a partition with SecureBoot variable to the phone + ModeSwitchProgressWrapper(ProgressText, MassStorageWarning); + LogFile.Log("Preparing phone for Mass Storage Mode", LogType.FileAndConsole); + var assembly = System.Reflection.Assembly.GetExecutingAssembly(); + + // Magic! + // The SBMSM resource is a compressed version of a raw NV-variable-partition. + // In this partition the SecureBoot variable is disabled and an extra variable is added which triggers Mass Storage Mode on next reboot. + // It overwrites the variable in a different NV-partition than where this variable is stored usually. + // This normally leads to endless-loops when the NV-variables are enumerated. + // But the partition contains an extra hack to break out the endless loops. + using (var stream = assembly.GetManifestResourceStream("WPinternals.SBMSM")) + { + using (DecompressedStream dec = new DecompressedStream(stream)) + { + using (System.IO.MemoryStream SB = new System.IO.MemoryStream()) // Must be a seekable stream! + { + dec.CopyTo(SB); + + // We don't need to check for the BACKUP_BS_NV partition here, + // because the SecureBoot flag is disabled here. + // So either the NV was already backupped or already overwritten. + + GPT GPT = FlashModel.ReadGPT(); + Partition Target = GPT.GetPartition("UEFI_BS_NV"); + + // We've been reading the GPT, so we let the phone reset once more to be sure that memory maps are the same + WPinternalsStatus LastStatus = WPinternalsStatus.Undefined; + List Parts = new List(); + FlashPart Part = new FlashPart(); + Part.StartSector = (uint)Target.FirstSector; + Part.Stream = SB; + Parts.Add(Part); + await LumiaV2UnlockBootViewModel.LumiaV2CustomFlash(PhoneNotifier, null, false, false, Parts, DoResetFirst: true, ClearFlashingStatusAtEnd: false, ShowProgress: false, + SetWorkingStatus: (m, s, v, a, st) => + { + if (SetWorkingStatus != null) + { + if ((st == WPinternalsStatus.Scanning) || (st == WPinternalsStatus.WaitingForManualReset)) + SetWorkingStatus(m, s, v, a, st); + else if ((LastStatus == WPinternalsStatus.Scanning) || (LastStatus == WPinternalsStatus.WaitingForManualReset)) + SetWorkingStatus(ProgressText, MassStorageWarning); + LastStatus = st; + } + }, + UpdateWorkingStatus: (m, s, v, st) => + { + if (UpdateWorkingStatus != null) + { + if ((st == WPinternalsStatus.Scanning) || (st == WPinternalsStatus.WaitingForManualReset)) + UpdateWorkingStatus(m, s, v, st); + else if ((LastStatus == WPinternalsStatus.Scanning) || (LastStatus == WPinternalsStatus.WaitingForManualReset)) + SetWorkingStatus(ProgressText, MassStorageWarning); + LastStatus = st; + } + }); + } + } + } + + if (PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_BadMassStorage) + throw new WPinternalsException("Phone is in Mass Storage mode, but the driver on PC failed to start"); + + // Wait for bootloader + if (PhoneNotifier.CurrentInterface != PhoneInterfaces.Lumia_MassStorage) + { + LogFile.Log("Waiting for Mass Storage Mode (1)...", LogType.FileOnly); + await PhoneNotifier.WaitForArrival(); + } + + if (PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_BadMassStorage) + throw new WPinternalsException("Phone is in Mass Storage mode, but the driver on PC failed to start"); + + // Wait for mass storage mode + if (PhoneNotifier.CurrentInterface != PhoneInterfaces.Lumia_MassStorage) + { + LogFile.Log("Waiting for Mass Storage Mode (2)...", LogType.FileOnly); + await PhoneNotifier.WaitForArrival(); + } + + if (PhoneNotifier.CurrentInterface == PhoneInterfaces.Lumia_BadMassStorage) + throw new WPinternalsException("Phone is in Mass Storage mode, but the driver on PC failed to start"); + + MassStorage Storage = null; + if (PhoneNotifier.CurrentModel is MassStorage) + Storage = (MassStorage)PhoneNotifier.CurrentModel; + + if (Storage == null) + ModeSwitchErrorWrapper("Failed to switch to Mass Storage Mode"); + else + ModeSwitchSuccessWrapper(); + } + catch (Exception Ex) + { + LogFile.LogException(Ex); + ModeSwitchErrorWrapper(Ex.Message); + } + + LogFile.EndAction("SwitchToMassStorageMode"); + }).Start(); + } + else + { + ModeSwitchErrorWrapper("Bootloader was not unlocked. First unlock bootloader before you try to switch to Mass Storage Mode."); + } + } + + internal async static Task SwitchTo(PhoneNotifierViewModel Notifier, PhoneInterfaces? TargetMode, string RequestedVolumeForMassStorage = "MainOS") + { + return await SwitchToWithProgress(Notifier, TargetMode, null, RequestedVolumeForMassStorage); + } + + internal async static Task SwitchToWithProgress(PhoneNotifierViewModel Notifier, PhoneInterfaces? TargetMode, ModeSwitchProgressHandler ModeSwitchProgress, string RequestedVolumeForMassStorage = "MainOS") + { + if (Notifier.CurrentInterface == TargetMode) + return Notifier.CurrentModel; + + IDisposable Result = null; + string LocalErrorMessage = null; + + AsyncAutoResetEvent Event = new AsyncAutoResetEvent(false); + + SwitchModeViewModel Switch = new SwitchModeViewModel( + Notifier, + TargetMode, + ModeSwitchProgress, + (string ErrorMessage) => + { + LocalErrorMessage = ErrorMessage; + Event.Set(); + }, + (IDisposable NewModel, PhoneInterfaces NewInterface) => + { + Result = NewModel; + Event.Set(); + } + ); + + await Event.WaitAsync(Timeout.InfiniteTimeSpan); + + if (LocalErrorMessage != null) + throw new WPinternalsException(LocalErrorMessage); + + return Result; + } + + internal async static Task SwitchToWithStatus(PhoneNotifierViewModel Notifier, PhoneInterfaces? TargetMode, SetWorkingStatus SetWorkingStatus = null, UpdateWorkingStatus UpdateWorkingStatus = null, string RequestedVolumeForMassStorage = "MainOS") + { + if (Notifier.CurrentInterface == TargetMode) + return Notifier.CurrentModel; + + IDisposable Result = null; + string LocalErrorMessage = null; + + AsyncAutoResetEvent Event = new AsyncAutoResetEvent(false); + + SwitchModeViewModel Switch = new SwitchModeViewModel( + Notifier, + TargetMode, + null, + (string ErrorMessage) => + { + LocalErrorMessage = ErrorMessage; + Event.Set(); + }, + (IDisposable NewModel, PhoneInterfaces NewInterface) => + { + Result = NewModel; + Event.Set(); + }, SetWorkingStatus, UpdateWorkingStatus + ); + + await Event.WaitAsync(Timeout.InfiniteTimeSpan); + + if (LocalErrorMessage != null) + throw new WPinternalsException(LocalErrorMessage); + + return Result; + } + } +} diff --git a/Views/About.xaml b/Views/About.xaml new file mode 100644 index 0000000..672f276 --- /dev/null +++ b/Views/About.xaml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + www.wpinternals.net + + + + ATF Team + + Smart GSM Team + + + + + + + + + + + + + license + + + + license + + + + license + + + + license + + + + license + + + + + + + diff --git a/Views/About.xaml.cs b/Views/About.xaml.cs new file mode 100644 index 0000000..bd04205 --- /dev/null +++ b/Views/About.xaml.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2018, Rene Lergner - wpinternals.net - @Heathcliff74xda +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; + +namespace WPinternals +{ + /// + /// Interaction logic for About.xaml + /// + public partial class About : UserControl + { + public About() + { + InitializeComponent(); + } + + private void HandleHyperlinkClick(object sender, RoutedEventArgs args) + { + Hyperlink link = args.Source as Hyperlink; + if (link != null) + { + Process.Start(link.NavigateUri.ToString()); + } + } + + private void Document_Loaded(object sender, RoutedEventArgs e) + { + (sender as FlowDocument).AddHandler(Hyperlink.ClickEvent, new RoutedEventHandler(HandleHyperlinkClick)); + } + } +} diff --git a/Views/BackupView.xaml b/Views/BackupView.xaml new file mode 100644 index 0000000..7979ef5 --- /dev/null +++ b/Views/BackupView.xaml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + unlocked + + + + + + + + + + + + + +