aboutsummaryrefslogtreecommitdiff
path: root/src/dtf
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2021-05-11 07:36:37 -0700
committerRob Mensching <rob@firegiant.com>2021-05-11 07:36:37 -0700
commit3f583916719eeef598d10a5d4e14ef14f008243b (patch)
tree3d528e0ddb5c0550954217c97059d2f19cd6152a /src/dtf
parent2e5ab696b8b4666d551b2a0532b95fb7fe6dbe03 (diff)
downloadwix-3f583916719eeef598d10a5d4e14ef14f008243b.tar.gz
wix-3f583916719eeef598d10a5d4e14ef14f008243b.tar.bz2
wix-3f583916719eeef598d10a5d4e14ef14f008243b.zip
Merge Dtf
Diffstat (limited to 'src/dtf')
-rw-r--r--src/dtf/CSharp.Build.props13
-rw-r--r--src/dtf/Cpp.Build.props104
-rw-r--r--src/dtf/Custom.Build.props20
-rw-r--r--src/dtf/Directory.Build.props29
-rw-r--r--src/dtf/Directory.Build.targets56
-rw-r--r--src/dtf/README.md2
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/AssemblyInfo.cs5
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabEngine.cs164
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabException.cs154
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabFileInfo.cs141
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabInfo.cs82
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabPacker.cs653
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabUnpacker.cs566
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/CabWorker.cs337
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/Errors.resourcesbin0 -> 1465 bytes
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/Errors.txt35
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/HandleManager.cs76
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/NativeMethods.cs407
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Cab/WixToolset.Dtf.Compression.Cab.csproj26
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/AssemblyInfo.cs5
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ConcatStream.cs157
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/CrcStream.cs250
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/WixToolset.Dtf.Compression.Zip.csproj21
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipCompressionMethod.cs80
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs478
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipException.cs60
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipFileInfo.cs104
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipFormat.cs697
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipInfo.cs82
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs489
-rw-r--r--src/dtf/WixToolset.Dtf.Compression.Zip/ZipUnpacker.cs336
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs57
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs430
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs664
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs781
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs307
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs69
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs90
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/CargoStream.cs192
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/Compression.cd175
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs371
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs31
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs212
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs117
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs71
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs206
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs22
-rw-r--r--src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj21
-rw-r--r--src/dtf/WixToolset.Dtf.MSBuild/WixToolset.Dtf.MSBuild.csproj44
-rw-r--r--src/dtf/WixToolset.Dtf.MSBuild/WixToolset.Dtf.MSBuild.nuspec18
-rw-r--r--src/dtf/WixToolset.Dtf.MSBuild/build/WixToolset.Dtf.MSBuild.props8
-rw-r--r--src/dtf/WixToolset.Dtf.MSBuild/tools/wix.ca.targets123
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/AssemblyInfo.cs5
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/BitmapResource.cs57
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/FixedFileVersionInfo.cs183
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/GroupIconInfo.cs119
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/GroupIconResource.cs120
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/NativeMethods.cs55
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/Resource.cs225
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/ResourceCollection.cs340
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/ResourceType.cs198
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/VersionEnums.cs86
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/VersionInfo.cs270
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/VersionResource.cs415
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/VersionStringTable.cs231
-rw-r--r--src/dtf/WixToolset.Dtf.Resources/WixToolset.Dtf.Resources.csproj17
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs6
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs60
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs150
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs214
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs501
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs296
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs992
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj21
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs1169
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPath.cs1073
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/PatchPackage.cs259
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/TransformInfo.cs154
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller.Package/WixToolset.Dtf.WindowsInstaller.Package.csproj23
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnCollection.cs333
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnEnums.cs689
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnInfo.cs297
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ComponentInfo.cs276
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ComponentInstallation.cs382
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/CustomActionAttribute.cs55
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/CustomActionProxy.cs321
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs933
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/DatabaseQuery.cs412
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/DatabaseTransform.cs278
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/EmbeddedUIProxy.cs231
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Enums.cs909
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Errors.resourcesbin0 -> 31461 bytes
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Errors.txt404
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Exceptions.cs573
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ExternalUIHandler.cs223
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/FeatureInfo.cs497
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/FeatureInstallation.cs174
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Handle.cs154
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/IEmbeddedUI.cs67
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/InstallCost.cs67
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Installation.cs100
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/InstallationPart.cs82
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Installer.cs890
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerAdvertise.cs270
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs472
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/MediaDisk.cs58
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/NativeMethods.cs309
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/PatchInstallation.cs413
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ProductInstallation.cs801
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs1048
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/RecordStream.cs92
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/RemotableNativeMethods.cs1171
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Session.cs946
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ShortcutTarget.cs104
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/SourceList.cs525
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/SourceMediaList.cs229
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/SummaryInfo.cs612
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/TableCollection.cs192
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/TableInfo.cs264
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Transaction.cs201
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/ValidationErrorInfo.cs46
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/View.cs625
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/WindowsInstaller.cd943
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/WixToolset.Dtf.WindowsInstaller.csproj30
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs469
-rw-r--r--src/dtf/WixToolset.Dtf.sln281
-rw-r--r--src/dtf/WixToolsetTests.Dtf.Compression.Cab/CabTest.cs1165
-rw-r--r--src/dtf/WixToolsetTests.Dtf.Compression.Cab/WixToolsetTests.Dtf.Compression.Cab.csproj32
-rw-r--r--src/dtf/WixToolsetTests.Dtf.Compression.Zip/WixToolsetTests.Dtf.Compression.Zip.csproj30
-rw-r--r--src/dtf/WixToolsetTests.Dtf.Compression.Zip/ZipTest.cs518
-rw-r--r--src/dtf/WixToolsetTests.Dtf.Compression/CompressionTestUtil.cs649
-rw-r--r--src/dtf/WixToolsetTests.Dtf.Compression/MisbehavingStreamContext.cs202
-rw-r--r--src/dtf/WixToolsetTests.Dtf.Compression/OptionStreamContext.cs42
-rw-r--r--src/dtf/WixToolsetTests.Dtf.Compression/WixToolsetTests.Dtf.Compression.csproj31
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller.CustomActions/CustomActionTest.cs206
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller.CustomActions/WixToolsetTests.Dtf.WindowsInstaller.CustomActions.csproj28
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller.Linq/LinqTest.cs509
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller.Linq/WixToolsetTests.Dtf.WindowsInstaller.Linq.csproj31
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller/EmbeddedExternalUI.cs173
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller/Schema.cs238
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerTest.cs409
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerTransactions.cs161
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerUtils.cs174
-rw-r--r--src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WixToolsetTests.Dtf.WindowsInstaller.csproj34
-rw-r--r--src/dtf/appveyor.cmd28
-rw-r--r--src/dtf/appveyor.yml42
-rw-r--r--src/dtf/nuget.config7
147 files changed, 40034 insertions, 0 deletions
diff --git a/src/dtf/CSharp.Build.props b/src/dtf/CSharp.Build.props
new file mode 100644
index 00000000..81d24ad1
--- /dev/null
+++ b/src/dtf/CSharp.Build.props
@@ -0,0 +1,13 @@
1<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
2<!--
3 Do NOT modify this file. Update the canonical version in Home\repo-template\src\CSharp.Build.props
4 then update all of the repos.
5-->
6<Project>
7 <PropertyGroup>
8 <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
9 <SignAssembly>true</SignAssembly>
10 <AssemblyOriginatorKeyFile>$([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)wix.snk))</AssemblyOriginatorKeyFile>
11 <NBGV_EmitThisAssemblyClass>false</NBGV_EmitThisAssemblyClass>
12 </PropertyGroup>
13</Project>
diff --git a/src/dtf/Cpp.Build.props b/src/dtf/Cpp.Build.props
new file mode 100644
index 00000000..44a042c7
--- /dev/null
+++ b/src/dtf/Cpp.Build.props
@@ -0,0 +1,104 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project>
5 <PropertyGroup>
6 <Platform Condition=" '$(Platform)' == '' OR '$(Platform)' == 'AnyCPU' ">Win32</Platform>
7 <IntDir>$(BaseIntermediateOutputPath)$(Configuration)\$(Platform)\</IntDir>
8 <OutDir>$(OutputPath)$(Platform)\</OutDir>
9 </PropertyGroup>
10
11 <PropertyGroup Condition="'$(WindowsTargetPlatformVersion)'=='' AND '$(VisualStudioVersion)'>='15.0'">
12 <WindowsTargetPlatformVersion>$([Microsoft.Build.Utilities.ToolLocationHelper]::GetLatestSDKTargetPlatformVersion('Windows', '10.0'))</WindowsTargetPlatformVersion>
13 </PropertyGroup>
14
15 <ItemDefinitionGroup>
16 <ClCompile>
17 <DisableSpecificWarnings>$(DisableSpecificCompilerWarnings)</DisableSpecificWarnings>
18 <WarningLevel>Level4</WarningLevel>
19 <AdditionalIncludeDirectories>$(ProjectDir)inc;$(MSBuildProjectDirectory);$(IntDir);$(SqlCESdkIncludePath);$(ProjectAdditionalIncludeDirectories);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
20 <PreprocessorDefinitions>WIN32;_WINDOWS;_WIN32_MSI=500;_WIN32_WINNT=0x0501;$(ArmPreprocessorDefinitions);$(UnicodePreprocessorDefinitions);_CRT_STDIO_LEGACY_WIDE_SPECIFIERS;_WINSOCK_DEPRECATED_NO_WARNINGS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
21 <PrecompiledHeader>Use</PrecompiledHeader>
22 <PrecompiledHeaderFile>precomp.h</PrecompiledHeaderFile>
23 <CallingConvention Condition="'$(Platform)'=='Win32'">StdCall</CallingConvention>
24 <TreatWarningAsError>true</TreatWarningAsError>
25 <ExceptionHandling>false</ExceptionHandling>
26 <AdditionalOptions>-YlprecompDefine</AdditionalOptions>
27 <AdditionalOptions Condition=" $(PlatformToolset.StartsWith('v14')) ">/Zc:threadSafeInit- %(AdditionalOptions)</AdditionalOptions>
28 <MultiProcessorCompilation Condition=" $(NUMBER_OF_PROCESSORS) &gt; 4 ">true</MultiProcessorCompilation>
29 </ClCompile>
30 <ResourceCompile>
31 <PreprocessorDefinitions>$(ArmPreprocessorDefinitions);%(PreprocessorDefinitions)</PreprocessorDefinitions>
32 <AdditionalIncludeDirectories>$(ProjectAdditionalResourceIncludeDirectories);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
33 </ResourceCompile>
34 <Lib>
35 <AdditionalLibraryDirectories>$(OutDir);$(AdditionalMultiTargetLibraryPath);$(ProjectAdditionalLibraryDirectories);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
36 </Lib>
37 <Link>
38 <SubSystem>$(ProjectSubSystem)</SubSystem>
39 <ModuleDefinitionFile>$(ProjectModuleDefinitionFile)</ModuleDefinitionFile>
40 <NoEntryPoint>$(ResourceOnlyDll)</NoEntryPoint>
41 <GenerateDebugInformation>true</GenerateDebugInformation>
42 <AdditionalDependencies>$(ProjectAdditionalLinkLibraries);advapi32.lib;comdlg32.lib;user32.lib;oleaut32.lib;gdi32.lib;shell32.lib;ole32.lib;version.lib;%(AdditionalDependencies)</AdditionalDependencies>
43 <AdditionalLibraryDirectories>$(OutDir);$(AdditionalMultiTargetLibraryPath);$(ArmLibraryDirectories);$(ProjectAdditionalLinkLibraryDirectories);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
44 <AdditionalOptions Condition=" $(PlatformToolset.StartsWith('v14')) ">/IGNORE:4099 %(AdditionalOptions)</AdditionalOptions>
45 </Link>
46 </ItemDefinitionGroup>
47
48 <ItemDefinitionGroup Condition=" '$(Platform)'=='Win32' and '$(PlatformToolset)'!='v100'">
49 <ClCompile>
50 <EnableEnhancedInstructionSet>NoExtensions</EnableEnhancedInstructionSet>
51 </ClCompile>
52 </ItemDefinitionGroup>
53 <ItemDefinitionGroup Condition=" '$(Platform)'=='arm' ">
54 <ClCompile>
55 <CallingConvention>CDecl</CallingConvention>
56 </ClCompile>
57 </ItemDefinitionGroup>
58 <ItemDefinitionGroup Condition=" '$(ConfigurationType)'=='StaticLibrary' ">
59 <ClCompile>
60 <DebugInformationFormat>OldStyle</DebugInformationFormat>
61 <OmitDefaultLibName>true</OmitDefaultLibName>
62 <IgnoreAllDefaultLibraries>true</IgnoreAllDefaultLibraries>
63 </ClCompile>
64 </ItemDefinitionGroup>
65 <ItemDefinitionGroup Condition=" '$(Configuration)'=='Debug' ">
66 <ClCompile>
67 <Optimization>Disabled</Optimization>
68 <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
69 <PreprocessorDefinitions>_DEBUG;DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
70 <RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
71 </ClCompile>
72 </ItemDefinitionGroup>
73 <ItemDefinitionGroup Condition=" '$(Configuration)'=='Debug' and '$(CLRSupport)'=='true' ">
74 <ClCompile>
75 <BasicRuntimeChecks></BasicRuntimeChecks>
76 <RuntimeLibrary>MultiThreadedDebugDll</RuntimeLibrary>
77 </ClCompile>
78 </ItemDefinitionGroup>
79 <ItemDefinitionGroup Condition=" '$(Configuration)'=='Release' ">
80 <ClCompile>
81 <Optimization>MinSpace</Optimization>
82 <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
83 <FunctionLevelLinking>true</FunctionLevelLinking>
84 <IntrinsicFunctions>true</IntrinsicFunctions>
85 <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
86 </ClCompile>
87 <Link>
88 <EnableCOMDATFolding>true</EnableCOMDATFolding>
89 <OptimizeReferences>true</OptimizeReferences>
90 </Link>
91 </ItemDefinitionGroup>
92 <ItemDefinitionGroup Condition=" '$(Configuration)'=='Release' and '$(CLRSupport)'=='true' ">
93 <ClCompile>
94 <BasicRuntimeChecks></BasicRuntimeChecks>
95 <RuntimeLibrary>MultiThreadedDll</RuntimeLibrary>
96 </ClCompile>
97 </ItemDefinitionGroup>
98 <ItemDefinitionGroup Condition=" '$(CLRSupport)'=='true' ">
99 <Link>
100 <KeyFile>$(LinkKeyFile)</KeyFile>
101 <DelaySign>$(LinkDelaySign)</DelaySign>
102 </Link>
103 </ItemDefinitionGroup>
104</Project>
diff --git a/src/dtf/Custom.Build.props b/src/dtf/Custom.Build.props
new file mode 100644
index 00000000..50429f77
--- /dev/null
+++ b/src/dtf/Custom.Build.props
@@ -0,0 +1,20 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project>
5 <PropertyGroup>
6 <DebugType>embedded</DebugType>
7 <PublishRepositoryUrl>true</PublishRepositoryUrl>
8 </PropertyGroup>
9
10 <PropertyGroup Condition=" '$(UsingMicrosoftNETSdk)'!='true' and '$(Configuration)' == 'Debug' ">
11 <DebugSymbols>true</DebugSymbols>
12 <Optimize>false</Optimize>
13 <DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
14 </PropertyGroup>
15 <PropertyGroup Condition=" '$(UsingMicrosoftNETSdk)'!='true' and '$(Configuration)' == 'Release' ">
16 <DebugSymbols>false</DebugSymbols>
17 <Optimize>true</Optimize>
18 <DefineConstants>$(DefineConstants);TRACE</DefineConstants>
19 </PropertyGroup>
20</Project>
diff --git a/src/dtf/Directory.Build.props b/src/dtf/Directory.Build.props
new file mode 100644
index 00000000..f83cc154
--- /dev/null
+++ b/src/dtf/Directory.Build.props
@@ -0,0 +1,29 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3<!--
4 Do NOT modify this file. Update the canonical version in Home\repo-template\src\Directory.Build.props
5 then update all of the repos.
6-->
7<Project>
8 <PropertyGroup>
9 <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
10 <EnableSourceLink Condition=" '$(NCrunch)' == '1' ">false</EnableSourceLink>
11 <MSBuildWarningsAsMessages>MSB3246</MSBuildWarningsAsMessages>
12
13 <ProjectName Condition=" '$(ProjectName)' == '' ">$(MSBuildProjectName)</ProjectName>
14 <BaseOutputPath>$([System.IO.Path]::GetFullPath($(MSBuildThisFileDirectory)..\build\))</BaseOutputPath>
15 <BaseIntermediateOutputPath>$(BaseOutputPath)obj\$(ProjectName)\</BaseIntermediateOutputPath>
16 <OutputPath>$(BaseOutputPath)$(Configuration)\</OutputPath>
17
18 <Authors>WiX Toolset Team</Authors>
19 <Company>WiX Toolset</Company>
20 <Copyright>Copyright (c) .NET Foundation and contributors. All rights reserved.</Copyright>
21 <PackageLicenseExpression>MS-RL</PackageLicenseExpression>
22 <Product>WiX Toolset</Product>
23 </PropertyGroup>
24
25 <Import Project="CSharp.Build.props" Condition=" '$(MSBuildProjectExtension)'=='.csproj' and Exists('CSharp.Build.props') " />
26 <Import Project="Cpp.Build.props" Condition=" Exists('Cpp.Build.props') And '$(MSBuildProjectExtension)'=='.vcxproj' " />
27 <Import Project="Wix.Build.props" Condition=" Exists('Wix.Build.props') And '$(MSBuildProjectExtension)'=='.wixproj' " />
28 <Import Project="Custom.Build.props" Condition=" Exists('Custom.Build.props') " />
29</Project>
diff --git a/src/dtf/Directory.Build.targets b/src/dtf/Directory.Build.targets
new file mode 100644
index 00000000..cb988931
--- /dev/null
+++ b/src/dtf/Directory.Build.targets
@@ -0,0 +1,56 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3<!--
4 Do NOT modify this file. Update the canonical version in Home\repo-template\src\Directory.Build.targets
5 then update all of the repos.
6-->
7<!--
8 Replace PackageReferences with ProjectReferences when the projects can be found in .sln.
9 See the original here: https://github.com/dotnet/sdk/issues/1151#issuecomment-385133284
10-->
11<Project>
12 <PropertyGroup>
13 <CreateDocumentation Condition=" '$(CreateDocumentationFile)'!='true' ">false</CreateDocumentation>
14 <DocumentationFile Condition=" '$(CreateDocumentationFile)'=='true' ">$(OutputPath)\$(AssemblyName).xml</DocumentationFile>
15 </PropertyGroup>
16
17 <PropertyGroup>
18 <ReplacePackageReferences>true</ReplacePackageReferences>
19 <TheSolutionPath Condition=" '$(NCrunch)'=='' ">$(SolutionPath)</TheSolutionPath>
20 <TheSolutionPath Condition=" '$(NCrunch)'=='1' ">$(NCrunchOriginalSolutionPath)</TheSolutionPath>
21 </PropertyGroup>
22
23 <Choose>
24 <When Condition="$(ReplacePackageReferences) AND '$(TheSolutionPath)' != '' AND '$(TheSolutionPath)' != '*undefined*' AND Exists('$(TheSolutionPath)')">
25
26 <PropertyGroup>
27 <SolutionFileContent>$([System.IO.File]::ReadAllText($(TheSolutionPath)))</SolutionFileContent>
28 <SmartSolutionDir>$([System.IO.Path]::GetDirectoryName( $(TheSolutionPath) ))</SmartSolutionDir>
29 <RegexPattern>(?&lt;="[PackageName]", ")(.*)(?=", ")</RegexPattern>
30 </PropertyGroup>
31
32 <ItemGroup>
33 <!-- Keep the identity of the PackageReference -->
34 <SmartPackageReference Include="@(PackageReference)">
35 <PackageName>%(Identity)</PackageName>
36 <InSolution>$(SolutionFileContent.Contains('\%(Identity).csproj'))</InSolution>
37 </SmartPackageReference>
38
39 <!-- Filter them by mapping them to another ItemGroup using the WithMetadataValue item function -->
40 <PackageInSolution Include="@(SmartPackageReference->WithMetadataValue('InSolution', True))">
41 <Pattern>$(RegexPattern.Replace('[PackageName]','%(PackageName)') )</Pattern>
42 <SmartPath>$([System.Text.RegularExpressions.Regex]::Match('$(SolutionFileContent)', '%(Pattern)'))</SmartPath>
43 </PackageInSolution>
44
45 <ProjectReference Include="@(PackageInSolution->'$(SmartSolutionDir)\%(SmartPath)' )"/>
46
47 <!-- Remove the package references that are now referenced as projects -->
48 <PackageReference Remove="@(PackageInSolution->'%(PackageName)' )"/>
49 </ItemGroup>
50
51 </When>
52 </Choose>
53
54 <Import Project="Wix.Build.targets" Condition=" Exists('Wix.Build.targets') And '$(MSBuildProjectExtension)'=='.wixproj' " />
55 <Import Project="Custom.Build.targets" Condition=" Exists('Custom.Build.targets') " />
56</Project>
diff --git a/src/dtf/README.md b/src/dtf/README.md
new file mode 100644
index 00000000..80f3b0e5
--- /dev/null
+++ b/src/dtf/README.md
@@ -0,0 +1,2 @@
1# Dtf
2WixToolset.Dtf - managed libraries for the Windows Installer
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/AssemblyInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/AssemblyInfo.cs
new file mode 100644
index 00000000..aea5bf2e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/AssemblyInfo.cs
@@ -0,0 +1,5 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3using System.Diagnostics.CodeAnalysis;
4
5[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "WixToolset.Dtf.Compression.Cab")]
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabEngine.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabEngine.cs
new file mode 100644
index 00000000..ab135fd8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabEngine.cs
@@ -0,0 +1,164 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5 using System;
6 using System.IO;
7 using System.Collections.Generic;
8
9 /// <summary>
10 /// Engine capable of packing and unpacking archives in the cabinet format.
11 /// </summary>
12 public class CabEngine : CompressionEngine
13 {
14 private CabPacker packer;
15 private CabUnpacker unpacker;
16
17 /// <summary>
18 /// Creates a new instance of the cabinet engine.
19 /// </summary>
20 public CabEngine()
21 : base()
22 {
23 }
24
25 /// <summary>
26 /// Disposes of resources allocated by the cabinet engine.
27 /// </summary>
28 /// <param name="disposing">If true, the method has been called directly
29 /// or indirectly by a user's code, so managed and unmanaged resources
30 /// will be disposed. If false, the method has been called by the runtime
31 /// from inside the finalizer, and only unmanaged resources will be
32 /// disposed.</param>
33 protected override void Dispose(bool disposing)
34 {
35 if (disposing)
36 {
37 if (packer != null)
38 {
39 packer.Dispose();
40 packer = null;
41 }
42 if (unpacker != null)
43 {
44 unpacker.Dispose();
45 unpacker = null;
46 }
47 }
48
49 base.Dispose(disposing);
50 }
51
52 private CabPacker Packer
53 {
54 get
55 {
56 if (this.packer == null)
57 {
58 this.packer = new CabPacker(this);
59 }
60
61 return this.packer;
62 }
63 }
64
65 private CabUnpacker Unpacker
66 {
67 get
68 {
69 if (this.unpacker == null)
70 {
71 this.unpacker = new CabUnpacker(this);
72 }
73
74 return this.unpacker;
75 }
76 }
77
78 /// <summary>
79 /// Creates a cabinet or chain of cabinets.
80 /// </summary>
81 /// <param name="streamContext">A context interface to handle opening
82 /// and closing of cabinet and file streams.</param>
83 /// <param name="files">The paths of the files in the archive (not
84 /// external file paths).</param>
85 /// <param name="maxArchiveSize">The maximum number of bytes for one
86 /// cabinet before the contents are chained to the next cabinet, or zero
87 /// for unlimited cabinet size.</param>
88 /// <exception cref="ArchiveException">The cabinet could not be
89 /// created.</exception>
90 /// <remarks>
91 /// The stream context implementation may provide a mapping from the
92 /// file paths within the cabinet to the external file paths.
93 /// <para>Smaller folder sizes can make it more efficient to extract
94 /// individual files out of large cabinet packages.</para>
95 /// </remarks>
96 public override void Pack(
97 IPackStreamContext streamContext,
98 IEnumerable<string> files,
99 long maxArchiveSize)
100 {
101 this.Packer.CompressionLevel = this.CompressionLevel;
102 this.Packer.UseTempFiles = this.UseTempFiles;
103 this.Packer.Pack(streamContext, files, maxArchiveSize);
104 }
105
106 /// <summary>
107 /// Checks whether a Stream begins with a header that indicates
108 /// it is a valid cabinet file.
109 /// </summary>
110 /// <param name="stream">Stream for reading the cabinet file.</param>
111 /// <returns>True if the stream is a valid cabinet file
112 /// (with no offset); false otherwise.</returns>
113 public override bool IsArchive(Stream stream)
114 {
115 return this.Unpacker.IsArchive(stream);
116 }
117
118 /// <summary>
119 /// Gets information about files in a cabinet or cabinet chain.
120 /// </summary>
121 /// <param name="streamContext">A context interface to handle opening
122 /// and closing of cabinet and file streams.</param>
123 /// <param name="fileFilter">A predicate that can determine
124 /// which files to process, optional.</param>
125 /// <returns>Information about files in the cabinet stream.</returns>
126 /// <exception cref="ArchiveException">The cabinet provided
127 /// by the stream context is not valid.</exception>
128 /// <remarks>
129 /// The <paramref name="fileFilter"/> predicate takes an internal file
130 /// path and returns true to include the file or false to exclude it.
131 /// </remarks>
132 public override IList<ArchiveFileInfo> GetFileInfo(
133 IUnpackStreamContext streamContext,
134 Predicate<string> fileFilter)
135 {
136 return this.Unpacker.GetFileInfo(streamContext, fileFilter);
137 }
138
139 /// <summary>
140 /// Extracts files from a cabinet or cabinet chain.
141 /// </summary>
142 /// <param name="streamContext">A context interface to handle opening
143 /// and closing of cabinet and file streams.</param>
144 /// <param name="fileFilter">An optional predicate that can determine
145 /// which files to process.</param>
146 /// <exception cref="ArchiveException">The cabinet provided
147 /// by the stream context is not valid.</exception>
148 /// <remarks>
149 /// The <paramref name="fileFilter"/> predicate takes an internal file
150 /// path and returns true to include the file or false to exclude it.
151 /// </remarks>
152 public override void Unpack(
153 IUnpackStreamContext streamContext,
154 Predicate<string> fileFilter)
155 {
156 this.Unpacker.Unpack(streamContext, fileFilter);
157 }
158
159 internal void ReportProgress(ArchiveProgressEventArgs e)
160 {
161 base.OnProgress(e);
162 }
163 }
164}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabException.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabException.cs
new file mode 100644
index 00000000..e03f9f3a
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabException.cs
@@ -0,0 +1,154 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5 using System;
6 using System.IO;
7 using System.Resources;
8 using System.Globalization;
9 using System.Runtime.Serialization;
10
11 /// <summary>
12 /// Exception class for cabinet operations.
13 /// </summary>
14 [Serializable]
15 public class CabException : ArchiveException
16 {
17 private static ResourceManager errorResources;
18 private int error;
19 private int errorCode;
20
21 /// <summary>
22 /// Creates a new CabException with a specified error message and a reference to the
23 /// inner exception that is the cause of this exception.
24 /// </summary>
25 /// <param name="message">The message that describes the error.</param>
26 /// <param name="innerException">The exception that is the cause of the current exception. If the
27 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
28 /// is raised in a catch block that handles the inner exception.</param>
29 public CabException(string message, Exception innerException)
30 : this(0, 0, message, innerException) { }
31
32 /// <summary>
33 /// Creates a new CabException with a specified error message.
34 /// </summary>
35 /// <param name="message">The message that describes the error.</param>
36 public CabException(string message)
37 : this(0, 0, message, null) { }
38
39 /// <summary>
40 /// Creates a new CabException.
41 /// </summary>
42 public CabException()
43 : this(0, 0, null, null) { }
44
45 internal CabException(int error, int errorCode, string message, Exception innerException)
46 : base(message, innerException)
47 {
48 this.error = error;
49 this.errorCode = errorCode;
50 }
51
52 internal CabException(int error, int errorCode, string message)
53 : this(error, errorCode, message, null) { }
54
55 /// <summary>
56 /// Initializes a new instance of the CabException class with serialized data.
57 /// </summary>
58 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
59 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
60 protected CabException(SerializationInfo info, StreamingContext context) : base(info, context)
61 {
62 if (info == null)
63 {
64 throw new ArgumentNullException("info");
65 }
66
67 this.error = info.GetInt32("cabError");
68 this.errorCode = info.GetInt32("cabErrorCode");
69 }
70
71 /// <summary>
72 /// Gets the FCI or FDI cabinet engine error number.
73 /// </summary>
74 /// <value>A cabinet engine error number, or 0 if the exception was
75 /// not related to a cabinet engine error number.</value>
76 public int Error
77 {
78 get
79 {
80 return this.error;
81 }
82 }
83
84 /// <summary>
85 /// Gets the Win32 error code.
86 /// </summary>
87 /// <value>A Win32 error code, or 0 if the exception was
88 /// not related to a Win32 error.</value>
89 public int ErrorCode
90 {
91 get
92 {
93 return this.errorCode;
94 }
95 }
96
97 internal static ResourceManager ErrorResources
98 {
99 get
100 {
101 if (errorResources == null)
102 {
103 errorResources = new ResourceManager(
104 typeof(CabException).Namespace + ".Errors",
105 typeof(CabException).Assembly);
106 }
107 return errorResources;
108 }
109 }
110
111 /// <summary>
112 /// Sets the SerializationInfo with information about the exception.
113 /// </summary>
114 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
115 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
116 public override void GetObjectData(SerializationInfo info, StreamingContext context)
117 {
118 if (info == null)
119 {
120 throw new ArgumentNullException("info");
121 }
122
123 info.AddValue("cabError", this.error);
124 info.AddValue("cabErrorCode", this.errorCode);
125 base.GetObjectData(info, context);
126 }
127
128 internal static string GetErrorMessage(int error, int errorCode, bool extracting)
129 {
130 const int FCI_ERROR_RESOURCE_OFFSET = 1000;
131 const int FDI_ERROR_RESOURCE_OFFSET = 2000;
132 int resourceOffset = (extracting ? FDI_ERROR_RESOURCE_OFFSET : FCI_ERROR_RESOURCE_OFFSET);
133
134 string msg = CabException.ErrorResources.GetString(
135 (resourceOffset + error).ToString(CultureInfo.InvariantCulture.NumberFormat),
136 CultureInfo.CurrentCulture);
137
138 if (msg == null)
139 {
140 msg = CabException.ErrorResources.GetString(
141 resourceOffset.ToString(CultureInfo.InvariantCulture.NumberFormat),
142 CultureInfo.CurrentCulture);
143 }
144
145 if (errorCode != 0)
146 {
147 const string GENERIC_ERROR_RESOURCE = "1";
148 string msg2 = CabException.ErrorResources.GetString(GENERIC_ERROR_RESOURCE, CultureInfo.CurrentCulture);
149 msg = String.Format(CultureInfo.InvariantCulture, "{0} " + msg2, msg, errorCode);
150 }
151 return msg;
152 }
153 }
154}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabFileInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabFileInfo.cs
new file mode 100644
index 00000000..ad4a813c
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabFileInfo.cs
@@ -0,0 +1,141 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5 using System;
6 using System.IO;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Object representing a compressed file within a cabinet package; provides operations for getting
11 /// the file properties and extracting the file.
12 /// </summary>
13 [Serializable]
14 public class CabFileInfo : ArchiveFileInfo
15 {
16 private int cabFolder;
17
18 /// <summary>
19 /// Creates a new CabinetFileInfo object representing a file within a cabinet in a specified path.
20 /// </summary>
21 /// <param name="cabinetInfo">An object representing the cabinet containing the file.</param>
22 /// <param name="filePath">The path to the file within the cabinet. Usually, this is a simple file
23 /// name, but if the cabinet contains a directory structure this may include the directory.</param>
24 public CabFileInfo(CabInfo cabinetInfo, string filePath)
25 : base(cabinetInfo, filePath)
26 {
27 if (cabinetInfo == null)
28 {
29 throw new ArgumentNullException("cabinetInfo");
30 }
31
32 this.cabFolder = -1;
33 }
34
35 /// <summary>
36 /// Creates a new CabinetFileInfo object with all parameters specified,
37 /// used internally when reading the metadata out of a cab.
38 /// </summary>
39 /// <param name="filePath">The internal path and name of the file in the cab.</param>
40 /// <param name="cabFolder">The folder number containing the file.</param>
41 /// <param name="cabNumber">The cabinet number where the file starts.</param>
42 /// <param name="attributes">The stored attributes of the file.</param>
43 /// <param name="lastWriteTime">The stored last write time of the file.</param>
44 /// <param name="length">The uncompressed size of the file.</param>
45 internal CabFileInfo(
46 string filePath,
47 int cabFolder,
48 int cabNumber,
49 FileAttributes attributes,
50 DateTime lastWriteTime,
51 long length)
52 : base(filePath, cabNumber, attributes, lastWriteTime, length)
53 {
54 this.cabFolder = cabFolder;
55 }
56
57 /// <summary>
58 /// Initializes a new instance of the CabinetFileInfo class with serialized data.
59 /// </summary>
60 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
61 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
62 protected CabFileInfo(SerializationInfo info, StreamingContext context)
63 : base(info, context)
64 {
65 this.cabFolder = info.GetInt32("cabFolder");
66 }
67
68 /// <summary>
69 /// Sets the SerializationInfo with information about the archive.
70 /// </summary>
71 /// <param name="info">The SerializationInfo that holds the serialized object data.</param>
72 /// <param name="context">The StreamingContext that contains contextual information
73 /// about the source or destination.</param>
74 public override void GetObjectData(SerializationInfo info, StreamingContext context)
75 {
76 base.GetObjectData(info, context);
77 info.AddValue("cabFolder", this.cabFolder);
78 }
79
80 /// <summary>
81 /// Gets or sets the cabinet that contains this file.
82 /// </summary>
83 /// <value>
84 /// The CabinetInfo instance that retrieved this file information -- this
85 /// may be null if the CabinetFileInfo object was returned directly from a
86 /// stream.
87 /// </value>
88 public CabInfo Cabinet
89 {
90 get
91 {
92 return (CabInfo) this.Archive;
93 }
94 }
95
96 /// <summary>
97 /// Gets the full path of the cabinet that contains this file.
98 /// </summary>
99 /// <value>The full path of the cabinet that contains this file.</value>
100 public string CabinetName
101 {
102 get
103 {
104 return this.ArchiveName;
105 }
106 }
107
108 /// <summary>
109 /// Gets the number of the folder containing this file.
110 /// </summary>
111 /// <value>The number of the cabinet folder containing this file.</value>
112 /// <remarks>A single folder or the first folder of a cabinet
113 /// (or chain of cabinets) is numbered 0.</remarks>
114 public int CabinetFolderNumber
115 {
116 get
117 {
118 if (this.cabFolder < 0)
119 {
120 this.Refresh();
121 }
122 return this.cabFolder;
123 }
124 }
125
126 /// <summary>
127 /// Refreshes the information in this object with new data retrieved
128 /// from an archive.
129 /// </summary>
130 /// <param name="newFileInfo">Fresh instance for the same file just
131 /// read from the archive.</param>
132 /// <remarks>
133 /// This implementation refreshes the <see cref="CabinetFolderNumber"/>.
134 /// </remarks>
135 protected override void Refresh(ArchiveFileInfo newFileInfo)
136 {
137 base.Refresh(newFileInfo);
138 this.cabFolder = ((CabFileInfo) newFileInfo).cabFolder;
139 }
140 }
141}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabInfo.cs
new file mode 100644
index 00000000..969dcbef
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabInfo.cs
@@ -0,0 +1,82 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Object representing a cabinet file on disk; provides access to
11 /// file-based operations on the cabinet file.
12 /// </summary>
13 /// <remarks>
14 /// Generally, the methods on this class are much easier to use than the
15 /// stream-based interfaces provided by the <see cref="CabEngine"/> class.
16 /// </remarks>
17 [Serializable]
18 public class CabInfo : ArchiveInfo
19 {
20 /// <summary>
21 /// Creates a new CabinetInfo object representing a cabinet file in a specified path.
22 /// </summary>
23 /// <param name="path">The path to the cabinet file. When creating a cabinet file, this file does not
24 /// necessarily exist yet.</param>
25 public CabInfo(string path)
26 : base(path)
27 {
28 }
29
30 /// <summary>
31 /// Initializes a new instance of the CabinetInfo class with serialized data.
32 /// </summary>
33 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
34 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
35 protected CabInfo(SerializationInfo info, StreamingContext context)
36 : base(info, context)
37 {
38 }
39
40 /// <summary>
41 /// Creates a compression engine that does the low-level work for
42 /// this object.
43 /// </summary>
44 /// <returns>A new <see cref="CabEngine"/> instance.</returns>
45 /// <remarks>
46 /// Each instance will be <see cref="CompressionEngine.Dispose()"/>d
47 /// immediately after use.
48 /// </remarks>
49 protected override CompressionEngine CreateCompressionEngine()
50 {
51 return new CabEngine();
52 }
53
54 /// <summary>
55 /// Gets information about the files contained in the archive.
56 /// </summary>
57 /// <returns>A list of <see cref="CabFileInfo"/> objects, each
58 /// containing information about a file in the archive.</returns>
59 public new IList<CabFileInfo> GetFiles()
60 {
61 IList<ArchiveFileInfo> files = base.GetFiles();
62 List<CabFileInfo> cabFiles = new List<CabFileInfo>(files.Count);
63 foreach (CabFileInfo cabFile in files) cabFiles.Add(cabFile);
64 return cabFiles.AsReadOnly();
65 }
66
67 /// <summary>
68 /// Gets information about the certain files contained in the archive file.
69 /// </summary>
70 /// <param name="searchPattern">The search string, such as
71 /// &quot;*.txt&quot;.</param>
72 /// <returns>A list of <see cref="CabFileInfo"/> objects, each containing
73 /// information about a file in the archive.</returns>
74 public new IList<CabFileInfo> GetFiles(string searchPattern)
75 {
76 IList<ArchiveFileInfo> files = base.GetFiles(searchPattern);
77 List<CabFileInfo> cabFiles = new List<CabFileInfo>(files.Count);
78 foreach (CabFileInfo cabFile in files) cabFiles.Add(cabFile);
79 return cabFiles.AsReadOnly();
80 }
81 }
82}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabPacker.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabPacker.cs
new file mode 100644
index 00000000..ec6e3bda
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabPacker.cs
@@ -0,0 +1,653 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections.Generic;
9 using System.Globalization;
10 using System.Runtime.InteropServices;
11 using System.Diagnostics.CodeAnalysis;
12
13 internal class CabPacker : CabWorker
14 {
15 private const string TempStreamName = "%%TEMP%%";
16
17 private NativeMethods.FCI.Handle fciHandle;
18
19 // These delegates need to be saved as member variables
20 // so that they don't get GC'd.
21 private NativeMethods.FCI.PFNALLOC fciAllocMemHandler;
22 private NativeMethods.FCI.PFNFREE fciFreeMemHandler;
23 private NativeMethods.FCI.PFNOPEN fciOpenStreamHandler;
24 private NativeMethods.FCI.PFNREAD fciReadStreamHandler;
25 private NativeMethods.FCI.PFNWRITE fciWriteStreamHandler;
26 private NativeMethods.FCI.PFNCLOSE fciCloseStreamHandler;
27 private NativeMethods.FCI.PFNSEEK fciSeekStreamHandler;
28 private NativeMethods.FCI.PFNFILEPLACED fciFilePlacedHandler;
29 private NativeMethods.FCI.PFNDELETE fciDeleteFileHandler;
30 private NativeMethods.FCI.PFNGETTEMPFILE fciGetTempFileHandler;
31
32 private NativeMethods.FCI.PFNGETNEXTCABINET fciGetNextCabinet;
33 private NativeMethods.FCI.PFNSTATUS fciCreateStatus;
34 private NativeMethods.FCI.PFNGETOPENINFO fciGetOpenInfo;
35
36 private IPackStreamContext context;
37
38 private FileAttributes fileAttributes;
39 private DateTime fileLastWriteTime;
40
41 private int maxCabBytes;
42
43 private long totalFolderBytesProcessedInCurrentCab;
44
45 private CompressionLevel compressionLevel;
46 private bool dontUseTempFiles;
47 private IList<Stream> tempStreams;
48
49 public CabPacker(CabEngine cabEngine)
50 : base(cabEngine)
51 {
52 this.fciAllocMemHandler = this.CabAllocMem;
53 this.fciFreeMemHandler = this.CabFreeMem;
54 this.fciOpenStreamHandler = this.CabOpenStreamEx;
55 this.fciReadStreamHandler = this.CabReadStreamEx;
56 this.fciWriteStreamHandler = this.CabWriteStreamEx;
57 this.fciCloseStreamHandler = this.CabCloseStreamEx;
58 this.fciSeekStreamHandler = this.CabSeekStreamEx;
59 this.fciFilePlacedHandler = this.CabFilePlaced;
60 this.fciDeleteFileHandler = this.CabDeleteFile;
61 this.fciGetTempFileHandler = this.CabGetTempFile;
62 this.fciGetNextCabinet = this.CabGetNextCabinet;
63 this.fciCreateStatus = this.CabCreateStatus;
64 this.fciGetOpenInfo = this.CabGetOpenInfo;
65 this.tempStreams = new List<Stream>();
66 this.compressionLevel = CompressionLevel.Normal;
67 }
68
69 public bool UseTempFiles
70 {
71 get
72 {
73 return !this.dontUseTempFiles;
74 }
75
76 set
77 {
78 this.dontUseTempFiles = !value;
79 }
80 }
81
82 public CompressionLevel CompressionLevel
83 {
84 get
85 {
86 return this.compressionLevel;
87 }
88
89 set
90 {
91 this.compressionLevel = value;
92 }
93 }
94
95 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
96 private void CreateFci(long maxArchiveSize)
97 {
98 NativeMethods.FCI.CCAB ccab = new NativeMethods.FCI.CCAB();
99 if (maxArchiveSize > 0 && maxArchiveSize < ccab.cb)
100 {
101 ccab.cb = Math.Max(
102 NativeMethods.FCI.MIN_DISK, (int) maxArchiveSize);
103 }
104
105 object maxFolderSizeOption = this.context.GetOption(
106 "maxFolderSize", null);
107 if (maxFolderSizeOption != null)
108 {
109 long maxFolderSize = Convert.ToInt64(
110 maxFolderSizeOption, CultureInfo.InvariantCulture);
111 if (maxFolderSize > 0 && maxFolderSize < ccab.cbFolderThresh)
112 {
113 ccab.cbFolderThresh = (int) maxFolderSize;
114 }
115 }
116
117 this.maxCabBytes = ccab.cb;
118 ccab.szCab = this.context.GetArchiveName(0);
119 if (ccab.szCab == null)
120 {
121 throw new FileNotFoundException(
122 "Cabinet name not provided by stream context.");
123 }
124 ccab.setID = (short) new Random().Next(
125 Int16.MinValue, Int16.MaxValue + 1);
126 this.CabNumbers[ccab.szCab] = 0;
127 this.currentArchiveName = ccab.szCab;
128 this.totalArchives = 1;
129 this.CabStream = null;
130
131 this.Erf.Clear();
132 this.fciHandle = NativeMethods.FCI.Create(
133 this.ErfHandle.AddrOfPinnedObject(),
134 this.fciFilePlacedHandler,
135 this.fciAllocMemHandler,
136 this.fciFreeMemHandler,
137 this.fciOpenStreamHandler,
138 this.fciReadStreamHandler,
139 this.fciWriteStreamHandler,
140 this.fciCloseStreamHandler,
141 this.fciSeekStreamHandler,
142 this.fciDeleteFileHandler,
143 this.fciGetTempFileHandler,
144 ccab,
145 IntPtr.Zero);
146 this.CheckError(false);
147 }
148
149 public void Pack(
150 IPackStreamContext streamContext,
151 IEnumerable<string> files,
152 long maxArchiveSize)
153 {
154 if (streamContext == null)
155 {
156 throw new ArgumentNullException("streamContext");
157 }
158
159 if (files == null)
160 {
161 throw new ArgumentNullException("files");
162 }
163
164 lock (this)
165 {
166 try
167 {
168 this.context = streamContext;
169
170 this.ResetProgressData();
171
172 this.CreateFci(maxArchiveSize);
173
174 foreach (string file in files)
175 {
176 FileAttributes attributes;
177 DateTime lastWriteTime;
178 Stream fileStream = this.context.OpenFileReadStream(
179 file,
180 out attributes,
181 out lastWriteTime);
182 if (fileStream != null)
183 {
184 this.totalFileBytes += fileStream.Length;
185 this.totalFiles++;
186 this.context.CloseFileReadStream(file, fileStream);
187 }
188 }
189
190 long uncompressedBytesInFolder = 0;
191 this.currentFileNumber = -1;
192
193 foreach (string file in files)
194 {
195 FileAttributes attributes;
196 DateTime lastWriteTime;
197 Stream fileStream = this.context.OpenFileReadStream(
198 file, out attributes, out lastWriteTime);
199 if (fileStream == null)
200 {
201 continue;
202 }
203
204 if (fileStream.Length >= (long) NativeMethods.FCI.MAX_FOLDER)
205 {
206 throw new NotSupportedException(String.Format(
207 CultureInfo.InvariantCulture,
208 "File {0} exceeds maximum file size " +
209 "for cabinet format.",
210 file));
211 }
212
213 if (uncompressedBytesInFolder > 0)
214 {
215 // Automatically create a new folder if this file
216 // won't fit in the current folder.
217 bool nextFolder = uncompressedBytesInFolder
218 + fileStream.Length >= (long) NativeMethods.FCI.MAX_FOLDER;
219
220 // Otherwise ask the client if it wants to
221 // move to the next folder.
222 if (!nextFolder)
223 {
224 object nextFolderOption = streamContext.GetOption(
225 "nextFolder",
226 new object[] { file, this.currentFolderNumber });
227 nextFolder = Convert.ToBoolean(
228 nextFolderOption, CultureInfo.InvariantCulture);
229 }
230
231 if (nextFolder)
232 {
233 this.FlushFolder();
234 uncompressedBytesInFolder = 0;
235 }
236 }
237
238 if (this.currentFolderTotalBytes > 0)
239 {
240 this.currentFolderTotalBytes = 0;
241 this.currentFolderNumber++;
242 uncompressedBytesInFolder = 0;
243 }
244
245 this.currentFileName = file;
246 this.currentFileNumber++;
247
248 this.currentFileTotalBytes = fileStream.Length;
249 this.currentFileBytesProcessed = 0;
250 this.OnProgress(ArchiveProgressType.StartFile);
251
252 uncompressedBytesInFolder += fileStream.Length;
253
254 this.AddFile(
255 file,
256 fileStream,
257 attributes,
258 lastWriteTime,
259 false,
260 this.CompressionLevel);
261 }
262
263 this.FlushFolder();
264 this.FlushCabinet();
265 }
266 finally
267 {
268 if (this.CabStream != null)
269 {
270 this.context.CloseArchiveWriteStream(
271 this.currentArchiveNumber,
272 this.currentArchiveName,
273 this.CabStream);
274 this.CabStream = null;
275 }
276
277 if (this.FileStream != null)
278 {
279 this.context.CloseFileReadStream(
280 this.currentFileName, this.FileStream);
281 this.FileStream = null;
282 }
283 this.context = null;
284
285 if (this.fciHandle != null)
286 {
287 this.fciHandle.Dispose();
288 this.fciHandle = null;
289 }
290 }
291 }
292 }
293
294 internal override int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv)
295 {
296 if (this.CabNumbers.ContainsKey(path))
297 {
298 Stream stream = this.CabStream;
299 if (stream == null)
300 {
301 short cabNumber = this.CabNumbers[path];
302
303 this.currentFolderTotalBytes = 0;
304
305 stream = this.context.OpenArchiveWriteStream(cabNumber, path, true, this.CabEngine);
306 if (stream == null)
307 {
308 throw new FileNotFoundException(
309 String.Format(CultureInfo.InvariantCulture, "Cabinet {0} not provided.", cabNumber));
310 }
311 this.currentArchiveName = path;
312
313 this.currentArchiveTotalBytes = Math.Min(
314 this.totalFolderBytesProcessedInCurrentCab, this.maxCabBytes);
315 this.currentArchiveBytesProcessed = 0;
316
317 this.OnProgress(ArchiveProgressType.StartArchive);
318 this.CabStream = stream;
319 }
320 path = CabWorker.CabStreamName;
321 }
322 else if (path == CabPacker.TempStreamName)
323 {
324 // Opening memory stream for a temp file.
325 Stream stream = new MemoryStream();
326 this.tempStreams.Add(stream);
327 int streamHandle = this.StreamHandles.AllocHandle(stream);
328 err = 0;
329 return streamHandle;
330 }
331 else if (path != CabWorker.CabStreamName)
332 {
333 // Opening a file on disk for a temp file.
334 path = Path.Combine(Path.GetTempPath(), path);
335 Stream stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite);
336 this.tempStreams.Add(stream);
337 stream = new DuplicateStream(stream);
338 int streamHandle = this.StreamHandles.AllocHandle(stream);
339 err = 0;
340 return streamHandle;
341 }
342 return base.CabOpenStreamEx(path, openFlags, shareMode, out err, pv);
343 }
344
345 internal override int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
346 {
347 int count = base.CabWriteStreamEx(streamHandle, memory, cb, out err, pv);
348 if (count > 0 && err == 0)
349 {
350 Stream stream = this.StreamHandles[streamHandle];
351 if (DuplicateStream.OriginalStream(stream) ==
352 DuplicateStream.OriginalStream(this.CabStream))
353 {
354 this.currentArchiveBytesProcessed += cb;
355 if (this.currentArchiveBytesProcessed > this.currentArchiveTotalBytes)
356 {
357 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
358 }
359 }
360 }
361 return count;
362 }
363
364 internal override int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv)
365 {
366 Stream stream = DuplicateStream.OriginalStream(this.StreamHandles[streamHandle]);
367
368 if (stream == DuplicateStream.OriginalStream(this.FileStream))
369 {
370 this.context.CloseFileReadStream(this.currentFileName, stream);
371 this.FileStream = null;
372 long remainder = this.currentFileTotalBytes - this.currentFileBytesProcessed;
373 this.currentFileBytesProcessed += remainder;
374 this.fileBytesProcessed += remainder;
375 this.OnProgress(ArchiveProgressType.FinishFile);
376
377 this.currentFileTotalBytes = 0;
378 this.currentFileBytesProcessed = 0;
379 this.currentFileName = null;
380 }
381 else if (stream == DuplicateStream.OriginalStream(this.CabStream))
382 {
383 if (stream.CanWrite)
384 {
385 stream.Flush();
386 }
387
388 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
389 this.OnProgress(ArchiveProgressType.FinishArchive);
390 this.currentArchiveNumber++;
391 this.totalArchives++;
392
393 this.context.CloseArchiveWriteStream(
394 this.currentArchiveNumber,
395 this.currentArchiveName,
396 stream);
397
398 this.currentArchiveName = this.NextCabinetName;
399 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes = 0;
400 this.totalFolderBytesProcessedInCurrentCab = 0;
401
402 this.CabStream = null;
403 }
404 else // Must be a temp stream
405 {
406 stream.Close();
407 this.tempStreams.Remove(stream);
408 }
409 return base.CabCloseStreamEx(streamHandle, out err, pv);
410 }
411
412 /// <summary>
413 /// Disposes of resources allocated by the cabinet engine.
414 /// </summary>
415 /// <param name="disposing">If true, the method has been called directly or indirectly by a user's code,
416 /// so managed and unmanaged resources will be disposed. If false, the method has been called by the
417 /// runtime from inside the finalizer, and only unmanaged resources will be disposed.</param>
418 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
419 protected override void Dispose(bool disposing)
420 {
421 try
422 {
423 if (disposing)
424 {
425 if (this.fciHandle != null)
426 {
427 this.fciHandle.Dispose();
428 this.fciHandle = null;
429 }
430 }
431 }
432 finally
433 {
434 base.Dispose(disposing);
435 }
436 }
437
438 private static NativeMethods.FCI.TCOMP GetCompressionType(CompressionLevel compLevel)
439 {
440 if (compLevel < CompressionLevel.Min)
441 {
442 return NativeMethods.FCI.TCOMP.TYPE_NONE;
443 }
444 else
445 {
446 if (compLevel > CompressionLevel.Max)
447 {
448 compLevel = CompressionLevel.Max;
449 }
450
451 int lzxWindowMax =
452 ((int) NativeMethods.FCI.TCOMP.LZX_WINDOW_HI >> (int) NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW) -
453 ((int) NativeMethods.FCI.TCOMP.LZX_WINDOW_LO >> (int) NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW);
454 int lzxWindow = lzxWindowMax *
455 (compLevel - CompressionLevel.Min) / (CompressionLevel.Max - CompressionLevel.Min);
456
457 return (NativeMethods.FCI.TCOMP) ((int) NativeMethods.FCI.TCOMP.TYPE_LZX |
458 ((int) NativeMethods.FCI.TCOMP.LZX_WINDOW_LO +
459 (lzxWindow << (int) NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW)));
460 }
461 }
462
463 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
464 private void AddFile(
465 string name,
466 Stream stream,
467 FileAttributes attributes,
468 DateTime lastWriteTime,
469 bool execute,
470 CompressionLevel compLevel)
471 {
472 this.FileStream = stream;
473 this.fileAttributes = attributes &
474 (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System);
475 this.fileLastWriteTime = lastWriteTime;
476 this.currentFileName = name;
477
478 NativeMethods.FCI.TCOMP tcomp = CabPacker.GetCompressionType(compLevel);
479
480 IntPtr namePtr = IntPtr.Zero;
481 try
482 {
483 Encoding nameEncoding = Encoding.ASCII;
484 if (Encoding.UTF8.GetByteCount(name) > name.Length)
485 {
486 nameEncoding = Encoding.UTF8;
487 this.fileAttributes |= FileAttributes.Normal; // _A_NAME_IS_UTF
488 }
489
490 byte[] nameBytes = nameEncoding.GetBytes(name);
491 namePtr = Marshal.AllocHGlobal(nameBytes.Length + 1);
492 Marshal.Copy(nameBytes, 0, namePtr, nameBytes.Length);
493 Marshal.WriteByte(namePtr, nameBytes.Length, 0);
494
495 this.Erf.Clear();
496 NativeMethods.FCI.AddFile(
497 this.fciHandle,
498 String.Empty,
499 namePtr,
500 execute,
501 this.fciGetNextCabinet,
502 this.fciCreateStatus,
503 this.fciGetOpenInfo,
504 tcomp);
505 }
506 finally
507 {
508 if (namePtr != IntPtr.Zero)
509 {
510 Marshal.FreeHGlobal(namePtr);
511 }
512 }
513
514 this.CheckError(false);
515 this.FileStream = null;
516 this.currentFileName = null;
517 }
518
519 private void FlushFolder()
520 {
521 this.Erf.Clear();
522 NativeMethods.FCI.FlushFolder(this.fciHandle, this.fciGetNextCabinet, this.fciCreateStatus);
523 this.CheckError(false);
524 }
525
526 private void FlushCabinet()
527 {
528 this.Erf.Clear();
529 NativeMethods.FCI.FlushCabinet(this.fciHandle, false, this.fciGetNextCabinet, this.fciCreateStatus);
530 this.CheckError(false);
531 }
532
533 private int CabGetOpenInfo(
534 string path,
535 out short date,
536 out short time,
537 out short attribs,
538 out int err,
539 IntPtr pv)
540 {
541 CompressionEngine.DateTimeToDosDateAndTime(this.fileLastWriteTime, out date, out time);
542 attribs = (short) this.fileAttributes;
543
544 Stream stream = this.FileStream;
545 this.FileStream = new DuplicateStream(stream);
546 int streamHandle = this.StreamHandles.AllocHandle(stream);
547 err = 0;
548 return streamHandle;
549 }
550
551 private int CabFilePlaced(
552 IntPtr pccab,
553 string filePath,
554 long fileSize,
555 int continuation,
556 IntPtr pv)
557 {
558 return 0;
559 }
560
561 private int CabGetNextCabinet(IntPtr pccab, uint prevCabSize, IntPtr pv)
562 {
563 NativeMethods.FCI.CCAB nextCcab = new NativeMethods.FCI.CCAB();
564 Marshal.PtrToStructure(pccab, nextCcab);
565
566 nextCcab.szDisk = String.Empty;
567 nextCcab.szCab = this.context.GetArchiveName(nextCcab.iCab);
568 this.CabNumbers[nextCcab.szCab] = (short) nextCcab.iCab;
569 this.NextCabinetName = nextCcab.szCab;
570
571 Marshal.StructureToPtr(nextCcab, pccab, false);
572 return 1;
573 }
574
575 private int CabCreateStatus(NativeMethods.FCI.STATUS typeStatus, uint cb1, uint cb2, IntPtr pv)
576 {
577 switch (typeStatus)
578 {
579 case NativeMethods.FCI.STATUS.FILE:
580 if (cb2 > 0 && this.currentFileBytesProcessed < this.currentFileTotalBytes)
581 {
582 if (this.currentFileBytesProcessed + cb2 > this.currentFileTotalBytes)
583 {
584 cb2 = (uint) this.currentFileTotalBytes - (uint) this.currentFileBytesProcessed;
585 }
586 this.currentFileBytesProcessed += cb2;
587 this.fileBytesProcessed += cb2;
588
589 this.OnProgress(ArchiveProgressType.PartialFile);
590 }
591 break;
592
593 case NativeMethods.FCI.STATUS.FOLDER:
594 if (cb1 == 0)
595 {
596 this.currentFolderTotalBytes = cb2 - this.totalFolderBytesProcessedInCurrentCab;
597 this.totalFolderBytesProcessedInCurrentCab = cb2;
598 }
599 else if (this.currentFolderTotalBytes == 0)
600 {
601 this.OnProgress(ArchiveProgressType.PartialArchive);
602 }
603 break;
604
605 case NativeMethods.FCI.STATUS.CABINET:
606 break;
607 }
608 return 0;
609 }
610
611 private int CabGetTempFile(IntPtr tempNamePtr, int tempNameSize, IntPtr pv)
612 {
613 string tempFileName;
614 if (this.UseTempFiles)
615 {
616 tempFileName = Path.GetFileName(Path.GetTempFileName());
617 }
618 else
619 {
620 tempFileName = CabPacker.TempStreamName;
621 }
622
623 byte[] tempNameBytes = Encoding.ASCII.GetBytes(tempFileName);
624 if (tempNameBytes.Length >= tempNameSize)
625 {
626 return -1;
627 }
628
629 Marshal.Copy(tempNameBytes, 0, tempNamePtr, tempNameBytes.Length);
630 Marshal.WriteByte(tempNamePtr, tempNameBytes.Length, 0); // null-terminator
631 return 1;
632 }
633
634 private int CabDeleteFile(string path, out int err, IntPtr pv)
635 {
636 try
637 {
638 // Deleting a temp file - don't bother if it is only a memory stream.
639 if (path != CabPacker.TempStreamName)
640 {
641 path = Path.Combine(Path.GetTempPath(), path);
642 File.Delete(path);
643 }
644 }
645 catch (IOException)
646 {
647 // Failure to delete a temp file is not fatal.
648 }
649 err = 0;
650 return 1;
651 }
652 }
653}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabUnpacker.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabUnpacker.cs
new file mode 100644
index 00000000..b0be4a15
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabUnpacker.cs
@@ -0,0 +1,566 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections.Generic;
9 using System.Globalization;
10 using System.Runtime.InteropServices;
11 using System.Diagnostics.CodeAnalysis;
12
13 internal class CabUnpacker : CabWorker
14 {
15 private NativeMethods.FDI.Handle fdiHandle;
16
17 // These delegates need to be saved as member variables
18 // so that they don't get GC'd.
19 private NativeMethods.FDI.PFNALLOC fdiAllocMemHandler;
20 private NativeMethods.FDI.PFNFREE fdiFreeMemHandler;
21 private NativeMethods.FDI.PFNOPEN fdiOpenStreamHandler;
22 private NativeMethods.FDI.PFNREAD fdiReadStreamHandler;
23 private NativeMethods.FDI.PFNWRITE fdiWriteStreamHandler;
24 private NativeMethods.FDI.PFNCLOSE fdiCloseStreamHandler;
25 private NativeMethods.FDI.PFNSEEK fdiSeekStreamHandler;
26
27 private IUnpackStreamContext context;
28
29 private List<ArchiveFileInfo> fileList;
30
31 private int folderId;
32
33 private Predicate<string> filter;
34
35 public CabUnpacker(CabEngine cabEngine)
36 : base(cabEngine)
37 {
38 this.fdiAllocMemHandler = this.CabAllocMem;
39 this.fdiFreeMemHandler = this.CabFreeMem;
40 this.fdiOpenStreamHandler = this.CabOpenStream;
41 this.fdiReadStreamHandler = this.CabReadStream;
42 this.fdiWriteStreamHandler = this.CabWriteStream;
43 this.fdiCloseStreamHandler = this.CabCloseStream;
44 this.fdiSeekStreamHandler = this.CabSeekStream;
45
46 this.fdiHandle = NativeMethods.FDI.Create(
47 this.fdiAllocMemHandler,
48 this.fdiFreeMemHandler,
49 this.fdiOpenStreamHandler,
50 this.fdiReadStreamHandler,
51 this.fdiWriteStreamHandler,
52 this.fdiCloseStreamHandler,
53 this.fdiSeekStreamHandler,
54 NativeMethods.FDI.CPU_80386,
55 this.ErfHandle.AddrOfPinnedObject());
56 if (this.Erf.Error)
57 {
58 int error = this.Erf.Oper;
59 int errorCode = this.Erf.Type;
60 this.ErfHandle.Free();
61 throw new CabException(
62 error,
63 errorCode,
64 CabException.GetErrorMessage(error, errorCode, true));
65 }
66 }
67
68 public bool IsArchive(Stream stream)
69 {
70 if (stream == null)
71 {
72 throw new ArgumentNullException("stream");
73 }
74
75 lock (this)
76 {
77 short id;
78 int folderCount, fileCount;
79 return this.IsCabinet(stream, out id, out folderCount, out fileCount);
80 }
81 }
82
83 public IList<ArchiveFileInfo> GetFileInfo(
84 IUnpackStreamContext streamContext,
85 Predicate<string> fileFilter)
86 {
87 if (streamContext == null)
88 {
89 throw new ArgumentNullException("streamContext");
90 }
91
92 lock (this)
93 {
94 this.context = streamContext;
95 this.filter = fileFilter;
96 this.NextCabinetName = String.Empty;
97 this.fileList = new List<ArchiveFileInfo>();
98 bool tmpSuppress = this.SuppressProgressEvents;
99 this.SuppressProgressEvents = true;
100 try
101 {
102 for (short cabNumber = 0;
103 this.NextCabinetName != null;
104 cabNumber++)
105 {
106 this.Erf.Clear();
107 this.CabNumbers[this.NextCabinetName] = cabNumber;
108
109 NativeMethods.FDI.Copy(
110 this.fdiHandle,
111 this.NextCabinetName,
112 String.Empty,
113 0,
114 this.CabListNotify,
115 IntPtr.Zero,
116 IntPtr.Zero);
117 this.CheckError(true);
118 }
119
120 List<ArchiveFileInfo> tmpFileList = this.fileList;
121 this.fileList = null;
122 return tmpFileList.AsReadOnly();
123 }
124 finally
125 {
126 this.SuppressProgressEvents = tmpSuppress;
127
128 if (this.CabStream != null)
129 {
130 this.context.CloseArchiveReadStream(
131 this.currentArchiveNumber,
132 this.currentArchiveName,
133 this.CabStream);
134 this.CabStream = null;
135 }
136
137 this.context = null;
138 }
139 }
140 }
141
142 public void Unpack(
143 IUnpackStreamContext streamContext,
144 Predicate<string> fileFilter)
145 {
146 lock (this)
147 {
148 IList<ArchiveFileInfo> files =
149 this.GetFileInfo(streamContext, fileFilter);
150
151 this.ResetProgressData();
152
153 if (files != null)
154 {
155 this.totalFiles = files.Count;
156
157 for (int i = 0; i < files.Count; i++)
158 {
159 this.totalFileBytes += files[i].Length;
160 if (files[i].ArchiveNumber >= this.totalArchives)
161 {
162 int totalArchives = files[i].ArchiveNumber + 1;
163 this.totalArchives = (short) totalArchives;
164 }
165 }
166 }
167
168 this.context = streamContext;
169 this.fileList = null;
170 this.NextCabinetName = String.Empty;
171 this.folderId = -1;
172 this.currentFileNumber = -1;
173
174 try
175 {
176 for (short cabNumber = 0;
177 this.NextCabinetName != null;
178 cabNumber++)
179 {
180 this.Erf.Clear();
181 this.CabNumbers[this.NextCabinetName] = cabNumber;
182
183 NativeMethods.FDI.Copy(
184 this.fdiHandle,
185 this.NextCabinetName,
186 String.Empty,
187 0,
188 this.CabExtractNotify,
189 IntPtr.Zero,
190 IntPtr.Zero);
191 this.CheckError(true);
192 }
193 }
194 finally
195 {
196 if (this.CabStream != null)
197 {
198 this.context.CloseArchiveReadStream(
199 this.currentArchiveNumber,
200 this.currentArchiveName,
201 this.CabStream);
202 this.CabStream = null;
203 }
204
205 if (this.FileStream != null)
206 {
207 this.context.CloseFileWriteStream(this.currentFileName, this.FileStream, FileAttributes.Normal, DateTime.Now);
208 this.FileStream = null;
209 }
210
211 this.context = null;
212 }
213 }
214 }
215
216 internal override int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv)
217 {
218 if (this.CabNumbers.ContainsKey(path))
219 {
220 Stream stream = this.CabStream;
221 if (stream == null)
222 {
223 short cabNumber = this.CabNumbers[path];
224
225 stream = this.context.OpenArchiveReadStream(cabNumber, path, this.CabEngine);
226 if (stream == null)
227 {
228 throw new FileNotFoundException(String.Format(CultureInfo.InvariantCulture, "Cabinet {0} not provided.", cabNumber));
229 }
230 this.currentArchiveName = path;
231 this.currentArchiveNumber = cabNumber;
232 if (this.totalArchives <= this.currentArchiveNumber)
233 {
234 int totalArchives = this.currentArchiveNumber + 1;
235 this.totalArchives = (short) totalArchives;
236 }
237 this.currentArchiveTotalBytes = stream.Length;
238 this.currentArchiveBytesProcessed = 0;
239
240 if (this.folderId != -3) // -3 is a special folderId that requires re-opening the same cab
241 {
242 this.OnProgress(ArchiveProgressType.StartArchive);
243 }
244 this.CabStream = stream;
245 }
246 path = CabWorker.CabStreamName;
247 }
248 return base.CabOpenStreamEx(path, openFlags, shareMode, out err, pv);
249 }
250
251 internal override int CabReadStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
252 {
253 int count = base.CabReadStreamEx(streamHandle, memory, cb, out err, pv);
254 if (err == 0 && this.CabStream != null)
255 {
256 if (this.fileList == null)
257 {
258 Stream stream = this.StreamHandles[streamHandle];
259 if (DuplicateStream.OriginalStream(stream) ==
260 DuplicateStream.OriginalStream(this.CabStream))
261 {
262 this.currentArchiveBytesProcessed += cb;
263 if (this.currentArchiveBytesProcessed > this.currentArchiveTotalBytes)
264 {
265 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
266 }
267 }
268 }
269 }
270 return count;
271 }
272
273 internal override int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
274 {
275 int count = base.CabWriteStreamEx(streamHandle, memory, cb, out err, pv);
276 if (count > 0 && err == 0)
277 {
278 this.currentFileBytesProcessed += cb;
279 this.fileBytesProcessed += cb;
280 this.OnProgress(ArchiveProgressType.PartialFile);
281 }
282 return count;
283 }
284
285 internal override int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv)
286 {
287 Stream stream = DuplicateStream.OriginalStream(this.StreamHandles[streamHandle]);
288
289 if (stream == DuplicateStream.OriginalStream(this.CabStream))
290 {
291 if (this.folderId != -3) // -3 is a special folderId that requires re-opening the same cab
292 {
293 this.OnProgress(ArchiveProgressType.FinishArchive);
294 }
295
296 this.context.CloseArchiveReadStream(this.currentArchiveNumber, this.currentArchiveName, stream);
297
298 this.currentArchiveName = this.NextCabinetName;
299 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes = 0;
300
301 this.CabStream = null;
302 }
303 return base.CabCloseStreamEx(streamHandle, out err, pv);
304 }
305
306 /// <summary>
307 /// Disposes of resources allocated by the cabinet engine.
308 /// </summary>
309 /// <param name="disposing">If true, the method has been called directly or indirectly by a user's code,
310 /// so managed and unmanaged resources will be disposed. If false, the method has been called by the
311 /// runtime from inside the finalizer, and only unmanaged resources will be disposed.</param>
312 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
313 protected override void Dispose(bool disposing)
314 {
315 try
316 {
317 if (disposing)
318 {
319 if (this.fdiHandle != null)
320 {
321 this.fdiHandle.Dispose();
322 this.fdiHandle = null;
323 }
324 }
325 }
326 finally
327 {
328 base.Dispose(disposing);
329 }
330 }
331
332 private static string GetFileName(NativeMethods.FDI.NOTIFICATION notification)
333 {
334 bool utf8Name = (notification.attribs & (ushort) FileAttributes.Normal) != 0; // _A_NAME_IS_UTF
335
336 // Non-utf8 names should be completely ASCII. But for compatibility with
337 // legacy tools, interpret them using the current (Default) ANSI codepage.
338 Encoding nameEncoding = utf8Name ? Encoding.UTF8 : Encoding.Default;
339
340 // Find how many bytes are in the string.
341 // Unfortunately there is no faster way.
342 int nameBytesCount = 0;
343 while (Marshal.ReadByte(notification.psz1, nameBytesCount) != 0)
344 {
345 nameBytesCount++;
346 }
347
348 byte[] nameBytes = new byte[nameBytesCount];
349 Marshal.Copy(notification.psz1, nameBytes, 0, nameBytesCount);
350 string name = nameEncoding.GetString(nameBytes);
351 if (Path.IsPathRooted(name))
352 {
353 name = name.Replace("" + Path.VolumeSeparatorChar, "");
354 }
355
356 return name;
357 }
358
359 private bool IsCabinet(Stream cabStream, out short id, out int cabFolderCount, out int fileCount)
360 {
361 int streamHandle = this.StreamHandles.AllocHandle(cabStream);
362 try
363 {
364 this.Erf.Clear();
365 NativeMethods.FDI.CABINFO fdici;
366 bool isCabinet = 0 != NativeMethods.FDI.IsCabinet(this.fdiHandle, streamHandle, out fdici);
367
368 if (this.Erf.Error)
369 {
370 if (((NativeMethods.FDI.ERROR) this.Erf.Oper) == NativeMethods.FDI.ERROR.UNKNOWN_CABINET_VERSION)
371 {
372 isCabinet = false;
373 }
374 else
375 {
376 throw new CabException(
377 this.Erf.Oper,
378 this.Erf.Type,
379 CabException.GetErrorMessage(this.Erf.Oper, this.Erf.Type, true));
380 }
381 }
382
383 id = fdici.setID;
384 cabFolderCount = (int) fdici.cFolders;
385 fileCount = (int) fdici.cFiles;
386 return isCabinet;
387 }
388 finally
389 {
390 this.StreamHandles.FreeHandle(streamHandle);
391 }
392 }
393
394 private int CabListNotify(NativeMethods.FDI.NOTIFICATIONTYPE notificationType, NativeMethods.FDI.NOTIFICATION notification)
395 {
396 switch (notificationType)
397 {
398 case NativeMethods.FDI.NOTIFICATIONTYPE.CABINET_INFO:
399 {
400 string nextCab = Marshal.PtrToStringAnsi(notification.psz1);
401 this.NextCabinetName = (nextCab.Length != 0 ? nextCab : null);
402 return 0; // Continue
403 }
404 case NativeMethods.FDI.NOTIFICATIONTYPE.PARTIAL_FILE:
405 {
406 // This notification can occur when examining the contents of a non-first cab file.
407 return 0; // Continue
408 }
409 case NativeMethods.FDI.NOTIFICATIONTYPE.COPY_FILE:
410 {
411 //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC
412
413 string name = CabUnpacker.GetFileName(notification);
414
415 if (this.filter == null || this.filter(name))
416 {
417 if (this.fileList != null)
418 {
419 FileAttributes attributes = (FileAttributes) notification.attribs &
420 (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System);
421 if (attributes == (FileAttributes) 0)
422 {
423 attributes = FileAttributes.Normal;
424 }
425 DateTime lastWriteTime;
426 CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime);
427 long length = notification.cb;
428
429 CabFileInfo fileInfo = new CabFileInfo(
430 name,
431 notification.iFolder,
432 notification.iCabinet,
433 attributes,
434 lastWriteTime,
435 length);
436 this.fileList.Add(fileInfo);
437 this.currentFileNumber = this.fileList.Count - 1;
438 this.fileBytesProcessed += notification.cb;
439 }
440 }
441
442 this.totalFiles++;
443 this.totalFileBytes += notification.cb;
444 return 0; // Continue
445 }
446 }
447 return 0;
448 }
449
450 private int CabExtractNotify(NativeMethods.FDI.NOTIFICATIONTYPE notificationType, NativeMethods.FDI.NOTIFICATION notification)
451 {
452 switch (notificationType)
453 {
454 case NativeMethods.FDI.NOTIFICATIONTYPE.CABINET_INFO:
455 {
456 if (this.NextCabinetName != null && this.NextCabinetName.StartsWith("?", StringComparison.Ordinal))
457 {
458 // We are just continuing the copy of a file that spanned cabinets.
459 // The next cabinet name needs to be preserved.
460 this.NextCabinetName = this.NextCabinetName.Substring(1);
461 }
462 else
463 {
464 string nextCab = Marshal.PtrToStringAnsi(notification.psz1);
465 this.NextCabinetName = (nextCab.Length != 0 ? nextCab : null);
466 }
467 return 0; // Continue
468 }
469 case NativeMethods.FDI.NOTIFICATIONTYPE.NEXT_CABINET:
470 {
471 string nextCab = Marshal.PtrToStringAnsi(notification.psz1);
472 this.CabNumbers[nextCab] = (short) notification.iCabinet;
473 this.NextCabinetName = "?" + this.NextCabinetName;
474 return 0; // Continue
475 }
476 case NativeMethods.FDI.NOTIFICATIONTYPE.COPY_FILE:
477 {
478 return this.CabExtractCopyFile(notification);
479 }
480 case NativeMethods.FDI.NOTIFICATIONTYPE.CLOSE_FILE_INFO:
481 {
482 return this.CabExtractCloseFile(notification);
483 }
484 }
485 return 0;
486 }
487
488 private int CabExtractCopyFile(NativeMethods.FDI.NOTIFICATION notification)
489 {
490 if (notification.iFolder != this.folderId)
491 {
492 if (notification.iFolder != -3) // -3 is a special folderId used when continuing a folder from a previous cab
493 {
494 if (this.folderId != -1) // -1 means we just started the extraction sequence
495 {
496 this.currentFolderNumber++;
497 }
498 }
499 this.folderId = notification.iFolder;
500 }
501
502 //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC
503
504 string name = CabUnpacker.GetFileName(notification);
505
506 if (this.filter == null || this.filter(name))
507 {
508 this.currentFileNumber++;
509 this.currentFileName = name;
510
511 this.currentFileBytesProcessed = 0;
512 this.currentFileTotalBytes = notification.cb;
513 this.OnProgress(ArchiveProgressType.StartFile);
514
515 DateTime lastWriteTime;
516 CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime);
517
518 Stream stream = this.context.OpenFileWriteStream(name, notification.cb, lastWriteTime);
519 if (stream != null)
520 {
521 this.FileStream = stream;
522 int streamHandle = this.StreamHandles.AllocHandle(stream);
523 return streamHandle;
524 }
525 else
526 {
527 this.fileBytesProcessed += notification.cb;
528 this.OnProgress(ArchiveProgressType.FinishFile);
529 this.currentFileName = null;
530 }
531 }
532 return 0; // Continue
533 }
534
535 private int CabExtractCloseFile(NativeMethods.FDI.NOTIFICATION notification)
536 {
537 Stream stream = this.StreamHandles[notification.hf];
538 this.StreamHandles.FreeHandle(notification.hf);
539
540 //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC
541
542 string name = CabUnpacker.GetFileName(notification);
543
544 FileAttributes attributes = (FileAttributes) notification.attribs &
545 (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System);
546 if (attributes == (FileAttributes) 0)
547 {
548 attributes = FileAttributes.Normal;
549 }
550 DateTime lastWriteTime;
551 CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime);
552
553 stream.Flush();
554 this.context.CloseFileWriteStream(name, stream, attributes, lastWriteTime);
555 this.FileStream = null;
556
557 long remainder = this.currentFileTotalBytes - this.currentFileBytesProcessed;
558 this.currentFileBytesProcessed += remainder;
559 this.fileBytesProcessed += remainder;
560 this.OnProgress(ArchiveProgressType.FinishFile);
561 this.currentFileName = null;
562
563 return 1; // Continue
564 }
565 }
566}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/CabWorker.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/CabWorker.cs
new file mode 100644
index 00000000..cb2a7263
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/CabWorker.cs
@@ -0,0 +1,337 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5 using System;
6 using System.IO;
7 using System.IO.IsolatedStorage;
8 using System.Text;
9 using System.Security;
10 using System.Collections.Generic;
11 using System.Runtime.InteropServices;
12 using System.Diagnostics.CodeAnalysis;
13
14 internal abstract class CabWorker : IDisposable
15 {
16 internal const string CabStreamName = "%%CAB%%";
17
18 private CabEngine cabEngine;
19
20 private HandleManager<Stream> streamHandles;
21 private Stream cabStream;
22 private Stream fileStream;
23
24 private NativeMethods.ERF erf;
25 private GCHandle erfHandle;
26
27 private IDictionary<string, short> cabNumbers;
28 private string nextCabinetName;
29
30 private bool suppressProgressEvents;
31
32 private byte[] buf;
33
34 // Progress data
35 protected string currentFileName;
36 protected int currentFileNumber;
37 protected int totalFiles;
38 protected long currentFileBytesProcessed;
39 protected long currentFileTotalBytes;
40 protected short currentFolderNumber;
41 protected long currentFolderTotalBytes;
42 protected string currentArchiveName;
43 protected short currentArchiveNumber;
44 protected short totalArchives;
45 protected long currentArchiveBytesProcessed;
46 protected long currentArchiveTotalBytes;
47 protected long fileBytesProcessed;
48 protected long totalFileBytes;
49
50 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
51 protected CabWorker(CabEngine cabEngine)
52 {
53 this.cabEngine = cabEngine;
54 this.streamHandles = new HandleManager<Stream>();
55 this.erf = new NativeMethods.ERF();
56 this.erfHandle = GCHandle.Alloc(this.erf, GCHandleType.Pinned);
57 this.cabNumbers = new Dictionary<string, short>(1);
58
59 // 32K seems to be the size of the largest chunks processed by cabinet.dll.
60 // But just in case, this buffer will auto-enlarge.
61 this.buf = new byte[32768];
62 }
63
64 ~CabWorker()
65 {
66 this.Dispose(false);
67 }
68
69 public CabEngine CabEngine
70 {
71 get
72 {
73 return this.cabEngine;
74 }
75 }
76
77 internal NativeMethods.ERF Erf
78 {
79 get
80 {
81 return this.erf;
82 }
83 }
84
85 internal GCHandle ErfHandle
86 {
87 get
88 {
89 return this.erfHandle;
90 }
91 }
92
93 internal HandleManager<Stream> StreamHandles
94 {
95 get
96 {
97 return this.streamHandles;
98 }
99 }
100
101 internal bool SuppressProgressEvents
102 {
103 get
104 {
105 return this.suppressProgressEvents;
106 }
107
108 set
109 {
110 this.suppressProgressEvents = value;
111 }
112 }
113
114 internal IDictionary<string, short> CabNumbers
115 {
116 get
117 {
118 return this.cabNumbers;
119 }
120 }
121
122 internal string NextCabinetName
123 {
124 get
125 {
126 return this.nextCabinetName;
127 }
128
129 set
130 {
131 this.nextCabinetName = value;
132 }
133 }
134
135 internal Stream CabStream
136 {
137 get
138 {
139 return this.cabStream;
140 }
141
142 set
143 {
144 this.cabStream = value;
145 }
146 }
147
148 internal Stream FileStream
149 {
150 get
151 {
152 return this.fileStream;
153 }
154
155 set
156 {
157 this.fileStream = value;
158 }
159 }
160
161 public void Dispose()
162 {
163 this.Dispose(true);
164 GC.SuppressFinalize(this);
165 }
166
167 protected void ResetProgressData()
168 {
169 this.currentFileName = null;
170 this.currentFileNumber = 0;
171 this.totalFiles = 0;
172 this.currentFileBytesProcessed = 0;
173 this.currentFileTotalBytes = 0;
174 this.currentFolderNumber = 0;
175 this.currentFolderTotalBytes = 0;
176 this.currentArchiveName = null;
177 this.currentArchiveNumber = 0;
178 this.totalArchives = 0;
179 this.currentArchiveBytesProcessed = 0;
180 this.currentArchiveTotalBytes = 0;
181 this.fileBytesProcessed = 0;
182 this.totalFileBytes = 0;
183 }
184
185 protected void OnProgress(ArchiveProgressType progressType)
186 {
187 if (!this.suppressProgressEvents)
188 {
189 ArchiveProgressEventArgs e = new ArchiveProgressEventArgs(
190 progressType,
191 this.currentFileName,
192 this.currentFileNumber >= 0 ? this.currentFileNumber : 0,
193 this.totalFiles,
194 this.currentFileBytesProcessed,
195 this.currentFileTotalBytes,
196 this.currentArchiveName,
197 this.currentArchiveNumber,
198 this.totalArchives,
199 this.currentArchiveBytesProcessed,
200 this.currentArchiveTotalBytes,
201 this.fileBytesProcessed,
202 this.totalFileBytes);
203 this.CabEngine.ReportProgress(e);
204 }
205 }
206
207 internal IntPtr CabAllocMem(int byteCount)
208 {
209 IntPtr memPointer = Marshal.AllocHGlobal((IntPtr) byteCount);
210 return memPointer;
211 }
212
213 internal void CabFreeMem(IntPtr memPointer)
214 {
215 Marshal.FreeHGlobal(memPointer);
216 }
217
218 internal int CabOpenStream(string path, int openFlags, int shareMode)
219 {
220 int err; return this.CabOpenStreamEx(path, openFlags, shareMode, out err, IntPtr.Zero);
221 }
222
223 internal virtual int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv)
224 {
225 path = path.Trim();
226 Stream stream = this.cabStream;
227 this.cabStream = new DuplicateStream(stream);
228 int streamHandle = this.streamHandles.AllocHandle(stream);
229 err = 0;
230 return streamHandle;
231 }
232
233 internal int CabReadStream(int streamHandle, IntPtr memory, int cb)
234 {
235 int err; return this.CabReadStreamEx(streamHandle, memory, cb, out err, IntPtr.Zero);
236 }
237
238 internal virtual int CabReadStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
239 {
240 Stream stream = this.streamHandles[streamHandle];
241 int count = (int) cb;
242 if (count > this.buf.Length)
243 {
244 this.buf = new byte[count];
245 }
246 count = stream.Read(this.buf, 0, count);
247 Marshal.Copy(this.buf, 0, memory, count);
248 err = 0;
249 return count;
250 }
251
252 internal int CabWriteStream(int streamHandle, IntPtr memory, int cb)
253 {
254 int err; return this.CabWriteStreamEx(streamHandle, memory, cb, out err, IntPtr.Zero);
255 }
256
257 internal virtual int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv)
258 {
259 Stream stream = this.streamHandles[streamHandle];
260 int count = (int) cb;
261 if (count > this.buf.Length)
262 {
263 this.buf = new byte[count];
264 }
265 Marshal.Copy(memory, this.buf, 0, count);
266 stream.Write(this.buf, 0, count);
267 err = 0;
268 return cb;
269 }
270
271 internal int CabCloseStream(int streamHandle)
272 {
273 int err; return this.CabCloseStreamEx(streamHandle, out err, IntPtr.Zero);
274 }
275
276 internal virtual int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv)
277 {
278 this.streamHandles.FreeHandle(streamHandle);
279 err = 0;
280 return 0;
281 }
282
283 internal int CabSeekStream(int streamHandle, int offset, int seekOrigin)
284 {
285 int err; return this.CabSeekStreamEx(streamHandle, offset, seekOrigin, out err, IntPtr.Zero);
286 }
287
288 internal virtual int CabSeekStreamEx(int streamHandle, int offset, int seekOrigin, out int err, IntPtr pv)
289 {
290 Stream stream = this.streamHandles[streamHandle];
291 offset = (int) stream.Seek(offset, (SeekOrigin) seekOrigin);
292 err = 0;
293 return offset;
294 }
295
296 /// <summary>
297 /// Disposes of resources allocated by the cabinet engine.
298 /// </summary>
299 /// <param name="disposing">If true, the method has been called directly or indirectly by a user's code,
300 /// so managed and unmanaged resources will be disposed. If false, the method has been called by the
301 /// runtime from inside the finalizer, and only unmanaged resources will be disposed.</param>
302 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
303 protected virtual void Dispose(bool disposing)
304 {
305 if (disposing)
306 {
307 if (this.cabStream != null)
308 {
309 this.cabStream.Close();
310 this.cabStream = null;
311 }
312
313 if (this.fileStream != null)
314 {
315 this.fileStream.Close();
316 this.fileStream = null;
317 }
318 }
319
320 if (this.erfHandle.IsAllocated)
321 {
322 this.erfHandle.Free();
323 }
324 }
325
326 protected void CheckError(bool extracting)
327 {
328 if (this.Erf.Error)
329 {
330 throw new CabException(
331 this.Erf.Oper,
332 this.Erf.Type,
333 CabException.GetErrorMessage(this.Erf.Oper, this.Erf.Type, extracting));
334 }
335 }
336 }
337}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.resources b/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.resources
new file mode 100644
index 00000000..d53d263c
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.resources
Binary files differ
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.txt b/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.txt
new file mode 100644
index 00000000..df5a95d3
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/Errors.txt
@@ -0,0 +1,35 @@
1;
2; Cabinet Error Messages
3;
4
5; Generic error message.
61=Error code: {1}
7
8;
9; Cabinet creation messages - offset by 1000
10;
111000=Unknown error creating cabinet.
121001=Failure opening file to be stored in cabinet.
131002=Failure reading file to be stored in cabinet.
141003=Could not allocate enough memory to create cabinet.
151004=Could not create a temporary file.
161005=Unknown compression type.
171006=Could not create cabinet file.
181007=Client requested abort.
191008=Failure compressing data.
20
21;
22; Cabinet extraction messages - offset by 2000
23;
242000=Unknown error extracting cabinet.
252001=Cabinet not found.
262002=Cabinet file does not have the correct format.
272003=Cabinet file has an unknown version number.
282004=Cabinet file is corrupt.
292005=Could not allocate enough memory to extract cabinet.
302006=Unknown compression type in a cabinet folder.
312007=Failure decompressing data from a cabinet file.
322008=Failure writing to target file.
332009=Cabinets in a set do not have the same RESERVE sizes.
342010=Cabinet returned on NEXT_CABINET is incorrect.
352011=Client requested abort.
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/HandleManager.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/HandleManager.cs
new file mode 100644
index 00000000..aad9a317
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/HandleManager.cs
@@ -0,0 +1,76 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5 using System;
6 using System.Collections.Generic;
7
8 /// <summary>
9 /// Generic class for managing allocations of integer handles
10 /// for objects of a certain type.
11 /// </summary>
12 /// <typeparam name="T">The type of objects the handles refer to.</typeparam>
13 internal sealed class HandleManager<T> where T : class
14 {
15 /// <summary>
16 /// Auto-resizing list of objects for which handles have been allocated.
17 /// Each handle is just an index into this list. When a handle is freed,
18 /// the list item at that index is set to null.
19 /// </summary>
20 private List<T> handles;
21
22 /// <summary>
23 /// Creates a new HandleManager instance.
24 /// </summary>
25 public HandleManager()
26 {
27 this.handles = new List<T>();
28 }
29
30 /// <summary>
31 /// Gets the object of a handle, or null if the handle is invalid.
32 /// </summary>
33 /// <param name="handle">The integer handle previously allocated
34 /// for the desired object.</param>
35 /// <returns>The object for which the handle was allocated.</returns>
36 public T this[int handle]
37 {
38 get
39 {
40 if (handle > 0 && handle <= this.handles.Count)
41 {
42 return this.handles[handle - 1];
43 }
44 else
45 {
46 return null;
47 }
48 }
49 }
50
51 /// <summary>
52 /// Allocates a new handle for an object.
53 /// </summary>
54 /// <param name="obj">Object that the handle will refer to.</param>
55 /// <returns>New handle that can be later used to retrieve the object.</returns>
56 public int AllocHandle(T obj)
57 {
58 this.handles.Add(obj);
59 int handle = this.handles.Count;
60 return handle;
61 }
62
63 /// <summary>
64 /// Frees a handle that was previously allocated. Afterward the handle
65 /// will be invalid and the object it referred to can no longer retrieved.
66 /// </summary>
67 /// <param name="handle">Handle to be freed.</param>
68 public void FreeHandle(int handle)
69 {
70 if (handle > 0 && handle <= this.handles.Count)
71 {
72 this.handles[handle - 1] = null;
73 }
74 }
75 }
76}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/NativeMethods.cs b/src/dtf/WixToolset.Dtf.Compression.Cab/NativeMethods.cs
new file mode 100644
index 00000000..562e96dd
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/NativeMethods.cs
@@ -0,0 +1,407 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Cab
4{
5using System;
6using System.Text;
7using System.Security;
8using System.Runtime.InteropServices;
9using System.Diagnostics.CodeAnalysis;
10
11/// <summary>
12/// Native DllImport methods and related structures and constants used for
13/// cabinet creation and extraction via cabinet.dll.
14/// </summary>
15internal static class NativeMethods
16{
17 /// <summary>
18 /// A direct import of constants, enums, structures, delegates, and functions from fci.h.
19 /// Refer to comments in fci.h for documentation.
20 /// </summary>
21 internal static class FCI
22 {
23 internal const int MIN_DISK = 32768;
24 internal const int MAX_DISK = Int32.MaxValue;
25 internal const int MAX_FOLDER = 0x7FFF8000;
26 internal const int MAX_FILENAME = 256;
27 internal const int MAX_CABINET_NAME = 256;
28 internal const int MAX_CAB_PATH = 256;
29 internal const int MAX_DISK_NAME = 256;
30
31 internal const int CPU_80386 = 1;
32
33 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr PFNALLOC(int cb);
34 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void PFNFREE(IntPtr pv);
35
36 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNOPEN(string path, int oflag, int pmode, out int err, IntPtr pv);
37 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNREAD(int fileHandle, IntPtr memory, int cb, out int err, IntPtr pv);
38 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNWRITE(int fileHandle, IntPtr memory, int cb, out int err, IntPtr pv);
39 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNCLOSE(int fileHandle, out int err, IntPtr pv);
40 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSEEK(int fileHandle, int dist, int seekType, out int err, IntPtr pv);
41 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNDELETE(string path, out int err, IntPtr pv);
42
43 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETNEXTCABINET(IntPtr pccab, uint cbPrevCab, IntPtr pv);
44 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNFILEPLACED(IntPtr pccab, string path, long fileSize, int continuation, IntPtr pv);
45 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETOPENINFO(string path, out short date, out short time, out short pattribs, out int err, IntPtr pv);
46 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSTATUS(STATUS typeStatus, uint cb1, uint cb2, IntPtr pv);
47 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETTEMPFILE(IntPtr tempNamePtr, int tempNameSize, IntPtr pv);
48
49 /// <summary>
50 /// Error codes that can be returned by FCI.
51 /// </summary>
52 internal enum ERROR : int
53 {
54 NONE,
55 OPEN_SRC,
56 READ_SRC,
57 ALLOC_FAIL,
58 TEMP_FILE,
59 BAD_COMPR_TYPE,
60 CAB_FILE,
61 USER_ABORT,
62 MCI_FAIL,
63 }
64
65 /// <summary>
66 /// FCI compression algorithm types and parameters.
67 /// </summary>
68 internal enum TCOMP : ushort
69 {
70 MASK_TYPE = 0x000F,
71 TYPE_NONE = 0x0000,
72 TYPE_MSZIP = 0x0001,
73 TYPE_QUANTUM = 0x0002,
74 TYPE_LZX = 0x0003,
75 BAD = 0x000F,
76
77 MASK_LZX_WINDOW = 0x1F00,
78 LZX_WINDOW_LO = 0x0F00,
79 LZX_WINDOW_HI = 0x1500,
80 SHIFT_LZX_WINDOW = 0x0008,
81
82 MASK_QUANTUM_LEVEL = 0x00F0,
83 QUANTUM_LEVEL_LO = 0x0010,
84 QUANTUM_LEVEL_HI = 0x0070,
85 SHIFT_QUANTUM_LEVEL = 0x0004,
86
87 MASK_QUANTUM_MEM = 0x1F00,
88 QUANTUM_MEM_LO = 0x0A00,
89 QUANTUM_MEM_HI = 0x1500,
90 SHIFT_QUANTUM_MEM = 0x0008,
91
92 MASK_RESERVED = 0xE000,
93 }
94
95 /// <summary>
96 /// Reason for FCI status callback.
97 /// </summary>
98 internal enum STATUS : uint
99 {
100 FILE = 0,
101 FOLDER = 1,
102 CABINET = 2,
103 }
104
105 [SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments")]
106 [DllImport("cabinet.dll", EntryPoint = "FCICreate", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
107 internal static extern Handle Create(IntPtr perf, PFNFILEPLACED pfnfcifp, PFNALLOC pfna, PFNFREE pfnf, PFNOPEN pfnopen, PFNREAD pfnread, PFNWRITE pfnwrite, PFNCLOSE pfnclose, PFNSEEK pfnseek, PFNDELETE pfndelete, PFNGETTEMPFILE pfnfcigtf, [MarshalAs(UnmanagedType.LPStruct)] CCAB pccab, IntPtr pv);
108
109 [DllImport("cabinet.dll", EntryPoint = "FCIAddFile", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
110 internal static extern int AddFile(Handle hfci, string pszSourceFile, IntPtr pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fExecute, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis, PFNGETOPENINFO pfnfcigoi, TCOMP typeCompress);
111
112 [DllImport("cabinet.dll", EntryPoint = "FCIFlushCabinet", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
113 internal static extern int FlushCabinet(Handle hfci, [MarshalAs(UnmanagedType.Bool)] bool fGetNextCab, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis);
114
115 [DllImport("cabinet.dll", EntryPoint = "FCIFlushFolder", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
116 internal static extern int FlushFolder(Handle hfci, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis);
117
118 [SuppressUnmanagedCodeSecurity]
119 [DllImport("cabinet.dll", EntryPoint = "FCIDestroy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
120 [return: MarshalAs(UnmanagedType.Bool)]
121 internal static extern bool Destroy(IntPtr hfci);
122
123 /// <summary>
124 /// Cabinet information structure used for FCI initialization and GetNextCabinet callback.
125 /// </summary>
126 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
127 internal class CCAB
128 {
129 internal int cb = MAX_DISK;
130 internal int cbFolderThresh = MAX_FOLDER;
131 internal int cbReserveCFHeader;
132 internal int cbReserveCFFolder;
133 internal int cbReserveCFData;
134 internal int iCab;
135 internal int iDisk;
136 internal int fFailOnIncompressible;
137 internal short setID;
138 [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_DISK_NAME )] internal string szDisk = String.Empty;
139 [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_CABINET_NAME)] internal string szCab = String.Empty;
140 [MarshalAs(UnmanagedType.ByValTStr, SizeConst=MAX_CAB_PATH )] internal string szCabPath = String.Empty;
141 }
142
143 /// <summary>
144 /// Ensures that the FCI handle is safely released.
145 /// </summary>
146 internal class Handle : SafeHandle
147 {
148 /// <summary>
149 /// Creates a new unintialized handle. The handle will be initialized
150 /// when it is marshalled back from native code.
151 /// </summary>
152 internal Handle()
153 : base(IntPtr.Zero, true)
154 {
155 }
156
157 /// <summary>
158 /// Checks if the handle is invalid. An FCI handle is invalid when it is zero.
159 /// </summary>
160 public override bool IsInvalid
161 {
162 get
163 {
164 return this.handle == IntPtr.Zero;
165 }
166 }
167
168 /// <summary>
169 /// Releases the handle by calling FDIDestroy().
170 /// </summary>
171 /// <returns>True if the release succeeded.</returns>
172 protected override bool ReleaseHandle()
173 {
174 return FCI.Destroy(this.handle);
175 }
176 }
177 }
178
179 /// <summary>
180 /// A direct import of constants, enums, structures, delegates, and functions from fdi.h.
181 /// Refer to comments in fdi.h for documentation.
182 /// </summary>
183 internal static class FDI
184 {
185 internal const int MAX_DISK = Int32.MaxValue;
186 internal const int MAX_FILENAME = 256;
187 internal const int MAX_CABINET_NAME = 256;
188 internal const int MAX_CAB_PATH = 256;
189 internal const int MAX_DISK_NAME = 256;
190
191 internal const int CPU_80386 = 1;
192
193 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr PFNALLOC(int cb);
194 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void PFNFREE(IntPtr pv);
195
196 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNOPEN(string path, int oflag, int pmode);
197 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNREAD(int hf, IntPtr pv, int cb);
198 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNWRITE(int hf, IntPtr pv, int cb);
199 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNCLOSE(int hf);
200 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSEEK(int hf, int dist, int seektype);
201
202 [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNNOTIFY(NOTIFICATIONTYPE fdint, NOTIFICATION fdin);
203
204 /// <summary>
205 /// Error codes that can be returned by FDI.
206 /// </summary>
207 internal enum ERROR : int
208 {
209 NONE,
210 CABINET_NOT_FOUND,
211 NOT_A_CABINET,
212 UNKNOWN_CABINET_VERSION,
213 CORRUPT_CABINET,
214 ALLOC_FAIL,
215 BAD_COMPR_TYPE,
216 MDI_FAIL,
217 TARGET_FILE,
218 RESERVE_MISMATCH,
219 WRONG_CABINET,
220 USER_ABORT,
221 }
222
223 /// <summary>
224 /// Type of notification message for the FDI Notify callback.
225 /// </summary>
226 internal enum NOTIFICATIONTYPE : int
227 {
228 CABINET_INFO,
229 PARTIAL_FILE,
230 COPY_FILE,
231 CLOSE_FILE_INFO,
232 NEXT_CABINET,
233 ENUMERATE,
234 }
235
236 [DllImport("cabinet.dll", EntryPoint = "FDICreate", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
237 internal static extern Handle Create([MarshalAs(UnmanagedType.FunctionPtr)] PFNALLOC pfnalloc, [MarshalAs(UnmanagedType.FunctionPtr)] PFNFREE pfnfree, PFNOPEN pfnopen, PFNREAD pfnread, PFNWRITE pfnwrite, PFNCLOSE pfnclose, PFNSEEK pfnseek, int cpuType, IntPtr perf);
238
239 [DllImport("cabinet.dll", EntryPoint = "FDICopy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
240 internal static extern int Copy(Handle hfdi, string pszCabinet, string pszCabPath, int flags, PFNNOTIFY pfnfdin, IntPtr pfnfdid, IntPtr pvUser);
241
242 [SuppressUnmanagedCodeSecurity]
243 [DllImport("cabinet.dll", EntryPoint = "FDIDestroy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
244 [return: MarshalAs(UnmanagedType.Bool)]
245 internal static extern bool Destroy(IntPtr hfdi);
246
247 [DllImport("cabinet.dll", EntryPoint = "FDIIsCabinet", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)]
248 [SuppressMessage("Microsoft.Portability", "CA1901:PInvokeDeclarationsShouldBePortable", Justification="FDI file handles definitely remain 4 bytes on 64bit platforms.")]
249 internal static extern int IsCabinet(Handle hfdi, int hf, out CABINFO pfdici);
250
251 /// <summary>
252 /// Cabinet information structure filled in by FDI IsCabinet.
253 /// </summary>
254 [StructLayout(LayoutKind.Sequential)]
255 internal struct CABINFO
256 {
257 internal int cbCabinet;
258 internal short cFolders;
259 internal short cFiles;
260 internal short setID;
261 internal short iCabinet;
262 internal int fReserve;
263 internal int hasprev;
264 internal int hasnext;
265 }
266
267 /// <summary>
268 /// Cabinet notification details passed to the FDI Notify callback.
269 /// </summary>
270 [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")]
271 [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
272 internal class NOTIFICATION
273 {
274 internal int cb;
275 internal IntPtr psz1;
276 internal IntPtr psz2;
277 internal IntPtr psz3;
278 internal IntPtr pv;
279
280 internal IntPtr hf_ptr;
281
282 internal short date;
283 internal short time;
284 internal short attribs;
285 internal short setID;
286 internal short iCabinet;
287 internal short iFolder;
288 internal int fdie;
289
290 // Unlike all the other file handles in FCI/FDI, this one is
291 // actually pointer-sized. Use a property to pretend it isn't.
292 internal int hf
293 {
294 get { return (int) this.hf_ptr; }
295 }
296 }
297
298 /// <summary>
299 /// Ensures that the FDI handle is safely released.
300 /// </summary>
301 internal class Handle : SafeHandle
302 {
303 /// <summary>
304 /// Creates a new unintialized handle. The handle will be initialized
305 /// when it is marshalled back from native code.
306 /// </summary>
307 internal Handle()
308 : base(IntPtr.Zero, true)
309 {
310 }
311
312 /// <summary>
313 /// Checks if the handle is invalid. An FDI handle is invalid when it is zero.
314 /// </summary>
315 public override bool IsInvalid
316 {
317 get
318 {
319 return this.handle == IntPtr.Zero;
320 }
321 }
322
323 /// <summary>
324 /// Releases the handle by calling FDIDestroy().
325 /// </summary>
326 /// <returns>True if the release succeeded.</returns>
327 protected override bool ReleaseHandle()
328 {
329 return FDI.Destroy(this.handle);
330 }
331 }
332 }
333
334 /// <summary>
335 /// Error info structure for FCI and FDI.
336 /// </summary>
337 /// <remarks>Before being passed to FCI or FDI, this structure is
338 /// pinned in memory via a GCHandle. The pinning is necessary
339 /// to be able to read the results, since the ERF structure doesn't
340 /// get marshalled back out after an error.</remarks>
341 [StructLayout(LayoutKind.Sequential)]
342 internal class ERF
343 {
344 private int erfOper;
345 private int erfType;
346 private int fError;
347
348 /// <summary>
349 /// Gets or sets the cabinet error code.
350 /// </summary>
351 internal int Oper
352 {
353 get
354 {
355 return this.erfOper;
356 }
357
358 set
359 {
360 this.erfOper = value;
361 }
362 }
363
364 /// <summary>
365 /// Gets or sets the Win32 error code.
366 /// </summary>
367 internal int Type
368 {
369 get
370 {
371 return this.erfType;
372 }
373
374 set
375 {
376 this.erfType = value;
377 }
378 }
379
380 /// <summary>
381 /// GCHandle doesn't like the bool type, so use an int underneath.
382 /// </summary>
383 internal bool Error
384 {
385 get
386 {
387 return this.fError != 0;
388 }
389
390 set
391 {
392 this.fError = value ? 1 : 0;
393 }
394 }
395
396 /// <summary>
397 /// Clears the error information.
398 /// </summary>
399 internal void Clear()
400 {
401 this.Oper = 0;
402 this.Type = 0;
403 this.Error = false;
404 }
405 }
406}
407}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Cab/WixToolset.Dtf.Compression.Cab.csproj b/src/dtf/WixToolset.Dtf.Compression.Cab/WixToolset.Dtf.Compression.Cab.csproj
new file mode 100644
index 00000000..6b2c8cf8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Cab/WixToolset.Dtf.Compression.Cab.csproj
@@ -0,0 +1,26 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.Compression.Cab</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.Compression.Cab</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Managed libraries for cabinet archive packing and unpacking</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <None Include="Errors.txt" />
15 <EmbeddedResource Include="Errors.resources" />
16 </ItemGroup>
17
18 <ItemGroup>
19 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
20 </ItemGroup>
21
22 <ItemGroup>
23 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
24 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
25 </ItemGroup>
26</Project>
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/AssemblyInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/AssemblyInfo.cs
new file mode 100644
index 00000000..f782bbb8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/AssemblyInfo.cs
@@ -0,0 +1,5 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3using System.Diagnostics.CodeAnalysis;
4
5[assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", Target = "WixToolset.Dtf.Compression.Zip")]
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ConcatStream.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ConcatStream.cs
new file mode 100644
index 00000000..20d675d9
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ConcatStream.cs
@@ -0,0 +1,157 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.IO;
7
8 /// <summary>
9 /// Used to trick a DeflateStream into reading from or writing to
10 /// a series of (chunked) streams instead of a single steream.
11 /// </summary>
12 internal class ConcatStream : Stream
13 {
14 private Stream source;
15 private long position;
16 private long length;
17 private Action<ConcatStream> nextStreamHandler;
18
19 public ConcatStream(Action<ConcatStream> nextStreamHandler)
20 {
21 if (nextStreamHandler == null)
22 {
23 throw new ArgumentNullException("nextStreamHandler");
24 }
25
26 this.nextStreamHandler = nextStreamHandler;
27 this.length = Int64.MaxValue;
28 }
29
30 public Stream Source
31 {
32 get { return this.source; }
33 set { this.source = value; }
34 }
35
36 public override bool CanRead
37 {
38 get { return true; }
39 }
40
41 public override bool CanWrite
42 {
43 get { return true; }
44 }
45
46 public override bool CanSeek
47 {
48 get { return false; }
49 }
50
51 public override long Length
52 {
53 get
54 {
55 return this.length;
56 }
57 }
58
59 public override long Position
60 {
61 get { return this.position; }
62 set { throw new NotSupportedException(); }
63 }
64
65 public override int Read(byte[] buffer, int offset, int count)
66 {
67 if (this.source == null)
68 {
69 this.nextStreamHandler(this);
70 }
71
72 count = (int) Math.Min(count, this.length - this.position);
73
74 int bytesRemaining = count;
75 while (bytesRemaining > 0)
76 {
77 if (this.source == null)
78 {
79 throw new InvalidOperationException();
80 }
81
82 int partialCount = (int) Math.Min(bytesRemaining,
83 this.source.Length - this.source.Position);
84
85 if (partialCount == 0)
86 {
87 this.nextStreamHandler(this);
88 continue;
89 }
90
91 partialCount = this.source.Read(
92 buffer, offset + count - bytesRemaining, partialCount);
93 bytesRemaining -= partialCount;
94 this.position += partialCount;
95 }
96
97 return count;
98 }
99
100 public override void Write(byte[] buffer, int offset, int count)
101 {
102 if (this.source == null)
103 {
104 this.nextStreamHandler(this);
105 }
106
107 int bytesRemaining = count;
108 while (bytesRemaining > 0)
109 {
110 if (this.source == null)
111 {
112 throw new InvalidOperationException();
113 }
114
115 int partialCount = (int) Math.Min(bytesRemaining,
116 Math.Max(0, this.length - this.source.Position));
117
118 if (partialCount == 0)
119 {
120 this.nextStreamHandler(this);
121 continue;
122 }
123
124 this.source.Write(
125 buffer, offset + count - bytesRemaining, partialCount);
126 bytesRemaining -= partialCount;
127 this.position += partialCount;
128 }
129 }
130
131 public override void Flush()
132 {
133 if (this.source != null)
134 {
135 this.source.Flush();
136 }
137 }
138
139 public override long Seek(long offset, SeekOrigin origin)
140 {
141 throw new NotSupportedException();
142 }
143
144 public override void SetLength(long value)
145 {
146 this.length = value;
147 }
148
149 public override void Close()
150 {
151 if (this.source != null)
152 {
153 this.source.Close();
154 }
155 }
156 }
157}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/CrcStream.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/CrcStream.cs
new file mode 100644
index 00000000..c645ecc1
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/CrcStream.cs
@@ -0,0 +1,250 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.IO;
7 using System.Diagnostics.CodeAnalysis;
8
9 /// <summary>
10 /// Wraps a source stream and calcaluates a CRC over all bytes that are read or written.
11 /// </summary>
12 /// <remarks>
13 /// The CRC algorithm matches that used in the standard ZIP file format.
14 /// </remarks>
15 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Crc")]
16 public class CrcStream : Stream
17 {
18 private Stream source;
19 private uint crc;
20
21 /// <summary>
22 /// Creates a new CrcStream instance from a source stream.
23 /// </summary>
24 /// <param name="source">Underlying stream where bytes will be read from or written to.</param>
25 public CrcStream(Stream source)
26 {
27 this.source = source;
28 }
29
30 /// <summary>
31 /// Gets the current CRC over all bytes that have been read or written
32 /// since this instance was created.
33 /// </summary>
34 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Crc")]
35 public uint Crc
36 {
37 get
38 {
39 return this.crc;
40 }
41 }
42
43 /// <summary>
44 /// Gets the underlying stream that this stream reads from or writes to.
45 /// </summary>
46 public Stream Source
47 {
48 get
49 {
50 return this.source;
51 }
52 }
53
54 /// <summary>
55 /// Gets a value indicating whether the source stream supports reading.
56 /// </summary>
57 /// <value>true if the stream supports reading; otherwise, false.</value>
58 public override bool CanRead
59 {
60 get
61 {
62 return this.source.CanRead;
63 }
64 }
65
66 /// <summary>
67 /// Gets a value indicating whether the source stream supports writing.
68 /// </summary>
69 /// <value>true if the stream supports writing; otherwise, false.</value>
70 public override bool CanWrite
71 {
72 get
73 {
74 return this.source.CanWrite;
75 }
76 }
77
78 /// <summary>
79 /// Gets a value indicating whether the source stream supports seeking.
80 /// </summary>
81 /// <value>true if the stream supports seeking; otherwise, false.</value>
82 public override bool CanSeek
83 {
84 get
85 {
86 return this.source.CanSeek;
87 }
88 }
89
90 /// <summary>
91 /// Gets the length of the source stream.
92 /// </summary>
93 public override long Length
94 {
95 get
96 {
97 return this.source.Length;
98 }
99 }
100
101 /// <summary>
102 /// Gets or sets the position of the source stream.
103 /// </summary>
104 public override long Position
105 {
106 get
107 {
108 return this.source.Position;
109 }
110
111 set
112 {
113 this.source.Position = value;
114 }
115 }
116
117 /// <summary>
118 /// Sets the position within the source stream.
119 /// </summary>
120 /// <param name="offset">A byte offset relative to the origin parameter.</param>
121 /// <param name="origin">A value of type SeekOrigin indicating
122 /// the reference point used to obtain the new position.</param>
123 /// <returns>The new position within the source stream.</returns>
124 /// <remarks>
125 /// Note the CRC is only calculated over bytes that are actually read or
126 /// written, so any bytes skipped by seeking will not contribute to the CRC.
127 /// </remarks>
128 public override long Seek(long offset, SeekOrigin origin)
129 {
130 return this.source.Seek(offset, origin);
131 }
132
133 /// <summary>
134 /// Sets the length of the source stream.
135 /// </summary>
136 /// <param name="value">The desired length of the
137 /// stream in bytes.</param>
138 public override void SetLength(long value)
139 {
140 this.source.SetLength(value);
141 }
142
143 /// <summary>
144 /// Reads a sequence of bytes from the source stream and advances
145 /// the position within the stream by the number of bytes read.
146 /// </summary>
147 /// <param name="buffer">An array of bytes. When this method returns, the buffer
148 /// contains the specified byte array with the values between offset and
149 /// (offset + count - 1) replaced by the bytes read from the current source.</param>
150 /// <param name="offset">The zero-based byte offset in buffer at which to begin
151 /// storing the data read from the current stream.</param>
152 /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
153 /// <returns>The total number of bytes read into the buffer. This can be less
154 /// than the number of bytes requested if that many bytes are not currently available,
155 /// or zero (0) if the end of the stream has been reached.</returns>
156 public override int Read(byte[] buffer, int offset, int count)
157 {
158 count = this.source.Read(buffer, offset, count);
159 this.UpdateCrc(buffer, offset, count);
160 return count;
161 }
162
163 /// <summary>
164 /// Writes a sequence of bytes to the source stream and advances the
165 /// current position within this stream by the number of bytes written.
166 /// </summary>
167 /// <param name="buffer">An array of bytes. This method copies count
168 /// bytes from buffer to the current stream.</param>
169 /// <param name="offset">The zero-based byte offset in buffer at which
170 /// to begin copying bytes to the current stream.</param>
171 /// <param name="count">The number of bytes to be written to the
172 /// current stream.</param>
173 public override void Write(byte[] buffer, int offset, int count)
174 {
175 this.source.Write(buffer, offset, count);
176 this.UpdateCrc(buffer, offset, count);
177 }
178
179 /// <summary>
180 /// Flushes the source stream.
181 /// </summary>
182 public override void Flush()
183 {
184 this.source.Flush();
185 }
186
187 /// <summary>
188 /// Closes the underlying stream.
189 /// </summary>
190 public override void Close()
191 {
192 this.source.Close();
193 base.Close();
194 }
195
196 /// <summary>
197 /// Updates the CRC with a range of bytes that were read or written.
198 /// </summary>
199 private void UpdateCrc(byte[] buffer, int offset, int count)
200 {
201 this.crc = ~this.crc;
202 for( ; count > 0; count--, offset++)
203 {
204 this.crc = (this.crc >> 8) ^
205 CrcStream.crcTable[(this.crc & 0xFF) ^ buffer[offset]];
206 }
207 this.crc = ~this.crc;
208 }
209
210 private static uint[] crcTable = MakeCrcTable();
211
212 /// <summary>
213 /// Computes a table that speeds up calculation of the CRC.
214 /// </summary>
215 private static uint[] MakeCrcTable()
216 {
217 const uint poly = 0x04C11DB7u;
218 uint[] crcTable = new uint[256];
219 for(uint n = 0; n < 256; n++)
220 {
221 uint c = CrcStream.Reflect(n, 8);
222 c = c << 24;
223 for(uint k = 0; k < 8; k++)
224 {
225 c = (c << 1) ^ ((c & 0x80000000u) != 0 ? poly : 0);
226 }
227 crcTable[n] = CrcStream.Reflect(c, 32);
228 }
229 return crcTable;
230 }
231
232 /// <summary>
233 /// Reflects the ordering of certain number of bits. For exmample when reflecting
234 /// one byte, bit one is swapped with bit eight, bit two with bit seven, etc.
235 /// </summary>
236 private static uint Reflect(uint value, int bits)
237 {
238 for (int i = 0; i < bits / 2; i++)
239 {
240 uint leftBit = 1u << (bits - 1 - i);
241 uint rightBit = 1u << i;
242 if (((value & leftBit) != 0) != ((value & rightBit) != 0))
243 {
244 value ^= leftBit | rightBit;
245 }
246 }
247 return value;
248 }
249 }
250}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/WixToolset.Dtf.Compression.Zip.csproj b/src/dtf/WixToolset.Dtf.Compression.Zip/WixToolset.Dtf.Compression.Zip.csproj
new file mode 100644
index 00000000..2f5f2f27
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/WixToolset.Dtf.Compression.Zip.csproj
@@ -0,0 +1,21 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.Compression.Zip</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.Compression.Zip</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Managed libraries for zip archive packing and unpacking</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
15 </ItemGroup>
16
17 <ItemGroup>
18 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
19 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
20 </ItemGroup>
21</Project>
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipCompressionMethod.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipCompressionMethod.cs
new file mode 100644
index 00000000..2e1c7567
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipCompressionMethod.cs
@@ -0,0 +1,80 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System.Diagnostics.CodeAnalysis;
6
7 /// <summary>
8 /// Identifies the compression method or &quot;algorithm&quot;
9 /// used for a single file within a zip archive.
10 /// </summary>
11 /// <remarks>
12 /// Proprietary zip implementations may define additional compression
13 /// methods outside of those included here.
14 /// </remarks>
15 public enum ZipCompressionMethod
16 {
17 /// <summary>
18 /// The file is stored (no compression)
19 /// </summary>
20 Store = 0,
21
22 /// <summary>
23 /// The file is Shrunk
24 /// </summary>
25 Shrink = 1,
26
27 /// <summary>
28 /// The file is Reduced with compression factor 1
29 /// </summary>
30 Reduce1 = 2,
31
32 /// <summary>
33 /// The file is Reduced with compression factor 2
34 /// </summary>
35 Reduce2 = 3,
36
37 /// <summary>
38 /// The file is Reduced with compression factor 3
39 /// </summary>
40 Reduce3 = 4,
41
42 /// <summary>
43 /// The file is Reduced with compression factor 4
44 /// </summary>
45 Reduce4 = 5,
46
47 /// <summary>
48 /// The file is Imploded
49 /// </summary>
50 Implode = 6,
51
52 /// <summary>
53 /// The file is Deflated;
54 /// the most common and widely-compatible form of zip compression.
55 /// </summary>
56 Deflate = 8,
57
58 /// <summary>
59 /// The file is Deflated using the enhanced Deflate64 method.
60 /// </summary>
61 Deflate64 = 9,
62
63 /// <summary>
64 /// The file is compressed using the BZIP2 algorithm.
65 /// </summary>
66 BZip2 = 12,
67
68 /// <summary>
69 /// The file is compressed using the LZMA algorithm.
70 /// </summary>
71 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lzma")]
72 Lzma = 14,
73
74 /// <summary>
75 /// The file is compressed using the PPMd algorithm.
76 /// </summary>
77 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppmd")]
78 Ppmd = 98
79 }
80}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs
new file mode 100644
index 00000000..36b4db89
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipEngine.cs
@@ -0,0 +1,478 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.IO;
7 using System.IO.Compression;
8 using System.Collections.Generic;
9 using System.Reflection;
10 using System.Diagnostics.CodeAnalysis;
11
12 /// <summary>
13 /// Engine capable of packing and unpacking archives in the zip format.
14 /// </summary>
15 public partial class ZipEngine : CompressionEngine
16 {
17 private static Dictionary<ZipCompressionMethod, Converter<Stream, Stream>>
18 compressionStreamCreators;
19 private static Dictionary<ZipCompressionMethod, Converter<Stream, Stream>>
20 decompressionStreamCreators;
21
22 private static void InitCompressionStreamCreators()
23 {
24 if (ZipEngine.compressionStreamCreators == null)
25 {
26 ZipEngine.compressionStreamCreators = new
27 Dictionary<ZipCompressionMethod, Converter<Stream, Stream>>();
28 ZipEngine.decompressionStreamCreators = new
29 Dictionary<ZipCompressionMethod, Converter<Stream, Stream>>();
30
31 ZipEngine.RegisterCompressionStreamCreator(
32 ZipCompressionMethod.Store,
33 CompressionMode.Compress,
34 delegate(Stream stream) {
35 return stream;
36 });
37 ZipEngine.RegisterCompressionStreamCreator(
38 ZipCompressionMethod.Deflate,
39 CompressionMode.Compress,
40 delegate(Stream stream) {
41 return new DeflateStream(stream, CompressionMode.Compress, true);
42 });
43 ZipEngine.RegisterCompressionStreamCreator(
44 ZipCompressionMethod.Store,
45 CompressionMode.Decompress,
46 delegate(Stream stream) {
47 return stream;
48 });
49 ZipEngine.RegisterCompressionStreamCreator(
50 ZipCompressionMethod.Deflate,
51 CompressionMode.Decompress,
52 delegate(Stream stream) {
53 return new DeflateStream(stream, CompressionMode.Decompress, true);
54 });
55 }
56 }
57
58 /// <summary>
59 /// Registers a delegate that can create a warpper stream for
60 /// compressing or uncompressing the data of a source stream.
61 /// </summary>
62 /// <param name="compressionMethod">Compression method being registered.</param>
63 /// <param name="compressionMode">Indicates registration for ether
64 /// compress or decompress mode.</param>
65 /// <param name="creator">Delegate being registered.</param>
66 /// <remarks>
67 /// For compression, the delegate accepts a stream that writes to the archive
68 /// and returns a wrapper stream that compresses bytes as they are written.
69 /// For decompression, the delegate accepts a stream that reads from the archive
70 /// and returns a wrapper stream that decompresses bytes as they are read.
71 /// This wrapper stream model follows the design used by
72 /// System.IO.Compression.DeflateStream, and indeed that class is used
73 /// to implement the Deflate compression method by default.
74 /// <para>To unregister a delegate, call this method again and pass
75 /// null for the delegate parameter.</para>
76 /// </remarks>
77 /// <example>
78 /// When the ZipEngine class is initialized, the Deflate compression method
79 /// is automatically registered like this:
80 /// <code>
81 /// ZipEngine.RegisterCompressionStreamCreator(
82 /// ZipCompressionMethod.Deflate,
83 /// CompressionMode.Compress,
84 /// delegate(Stream stream) {
85 /// return new DeflateStream(stream, CompressionMode.Compress, true);
86 /// });
87 /// ZipEngine.RegisterCompressionStreamCreator(
88 /// ZipCompressionMethod.Deflate,
89 /// CompressionMode.Decompress,
90 /// delegate(Stream stream) {
91 /// return new DeflateStream(stream, CompressionMode.Decompress, true);
92 /// });
93 /// </code></example>
94 public static void RegisterCompressionStreamCreator(
95 ZipCompressionMethod compressionMethod,
96 CompressionMode compressionMode,
97 Converter<Stream, Stream> creator)
98 {
99 ZipEngine.InitCompressionStreamCreators();
100 if (compressionMode == CompressionMode.Compress)
101 {
102 ZipEngine.compressionStreamCreators[compressionMethod] = creator;
103 }
104 else
105 {
106 ZipEngine.decompressionStreamCreators[compressionMethod] = creator;
107 }
108 }
109
110 // Progress data
111 private string currentFileName;
112 private int currentFileNumber;
113 private int totalFiles;
114 private long currentFileBytesProcessed;
115 private long currentFileTotalBytes;
116 private string mainArchiveName;
117 private string currentArchiveName;
118 private short currentArchiveNumber;
119 private short totalArchives;
120 private long currentArchiveBytesProcessed;
121 private long currentArchiveTotalBytes;
122 private long fileBytesProcessed;
123 private long totalFileBytes;
124 private string comment;
125
126 /// <summary>
127 /// Creates a new instance of the zip engine.
128 /// </summary>
129 public ZipEngine()
130 : base()
131 {
132 ZipEngine.InitCompressionStreamCreators();
133 }
134
135 /// <summary>
136 /// Gets the comment from the last-examined archive,
137 /// or sets the comment to be added to any created archives.
138 /// </summary>
139 public string ArchiveComment
140 {
141 get
142 {
143 return this.comment;
144 }
145 set
146 {
147 this.comment = value;
148 }
149 }
150
151 /// <summary>
152 /// Checks whether a Stream begins with a header that indicates
153 /// it is a valid archive file.
154 /// </summary>
155 /// <param name="stream">Stream for reading the archive file.</param>
156 /// <returns>True if the stream is a valid zip archive
157 /// (with no offset); false otherwise.</returns>
158 public override bool IsArchive(Stream stream)
159 {
160 if (stream == null)
161 {
162 throw new ArgumentNullException("stream");
163 }
164
165 if (stream.Length - stream.Position < 4)
166 {
167 return false;
168 }
169
170 BinaryReader reader = new BinaryReader(stream);
171 uint sig = reader.ReadUInt32();
172 switch (sig)
173 {
174 case ZipFileHeader.LFHSIG:
175 case ZipEndOfCentralDirectory.EOCDSIG:
176 case ZipEndOfCentralDirectory.EOCD64SIG:
177 case ZipFileHeader.SPANSIG:
178 case ZipFileHeader.SPANSIG2:
179 return true;
180 default:
181 return false;
182 }
183 }
184
185 /// <summary>
186 /// Gets the offset of an archive that is positioned 0 or more bytes
187 /// from the start of the Stream.
188 /// </summary>
189 /// <param name="stream">A stream for reading the archive.</param>
190 /// <returns>The offset in bytes of the archive,
191 /// or -1 if no archive is found in the Stream.</returns>
192 /// <remarks>The archive must begin on a 4-byte boundary.</remarks>
193 public override long FindArchiveOffset(Stream stream)
194 {
195 long offset = base.FindArchiveOffset(stream);
196 if (offset > 0)
197 {
198 // Some self-extract packages include the exe stub in file offset calculations.
199 // Check the first header directory offset to decide whether the entire
200 // archive needs to be offset or not.
201
202 ZipEndOfCentralDirectory eocd = this.GetEOCD(null, stream);
203 if (eocd != null && eocd.totalEntries > 0)
204 {
205 stream.Seek(eocd.dirOffset, SeekOrigin.Begin);
206
207 ZipFileHeader header = new ZipFileHeader();
208 if (header.Read(stream, true) && header.localHeaderOffset < stream.Length)
209 {
210 stream.Seek(header.localHeaderOffset, SeekOrigin.Begin);
211 if (header.Read(stream, false))
212 {
213 return 0;
214 }
215 }
216 }
217 }
218
219 return offset;
220 }
221
222 /// <summary>
223 /// Gets information about files in a zip archive or archive chain.
224 /// </summary>
225 /// <param name="streamContext">A context interface to handle opening
226 /// and closing of archive and file streams.</param>
227 /// <param name="fileFilter">A predicate that can determine
228 /// which files to process, optional.</param>
229 /// <returns>Information about files in the archive stream.</returns>
230 /// <exception cref="ArchiveException">The archive provided
231 /// by the stream context is not valid.</exception>
232 /// <remarks>
233 /// The <paramref name="fileFilter"/> predicate takes an internal file
234 /// path and returns true to include the file or false to exclude it.
235 /// </remarks>
236 public override IList<ArchiveFileInfo> GetFileInfo(
237 IUnpackStreamContext streamContext,
238 Predicate<string> fileFilter)
239 {
240 if (streamContext == null)
241 {
242 throw new ArgumentNullException("streamContext");
243 }
244
245 lock (this)
246 {
247 IList<ZipFileHeader> headers = this.GetCentralDirectory(streamContext);
248 if (headers == null)
249 {
250 throw new ZipException("Zip central directory not found.");
251 }
252
253 List<ArchiveFileInfo> files = new List<ArchiveFileInfo>(headers.Count);
254 foreach (ZipFileHeader header in headers)
255 {
256 if (!header.IsDirectory &&
257 (fileFilter == null || fileFilter(header.fileName)))
258 {
259 files.Add(header.ToZipFileInfo());
260 }
261 }
262
263 return files.AsReadOnly();
264 }
265 }
266
267 /// <summary>
268 /// Reads all the file headers from the central directory in the main archive.
269 /// </summary>
270 private IList<ZipFileHeader> GetCentralDirectory(IUnpackStreamContext streamContext)
271 {
272 Stream archiveStream = null;
273 this.currentArchiveNumber = 0;
274 try
275 {
276 List<ZipFileHeader> headers = new List<ZipFileHeader>();
277 archiveStream = this.OpenArchive(streamContext, 0);
278
279 ZipEndOfCentralDirectory eocd = this.GetEOCD(streamContext, archiveStream);
280 if (eocd == null)
281 {
282 return null;
283 }
284 else if (eocd.totalEntries == 0)
285 {
286 return headers;
287 }
288
289 headers.Capacity = (int) eocd.totalEntries;
290
291 if (eocd.dirOffset > archiveStream.Length - ZipFileHeader.CFH_FIXEDSIZE)
292 {
293 streamContext.CloseArchiveReadStream(
294 this.currentArchiveNumber, String.Empty, archiveStream);
295 archiveStream = null;
296 }
297 else
298 {
299 archiveStream.Seek(eocd.dirOffset, SeekOrigin.Begin);
300 uint sig = new BinaryReader(archiveStream).ReadUInt32();
301 if (sig != ZipFileHeader.CFHSIG)
302 {
303 streamContext.CloseArchiveReadStream(
304 this.currentArchiveNumber, String.Empty, archiveStream);
305 archiveStream = null;
306 }
307 }
308
309 if (archiveStream == null)
310 {
311 this.currentArchiveNumber = (short) (eocd.dirStartDiskNumber + 1);
312 archiveStream = streamContext.OpenArchiveReadStream(
313 this.currentArchiveNumber, String.Empty, this);
314
315 if (archiveStream == null)
316 {
317 return null;
318 }
319 }
320
321 archiveStream.Seek(eocd.dirOffset, SeekOrigin.Begin);
322
323 while (headers.Count < eocd.totalEntries)
324 {
325 ZipFileHeader header = new ZipFileHeader();
326 if (!header.Read(archiveStream, true))
327 {
328 throw new ZipException(
329 "Missing or invalid central directory file header");
330 }
331
332 headers.Add(header);
333
334 if (headers.Count < eocd.totalEntries &&
335 archiveStream.Position == archiveStream.Length)
336 {
337 streamContext.CloseArchiveReadStream(
338 this.currentArchiveNumber, String.Empty, archiveStream);
339 this.currentArchiveNumber++;
340 archiveStream = streamContext.OpenArchiveReadStream(
341 this.currentArchiveNumber, String.Empty, this);
342 if (archiveStream == null)
343 {
344 this.currentArchiveNumber = 0;
345 archiveStream = streamContext.OpenArchiveReadStream(
346 this.currentArchiveNumber, String.Empty, this);
347 }
348 }
349 }
350
351 return headers;
352 }
353 finally
354 {
355 if (archiveStream != null)
356 {
357 streamContext.CloseArchiveReadStream(
358 this.currentArchiveNumber, String.Empty, archiveStream);
359 }
360 }
361 }
362
363 /// <summary>
364 /// Locates and reads the end of central directory record near the
365 /// end of the archive.
366 /// </summary>
367 [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
368 [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "streamContext")]
369 private ZipEndOfCentralDirectory GetEOCD(
370 IUnpackStreamContext streamContext, Stream archiveStream)
371 {
372 BinaryReader reader = new BinaryReader(archiveStream);
373 long offset = archiveStream.Length
374 - ZipEndOfCentralDirectory.EOCD_RECORD_FIXEDSIZE;
375 while (offset >= 0)
376 {
377 archiveStream.Seek(offset, SeekOrigin.Begin);
378
379 uint sig = reader.ReadUInt32();
380 if (sig == ZipEndOfCentralDirectory.EOCDSIG)
381 {
382 break;
383 }
384
385 offset--;
386 }
387
388 if (offset < 0)
389 {
390 return null;
391 }
392
393 ZipEndOfCentralDirectory eocd = new ZipEndOfCentralDirectory();
394 archiveStream.Seek(offset, SeekOrigin.Begin);
395 if (!eocd.Read(archiveStream))
396 {
397 throw new ZipException("Invalid end of central directory record");
398 }
399
400 if (eocd.dirOffset == (long) UInt32.MaxValue)
401 {
402 string saveComment = eocd.comment;
403
404 archiveStream.Seek(
405 offset - Zip64EndOfCentralDirectoryLocator.EOCDL64_SIZE,
406 SeekOrigin.Begin);
407
408 Zip64EndOfCentralDirectoryLocator eocdl =
409 new Zip64EndOfCentralDirectoryLocator();
410 if (!eocdl.Read(archiveStream))
411 {
412 throw new ZipException("Missing or invalid end of " +
413 "central directory record locator");
414 }
415
416 if (eocdl.dirStartDiskNumber == eocdl.totalDisks - 1)
417 {
418 // ZIP64 eocd is entirely in current stream.
419 archiveStream.Seek(eocdl.dirOffset, SeekOrigin.Begin);
420 if (!eocd.Read(archiveStream))
421 {
422 throw new ZipException("Missing or invalid ZIP64 end of " +
423 "central directory record");
424 }
425 }
426 else if (streamContext == null)
427 {
428 return null;
429 }
430 else
431 {
432 // TODO: handle EOCD64 spanning archives!
433 throw new NotImplementedException("Zip implementation does not " +
434 "handle end of central directory record that spans archives.");
435 }
436
437 eocd.comment = saveComment;
438 }
439
440 return eocd;
441 }
442
443 private void ResetProgressData()
444 {
445 this.currentFileName = null;
446 this.currentFileNumber = 0;
447 this.totalFiles = 0;
448 this.currentFileBytesProcessed = 0;
449 this.currentFileTotalBytes = 0;
450 this.currentArchiveName = null;
451 this.currentArchiveNumber = 0;
452 this.totalArchives = 0;
453 this.currentArchiveBytesProcessed = 0;
454 this.currentArchiveTotalBytes = 0;
455 this.fileBytesProcessed = 0;
456 this.totalFileBytes = 0;
457 }
458
459 private void OnProgress(ArchiveProgressType progressType)
460 {
461 ArchiveProgressEventArgs e = new ArchiveProgressEventArgs(
462 progressType,
463 this.currentFileName,
464 this.currentFileNumber >= 0 ? this.currentFileNumber : 0,
465 this.totalFiles,
466 this.currentFileBytesProcessed,
467 this.currentFileTotalBytes,
468 this.currentArchiveName,
469 this.currentArchiveNumber,
470 this.totalArchives,
471 this.currentArchiveBytesProcessed,
472 this.currentArchiveTotalBytes,
473 this.fileBytesProcessed,
474 this.totalFileBytes);
475 this.OnProgress(e);
476 }
477 }
478}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipException.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipException.cs
new file mode 100644
index 00000000..50fd6156
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipException.cs
@@ -0,0 +1,60 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.IO;
7 using System.Resources;
8 using System.Globalization;
9 using System.Runtime.Serialization;
10
11 /// <summary>
12 /// Exception class for zip operations.
13 /// </summary>
14 [Serializable]
15 public class ZipException : ArchiveException
16 {
17 /// <summary>
18 /// Creates a new ZipException with a specified error message and a reference to the
19 /// inner exception that is the cause of this exception.
20 /// </summary>
21 /// <param name="message">The message that describes the error.</param>
22 /// <param name="innerException">The exception that is the cause of the current exception. If the
23 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
24 /// is raised in a catch block that handles the inner exception.</param>
25 public ZipException(string message, Exception innerException)
26 : base(message, innerException) { }
27
28 /// <summary>
29 /// Creates a new ZipException with a specified error message.
30 /// </summary>
31 /// <param name="message">The message that describes the error.</param>
32 public ZipException(string message)
33 : this(message, null) { }
34
35 /// <summary>
36 /// Creates a new ZipException.
37 /// </summary>
38 public ZipException()
39 : this(null, null) { }
40
41 /// <summary>
42 /// Initializes a new instance of the ZipException class with serialized data.
43 /// </summary>
44 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
45 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
46 protected ZipException(SerializationInfo info, StreamingContext context) : base(info, context)
47 {
48 }
49
50 /// <summary>
51 /// Sets the SerializationInfo with information about the exception.
52 /// </summary>
53 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
54 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
55 public override void GetObjectData(SerializationInfo info, StreamingContext context)
56 {
57 base.GetObjectData(info, context);
58 }
59 }
60}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFileInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFileInfo.cs
new file mode 100644
index 00000000..d865bbba
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFileInfo.cs
@@ -0,0 +1,104 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.IO;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Object representing a compressed file within a zip package; provides operations for getting
11 /// the file properties and extracting the file.
12 /// </summary>
13 [Serializable]
14 public class ZipFileInfo : ArchiveFileInfo
15 {
16 private long compressedLength;
17 private ZipCompressionMethod compressionMethod;
18
19 /// <summary>
20 /// Creates a new ZipFileInfo object representing a file within a zip in a specified path.
21 /// </summary>
22 /// <param name="zipInfo">An object representing the zip archive containing the file.</param>
23 /// <param name="filePath">The path to the file within the zip archive. Usually, this is a simple file
24 /// name, but if the zip archive contains a directory structure this may include the directory.</param>
25 public ZipFileInfo(ZipInfo zipInfo, string filePath)
26 : base(zipInfo, filePath)
27 {
28 if (zipInfo == null)
29 {
30 throw new ArgumentNullException("zipInfo");
31 }
32 }
33
34 /// <summary>
35 /// Creates a new ZipFileInfo object with all parameters specified,
36 /// used internally when reading the metadata out of a zip archive.
37 /// </summary>
38 /// <param name="filePath">The internal path and name of the file in the zip archive.</param>
39 /// <param name="zipNumber">The zip archive number where the file starts.</param>
40 /// <param name="attributes">The stored attributes of the file.</param>
41 /// <param name="lastWriteTime">The stored last write time of the file.</param>
42 /// <param name="length">The uncompressed size of the file.</param>
43 /// <param name="compressedLength">The compressed size of the file.</param>
44 /// <param name="compressionMethod">Compression algorithm used for this file.</param>
45 internal ZipFileInfo(
46 string filePath,
47 int zipNumber,
48 FileAttributes attributes,
49 DateTime lastWriteTime,
50 long length,
51 long compressedLength,
52 ZipCompressionMethod compressionMethod)
53 : base(filePath, zipNumber, attributes, lastWriteTime, length)
54 {
55 this.compressedLength = compressedLength;
56 this.compressionMethod = compressionMethod;
57 }
58
59 /// <summary>
60 /// Initializes a new instance of the ZipFileInfo class with serialized data.
61 /// </summary>
62 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
63 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
64 protected ZipFileInfo(SerializationInfo info, StreamingContext context)
65 : base(info, context)
66 {
67 this.compressedLength = info.GetInt64("compressedLength");
68 }
69
70 /// <summary>
71 /// Gets the compressed size of the file in bytes.
72 /// </summary>
73 public long CompressedLength
74 {
75 get
76 {
77 return this.compressedLength;
78 }
79 }
80
81 /// <summary>
82 /// Gets the method used to compress this file.
83 /// </summary>
84 public ZipCompressionMethod CompressionMethod
85 {
86 get
87 {
88 return this.compressionMethod;
89 }
90 }
91
92 /// <summary>
93 /// Sets the SerializationInfo with information about the archive.
94 /// </summary>
95 /// <param name="info">The SerializationInfo that holds the serialized object data.</param>
96 /// <param name="context">The StreamingContext that contains contextual information
97 /// about the source or destination.</param>
98 public override void GetObjectData(SerializationInfo info, StreamingContext context)
99 {
100 base.GetObjectData(info, context);
101 info.AddValue("compressedLength", this.compressedLength);
102 }
103 }
104}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFormat.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFormat.cs
new file mode 100644
index 00000000..dc5e1137
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipFormat.cs
@@ -0,0 +1,697 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.Globalization;
7 using System.IO;
8 using System.Text;
9 using System.Collections.Generic;
10 using System.Diagnostics.CodeAnalysis;
11
12 [Flags]
13 internal enum ZipFileFlags : ushort
14 {
15 None = 0x0000,
16 Encrypt = 0x0001,
17 CompressOption1 = 0x0002,
18 CompressOption2 = 0x0004,
19 DataDescriptor = 0x0008,
20 StrongEncrypt = 0x0040,
21 UTF8 = 0x0800
22 }
23
24 internal enum ZipExtraFileFieldType : ushort
25 {
26 ZIP64 = 0x0001,
27 NTFS_TIMES = 0x000A,
28 NTFS_ACLS = 0x4453,
29 EXTIME = 0x5455
30 }
31
32 internal class ZipFileHeader
33 {
34 public const uint LFHSIG = 0x04034B50;
35 public const uint CFHSIG = 0x02014B50;
36
37 public const uint SPANSIG = 0x08074b50;
38 public const uint SPANSIG2 = 0x30304b50;
39
40 public const uint LFH_FIXEDSIZE = 30;
41 public const uint CFH_FIXEDSIZE = 46;
42
43 public ushort versionMadeBy;
44 public ushort versionNeeded;
45 public ZipFileFlags flags;
46 public ZipCompressionMethod compressionMethod;
47 public short lastModTime;
48 public short lastModDate;
49 public uint crc32;
50 public uint compressedSize;
51 public uint uncompressedSize;
52 public ushort diskStart;
53 public ushort internalFileAttrs;
54 public uint externalFileAttrs;
55 public uint localHeaderOffset;
56 public string fileName;
57 public ZipExtraFileField[] extraFields;
58 public string fileComment;
59 public bool zip64;
60
61 public ZipFileHeader()
62 {
63 this.versionMadeBy = 20;
64 this.versionNeeded = 20;
65 }
66
67 public ZipFileHeader(ZipFileInfo fileInfo, bool zip64)
68 : this()
69 {
70 this.flags = ZipFileFlags.None;
71 this.compressionMethod = fileInfo.CompressionMethod;
72 this.fileName = Path.Combine(fileInfo.Path, fileInfo.Name);
73 CompressionEngine.DateTimeToDosDateAndTime(
74 fileInfo.LastWriteTime, out this.lastModDate, out this.lastModTime);
75 this.zip64 = zip64;
76
77 if (this.zip64)
78 {
79 this.compressedSize = UInt32.MaxValue;
80 this.uncompressedSize = UInt32.MaxValue;
81 this.diskStart = UInt16.MaxValue;
82 this.versionMadeBy = 45;
83 this.versionNeeded = 45;
84 ZipExtraFileField field = new ZipExtraFileField();
85 field.fieldType = ZipExtraFileFieldType.ZIP64;
86 field.SetZip64Data(
87 fileInfo.CompressedLength,
88 fileInfo.Length,
89 0,
90 fileInfo.ArchiveNumber);
91 this.extraFields = new ZipExtraFileField[] { field };
92 }
93 else
94 {
95 this.compressedSize = (uint) fileInfo.CompressedLength;
96 this.uncompressedSize = (uint) fileInfo.Length;
97 this.diskStart = (ushort) fileInfo.ArchiveNumber;
98 }
99 }
100
101 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "compressedSize")]
102 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "uncompressedSize")]
103 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "crc32")]
104 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "localHeaderOffset")]
105 public void Update(
106 long compressedSize,
107 long uncompressedSize,
108 uint crc32,
109 long localHeaderOffset,
110 int archiveNumber)
111 {
112 this.crc32 = crc32;
113
114 if (this.zip64)
115 {
116 this.compressedSize = UInt32.MaxValue;
117 this.uncompressedSize = UInt32.MaxValue;
118 this.localHeaderOffset = UInt32.MaxValue;
119 this.diskStart = UInt16.MaxValue;
120
121 if (this.extraFields != null)
122 {
123 foreach (ZipExtraFileField field in this.extraFields)
124 {
125 if (field.fieldType == ZipExtraFileFieldType.ZIP64)
126 {
127 field.SetZip64Data(
128 compressedSize,
129 uncompressedSize,
130 localHeaderOffset,
131 archiveNumber);
132 }
133 }
134 }
135 }
136 else
137 {
138 this.compressedSize = (uint) compressedSize;
139 this.uncompressedSize = (uint) uncompressedSize;
140 this.localHeaderOffset = (uint) localHeaderOffset;
141 this.diskStart = (ushort) archiveNumber;
142 }
143 }
144
145 public bool Read(Stream stream, bool central)
146 {
147 long startPos = stream.Position;
148
149 if (stream.Length - startPos <
150 (central ? CFH_FIXEDSIZE : LFH_FIXEDSIZE))
151 {
152 return false;
153 }
154
155 BinaryReader reader = new BinaryReader(stream);
156 uint sig = reader.ReadUInt32();
157
158 if (sig == SPANSIG || sig == SPANSIG2)
159 {
160 // Spanned zip files may optionally begin with a special marker.
161 // Just ignore it and move on.
162 sig = reader.ReadUInt32();
163 }
164
165 if (sig != (central ? CFHSIG : LFHSIG))
166 {
167 return false;
168 }
169
170 this.versionMadeBy = (central ? reader.ReadUInt16() : (ushort) 0);
171 this.versionNeeded = reader.ReadUInt16();
172 this.flags = (ZipFileFlags) reader.ReadUInt16();
173 this.compressionMethod = (ZipCompressionMethod) reader.ReadUInt16();
174 this.lastModTime = reader.ReadInt16();
175 this.lastModDate = reader.ReadInt16();
176 this.crc32 = reader.ReadUInt32();
177 this.compressedSize = reader.ReadUInt32();
178 this.uncompressedSize = reader.ReadUInt32();
179
180 this.zip64 = this.uncompressedSize == UInt32.MaxValue;
181
182 int fileNameLength = reader.ReadUInt16();
183 int extraFieldLength = reader.ReadUInt16();
184 int fileCommentLength;
185
186 if (central)
187 {
188 fileCommentLength = reader.ReadUInt16();
189
190 this.diskStart = reader.ReadUInt16();
191 this.internalFileAttrs = reader.ReadUInt16();
192 this.externalFileAttrs = reader.ReadUInt32();
193 this.localHeaderOffset = reader.ReadUInt32();
194 }
195 else
196 {
197 fileCommentLength = 0;
198 this.diskStart = 0;
199 this.internalFileAttrs = 0;
200 this.externalFileAttrs = 0;
201 this.localHeaderOffset = 0;
202 }
203
204 if (stream.Length - stream.Position <
205 fileNameLength + extraFieldLength + fileCommentLength)
206 {
207 return false;
208 }
209
210 Encoding headerEncoding = ((this.flags | ZipFileFlags.UTF8) != 0 ?
211 Encoding.UTF8 : Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage));
212
213 byte[] fileNameBytes = reader.ReadBytes(fileNameLength);
214 this.fileName = headerEncoding.GetString(fileNameBytes).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
215
216 List<ZipExtraFileField> fields = new List<ZipExtraFileField>();
217 while (extraFieldLength > 0)
218 {
219 ZipExtraFileField field = new ZipExtraFileField();
220 if (!field.Read(stream, ref extraFieldLength))
221 {
222 return false;
223 }
224 fields.Add(field);
225 if (field.fieldType == ZipExtraFileFieldType.ZIP64)
226 {
227 this.zip64 = true;
228 }
229 }
230 this.extraFields = fields.ToArray();
231
232 byte[] fileCommentBytes = reader.ReadBytes(fileCommentLength);
233 this.fileComment = headerEncoding.GetString(fileCommentBytes);
234
235 return true;
236 }
237
238 public void Write(Stream stream, bool central)
239 {
240 byte[] fileNameBytes = (this.fileName != null
241 ? Encoding.UTF8.GetBytes(this.fileName) : new byte[0]);
242 byte[] fileCommentBytes = (this.fileComment != null
243 ? Encoding.UTF8.GetBytes(this.fileComment) : new byte[0]);
244 bool useUtf8 =
245 (this.fileName != null && fileNameBytes.Length > this.fileName.Length) ||
246 (this.fileComment != null && fileCommentBytes.Length > this.fileComment.Length);
247 if (useUtf8)
248 {
249 this.flags |= ZipFileFlags.UTF8;
250 }
251
252 BinaryWriter writer = new BinaryWriter(stream);
253 writer.Write(central ? CFHSIG : LFHSIG);
254 if (central)
255 {
256 writer.Write(this.versionMadeBy);
257 }
258 writer.Write(this.versionNeeded);
259 writer.Write((ushort) this.flags);
260 writer.Write((ushort) this.compressionMethod);
261 writer.Write(this.lastModTime);
262 writer.Write(this.lastModDate);
263 writer.Write(this.crc32);
264 writer.Write(this.compressedSize);
265 writer.Write(this.uncompressedSize);
266
267 ushort extraFieldLength = 0;
268 if (this.extraFields != null)
269 {
270 foreach (ZipExtraFileField field in this.extraFields)
271 {
272 if (field.data != null)
273 {
274 extraFieldLength += (ushort) (4 + field.data.Length);
275 }
276 }
277 }
278
279 writer.Write((ushort) fileNameBytes.Length);
280 writer.Write(extraFieldLength);
281
282 if (central)
283 {
284 writer.Write((ushort) fileCommentBytes.Length);
285
286 writer.Write(this.diskStart);
287 writer.Write(this.internalFileAttrs);
288 writer.Write(this.externalFileAttrs);
289 writer.Write(this.localHeaderOffset);
290 }
291
292 writer.Write(fileNameBytes);
293
294 if (this.extraFields != null)
295 {
296 foreach (ZipExtraFileField field in this.extraFields)
297 {
298 if (field.data != null)
299 {
300 field.Write(stream);
301 }
302 }
303 }
304
305 if (central)
306 {
307 writer.Write(fileCommentBytes);
308 }
309 }
310
311 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "compressedSize")]
312 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "uncompressedSize")]
313 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "crc32")]
314 [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "localHeaderOffset")]
315 public void GetZip64Fields(
316 out long compressedSize,
317 out long uncompressedSize,
318 out long localHeaderOffset,
319 out int archiveNumber,
320 out uint crc)
321 {
322 compressedSize = this.compressedSize;
323 uncompressedSize = this.uncompressedSize;
324 localHeaderOffset = this.localHeaderOffset;
325 archiveNumber = this.diskStart;
326 crc = this.crc32;
327
328 foreach (ZipExtraFileField field in this.extraFields)
329 {
330 if (field.fieldType == ZipExtraFileFieldType.ZIP64)
331 {
332 field.GetZip64Data(
333 out compressedSize,
334 out uncompressedSize,
335 out localHeaderOffset,
336 out archiveNumber);
337 }
338 }
339 }
340
341 public ZipFileInfo ToZipFileInfo()
342 {
343 string name = this.fileName;
344
345 long compressedSizeL;
346 long uncompressedSizeL;
347 long localHeaderOffsetL;
348 int archiveNumberL;
349 uint crc;
350 this.GetZip64Fields(
351 out compressedSizeL,
352 out uncompressedSizeL,
353 out localHeaderOffsetL,
354 out archiveNumberL,
355 out crc);
356
357 DateTime dateTime;
358 CompressionEngine.DosDateAndTimeToDateTime(
359 this.lastModDate,
360 this.lastModTime,
361 out dateTime);
362 FileAttributes attrs = FileAttributes.Normal;
363 // TODO: look for attrs or times in extra fields
364
365 return new ZipFileInfo(name, archiveNumberL, attrs, dateTime,
366 uncompressedSizeL, compressedSizeL, this.compressionMethod);
367 }
368
369 public bool IsDirectory
370 {
371 get
372 {
373 return this.fileName != null &&
374 (this.fileName.EndsWith("/", StringComparison.Ordinal) ||
375 this.fileName.EndsWith("\\", StringComparison.Ordinal));
376 }
377 }
378
379 public int GetSize(bool central)
380 {
381 int size = 30;
382
383 int fileNameSize = (this.fileName != null
384 ? Encoding.UTF8.GetByteCount(this.fileName) : 0);
385 size += fileNameSize;
386
387 if (this.extraFields != null)
388 {
389 foreach (ZipExtraFileField field in this.extraFields)
390 {
391 if (field.data != null)
392 {
393 size += 4 + field.data.Length;
394 }
395 }
396 }
397
398 if (central)
399 {
400 size += 16;
401
402 int fileCommentSize = (this.fileComment != null
403 ? Encoding.UTF8.GetByteCount(this.fileComment) : 0);
404 size += fileCommentSize;
405 }
406
407 return size;
408 }
409 }
410
411 internal class ZipExtraFileField
412 {
413 public ZipExtraFileFieldType fieldType;
414 public byte[] data;
415
416 public bool Read(Stream stream, ref int bytesRemaining)
417 {
418 if (bytesRemaining < 4)
419 {
420 return false;
421 }
422
423 BinaryReader reader = new BinaryReader(stream);
424
425 this.fieldType = (ZipExtraFileFieldType) reader.ReadUInt16();
426 ushort dataSize = reader.ReadUInt16();
427 bytesRemaining -= 4;
428
429 if (bytesRemaining < dataSize)
430 {
431 return false;
432 }
433
434 this.data = reader.ReadBytes(dataSize);
435 bytesRemaining -= dataSize;
436
437 return true;
438 }
439
440 public void Write(Stream stream)
441 {
442 BinaryWriter writer = new BinaryWriter(stream);
443 writer.Write((ushort) this.fieldType);
444
445 byte[] dataBytes = (this.data != null ? this.data : new byte[0]);
446 writer.Write((ushort) dataBytes.Length);
447 writer.Write(dataBytes);
448 }
449
450 public bool GetZip64Data(
451 out long compressedSize,
452 out long uncompressedSize,
453 out long localHeaderOffset,
454 out int diskStart)
455 {
456 uncompressedSize = 0;
457 compressedSize = 0;
458 localHeaderOffset = 0;
459 diskStart = 0;
460
461 if (this.fieldType != ZipExtraFileFieldType.ZIP64 ||
462 this.data == null || this.data.Length != 28)
463 {
464 return false;
465 }
466
467 using (MemoryStream dataStream = new MemoryStream(this.data))
468 {
469 BinaryReader reader = new BinaryReader(dataStream);
470 uncompressedSize = reader.ReadInt64();
471 compressedSize = reader.ReadInt64();
472 localHeaderOffset = reader.ReadInt64();
473 diskStart = reader.ReadInt32();
474 }
475
476 return true;
477 }
478
479 public bool SetZip64Data(
480 long compressedSize,
481 long uncompressedSize,
482 long localHeaderOffset,
483 int diskStart)
484 {
485 if (this.fieldType != ZipExtraFileFieldType.ZIP64)
486 {
487 return false;
488 }
489
490 using (MemoryStream dataStream = new MemoryStream())
491 {
492 BinaryWriter writer = new BinaryWriter(dataStream);
493 writer.Write(uncompressedSize);
494 writer.Write(compressedSize);
495 writer.Write(localHeaderOffset);
496 writer.Write(diskStart);
497 this.data = dataStream.ToArray();
498 }
499
500 return true;
501 }
502 }
503
504 internal class ZipEndOfCentralDirectory
505 {
506 public const uint EOCDSIG = 0x06054B50;
507 public const uint EOCD64SIG = 0x06064B50;
508
509 public const uint EOCD_RECORD_FIXEDSIZE = 22;
510 public const uint EOCD64_RECORD_FIXEDSIZE = 56;
511
512 public ushort versionMadeBy;
513 public ushort versionNeeded;
514 public uint diskNumber;
515 public uint dirStartDiskNumber;
516 public long entriesOnDisk;
517 public long totalEntries;
518 public long dirSize;
519 public long dirOffset;
520 public string comment;
521 public bool zip64;
522
523 public ZipEndOfCentralDirectory()
524 {
525 this.versionMadeBy = 20;
526 this.versionNeeded = 20;
527 }
528
529 public bool Read(Stream stream)
530 {
531 long startPos = stream.Position;
532
533 if (stream.Length - startPos < EOCD_RECORD_FIXEDSIZE)
534 {
535 return false;
536 }
537
538 BinaryReader reader = new BinaryReader(stream);
539 uint sig = reader.ReadUInt32();
540
541 this.zip64 = false;
542 if (sig != EOCDSIG)
543 {
544 if (sig == EOCD64SIG)
545 {
546 this.zip64 = true;
547 }
548 else
549 {
550 return false;
551 }
552 }
553
554 if (this.zip64)
555 {
556 if (stream.Length - startPos < EOCD64_RECORD_FIXEDSIZE)
557 {
558 return false;
559 }
560
561 long recordSize = reader.ReadInt64();
562 this.versionMadeBy = reader.ReadUInt16();
563 this.versionNeeded = reader.ReadUInt16();
564 this.diskNumber = reader.ReadUInt32();
565 this.dirStartDiskNumber = reader.ReadUInt32();
566 this.entriesOnDisk = reader.ReadInt64();
567 this.totalEntries = reader.ReadInt64();
568 this.dirSize = reader.ReadInt64();
569 this.dirOffset = reader.ReadInt64();
570
571 // Ignore any extended zip64 eocd data.
572 long exDataSize = recordSize + 12 - EOCD64_RECORD_FIXEDSIZE;
573
574 if (stream.Length - stream.Position < exDataSize)
575 {
576 return false;
577 }
578
579 stream.Seek(exDataSize, SeekOrigin.Current);
580
581 this.comment = null;
582 }
583 else
584 {
585 this.diskNumber = reader.ReadUInt16();
586 this.dirStartDiskNumber = reader.ReadUInt16();
587 this.entriesOnDisk = reader.ReadUInt16();
588 this.totalEntries = reader.ReadUInt16();
589 this.dirSize = reader.ReadUInt32();
590 this.dirOffset = reader.ReadUInt32();
591
592 int commentLength = reader.ReadUInt16();
593
594 if (stream.Length - stream.Position < commentLength)
595 {
596 return false;
597 }
598
599 byte[] commentBytes = reader.ReadBytes(commentLength);
600 this.comment = Encoding.UTF8.GetString(commentBytes);
601 }
602
603 return true;
604 }
605
606 public void Write(Stream stream)
607 {
608 BinaryWriter writer = new BinaryWriter(stream);
609
610 if (this.zip64)
611 {
612 writer.Write(EOCD64SIG);
613 writer.Write((long) EOCD64_RECORD_FIXEDSIZE);
614 writer.Write(this.versionMadeBy);
615 writer.Write(this.versionNeeded);
616 writer.Write(this.diskNumber);
617 writer.Write(this.dirStartDiskNumber);
618 writer.Write(this.entriesOnDisk);
619 writer.Write(this.totalEntries);
620 writer.Write(this.dirSize);
621 writer.Write(this.dirOffset);
622 }
623 else
624 {
625 writer.Write(EOCDSIG);
626 writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.diskNumber));
627 writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.dirStartDiskNumber));
628 writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.entriesOnDisk));
629 writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.totalEntries));
630 writer.Write((uint) Math.Min((long) UInt32.MaxValue, this.dirSize));
631 writer.Write((uint) Math.Min((long) UInt32.MaxValue, this.dirOffset));
632
633 byte[] commentBytes = (this.comment != null
634 ? Encoding.UTF8.GetBytes(this.comment) : new byte[0]);
635 writer.Write((ushort) commentBytes.Length);
636 writer.Write(commentBytes);
637 }
638 }
639
640 public int GetSize(bool zip64Size)
641 {
642 if (zip64Size)
643 {
644 return 56;
645 }
646 else
647 {
648 int commentSize = (this.comment != null
649 ? Encoding.UTF8.GetByteCount(this.comment) : 0);
650 return 22 + commentSize;
651 }
652 }
653 }
654
655 internal class Zip64EndOfCentralDirectoryLocator
656 {
657 public const uint EOCDL64SIG = 0x07064B50;
658
659 public const uint EOCDL64_SIZE = 20;
660
661 public uint dirStartDiskNumber;
662 public long dirOffset;
663 public uint totalDisks;
664
665 public bool Read(Stream stream)
666 {
667 long startPos = stream.Position;
668 if (stream.Length - startPos < EOCDL64_SIZE)
669 {
670 return false;
671 }
672
673 BinaryReader reader = new BinaryReader(stream);
674 uint sig = reader.ReadUInt32();
675
676 if (sig != EOCDL64SIG)
677 {
678 return false;
679 }
680
681 this.dirStartDiskNumber = reader.ReadUInt32();
682 this.dirOffset = reader.ReadInt64();
683 this.totalDisks = reader.ReadUInt32();
684
685 return true;
686 }
687
688 public void Write(Stream stream)
689 {
690 BinaryWriter writer = new BinaryWriter(stream);
691 writer.Write(EOCDL64SIG);
692 writer.Write(this.dirStartDiskNumber);
693 writer.Write(this.dirOffset);
694 writer.Write(this.totalDisks);
695 }
696 }
697}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipInfo.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipInfo.cs
new file mode 100644
index 00000000..73f65fa0
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipInfo.cs
@@ -0,0 +1,82 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Object representing a zip file on disk; provides access to
11 /// file-based operations on the zip file.
12 /// </summary>
13 /// <remarks>
14 /// Generally, the methods on this class are much easier to use than the
15 /// stream-based interfaces provided by the <see cref="ZipEngine"/> class.
16 /// </remarks>
17 [Serializable]
18 public class ZipInfo : ArchiveInfo
19 {
20 /// <summary>
21 /// Creates a new CabinetInfo object representing a zip file in a specified path.
22 /// </summary>
23 /// <param name="path">The path to the zip file. When creating a zip file, this file does not
24 /// necessarily exist yet.</param>
25 public ZipInfo(string path)
26 : base(path)
27 {
28 }
29
30 /// <summary>
31 /// Initializes a new instance of the CabinetInfo class with serialized data.
32 /// </summary>
33 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
34 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
35 protected ZipInfo(SerializationInfo info, StreamingContext context)
36 : base(info, context)
37 {
38 }
39
40 /// <summary>
41 /// Creates a compression engine that does the low-level work for
42 /// this object.
43 /// </summary>
44 /// <returns>A new <see cref="ZipEngine"/> instance.</returns>
45 /// <remarks>
46 /// Each instance will be <see cref="CompressionEngine.Dispose()"/>d
47 /// immediately after use.
48 /// </remarks>
49 protected override CompressionEngine CreateCompressionEngine()
50 {
51 return new ZipEngine();
52 }
53
54 /// <summary>
55 /// Gets information about the files contained in the archive.
56 /// </summary>
57 /// <returns>A list of <see cref="ZipFileInfo"/> objects, each
58 /// containing information about a file in the archive.</returns>
59 public new IList<ZipFileInfo> GetFiles()
60 {
61 IList<ArchiveFileInfo> files = base.GetFiles();
62 List<ZipFileInfo> zipFiles = new List<ZipFileInfo>(files.Count);
63 foreach (ZipFileInfo zipFile in files) zipFiles.Add(zipFile);
64 return zipFiles.AsReadOnly();
65 }
66
67 /// <summary>
68 /// Gets information about the certain files contained in the archive file.
69 /// </summary>
70 /// <param name="searchPattern">The search string, such as
71 /// &quot;*.txt&quot;.</param>
72 /// <returns>A list of <see cref="ZipFileInfo"/> objects, each containing
73 /// information about a file in the archive.</returns>
74 public new IList<ZipFileInfo> GetFiles(string searchPattern)
75 {
76 IList<ArchiveFileInfo> files = base.GetFiles(searchPattern);
77 List<ZipFileInfo> zipFiles = new List<ZipFileInfo>(files.Count);
78 foreach (ZipFileInfo zipFile in files) zipFiles.Add(zipFile);
79 return zipFiles.AsReadOnly();
80 }
81 }
82}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs
new file mode 100644
index 00000000..b9c183d3
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipPacker.cs
@@ -0,0 +1,489 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.IO;
7 using System.IO.Compression;
8 using System.Collections.Generic;
9 using System.Globalization;
10
11 public partial class ZipEngine
12 {
13 /// <summary>
14 /// Creates a zip archive or chain of zip archives.
15 /// </summary>
16 /// <param name="streamContext">A context interface to handle opening
17 /// and closing of archive and file streams.</param>
18 /// <param name="files">An array of file lists. Each list is
19 /// compressed into one stream in the archive.</param>
20 /// <param name="maxArchiveSize">The maximum number of bytes for one archive
21 /// before the contents are chained to the next archive, or zero for unlimited
22 /// archive size.</param>
23 /// <exception cref="ArchiveException">The archive could not be
24 /// created.</exception>
25 /// <remarks>
26 /// The stream context implementation may provide a mapping from the file
27 /// paths within the archive to the external file paths.
28 /// </remarks>
29 public override void Pack(
30 IPackStreamContext streamContext,
31 IEnumerable<string> files,
32 long maxArchiveSize)
33 {
34 if (streamContext == null)
35 {
36 throw new ArgumentNullException("streamContext");
37 }
38
39 if (files == null)
40 {
41 throw new ArgumentNullException("files");
42 }
43
44 lock (this)
45 {
46 Stream archiveStream = null;
47 try
48 {
49 this.ResetProgressData();
50 this.totalArchives = 1;
51
52 object forceZip64Value = streamContext.GetOption("forceZip64", null);
53 bool forceZip64 = Convert.ToBoolean(
54 forceZip64Value, CultureInfo.InvariantCulture);
55
56 // Count the total number of files and bytes to be compressed.
57 foreach (string file in files)
58 {
59 FileAttributes attributes;
60 DateTime lastWriteTime;
61 Stream fileStream = streamContext.OpenFileReadStream(
62 file,
63 out attributes,
64 out lastWriteTime);
65 if (fileStream != null)
66 {
67 this.totalFileBytes += fileStream.Length;
68 this.totalFiles++;
69 streamContext.CloseFileReadStream(file, fileStream);
70 }
71 }
72
73 List<ZipFileHeader> fileHeaders = new List<ZipFileHeader>();
74 this.currentFileNumber = -1;
75
76 if (this.currentArchiveName == null)
77 {
78 this.mainArchiveName = streamContext.GetArchiveName(0);
79 this.currentArchiveName = this.mainArchiveName;
80
81 if (String.IsNullOrEmpty(this.currentArchiveName))
82 {
83 throw new FileNotFoundException("No name provided for archive.");
84 }
85 }
86
87 this.OnProgress(ArchiveProgressType.StartArchive);
88
89 // Compress files one by one, saving header info for each.
90 foreach (string file in files)
91 {
92 ZipFileHeader fileHeader = this.PackOneFile(
93 streamContext,
94 file,
95 maxArchiveSize,
96 forceZip64,
97 ref archiveStream);
98
99 if (fileHeader != null)
100 {
101 fileHeaders.Add(fileHeader);
102 }
103
104 this.currentArchiveTotalBytes = (archiveStream != null ?
105 archiveStream.Position : 0);
106 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
107 }
108
109 bool zip64 = forceZip64 || this.totalFiles > UInt16.MaxValue;
110
111 // Write the central directory composed of all the file headers.
112 uint centralDirStartArchiveNumber = 0;
113 long centralDirStartPosition = 0;
114 long centralDirSize = 0;
115 for (int i = 0; i < fileHeaders.Count; i++)
116 {
117 ZipFileHeader fileHeader = fileHeaders[i];
118
119 int headerSize = fileHeader.GetSize(true);
120 centralDirSize += headerSize;
121
122 this.CheckArchiveWriteStream(
123 streamContext,
124 maxArchiveSize,
125 headerSize,
126 ref archiveStream);
127
128 if (i == 0)
129 {
130 centralDirStartArchiveNumber = (uint) this.currentArchiveNumber;
131 centralDirStartPosition = archiveStream.Position;
132 }
133
134 fileHeader.Write(archiveStream, true);
135 if (fileHeader.zip64)
136 {
137 zip64 = true;
138 }
139 }
140
141 this.currentArchiveTotalBytes =
142 (archiveStream != null ? archiveStream.Position : 0);
143 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
144
145 ZipEndOfCentralDirectory eocd = new ZipEndOfCentralDirectory();
146 eocd.dirStartDiskNumber = centralDirStartArchiveNumber;
147 eocd.entriesOnDisk = fileHeaders.Count;
148 eocd.totalEntries = fileHeaders.Count;
149 eocd.dirSize = centralDirSize;
150 eocd.dirOffset = centralDirStartPosition;
151 eocd.comment = this.comment;
152
153 Zip64EndOfCentralDirectoryLocator eocdl =
154 new Zip64EndOfCentralDirectoryLocator();
155
156 int maxFooterSize = eocd.GetSize(false);
157 if (archiveStream != null && (zip64 || archiveStream.Position >
158 ((long) UInt32.MaxValue) - eocd.GetSize(false)))
159 {
160 maxFooterSize += eocd.GetSize(true) + (int)
161 Zip64EndOfCentralDirectoryLocator.EOCDL64_SIZE;
162 zip64 = true;
163 }
164
165 this.CheckArchiveWriteStream(
166 streamContext,
167 maxArchiveSize,
168 maxFooterSize,
169 ref archiveStream);
170 eocd.diskNumber = (uint) this.currentArchiveNumber;
171
172 if (zip64)
173 {
174 eocd.versionMadeBy = 45;
175 eocd.versionNeeded = 45;
176 eocd.zip64 = true;
177 eocdl.dirOffset = archiveStream.Position;
178 eocdl.dirStartDiskNumber = (uint) this.currentArchiveNumber;
179 eocdl.totalDisks = (uint) this.currentArchiveNumber + 1;
180 eocd.Write(archiveStream);
181 eocdl.Write(archiveStream);
182
183 eocd.dirOffset = UInt32.MaxValue;
184 eocd.dirStartDiskNumber = UInt16.MaxValue;
185 }
186
187 eocd.zip64 = false;
188 eocd.Write(archiveStream);
189
190 this.currentArchiveTotalBytes = archiveStream.Position;
191 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
192 }
193 finally
194 {
195 if (archiveStream != null)
196 {
197 streamContext.CloseArchiveWriteStream(
198 this.currentArchiveNumber, this.mainArchiveName, archiveStream);
199 this.OnProgress(ArchiveProgressType.FinishArchive);
200 }
201 }
202 }
203 }
204
205 /// <summary>
206 /// Moves to the next archive in the sequence if necessary.
207 /// </summary>
208 private void CheckArchiveWriteStream(
209 IPackStreamContext streamContext,
210 long maxArchiveSize,
211 long requiredSize,
212 ref Stream archiveStream)
213 {
214 if (archiveStream != null &&
215 archiveStream.Length > 0 && maxArchiveSize > 0)
216 {
217 long sizeRemaining = maxArchiveSize - archiveStream.Length;
218 if (sizeRemaining < requiredSize)
219 {
220 string nextArchiveName = streamContext.GetArchiveName(
221 this.currentArchiveNumber + 1);
222
223 if (String.IsNullOrEmpty(nextArchiveName))
224 {
225 throw new FileNotFoundException("No name provided for archive #" +
226 this.currentArchiveNumber + 1);
227 }
228
229 this.currentArchiveTotalBytes = archiveStream.Position;
230 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
231
232 streamContext.CloseArchiveWriteStream(
233 this.currentArchiveNumber,
234 nextArchiveName,
235 archiveStream);
236 archiveStream = null;
237
238 this.OnProgress(ArchiveProgressType.FinishArchive);
239
240 this.currentArchiveNumber++;
241 this.totalArchives++;
242 this.currentArchiveBytesProcessed = 0;
243 this.currentArchiveTotalBytes = 0;
244 }
245 }
246
247 if (archiveStream == null)
248 {
249 if (this.currentArchiveNumber > 0)
250 {
251 this.OnProgress(ArchiveProgressType.StartArchive);
252 }
253
254 archiveStream = streamContext.OpenArchiveWriteStream(
255 this.currentArchiveNumber, this.mainArchiveName, true, this);
256
257 if (archiveStream == null)
258 {
259 throw new FileNotFoundException("Stream not provided for archive #" +
260 this.currentArchiveNumber);
261 }
262 }
263 }
264
265 /// <summary>
266 /// Adds one file to a zip archive in the process of being created.
267 /// </summary>
268 private ZipFileHeader PackOneFile(
269 IPackStreamContext streamContext,
270 string file,
271 long maxArchiveSize,
272 bool forceZip64,
273 ref Stream archiveStream)
274 {
275 Stream fileStream = null;
276 int headerArchiveNumber = 0;
277 try
278 {
279 // TODO: call GetOption to get compression method for the specific file
280 ZipCompressionMethod compressionMethod = ZipCompressionMethod.Deflate;
281 if (this.CompressionLevel == WixToolset.Dtf.Compression.CompressionLevel.None)
282 {
283 compressionMethod = ZipCompressionMethod.Store;
284 }
285
286 Converter<Stream, Stream> compressionStreamCreator;
287 if (!ZipEngine.compressionStreamCreators.TryGetValue(
288 compressionMethod, out compressionStreamCreator))
289 {
290 return null;
291 }
292
293 FileAttributes attributes;
294 DateTime lastWriteTime;
295 fileStream = streamContext.OpenFileReadStream(
296 file, out attributes, out lastWriteTime);
297 if (fileStream == null)
298 {
299 return null;
300 }
301
302 this.currentFileName = file;
303 this.currentFileNumber++;
304
305 this.currentFileTotalBytes = fileStream.Length;
306 this.currentFileBytesProcessed = 0;
307 this.OnProgress(ArchiveProgressType.StartFile);
308
309 ZipFileInfo fileInfo = new ZipFileInfo(
310 file,
311 this.currentArchiveNumber,
312 attributes,
313 lastWriteTime,
314 fileStream.Length,
315 0,
316 compressionMethod);
317
318 bool zip64 = forceZip64 || fileStream.Length >= (long) UInt32.MaxValue;
319 ZipFileHeader fileHeader = new ZipFileHeader(fileInfo, zip64);
320
321 this.CheckArchiveWriteStream(
322 streamContext,
323 maxArchiveSize,
324 fileHeader.GetSize(false),
325 ref archiveStream);
326
327 long headerPosition = archiveStream.Position;
328 fileHeader.Write(archiveStream, false);
329 headerArchiveNumber = this.currentArchiveNumber;
330
331 uint crc;
332 long bytesWritten = this.PackFileBytes(
333 streamContext,
334 fileStream,
335 maxArchiveSize,
336 compressionStreamCreator,
337 ref archiveStream,
338 out crc);
339
340 fileHeader.Update(
341 bytesWritten,
342 fileStream.Length,
343 crc,
344 headerPosition,
345 headerArchiveNumber);
346
347 streamContext.CloseFileReadStream(file, fileStream);
348 fileStream = null;
349
350 // Go back and rewrite the updated file header.
351 if (this.currentArchiveNumber == headerArchiveNumber)
352 {
353 long fileEndPosition = archiveStream.Position;
354 archiveStream.Seek(headerPosition, SeekOrigin.Begin);
355 fileHeader.Write(archiveStream, false);
356 archiveStream.Seek(fileEndPosition, SeekOrigin.Begin);
357 }
358 else
359 {
360 // The file spanned archives, so temporarily reopen
361 // the archive where it started.
362 string headerArchiveName = streamContext.GetArchiveName(
363 headerArchiveNumber + 1);
364 Stream headerStream = null;
365 try
366 {
367 headerStream = streamContext.OpenArchiveWriteStream(
368 headerArchiveNumber, headerArchiveName, false, this);
369 headerStream.Seek(headerPosition, SeekOrigin.Begin);
370 fileHeader.Write(headerStream, false);
371 }
372 finally
373 {
374 if (headerStream != null)
375 {
376 streamContext.CloseArchiveWriteStream(
377 headerArchiveNumber, headerArchiveName, headerStream);
378 }
379 }
380 }
381
382 this.OnProgress(ArchiveProgressType.FinishFile);
383
384 return fileHeader;
385 }
386 finally
387 {
388 if (fileStream != null)
389 {
390 streamContext.CloseFileReadStream(
391 this.currentFileName, fileStream);
392 }
393 }
394 }
395
396 /// <summary>
397 /// Writes compressed bytes of one file to the archive,
398 /// keeping track of the CRC and number of bytes written.
399 /// </summary>
400 private long PackFileBytes(
401 IPackStreamContext streamContext,
402 Stream fileStream,
403 long maxArchiveSize,
404 Converter<Stream, Stream> compressionStreamCreator,
405 ref Stream archiveStream,
406 out uint crc)
407 {
408 long writeStartPosition = archiveStream.Position;
409 long bytesWritten = 0;
410 CrcStream fileCrcStream = new CrcStream(fileStream);
411
412 ConcatStream concatStream = new ConcatStream(
413 delegate(ConcatStream s)
414 {
415 Stream sourceStream = s.Source;
416 bytesWritten += sourceStream.Position - writeStartPosition;
417
418 this.CheckArchiveWriteStream(
419 streamContext,
420 maxArchiveSize,
421 1,
422 ref sourceStream);
423
424 writeStartPosition = sourceStream.Position;
425 s.Source = sourceStream;
426 });
427
428 concatStream.Source = archiveStream;
429
430 if (maxArchiveSize > 0)
431 {
432 concatStream.SetLength(maxArchiveSize);
433 }
434
435 Stream compressionStream = compressionStreamCreator(concatStream);
436
437 try
438 {
439 byte[] buf = new byte[4096];
440 long bytesRemaining = fileStream.Length;
441 int counter = 0;
442 while (bytesRemaining > 0)
443 {
444 int count = (int) Math.Min(
445 bytesRemaining, (long) buf.Length);
446
447 count = fileCrcStream.Read(buf, 0, count);
448 if (count <= 0)
449 {
450 throw new ZipException(
451 "Failed to read file: " + this.currentFileName);
452 }
453
454 compressionStream.Write(buf, 0, count);
455 bytesRemaining -= count;
456
457 this.fileBytesProcessed += count;
458 this.currentFileBytesProcessed += count;
459 this.currentArchiveTotalBytes = concatStream.Source.Position;
460 this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes;
461
462 if (++counter % 16 == 0) // Report every 64K
463 {
464 this.OnProgress(ArchiveProgressType.PartialFile);
465 }
466 }
467
468 if (compressionStream is DeflateStream)
469 {
470 compressionStream.Close();
471 }
472 else
473 {
474 compressionStream.Flush();
475 }
476 }
477 finally
478 {
479 archiveStream = concatStream.Source;
480 }
481
482 bytesWritten += archiveStream.Position - writeStartPosition;
483
484 crc = fileCrcStream.Crc;
485
486 return bytesWritten;
487 }
488 }
489}
diff --git a/src/dtf/WixToolset.Dtf.Compression.Zip/ZipUnpacker.cs b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipUnpacker.cs
new file mode 100644
index 00000000..681c0e46
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression.Zip/ZipUnpacker.cs
@@ -0,0 +1,336 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression.Zip
4{
5 using System;
6 using System.IO;
7 using System.IO.Compression;
8 using System.Collections.Generic;
9
10 public partial class ZipEngine
11 {
12 /// <summary>
13 /// Extracts files from a zip archive or archive chain.
14 /// </summary>
15 /// <param name="streamContext">A context interface to handle opening
16 /// and closing of archive and file streams.</param>
17 /// <param name="fileFilter">An optional predicate that can determine
18 /// which files to process.</param>
19 /// <exception cref="ArchiveException">The archive provided
20 /// by the stream context is not valid.</exception>
21 /// <remarks>
22 /// The <paramref name="fileFilter"/> predicate takes an internal file
23 /// path and returns true to include the file or false to exclude it.
24 /// </remarks>
25 public override void Unpack(
26 IUnpackStreamContext streamContext,
27 Predicate<string> fileFilter)
28 {
29 if (streamContext == null)
30 {
31 throw new ArgumentNullException("streamContext");
32 }
33
34 lock (this)
35 {
36 IList<ZipFileHeader> allHeaders = this.GetCentralDirectory(streamContext);
37 if (allHeaders == null)
38 {
39 throw new ZipException("Zip central directory not found.");
40 }
41
42 IList<ZipFileHeader> headers = new List<ZipFileHeader>(allHeaders.Count);
43 foreach (ZipFileHeader header in allHeaders)
44 {
45 if (!header.IsDirectory &&
46 (fileFilter == null || fileFilter(header.fileName)))
47 {
48 headers.Add(header);
49 }
50 }
51
52 this.ResetProgressData();
53
54 // Count the total number of files and bytes to be compressed.
55 this.totalFiles = headers.Count;
56 foreach (ZipFileHeader header in headers)
57 {
58 long compressedSize;
59 long uncompressedSize;
60 long localHeaderOffset;
61 int archiveNumber;
62 uint crc;
63 header.GetZip64Fields(
64 out compressedSize,
65 out uncompressedSize,
66 out localHeaderOffset,
67 out archiveNumber,
68 out crc);
69
70 this.totalFileBytes += uncompressedSize;
71 if (archiveNumber >= this.totalArchives)
72 {
73 this.totalArchives = (short) (archiveNumber + 1);
74 }
75 }
76
77 this.currentArchiveNumber = -1;
78 this.currentFileNumber = -1;
79 Stream archiveStream = null;
80 try
81 {
82 foreach (ZipFileHeader header in headers)
83 {
84 this.currentFileNumber++;
85 this.UnpackOneFile(streamContext, header, ref archiveStream);
86 }
87 }
88 finally
89 {
90 if (archiveStream != null)
91 {
92 streamContext.CloseArchiveReadStream(
93 0, String.Empty, archiveStream);
94 this.currentArchiveNumber--;
95 this.OnProgress(ArchiveProgressType.FinishArchive);
96 }
97 }
98 }
99 }
100
101 /// <summary>
102 /// Unpacks a single file from an archive or archive chain.
103 /// </summary>
104 private void UnpackOneFile(
105 IUnpackStreamContext streamContext,
106 ZipFileHeader header,
107 ref Stream archiveStream)
108 {
109 ZipFileInfo fileInfo = null;
110 Stream fileStream = null;
111 try
112 {
113 Converter<Stream, Stream> compressionStreamCreator;
114 if (!ZipEngine.decompressionStreamCreators.TryGetValue(
115 header.compressionMethod, out compressionStreamCreator))
116 {
117 // Silently skip files of an unsupported compression method.
118 return;
119 }
120
121 long compressedSize;
122 long uncompressedSize;
123 long localHeaderOffset;
124 int archiveNumber;
125 uint crc;
126 header.GetZip64Fields(
127 out compressedSize,
128 out uncompressedSize,
129 out localHeaderOffset,
130 out archiveNumber,
131 out crc);
132
133 if (this.currentArchiveNumber != archiveNumber + 1)
134 {
135 if (archiveStream != null)
136 {
137 streamContext.CloseArchiveReadStream(
138 this.currentArchiveNumber,
139 String.Empty,
140 archiveStream);
141 archiveStream = null;
142
143 this.OnProgress(ArchiveProgressType.FinishArchive);
144 this.currentArchiveName = null;
145 }
146
147 this.currentArchiveNumber = (short) (archiveNumber + 1);
148 this.currentArchiveBytesProcessed = 0;
149 this.currentArchiveTotalBytes = 0;
150
151 archiveStream = this.OpenArchive(
152 streamContext, this.currentArchiveNumber);
153
154 FileStream archiveFileStream = archiveStream as FileStream;
155 this.currentArchiveName = (archiveFileStream != null ?
156 Path.GetFileName(archiveFileStream.Name) : null);
157
158 this.currentArchiveTotalBytes = archiveStream.Length;
159 this.currentArchiveNumber--;
160 this.OnProgress(ArchiveProgressType.StartArchive);
161 this.currentArchiveNumber++;
162 }
163
164 archiveStream.Seek(localHeaderOffset, SeekOrigin.Begin);
165
166 ZipFileHeader localHeader = new ZipFileHeader();
167 if (!localHeader.Read(archiveStream, false) ||
168 !ZipEngine.AreFilePathsEqual(localHeader.fileName, header.fileName))
169 {
170 string msg = "Could not read file: " + header.fileName;
171 throw new ZipException(msg);
172 }
173
174 fileInfo = header.ToZipFileInfo();
175
176 fileStream = streamContext.OpenFileWriteStream(
177 fileInfo.FullName,
178 fileInfo.Length,
179 fileInfo.LastWriteTime);
180
181 if (fileStream != null)
182 {
183 this.currentFileName = header.fileName;
184 this.currentFileBytesProcessed = 0;
185 this.currentFileTotalBytes = fileInfo.Length;
186 this.currentArchiveNumber--;
187 this.OnProgress(ArchiveProgressType.StartFile);
188 this.currentArchiveNumber++;
189
190 this.UnpackFileBytes(
191 streamContext,
192 fileInfo.FullName,
193 fileInfo.CompressedLength,
194 fileInfo.Length,
195 header.crc32,
196 fileStream,
197 compressionStreamCreator,
198 ref archiveStream);
199 }
200 }
201 finally
202 {
203 if (fileStream != null)
204 {
205 streamContext.CloseFileWriteStream(
206 fileInfo.FullName,
207 fileStream,
208 fileInfo.Attributes,
209 fileInfo.LastWriteTime);
210
211 this.currentArchiveNumber--;
212 this.OnProgress(ArchiveProgressType.FinishFile);
213 this.currentArchiveNumber++;
214 }
215 }
216 }
217
218 /// <summary>
219 /// Compares two internal file paths while ignoring case and slash differences.
220 /// </summary>
221 /// <param name="path1">The first path to compare.</param>
222 /// <param name="path2">The second path to compare.</param>
223 /// <returns>True if the paths are equivalent.</returns>
224 private static bool AreFilePathsEqual(string path1, string path2)
225 {
226 path1 = path1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
227 path2 = path2.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
228 return String.Compare(path1, path2, StringComparison.OrdinalIgnoreCase) == 0;
229 }
230
231 private Stream OpenArchive(IUnpackStreamContext streamContext, int archiveNumber)
232 {
233 Stream archiveStream = streamContext.OpenArchiveReadStream(
234 archiveNumber, String.Empty, this);
235 if (archiveStream == null && archiveNumber != 0)
236 {
237 archiveStream = streamContext.OpenArchiveReadStream(
238 0, String.Empty, this);
239 }
240
241 if (archiveStream == null)
242 {
243 throw new FileNotFoundException("Archive stream not provided.");
244 }
245
246 return archiveStream;
247 }
248
249 /// <summary>
250 /// Decompresses bytes for one file from an archive or archive chain,
251 /// checking the crc at the end.
252 /// </summary>
253 private void UnpackFileBytes(
254 IUnpackStreamContext streamContext,
255 string fileName,
256 long compressedSize,
257 long uncompressedSize,
258 uint crc,
259 Stream fileStream,
260 Converter<Stream, Stream> compressionStreamCreator,
261 ref Stream archiveStream)
262 {
263 CrcStream crcStream = new CrcStream(fileStream);
264
265 ConcatStream concatStream = new ConcatStream(
266 delegate(ConcatStream s)
267 {
268 this.currentArchiveBytesProcessed = s.Source.Position;
269 streamContext.CloseArchiveReadStream(
270 this.currentArchiveNumber,
271 String.Empty,
272 s.Source);
273
274 this.currentArchiveNumber--;
275 this.OnProgress(ArchiveProgressType.FinishArchive);
276 this.currentArchiveNumber += 2;
277 this.currentArchiveName = null;
278 this.currentArchiveBytesProcessed = 0;
279 this.currentArchiveTotalBytes = 0;
280
281 s.Source = this.OpenArchive(streamContext, this.currentArchiveNumber);
282
283 FileStream archiveFileStream = s.Source as FileStream;
284 this.currentArchiveName = (archiveFileStream != null ?
285 Path.GetFileName(archiveFileStream.Name) : null);
286
287 this.currentArchiveTotalBytes = s.Source.Length;
288 this.currentArchiveNumber--;
289 this.OnProgress(ArchiveProgressType.StartArchive);
290 this.currentArchiveNumber++;
291 });
292
293 concatStream.Source = archiveStream;
294 concatStream.SetLength(compressedSize);
295
296 Stream decompressionStream = compressionStreamCreator(concatStream);
297
298 try
299 {
300 byte[] buf = new byte[4096];
301 long bytesRemaining = uncompressedSize;
302 int counter = 0;
303 while (bytesRemaining > 0)
304 {
305 int count = (int) Math.Min(buf.Length, bytesRemaining);
306 count = decompressionStream.Read(buf, 0, count);
307 crcStream.Write(buf, 0, count);
308 bytesRemaining -= count;
309
310 this.fileBytesProcessed += count;
311 this.currentFileBytesProcessed += count;
312 this.currentArchiveBytesProcessed = concatStream.Source.Position;
313
314 if (++counter % 16 == 0) // Report every 64K
315 {
316 this.currentArchiveNumber--;
317 this.OnProgress(ArchiveProgressType.PartialFile);
318 this.currentArchiveNumber++;
319 }
320 }
321 }
322 finally
323 {
324 archiveStream = concatStream.Source;
325 }
326
327 crcStream.Flush();
328
329 if (crcStream.Crc != crc)
330 {
331 throw new ZipException("CRC check failed for file: " + fileName);
332 }
333 }
334 }
335}
336
diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs
new file mode 100644
index 00000000..a53e862c
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveException.cs
@@ -0,0 +1,57 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7 using System.Runtime.Serialization;
8
9 /// <summary>
10 /// Base exception class for compression operations. Compression libraries should
11 /// derive subclass exceptions with more specific error information relevent to the
12 /// file format.
13 /// </summary>
14 [Serializable]
15 public class ArchiveException : IOException
16 {
17 /// <summary>
18 /// Creates a new ArchiveException with a specified error message and a reference to the
19 /// inner exception that is the cause of this exception.
20 /// </summary>
21 /// <param name="message">The message that describes the error.</param>
22 /// <param name="innerException">The exception that is the cause of the current exception. If the
23 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
24 /// is raised in a catch block that handles the inner exception.</param>
25 public ArchiveException(string message, Exception innerException)
26 : base(message, innerException)
27 {
28 }
29
30 /// <summary>
31 /// Creates a new ArchiveException with a specified error message.
32 /// </summary>
33 /// <param name="message">The message that describes the error.</param>
34 public ArchiveException(string message)
35 : this(message, null)
36 {
37 }
38
39 /// <summary>
40 /// Creates a new ArchiveException.
41 /// </summary>
42 public ArchiveException()
43 : this(null, null)
44 {
45 }
46
47 /// <summary>
48 /// Initializes a new instance of the ArchiveException class with serialized data.
49 /// </summary>
50 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
51 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
52 protected ArchiveException(SerializationInfo info, StreamingContext context)
53 : base(info, context)
54 {
55 }
56 }
57}
diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs
new file mode 100644
index 00000000..adcae3ec
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveFileInfo.cs
@@ -0,0 +1,430 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7 using System.Runtime.Serialization;
8 using System.Diagnostics.CodeAnalysis;
9
10 /// <summary>
11 /// Abstract object representing a compressed file within an archive;
12 /// provides operations for getting the file properties and unpacking
13 /// the file.
14 /// </summary>
15 [Serializable]
16 public abstract class ArchiveFileInfo : FileSystemInfo
17 {
18 private ArchiveInfo archiveInfo;
19 private string name;
20 private string path;
21
22 private bool initialized;
23 private bool exists;
24 private int archiveNumber;
25 private FileAttributes attributes;
26 private DateTime lastWriteTime;
27 private long length;
28
29 /// <summary>
30 /// Creates a new ArchiveFileInfo object representing a file within
31 /// an archive in a specified path.
32 /// </summary>
33 /// <param name="archiveInfo">An object representing the archive
34 /// containing the file.</param>
35 /// <param name="filePath">The path to the file within the archive.
36 /// Usually, this is a simple file name, but if the archive contains
37 /// a directory structure this may include the directory.</param>
38 protected ArchiveFileInfo(ArchiveInfo archiveInfo, string filePath)
39 : base()
40 {
41 if (filePath == null)
42 {
43 throw new ArgumentNullException("filePath");
44 }
45
46 this.Archive = archiveInfo;
47
48 this.name = System.IO.Path.GetFileName(filePath);
49 this.path = System.IO.Path.GetDirectoryName(filePath);
50
51 this.attributes = FileAttributes.Normal;
52 this.lastWriteTime = DateTime.MinValue;
53 }
54
55 /// <summary>
56 /// Creates a new ArchiveFileInfo object with all parameters specified;
57 /// used by subclasses when reading the metadata out of an archive.
58 /// </summary>
59 /// <param name="filePath">The internal path and name of the file in
60 /// the archive.</param>
61 /// <param name="archiveNumber">The archive number where the file
62 /// starts.</param>
63 /// <param name="attributes">The stored attributes of the file.</param>
64 /// <param name="lastWriteTime">The stored last write time of the
65 /// file.</param>
66 /// <param name="length">The uncompressed size of the file.</param>
67 protected ArchiveFileInfo(
68 string filePath,
69 int archiveNumber,
70 FileAttributes attributes,
71 DateTime lastWriteTime,
72 long length)
73 : this(null, filePath)
74 {
75 this.exists = true;
76 this.archiveNumber = archiveNumber;
77 this.attributes = attributes;
78 this.lastWriteTime = lastWriteTime;
79 this.length = length;
80 this.initialized = true;
81 }
82
83 /// <summary>
84 /// Initializes a new instance of the ArchiveFileInfo class with
85 /// serialized data.
86 /// </summary>
87 /// <param name="info">The SerializationInfo that holds the serialized
88 /// object data about the exception being thrown.</param>
89 /// <param name="context">The StreamingContext that contains contextual
90 /// information about the source or destination.</param>
91 protected ArchiveFileInfo(SerializationInfo info, StreamingContext context)
92 : base(info, context)
93 {
94 this.archiveInfo = (ArchiveInfo) info.GetValue(
95 "archiveInfo", typeof(ArchiveInfo));
96 this.name = info.GetString("name");
97 this.path = info.GetString("path");
98 this.initialized = info.GetBoolean("initialized");
99 this.exists = info.GetBoolean("exists");
100 this.archiveNumber = info.GetInt32("archiveNumber");
101 this.attributes = (FileAttributes) info.GetValue(
102 "attributes", typeof(FileAttributes));
103 this.lastWriteTime = info.GetDateTime("lastWriteTime");
104 this.length = info.GetInt64("length");
105 }
106
107 /// <summary>
108 /// Gets the name of the file.
109 /// </summary>
110 /// <value>The name of the file, not including any path.</value>
111 public override string Name
112 {
113 get
114 {
115 return this.name;
116 }
117 }
118
119 /// <summary>
120 /// Gets the internal path of the file in the archive.
121 /// </summary>
122 /// <value>The internal path of the file in the archive, not including
123 /// the file name.</value>
124 public string Path
125 {
126 get
127 {
128 return this.path;
129 }
130 }
131
132 /// <summary>
133 /// Gets the full path to the file.
134 /// </summary>
135 /// <value>The full path to the file, including the full path to the
136 /// archive, the internal path in the archive, and the file name.</value>
137 /// <remarks>
138 /// For example, the path <c>"C:\archive.cab\file.txt"</c> refers to
139 /// a file "file.txt" inside the archive "archive.cab".
140 /// </remarks>
141 public override string FullName
142 {
143 get
144 {
145 string fullName = System.IO.Path.Combine(this.Path, this.Name);
146
147 if (this.Archive != null)
148 {
149 fullName = System.IO.Path.Combine(this.ArchiveName, fullName);
150 }
151
152 return fullName;
153 }
154 }
155
156 /// <summary>
157 /// Gets or sets the archive that contains this file.
158 /// </summary>
159 /// <value>
160 /// The ArchiveInfo instance that retrieved this file information -- this
161 /// may be null if the ArchiveFileInfo object was returned directly from
162 /// a stream.
163 /// </value>
164 public ArchiveInfo Archive
165 {
166 get
167 {
168 return (ArchiveInfo) this.archiveInfo;
169 }
170
171 internal set
172 {
173 this.archiveInfo = value;
174
175 // protected instance members inherited from FileSystemInfo:
176 this.OriginalPath = (value != null ? value.FullName : null);
177 this.FullPath = this.OriginalPath;
178 }
179 }
180
181 /// <summary>
182 /// Gets the full path of the archive that contains this file.
183 /// </summary>
184 /// <value>The full path of the archive that contains this file.</value>
185 public string ArchiveName
186 {
187 get
188 {
189 return this.Archive != null ? this.Archive.FullName : null;
190 }
191 }
192
193 /// <summary>
194 /// Gets the number of the archive where this file starts.
195 /// </summary>
196 /// <value>The number of the archive where this file starts.</value>
197 /// <remarks>A single archive or the first archive in a chain is
198 /// numbered 0.</remarks>
199 public int ArchiveNumber
200 {
201 get
202 {
203 return this.archiveNumber;
204 }
205 }
206
207 /// <summary>
208 /// Checks if the file exists within the archive.
209 /// </summary>
210 /// <value>True if the file exists, false otherwise.</value>
211 public override bool Exists
212 {
213 get
214 {
215 if (!this.initialized)
216 {
217 this.Refresh();
218 }
219
220 return this.exists;
221 }
222 }
223
224 /// <summary>
225 /// Gets the uncompressed size of the file.
226 /// </summary>
227 /// <value>The uncompressed size of the file in bytes.</value>
228 public long Length
229 {
230 get
231 {
232 if (!this.initialized)
233 {
234 this.Refresh();
235 }
236
237 return this.length;
238 }
239 }
240
241 /// <summary>
242 /// Gets the attributes of the file.
243 /// </summary>
244 /// <value>The attributes of the file as stored in the archive.</value>
245 public new FileAttributes Attributes
246 {
247 get
248 {
249 if (!this.initialized)
250 {
251 this.Refresh();
252 }
253
254 return this.attributes;
255 }
256 }
257
258 /// <summary>
259 /// Gets the last modification time of the file.
260 /// </summary>
261 /// <value>The last modification time of the file as stored in the
262 /// archive.</value>
263 public new DateTime LastWriteTime
264 {
265 get
266 {
267 if (!this.initialized)
268 {
269 this.Refresh();
270 }
271
272 return this.lastWriteTime;
273 }
274 }
275
276 /// <summary>
277 /// Sets the SerializationInfo with information about the archive.
278 /// </summary>
279 /// <param name="info">The SerializationInfo that holds the serialized
280 /// object data.</param>
281 /// <param name="context">The StreamingContext that contains contextual
282 /// information about the source or destination.</param>
283 public override void GetObjectData(
284 SerializationInfo info, StreamingContext context)
285 {
286 base.GetObjectData(info, context);
287 info.AddValue("archiveInfo", this.archiveInfo);
288 info.AddValue("name", this.name);
289 info.AddValue("path", this.path);
290 info.AddValue("initialized", this.initialized);
291 info.AddValue("exists", this.exists);
292 info.AddValue("archiveNumber", this.archiveNumber);
293 info.AddValue("attributes", this.attributes);
294 info.AddValue("lastWriteTime", this.lastWriteTime);
295 info.AddValue("length", this.length);
296 }
297
298 /// <summary>
299 /// Gets the full path to the file.
300 /// </summary>
301 /// <returns>The same as <see cref="FullName"/></returns>
302 public override string ToString()
303 {
304 return this.FullName;
305 }
306
307 /// <summary>
308 /// Deletes the file. NOT SUPPORTED.
309 /// </summary>
310 /// <exception cref="NotSupportedException">Files cannot be deleted
311 /// from an existing archive.</exception>
312 public override void Delete()
313 {
314 throw new NotSupportedException();
315 }
316
317 /// <summary>
318 /// Refreshes the attributes and other cached information about the file,
319 /// by re-reading the information from the archive.
320 /// </summary>
321 public new void Refresh()
322 {
323 base.Refresh();
324
325 if (this.Archive != null)
326 {
327 string filePath = System.IO.Path.Combine(this.Path, this.Name);
328 ArchiveFileInfo updatedFile = this.Archive.GetFile(filePath);
329 if (updatedFile == null)
330 {
331 throw new FileNotFoundException(
332 "File not found in archive.", filePath);
333 }
334
335 this.Refresh(updatedFile);
336 }
337 }
338
339 /// <summary>
340 /// Extracts the file.
341 /// </summary>
342 /// <param name="destFileName">The destination path where the file
343 /// will be extracted.</param>
344 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
345 public void CopyTo(string destFileName)
346 {
347 this.CopyTo(destFileName, false);
348 }
349
350 /// <summary>
351 /// Extracts the file, optionally overwriting any existing file.
352 /// </summary>
353 /// <param name="destFileName">The destination path where the file
354 /// will be extracted.</param>
355 /// <param name="overwrite">If true, <paramref name="destFileName"/>
356 /// will be overwritten if it exists.</param>
357 /// <exception cref="IOException"><paramref name="overwrite"/> is false
358 /// and <paramref name="destFileName"/> exists.</exception>
359 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
360 public void CopyTo(string destFileName, bool overwrite)
361 {
362 if (destFileName == null)
363 {
364 throw new ArgumentNullException("destFileName");
365 }
366
367 if (!overwrite && File.Exists(destFileName))
368 {
369 throw new IOException();
370 }
371
372 if (this.Archive == null)
373 {
374 throw new InvalidOperationException();
375 }
376
377 this.Archive.UnpackFile(
378 System.IO.Path.Combine(this.Path, this.Name), destFileName);
379 }
380
381 /// <summary>
382 /// Opens the archive file for reading without actually extracting the
383 /// file to disk.
384 /// </summary>
385 /// <returns>
386 /// A stream for reading directly from the packed file. Like any stream
387 /// this should be closed/disposed as soon as it is no longer needed.
388 /// </returns>
389 public Stream OpenRead()
390 {
391 return this.Archive.OpenRead(System.IO.Path.Combine(this.Path, this.Name));
392 }
393
394 /// <summary>
395 /// Opens the archive file reading text with UTF-8 encoding without
396 /// actually extracting the file to disk.
397 /// </summary>
398 /// <returns>
399 /// A reader for reading text directly from the packed file. Like any reader
400 /// this should be closed/disposed as soon as it is no longer needed.
401 /// </returns>
402 /// <remarks>
403 /// To open an archived text file with different encoding, use the
404 /// <see cref="OpenRead" /> method and pass the returned stream to one of
405 /// the <see cref="StreamReader" /> constructor overloads.
406 /// </remarks>
407 public StreamReader OpenText()
408 {
409 return this.Archive.OpenText(System.IO.Path.Combine(this.Path, this.Name));
410 }
411
412 /// <summary>
413 /// Refreshes the information in this object with new data retrieved
414 /// from an archive.
415 /// </summary>
416 /// <param name="newFileInfo">Fresh instance for the same file just
417 /// read from the archive.</param>
418 /// <remarks>
419 /// Subclasses may override this method to refresh sublcass fields.
420 /// However they should always call the base implementation first.
421 /// </remarks>
422 protected virtual void Refresh(ArchiveFileInfo newFileInfo)
423 {
424 this.exists = newFileInfo.exists;
425 this.length = newFileInfo.length;
426 this.attributes = newFileInfo.attributes;
427 this.lastWriteTime = newFileInfo.lastWriteTime;
428 }
429 }
430}
diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs
new file mode 100644
index 00000000..8be3bff4
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveFileStreamContext.cs
@@ -0,0 +1,664 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7 using System.Collections.Generic;
8
9 /// <summary>
10 /// Provides a basic implementation of the archive pack and unpack stream context
11 /// interfaces, based on a list of archive files, a default directory, and an
12 /// optional mapping from internal to external file paths.
13 /// </summary>
14 /// <remarks>
15 /// This class can also handle creating or extracting chained archive packages.
16 /// </remarks>
17 public class ArchiveFileStreamContext
18 : IPackStreamContext, IUnpackStreamContext
19 {
20 private IList<string> archiveFiles;
21 private string directory;
22 private IDictionary<string, string> files;
23 private bool extractOnlyNewerFiles;
24 private bool enableOffsetOpen;
25
26 #region Constructors
27
28 /// <summary>
29 /// Creates a new ArchiveFileStreamContext with a archive file and
30 /// no default directory or file mapping.
31 /// </summary>
32 /// <param name="archiveFile">The path to a archive file that will be
33 /// created or extracted.</param>
34 public ArchiveFileStreamContext(string archiveFile)
35 : this(archiveFile, null, null)
36 {
37 }
38
39 /// <summary>
40 /// Creates a new ArchiveFileStreamContext with a archive file, default
41 /// directory and mapping from internal to external file paths.
42 /// </summary>
43 /// <param name="archiveFile">The path to a archive file that will be
44 /// created or extracted.</param>
45 /// <param name="directory">The default root directory where files will be
46 /// located, optional.</param>
47 /// <param name="files">A mapping from internal file paths to external file
48 /// paths, optional.</param>
49 /// <remarks>
50 /// If the mapping is not null and a file is not included in the mapping,
51 /// the file will be skipped.
52 /// <para>If the external path in the mapping is a simple file name or
53 /// relative file path, it will be concatenated onto the default directory,
54 /// if one was specified.</para>
55 /// <para>For more about how the default directory and files mapping are
56 /// used, see <see cref="OpenFileReadStream"/> and
57 /// <see cref="OpenFileWriteStream"/>.</para>
58 /// </remarks>
59 public ArchiveFileStreamContext(
60 string archiveFile,
61 string directory,
62 IDictionary<string, string> files)
63 : this(new string[] { archiveFile }, directory, files)
64 {
65 if (archiveFile == null)
66 {
67 throw new ArgumentNullException("archiveFile");
68 }
69 }
70
71 /// <summary>
72 /// Creates a new ArchiveFileStreamContext with a list of archive files,
73 /// a default directory and a mapping from internal to external file paths.
74 /// </summary>
75 /// <param name="archiveFiles">A list of paths to archive files that will be
76 /// created or extracted.</param>
77 /// <param name="directory">The default root directory where files will be
78 /// located, optional.</param>
79 /// <param name="files">A mapping from internal file paths to external file
80 /// paths, optional.</param>
81 /// <remarks>
82 /// When creating chained archives, the <paramref name="archiveFiles"/> list
83 /// should include at least enough archives to handle the entire set of
84 /// input files, based on the maximum archive size that is passed to the
85 /// <see cref="CompressionEngine"/>.<see
86 /// cref="CompressionEngine.Pack(IPackStreamContext,IEnumerable&lt;string&gt;,long)"/>.
87 /// <para>If the mapping is not null and a file is not included in the mapping,
88 /// the file will be skipped.</para>
89 /// <para>If the external path in the mapping is a simple file name or
90 /// relative file path, it will be concatenated onto the default directory,
91 /// if one was specified.</para>
92 /// <para>For more about how the default directory and files mapping are used,
93 /// see <see cref="OpenFileReadStream"/> and
94 /// <see cref="OpenFileWriteStream"/>.</para>
95 /// </remarks>
96 public ArchiveFileStreamContext(
97 IList<string> archiveFiles,
98 string directory,
99 IDictionary<string, string> files)
100 {
101 if (archiveFiles == null || archiveFiles.Count == 0)
102 {
103 throw new ArgumentNullException("archiveFiles");
104 }
105
106 this.archiveFiles = archiveFiles;
107 this.directory = directory;
108 this.files = files;
109 }
110
111 #endregion
112
113 #region Properties
114
115 /// <summary>
116 /// Gets or sets the list of archive files that are created or extracted.
117 /// </summary>
118 /// <value>The list of archive files that are created or extracted.</value>
119 public IList<string> ArchiveFiles
120 {
121 get
122 {
123 return this.archiveFiles;
124 }
125 }
126
127 /// <summary>
128 /// Gets or sets the default root directory where files are located.
129 /// </summary>
130 /// <value>The default root directory where files are located.</value>
131 /// <remarks>
132 /// For details about how the default directory is used,
133 /// see <see cref="OpenFileReadStream"/> and <see cref="OpenFileWriteStream"/>.
134 /// </remarks>
135 public string Directory
136 {
137 get
138 {
139 return this.directory;
140 }
141 }
142
143 /// <summary>
144 /// Gets or sets the mapping from internal file paths to external file paths.
145 /// </summary>
146 /// <value>A mapping from internal file paths to external file paths.</value>
147 /// <remarks>
148 /// For details about how the files mapping is used,
149 /// see <see cref="OpenFileReadStream"/> and <see cref="OpenFileWriteStream"/>.
150 /// </remarks>
151 public IDictionary<string, string> Files
152 {
153 get
154 {
155 return this.files;
156 }
157 }
158
159 /// <summary>
160 /// Gets or sets a flag that can prevent extracted files from overwriting
161 /// newer files that already exist.
162 /// </summary>
163 /// <value>True to prevent overwriting newer files that already exist
164 /// during extraction; false to always extract from the archive regardless
165 /// of existing files.</value>
166 public bool ExtractOnlyNewerFiles
167 {
168 get
169 {
170 return this.extractOnlyNewerFiles;
171 }
172
173 set
174 {
175 this.extractOnlyNewerFiles = value;
176 }
177 }
178
179 /// <summary>
180 /// Gets or sets a flag that enables creating or extracting an archive
181 /// at an offset within an existing file. (This is typically used to open
182 /// archive-based self-extracting packages.)
183 /// </summary>
184 /// <value>True to search an existing package file for an archive offset
185 /// or the end of the file;/ false to always create or open a plain
186 /// archive file.</value>
187 public bool EnableOffsetOpen
188 {
189 get
190 {
191 return this.enableOffsetOpen;
192 }
193
194 set
195 {
196 this.enableOffsetOpen = value;
197 }
198 }
199
200 #endregion
201
202 #region IPackStreamContext Members
203
204 /// <summary>
205 /// Gets the name of the archive with a specified number.
206 /// </summary>
207 /// <param name="archiveNumber">The 0-based index of the archive within
208 /// the chain.</param>
209 /// <returns>The name of the requested archive. May be an empty string
210 /// for non-chained archives, but may never be null.</returns>
211 /// <remarks>This method returns the file name of the archive from the
212 /// <see cref="archiveFiles"/> list with the specified index, or an empty
213 /// string if the archive number is outside the bounds of the list. The
214 /// file name should not include any directory path.</remarks>
215 public virtual string GetArchiveName(int archiveNumber)
216 {
217 if (archiveNumber < this.archiveFiles.Count)
218 {
219 return Path.GetFileName(this.archiveFiles[archiveNumber]);
220 }
221
222 return String.Empty;
223 }
224
225 /// <summary>
226 /// Opens a stream for writing an archive.
227 /// </summary>
228 /// <param name="archiveNumber">The 0-based index of the archive within
229 /// the chain.</param>
230 /// <param name="archiveName">The name of the archive that was returned
231 /// by <see cref="GetArchiveName"/>.</param>
232 /// <param name="truncate">True if the stream should be truncated when
233 /// opened (if it already exists); false if an existing stream is being
234 /// re-opened for writing additional data.</param>
235 /// <param name="compressionEngine">Instance of the compression engine
236 /// doing the operations.</param>
237 /// <returns>A writable Stream where the compressed archive bytes will be
238 /// written, or null to cancel the archive creation.</returns>
239 /// <remarks>
240 /// This method opens the file from the <see cref="ArchiveFiles"/> list
241 /// with the specified index. If the archive number is outside the bounds
242 /// of the list, this method returns null.
243 /// <para>If the <see cref="EnableOffsetOpen"/> flag is set, this method
244 /// will seek to the start of any existing archive in the file, or to the
245 /// end of the file if the existing file is not an archive.</para>
246 /// </remarks>
247 public virtual Stream OpenArchiveWriteStream(
248 int archiveNumber,
249 string archiveName,
250 bool truncate,
251 CompressionEngine compressionEngine)
252 {
253 if (archiveNumber >= this.archiveFiles.Count)
254 {
255 return null;
256 }
257
258 if (String.IsNullOrEmpty(archiveName))
259 {
260 throw new ArgumentNullException("archiveName");
261 }
262
263 // All archives must be in the same directory,
264 // so always use the directory from the first archive.
265 string archiveFile = Path.Combine(
266 Path.GetDirectoryName(this.archiveFiles[0]), archiveName);
267 Stream stream = File.Open(
268 archiveFile,
269 (truncate ? FileMode.OpenOrCreate : FileMode.Open),
270 FileAccess.ReadWrite);
271
272 if (this.enableOffsetOpen)
273 {
274 long offset = compressionEngine.FindArchiveOffset(
275 new DuplicateStream(stream));
276
277 // If this is not an archive file, append the archive to it.
278 if (offset < 0)
279 {
280 offset = stream.Length;
281 }
282
283 if (offset > 0)
284 {
285 stream = new OffsetStream(stream, offset);
286 }
287
288 stream.Seek(0, SeekOrigin.Begin);
289 }
290
291 if (truncate)
292 {
293 // Truncate the stream, in case a larger old archive starts here.
294 stream.SetLength(0);
295 }
296
297 return stream;
298 }
299
300 /// <summary>
301 /// Closes a stream where an archive package was written.
302 /// </summary>
303 /// <param name="archiveNumber">The 0-based index of the archive within
304 /// the chain.</param>
305 /// <param name="archiveName">The name of the archive that was previously
306 /// returned by <see cref="GetArchiveName"/>.</param>
307 /// <param name="stream">A stream that was previously returned by
308 /// <see cref="OpenArchiveWriteStream"/> and is now ready to be closed.</param>
309 public virtual void CloseArchiveWriteStream(
310 int archiveNumber,
311 string archiveName,
312 Stream stream)
313 {
314 if (stream != null)
315 {
316 stream.Close();
317
318 FileStream fileStream = stream as FileStream;
319 if (fileStream != null)
320 {
321 string streamFile = fileStream.Name;
322 if (!String.IsNullOrEmpty(archiveName) &&
323 archiveName != Path.GetFileName(streamFile))
324 {
325 string archiveFile = Path.Combine(
326 Path.GetDirectoryName(this.archiveFiles[0]), archiveName);
327 if (File.Exists(archiveFile))
328 {
329 File.Delete(archiveFile);
330 }
331 File.Move(streamFile, archiveFile);
332 }
333 }
334 }
335 }
336
337 /// <summary>
338 /// Opens a stream to read a file that is to be included in an archive.
339 /// </summary>
340 /// <param name="path">The path of the file within the archive.</param>
341 /// <param name="attributes">The returned attributes of the opened file,
342 /// to be stored in the archive.</param>
343 /// <param name="lastWriteTime">The returned last-modified time of the
344 /// opened file, to be stored in the archive.</param>
345 /// <returns>A readable Stream where the file bytes will be read from
346 /// before they are compressed, or null to skip inclusion of the file and
347 /// continue to the next file.</returns>
348 /// <remarks>
349 /// This method opens a file using the following logic:
350 /// <list>
351 /// <item>If the <see cref="Directory"/> and the <see cref="Files"/> mapping
352 /// are both null, the path is treated as relative to the current directory,
353 /// and that file is opened.</item>
354 /// <item>If the <see cref="Directory"/> is not null but the <see cref="Files"/>
355 /// mapping is null, the path is treated as relative to that directory, and
356 /// that file is opened.</item>
357 /// <item>If the <see cref="Directory"/> is null but the <see cref="Files"/>
358 /// mapping is not null, the path parameter is used as a key into the mapping,
359 /// and the resulting value is the file path that is opened, relative to the
360 /// current directory (or it may be an absolute path). If no mapping exists,
361 /// the file is skipped.</item>
362 /// <item>If both the <see cref="Directory"/> and the <see cref="Files"/>
363 /// mapping are specified, the path parameter is used as a key into the
364 /// mapping, and the resulting value is the file path that is opened, relative
365 /// to the specified directory (or it may be an absolute path). If no mapping
366 /// exists, the file is skipped.</item>
367 /// </list>
368 /// </remarks>
369 public virtual Stream OpenFileReadStream(
370 string path, out FileAttributes attributes, out DateTime lastWriteTime)
371 {
372 string filePath = this.TranslateFilePath(path);
373
374 if (filePath == null)
375 {
376 attributes = FileAttributes.Normal;
377 lastWriteTime = DateTime.Now;
378 return null;
379 }
380
381 attributes = File.GetAttributes(filePath);
382 lastWriteTime = File.GetLastWriteTime(filePath);
383 return File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
384 }
385
386 /// <summary>
387 /// Closes a stream that has been used to read a file.
388 /// </summary>
389 /// <param name="path">The path of the file within the archive; the same as
390 /// the path provided when the stream was opened.</param>
391 /// <param name="stream">A stream that was previously returned by
392 /// <see cref="OpenFileReadStream"/> and is now ready to be closed.</param>
393 public virtual void CloseFileReadStream(string path, Stream stream)
394 {
395 if (stream != null)
396 {
397 stream.Close();
398 }
399 }
400
401 /// <summary>
402 /// Gets extended parameter information specific to the compression format
403 /// being used.
404 /// </summary>
405 /// <param name="optionName">Name of the option being requested.</param>
406 /// <param name="parameters">Parameters for the option; for per-file options,
407 /// the first parameter is typically the internal file path.</param>
408 /// <returns>Option value, or null to use the default behavior.</returns>
409 /// <remarks>
410 /// This implementation does not handle any options. Subclasses may override
411 /// this method to allow for non-default behavior.
412 /// </remarks>
413 public virtual object GetOption(string optionName, object[] parameters)
414 {
415 return null;
416 }
417
418 #endregion
419
420 #region IUnpackStreamContext Members
421
422 /// <summary>
423 /// Opens the archive stream for reading.
424 /// </summary>
425 /// <param name="archiveNumber">The zero-based index of the archive to
426 /// open.</param>
427 /// <param name="archiveName">The name of the archive being opened.</param>
428 /// <param name="compressionEngine">Instance of the compression engine
429 /// doing the operations.</param>
430 /// <returns>A stream from which archive bytes are read, or null to cancel
431 /// extraction of the archive.</returns>
432 /// <remarks>
433 /// This method opens the file from the <see cref="ArchiveFiles"/> list with
434 /// the specified index. If the archive number is outside the bounds of the
435 /// list, this method returns null.
436 /// <para>If the <see cref="EnableOffsetOpen"/> flag is set, this method will
437 /// seek to the start of any existing archive in the file, or to the end of
438 /// the file if the existing file is not an archive.</para>
439 /// </remarks>
440 public virtual Stream OpenArchiveReadStream(
441 int archiveNumber, string archiveName, CompressionEngine compressionEngine)
442 {
443 if (archiveNumber >= this.archiveFiles.Count)
444 {
445 return null;
446 }
447
448 string archiveFile = this.archiveFiles[archiveNumber];
449 Stream stream = File.Open(
450 archiveFile, FileMode.Open, FileAccess.Read, FileShare.Read);
451
452 if (this.enableOffsetOpen)
453 {
454 long offset = compressionEngine.FindArchiveOffset(
455 new DuplicateStream(stream));
456 if (offset > 0)
457 {
458 stream = new OffsetStream(stream, offset);
459 }
460 else
461 {
462 stream.Seek(0, SeekOrigin.Begin);
463 }
464 }
465
466 return stream;
467 }
468
469 /// <summary>
470 /// Closes a stream where an archive was read.
471 /// </summary>
472 /// <param name="archiveNumber">The archive number of the stream
473 /// to close.</param>
474 /// <param name="archiveName">The name of the archive being closed.</param>
475 /// <param name="stream">The stream that was previously returned by
476 /// <see cref="OpenArchiveReadStream"/> and is now ready to be closed.</param>
477 public virtual void CloseArchiveReadStream(
478 int archiveNumber, string archiveName, Stream stream)
479 {
480 if (stream != null)
481 {
482 stream.Close();
483 }
484 }
485
486 /// <summary>
487 /// Opens a stream for writing extracted file bytes.
488 /// </summary>
489 /// <param name="path">The path of the file within the archive.</param>
490 /// <param name="fileSize">The uncompressed size of the file to be
491 /// extracted.</param>
492 /// <param name="lastWriteTime">The last write time of the file to be
493 /// extracted.</param>
494 /// <returns>A stream where extracted file bytes are to be written, or null
495 /// to skip extraction of the file and continue to the next file.</returns>
496 /// <remarks>
497 /// This method opens a file using the following logic:
498 /// <list>
499 /// <item>If the <see cref="Directory"/> and the <see cref="Files"/> mapping
500 /// are both null, the path is treated as relative to the current directory,
501 /// and that file is opened.</item>
502 /// <item>If the <see cref="Directory"/> is not null but the <see cref="Files"/>
503 /// mapping is null, the path is treated as relative to that directory, and
504 /// that file is opened.</item>
505 /// <item>If the <see cref="Directory"/> is null but the <see cref="Files"/>
506 /// mapping is not null, the path parameter is used as a key into the mapping,
507 /// and the resulting value is the file path that is opened, relative to the
508 /// current directory (or it may be an absolute path). If no mapping exists,
509 /// the file is skipped.</item>
510 /// <item>If both the <see cref="Directory"/> and the <see cref="Files"/>
511 /// mapping are specified, the path parameter is used as a key into the
512 /// mapping, and the resulting value is the file path that is opened,
513 /// relative to the specified directory (or it may be an absolute path).
514 /// If no mapping exists, the file is skipped.</item>
515 /// </list>
516 /// <para>If the <see cref="ExtractOnlyNewerFiles"/> flag is set, the file
517 /// is skipped if a file currently exists in the same path with an equal
518 /// or newer write time.</para>
519 /// </remarks>
520 public virtual Stream OpenFileWriteStream(
521 string path,
522 long fileSize,
523 DateTime lastWriteTime)
524 {
525 string filePath = this.TranslateFilePath(path);
526
527 if (filePath == null)
528 {
529 return null;
530 }
531
532 FileInfo file = new FileInfo(filePath);
533 if (file.Exists)
534 {
535 if (this.extractOnlyNewerFiles && lastWriteTime != DateTime.MinValue)
536 {
537 if (file.LastWriteTime >= lastWriteTime)
538 {
539 return null;
540 }
541 }
542
543 // Clear attributes that will prevent overwriting the file.
544 // (The final attributes will be set after the file is unpacked.)
545 FileAttributes attributesToClear =
546 FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System;
547 if ((file.Attributes & attributesToClear) != 0)
548 {
549 file.Attributes &= ~attributesToClear;
550 }
551 }
552
553 if (!file.Directory.Exists)
554 {
555 file.Directory.Create();
556 }
557
558 return File.Open(
559 filePath, FileMode.Create, FileAccess.Write, FileShare.None);
560 }
561
562 /// <summary>
563 /// Closes a stream where an extracted file was written.
564 /// </summary>
565 /// <param name="path">The path of the file within the archive.</param>
566 /// <param name="stream">The stream that was previously returned by
567 /// <see cref="OpenFileWriteStream"/> and is now ready to be closed.</param>
568 /// <param name="attributes">The attributes of the extracted file.</param>
569 /// <param name="lastWriteTime">The last write time of the file.</param>
570 /// <remarks>
571 /// After closing the extracted file stream, this method applies the date
572 /// and attributes to that file.
573 /// </remarks>
574 public virtual void CloseFileWriteStream(
575 string path,
576 Stream stream,
577 FileAttributes attributes,
578 DateTime lastWriteTime)
579 {
580 if (stream != null)
581 {
582 stream.Close();
583 }
584
585 string filePath = this.TranslateFilePath(path);
586 if (filePath != null)
587 {
588 FileInfo file = new FileInfo(filePath);
589
590 if (lastWriteTime != DateTime.MinValue)
591 {
592 try
593 {
594 file.LastWriteTime = lastWriteTime;
595 }
596 catch (ArgumentException)
597 {
598 }
599 catch (IOException)
600 {
601 }
602 }
603
604 try
605 {
606 file.Attributes = attributes;
607 }
608 catch (IOException)
609 {
610 }
611 }
612 }
613
614 #endregion
615
616 #region Private utility methods
617
618 /// <summary>
619 /// Translates an internal file path to an external file path using the
620 /// <see cref="Directory"/> and the <see cref="Files"/> mapping, according to
621 /// rules documented in <see cref="OpenFileReadStream"/> and
622 /// <see cref="OpenFileWriteStream"/>.
623 /// </summary>
624 /// <param name="path">The path of the file with the archive.</param>
625 /// <returns>The external path of the file, or null if there is no
626 /// valid translation.</returns>
627 private string TranslateFilePath(string path)
628 {
629 string filePath;
630 if (this.files != null)
631 {
632 filePath = this.files[path];
633 }
634 else
635 {
636 this.ValidateArchivePath(path);
637
638 filePath = path;
639 }
640
641 if (filePath != null)
642 {
643 if (this.directory != null)
644 {
645 filePath = Path.Combine(this.directory, filePath);
646 }
647 }
648
649 return filePath;
650 }
651
652 private void ValidateArchivePath(string filePath)
653 {
654 string basePath = Path.GetFullPath(String.IsNullOrEmpty(this.directory) ? Environment.CurrentDirectory : this.directory);
655 string path = Path.GetFullPath(Path.Combine(basePath, filePath));
656 if (!path.StartsWith(basePath, StringComparison.InvariantCultureIgnoreCase))
657 {
658 throw new InvalidDataException("Archive cannot contain files with absolute or traversal paths.");
659 }
660 }
661
662 #endregion
663 }
664}
diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs
new file mode 100644
index 00000000..b5da4ea8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveInfo.cs
@@ -0,0 +1,781 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Text;
10 using System.Text.RegularExpressions;
11 using System.Runtime.Serialization;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// Abstract object representing a compressed archive on disk;
16 /// provides access to file-based operations on the archive.
17 /// </summary>
18 [Serializable]
19 public abstract class ArchiveInfo : FileSystemInfo
20 {
21 /// <summary>
22 /// Creates a new ArchiveInfo object representing an archive in a
23 /// specified path.
24 /// </summary>
25 /// <param name="path">The path to the archive. When creating an archive,
26 /// this file does not necessarily exist yet.</param>
27 protected ArchiveInfo(string path) : base()
28 {
29 if (path == null)
30 {
31 throw new ArgumentNullException("path");
32 }
33
34 // protected instance members inherited from FileSystemInfo:
35 this.OriginalPath = path;
36 this.FullPath = Path.GetFullPath(path);
37 }
38
39 /// <summary>
40 /// Initializes a new instance of the ArchiveInfo class with serialized data.
41 /// </summary>
42 /// <param name="info">The SerializationInfo that holds the serialized object
43 /// data about the exception being thrown.</param>
44 /// <param name="context">The StreamingContext that contains contextual
45 /// information about the source or destination.</param>
46 protected ArchiveInfo(SerializationInfo info, StreamingContext context)
47 : base(info, context)
48 {
49 }
50
51 /// <summary>
52 /// Gets the directory that contains the archive.
53 /// </summary>
54 /// <value>A DirectoryInfo object representing the parent directory of the
55 /// archive.</value>
56 public DirectoryInfo Directory
57 {
58 get
59 {
60 return new DirectoryInfo(Path.GetDirectoryName(this.FullName));
61 }
62 }
63
64 /// <summary>
65 /// Gets the full path of the directory that contains the archive.
66 /// </summary>
67 /// <value>The full path of the directory that contains the archive.</value>
68 public string DirectoryName
69 {
70 get
71 {
72 return Path.GetDirectoryName(this.FullName);
73 }
74 }
75
76 /// <summary>
77 /// Gets the size of the archive.
78 /// </summary>
79 /// <value>The size of the archive in bytes.</value>
80 public long Length
81 {
82 get
83 {
84 return new FileInfo(this.FullName).Length;
85 }
86 }
87
88 /// <summary>
89 /// Gets the file name of the archive.
90 /// </summary>
91 /// <value>The file name of the archive, not including any path.</value>
92 public override string Name
93 {
94 get
95 {
96 return Path.GetFileName(this.FullName);
97 }
98 }
99
100 /// <summary>
101 /// Checks if the archive exists.
102 /// </summary>
103 /// <value>True if the archive exists; else false.</value>
104 public override bool Exists
105 {
106 get
107 {
108 return File.Exists(this.FullName);
109 }
110 }
111
112 /// <summary>
113 /// Gets the full path of the archive.
114 /// </summary>
115 /// <returns>The full path of the archive.</returns>
116 public override string ToString()
117 {
118 return this.FullName;
119 }
120
121 /// <summary>
122 /// Deletes the archive.
123 /// </summary>
124 public override void Delete()
125 {
126 File.Delete(this.FullName);
127 }
128
129 /// <summary>
130 /// Copies an existing archive to another location.
131 /// </summary>
132 /// <param name="destFileName">The destination file path.</param>
133 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
134 public void CopyTo(string destFileName)
135 {
136 File.Copy(this.FullName, destFileName);
137 }
138
139 /// <summary>
140 /// Copies an existing archive to another location, optionally
141 /// overwriting the destination file.
142 /// </summary>
143 /// <param name="destFileName">The destination file path.</param>
144 /// <param name="overwrite">If true, the destination file will be
145 /// overwritten if it exists.</param>
146 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
147 public void CopyTo(string destFileName, bool overwrite)
148 {
149 File.Copy(this.FullName, destFileName, overwrite);
150 }
151
152 /// <summary>
153 /// Moves an existing archive to another location.
154 /// </summary>
155 /// <param name="destFileName">The destination file path.</param>
156 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
157 public void MoveTo(string destFileName)
158 {
159 File.Move(this.FullName, destFileName);
160 this.FullPath = Path.GetFullPath(destFileName);
161 }
162
163 /// <summary>
164 /// Checks if the archive contains a valid archive header.
165 /// </summary>
166 /// <returns>True if the file is a valid archive; false otherwise.</returns>
167 public bool IsValid()
168 {
169 using (Stream stream = File.OpenRead(this.FullName))
170 {
171 using (CompressionEngine compressionEngine = this.CreateCompressionEngine())
172 {
173 return compressionEngine.FindArchiveOffset(stream) >= 0;
174 }
175 }
176 }
177
178 /// <summary>
179 /// Gets information about the files contained in the archive.
180 /// </summary>
181 /// <returns>A list of <see cref="ArchiveFileInfo"/> objects, each
182 /// containing information about a file in the archive.</returns>
183 public IList<ArchiveFileInfo> GetFiles()
184 {
185 return this.InternalGetFiles((Predicate<string>) null);
186 }
187
188 /// <summary>
189 /// Gets information about the certain files contained in the archive file.
190 /// </summary>
191 /// <param name="searchPattern">The search string, such as
192 /// &quot;*.txt&quot;.</param>
193 /// <returns>A list of <see cref="ArchiveFileInfo"/> objects, each containing
194 /// information about a file in the archive.</returns>
195 public IList<ArchiveFileInfo> GetFiles(string searchPattern)
196 {
197 if (searchPattern == null)
198 {
199 throw new ArgumentNullException("searchPattern");
200 }
201
202 string regexPattern = String.Format(
203 CultureInfo.InvariantCulture,
204 "^{0}$",
205 Regex.Escape(searchPattern).Replace("\\*", ".*").Replace("\\?", "."));
206 Regex regex = new Regex(
207 regexPattern,
208 RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
209
210 return this.InternalGetFiles(
211 delegate(string match)
212 {
213 return regex.IsMatch(match);
214 });
215 }
216
217 /// <summary>
218 /// Extracts all files from an archive to a destination directory.
219 /// </summary>
220 /// <param name="destDirectory">Directory where the files are to be
221 /// extracted.</param>
222 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
223 public void Unpack(string destDirectory)
224 {
225 this.Unpack(destDirectory, null);
226 }
227
228 /// <summary>
229 /// Extracts all files from an archive to a destination directory,
230 /// optionally extracting only newer files.
231 /// </summary>
232 /// <param name="destDirectory">Directory where the files are to be
233 /// extracted.</param>
234 /// <param name="progressHandler">Handler for receiving progress
235 /// information; this may be null if progress is not desired.</param>
236 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
237 public void Unpack(
238 string destDirectory,
239 EventHandler<ArchiveProgressEventArgs> progressHandler)
240 {
241 using (CompressionEngine compressionEngine = this.CreateCompressionEngine())
242 {
243 compressionEngine.Progress += progressHandler;
244 ArchiveFileStreamContext streamContext =
245 new ArchiveFileStreamContext(this.FullName, destDirectory, null);
246 streamContext.EnableOffsetOpen = true;
247 compressionEngine.Unpack(streamContext, null);
248 }
249 }
250
251 /// <summary>
252 /// Extracts a single file from the archive.
253 /// </summary>
254 /// <param name="fileName">The name of the file in the archive. Also
255 /// includes the internal path of the file, if any. File name matching
256 /// is case-insensitive.</param>
257 /// <param name="destFileName">The path where the file is to be
258 /// extracted on disk.</param>
259 /// <remarks>If <paramref name="destFileName"/> already exists,
260 /// it will be overwritten.</remarks>
261 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
262 public void UnpackFile(string fileName, string destFileName)
263 {
264 if (fileName == null)
265 {
266 throw new ArgumentNullException("fileName");
267 }
268
269 if (destFileName == null)
270 {
271 throw new ArgumentNullException("destFileName");
272 }
273
274 this.UnpackFiles(
275 new string[] { fileName },
276 null,
277 new string[] { destFileName });
278 }
279
280 /// <summary>
281 /// Extracts multiple files from the archive.
282 /// </summary>
283 /// <param name="fileNames">The names of the files in the archive.
284 /// Each name includes the internal path of the file, if any. File name
285 /// matching is case-insensitive.</param>
286 /// <param name="destDirectory">This parameter may be null, but if
287 /// specified it is the root directory for any relative paths in
288 /// <paramref name="destFileNames"/>.</param>
289 /// <param name="destFileNames">The paths where the files are to be
290 /// extracted on disk. If this parameter is null, the files will be
291 /// extracted with the names from the archive.</param>
292 /// <remarks>
293 /// If any extracted files already exist on disk, they will be overwritten.
294 /// <p>The <paramref name="destDirectory"/> and
295 /// <paramref name="destFileNames"/> parameters cannot both be null.</p>
296 /// </remarks>
297 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
298 public void UnpackFiles(
299 IList<string> fileNames,
300 string destDirectory,
301 IList<string> destFileNames)
302 {
303 this.UnpackFiles(fileNames, destDirectory, destFileNames, null);
304 }
305
306 /// <summary>
307 /// Extracts multiple files from the archive, optionally extracting
308 /// only newer files.
309 /// </summary>
310 /// <param name="fileNames">The names of the files in the archive.
311 /// Each name includes the internal path of the file, if any. File name
312 /// matching is case-insensitive.</param>
313 /// <param name="destDirectory">This parameter may be null, but if
314 /// specified it is the root directory for any relative paths in
315 /// <paramref name="destFileNames"/>.</param>
316 /// <param name="destFileNames">The paths where the files are to be
317 /// extracted on disk. If this parameter is null, the files will be
318 /// extracted with the names from the archive.</param>
319 /// <param name="progressHandler">Handler for receiving progress information;
320 /// this may be null if progress is not desired.</param>
321 /// <remarks>
322 /// If any extracted files already exist on disk, they will be overwritten.
323 /// <p>The <paramref name="destDirectory"/> and
324 /// <paramref name="destFileNames"/> parameters cannot both be null.</p>
325 /// </remarks>
326 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
327 public void UnpackFiles(
328 IList<string> fileNames,
329 string destDirectory,
330 IList<string> destFileNames,
331 EventHandler<ArchiveProgressEventArgs> progressHandler)
332 {
333 if (fileNames == null)
334 {
335 throw new ArgumentNullException("fileNames");
336 }
337
338 if (destFileNames == null)
339 {
340 if (destDirectory == null)
341 {
342 throw new ArgumentNullException("destFileNames");
343 }
344
345 destFileNames = fileNames;
346 }
347
348 if (destFileNames.Count != fileNames.Count)
349 {
350 throw new ArgumentOutOfRangeException("destFileNames");
351 }
352
353 IDictionary<string, string> files =
354 ArchiveInfo.CreateStringDictionary(fileNames, destFileNames);
355 this.UnpackFileSet(files, destDirectory, progressHandler);
356 }
357
358 /// <summary>
359 /// Extracts multiple files from the archive.
360 /// </summary>
361 /// <param name="fileNames">A mapping from internal file paths to
362 /// external file paths. Case-senstivity when matching internal paths
363 /// depends on the IDictionary implementation.</param>
364 /// <param name="destDirectory">This parameter may be null, but if
365 /// specified it is the root directory for any relative external paths
366 /// in <paramref name="fileNames"/>.</param>
367 /// <remarks>
368 /// If any extracted files already exist on disk, they will be overwritten.
369 /// </remarks>
370 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
371 public void UnpackFileSet(
372 IDictionary<string, string> fileNames,
373 string destDirectory)
374 {
375 this.UnpackFileSet(fileNames, destDirectory, null);
376 }
377
378 /// <summary>
379 /// Extracts multiple files from the archive.
380 /// </summary>
381 /// <param name="fileNames">A mapping from internal file paths to
382 /// external file paths. Case-senstivity when matching internal
383 /// paths depends on the IDictionary implementation.</param>
384 /// <param name="destDirectory">This parameter may be null, but if
385 /// specified it is the root directory for any relative external
386 /// paths in <paramref name="fileNames"/>.</param>
387 /// <param name="progressHandler">Handler for receiving progress
388 /// information; this may be null if progress is not desired.</param>
389 /// <remarks>
390 /// If any extracted files already exist on disk, they will be overwritten.
391 /// </remarks>
392 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")]
393 public void UnpackFileSet(
394 IDictionary<string, string> fileNames,
395 string destDirectory,
396 EventHandler<ArchiveProgressEventArgs> progressHandler)
397 {
398 if (fileNames == null)
399 {
400 throw new ArgumentNullException("fileNames");
401 }
402
403 using (CompressionEngine compressionEngine = this.CreateCompressionEngine())
404 {
405 compressionEngine.Progress += progressHandler;
406 ArchiveFileStreamContext streamContext =
407 new ArchiveFileStreamContext(this.FullName, destDirectory, fileNames);
408 streamContext.EnableOffsetOpen = true;
409 compressionEngine.Unpack(
410 streamContext,
411 delegate(string match)
412 {
413 return fileNames.ContainsKey(match);
414 });
415 }
416 }
417
418 /// <summary>
419 /// Opens a file inside the archive for reading without actually
420 /// extracting the file to disk.
421 /// </summary>
422 /// <param name="fileName">The name of the file in the archive. Also
423 /// includes the internal path of the file, if any. File name matching
424 /// is case-insensitive.</param>
425 /// <returns>
426 /// A stream for reading directly from the packed file. Like any stream
427 /// this should be closed/disposed as soon as it is no longer needed.
428 /// </returns>
429 public Stream OpenRead(string fileName)
430 {
431 Stream archiveStream = File.OpenRead(this.FullName);
432 CompressionEngine compressionEngine = this.CreateCompressionEngine();
433 Stream fileStream = compressionEngine.Unpack(archiveStream, fileName);
434
435 // Attach the archiveStream and compressionEngine to the
436 // fileStream so they get disposed when the fileStream is disposed.
437 return new CargoStream(fileStream, archiveStream, compressionEngine);
438 }
439
440 /// <summary>
441 /// Opens a file inside the archive for reading text with UTF-8 encoding
442 /// without actually extracting the file to disk.
443 /// </summary>
444 /// <param name="fileName">The name of the file in the archive. Also
445 /// includes the internal path of the file, if any. File name matching
446 /// is case-insensitive.</param>
447 /// <returns>
448 /// A reader for reading text directly from the packed file. Like any reader
449 /// this should be closed/disposed as soon as it is no longer needed.
450 /// </returns>
451 /// <remarks>
452 /// To open an archived text file with different encoding, use the
453 /// <see cref="OpenRead" /> method and pass the returned stream to one of
454 /// the <see cref="StreamReader" /> constructor overloads.
455 /// </remarks>
456 public StreamReader OpenText(string fileName)
457 {
458 return new StreamReader(this.OpenRead(fileName));
459 }
460
461 /// <summary>
462 /// Compresses all files in a directory into the archive.
463 /// Does not include subdirectories.
464 /// </summary>
465 /// <param name="sourceDirectory">The directory containing the
466 /// files to be included.</param>
467 /// <remarks>
468 /// Uses maximum compression level.
469 /// </remarks>
470 public void Pack(string sourceDirectory)
471 {
472 this.Pack(sourceDirectory, false, CompressionLevel.Max, null);
473 }
474
475 /// <summary>
476 /// Compresses all files in a directory into the archive, optionally
477 /// including subdirectories.
478 /// </summary>
479 /// <param name="sourceDirectory">This is the root directory
480 /// for to pack all files.</param>
481 /// <param name="includeSubdirectories">If true, recursively include
482 /// files in subdirectories.</param>
483 /// <param name="compLevel">The compression level used when creating
484 /// the archive.</param>
485 /// <param name="progressHandler">Handler for receiving progress information;
486 /// this may be null if progress is not desired.</param>
487 /// <remarks>
488 /// The files are stored in the archive using their relative file paths in
489 /// the directory tree, if supported by the archive file format.
490 /// </remarks>
491 public void Pack(
492 string sourceDirectory,
493 bool includeSubdirectories,
494 CompressionLevel compLevel,
495 EventHandler<ArchiveProgressEventArgs> progressHandler)
496 {
497 IList<string> files = this.GetRelativeFilePathsInDirectoryTree(
498 sourceDirectory, includeSubdirectories);
499 this.PackFiles(sourceDirectory, files, files, compLevel, progressHandler);
500 }
501
502 /// <summary>
503 /// Compresses files into the archive, specifying the names used to
504 /// store the files in the archive.
505 /// </summary>
506 /// <param name="sourceDirectory">This parameter may be null, but
507 /// if specified it is the root directory
508 /// for any relative paths in <paramref name="sourceFileNames"/>.</param>
509 /// <param name="sourceFileNames">The list of files to be included in
510 /// the archive.</param>
511 /// <param name="fileNames">The names of the files as they are stored
512 /// in the archive. Each name
513 /// includes the internal path of the file, if any. This parameter may
514 /// be null, in which case the files are stored in the archive with their
515 /// source file names and no path information.</param>
516 /// <remarks>
517 /// Uses maximum compression level.
518 /// <p>Duplicate items in the <paramref name="fileNames"/> array will cause
519 /// an <see cref="ArchiveException"/>.</p>
520 /// </remarks>
521 public void PackFiles(
522 string sourceDirectory,
523 IList<string> sourceFileNames,
524 IList<string> fileNames)
525 {
526 this.PackFiles(
527 sourceDirectory,
528 sourceFileNames,
529 fileNames,
530 CompressionLevel.Max,
531 null);
532 }
533
534 /// <summary>
535 /// Compresses files into the archive, specifying the names used to
536 /// store the files in the archive.
537 /// </summary>
538 /// <param name="sourceDirectory">This parameter may be null, but if
539 /// specified it is the root directory
540 /// for any relative paths in <paramref name="sourceFileNames"/>.</param>
541 /// <param name="sourceFileNames">The list of files to be included in
542 /// the archive.</param>
543 /// <param name="fileNames">The names of the files as they are stored in
544 /// the archive. Each name includes the internal path of the file, if any.
545 /// This parameter may be null, in which case the files are stored in the
546 /// archive with their source file names and no path information.</param>
547 /// <param name="compLevel">The compression level used when creating the
548 /// archive.</param>
549 /// <param name="progressHandler">Handler for receiving progress information;
550 /// this may be null if progress is not desired.</param>
551 /// <remarks>
552 /// Duplicate items in the <paramref name="fileNames"/> array will cause
553 /// an <see cref="ArchiveException"/>.
554 /// </remarks>
555 public void PackFiles(
556 string sourceDirectory,
557 IList<string> sourceFileNames,
558 IList<string> fileNames,
559 CompressionLevel compLevel,
560 EventHandler<ArchiveProgressEventArgs> progressHandler)
561 {
562 if (sourceFileNames == null)
563 {
564 throw new ArgumentNullException("sourceFileNames");
565 }
566
567 if (fileNames == null)
568 {
569 string[] fileNamesArray = new string[sourceFileNames.Count];
570 for (int i = 0; i < sourceFileNames.Count; i++)
571 {
572 fileNamesArray[i] = Path.GetFileName(sourceFileNames[i]);
573 }
574
575 fileNames = fileNamesArray;
576 }
577 else if (fileNames.Count != sourceFileNames.Count)
578 {
579 throw new ArgumentOutOfRangeException("fileNames");
580 }
581
582 using (CompressionEngine compressionEngine = this.CreateCompressionEngine())
583 {
584 compressionEngine.Progress += progressHandler;
585 IDictionary<string, string> contextFiles =
586 ArchiveInfo.CreateStringDictionary(fileNames, sourceFileNames);
587 ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext(
588 this.FullName, sourceDirectory, contextFiles);
589 streamContext.EnableOffsetOpen = true;
590 compressionEngine.CompressionLevel = compLevel;
591 compressionEngine.Pack(streamContext, fileNames);
592 }
593 }
594
595 /// <summary>
596 /// Compresses files into the archive, specifying the names used
597 /// to store the files in the archive.
598 /// </summary>
599 /// <param name="sourceDirectory">This parameter may be null, but if
600 /// specified it is the root directory
601 /// for any relative paths in <paramref name="fileNames"/>.</param>
602 /// <param name="fileNames">A mapping from internal file paths to
603 /// external file paths.</param>
604 /// <remarks>
605 /// Uses maximum compression level.
606 /// </remarks>
607 public void PackFileSet(
608 string sourceDirectory,
609 IDictionary<string, string> fileNames)
610 {
611 this.PackFileSet(sourceDirectory, fileNames, CompressionLevel.Max, null);
612 }
613
614 /// <summary>
615 /// Compresses files into the archive, specifying the names used to
616 /// store the files in the archive.
617 /// </summary>
618 /// <param name="sourceDirectory">This parameter may be null, but if
619 /// specified it is the root directory
620 /// for any relative paths in <paramref name="fileNames"/>.</param>
621 /// <param name="fileNames">A mapping from internal file paths to
622 /// external file paths.</param>
623 /// <param name="compLevel">The compression level used when creating
624 /// the archive.</param>
625 /// <param name="progressHandler">Handler for receiving progress information;
626 /// this may be null if progress is not desired.</param>
627 public void PackFileSet(
628 string sourceDirectory,
629 IDictionary<string, string> fileNames,
630 CompressionLevel compLevel,
631 EventHandler<ArchiveProgressEventArgs> progressHandler)
632 {
633 if (fileNames == null)
634 {
635 throw new ArgumentNullException("fileNames");
636 }
637
638 string[] fileNamesArray = new string[fileNames.Count];
639 fileNames.Keys.CopyTo(fileNamesArray, 0);
640
641 using (CompressionEngine compressionEngine = this.CreateCompressionEngine())
642 {
643 compressionEngine.Progress += progressHandler;
644 ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext(
645 this.FullName, sourceDirectory, fileNames);
646 streamContext.EnableOffsetOpen = true;
647 compressionEngine.CompressionLevel = compLevel;
648 compressionEngine.Pack(streamContext, fileNamesArray);
649 }
650 }
651
652 /// <summary>
653 /// Given a directory, gets the relative paths of all files in the
654 /// directory, optionally including all subdirectories.
655 /// </summary>
656 /// <param name="dir">The directory to search.</param>
657 /// <param name="includeSubdirectories">True to include subdirectories
658 /// in the search.</param>
659 /// <returns>A list of file paths relative to the directory.</returns>
660 internal IList<string> GetRelativeFilePathsInDirectoryTree(
661 string dir, bool includeSubdirectories)
662 {
663 IList<string> fileList = new List<string>();
664 this.RecursiveGetRelativeFilePathsInDirectoryTree(
665 dir, String.Empty, includeSubdirectories, fileList);
666 return fileList;
667 }
668
669 /// <summary>
670 /// Retrieves information about one file from this archive.
671 /// </summary>
672 /// <param name="path">Path of the file in the archive.</param>
673 /// <returns>File information, or null if the file was not found
674 /// in the archive.</returns>
675 internal ArchiveFileInfo GetFile(string path)
676 {
677 IList<ArchiveFileInfo> files = this.InternalGetFiles(
678 delegate(string match)
679 {
680 return String.Compare(
681 match, path, true, CultureInfo.InvariantCulture) == 0;
682 });
683 return (files != null && files.Count > 0 ? files[0] : null);
684 }
685
686 /// <summary>
687 /// Creates a compression engine that does the low-level work for
688 /// this object.
689 /// </summary>
690 /// <returns>A new compression engine instance that matches the specific
691 /// subclass of archive.</returns>
692 /// <remarks>
693 /// Each instance will be <see cref="CompressionEngine.Dispose()"/>d
694 /// immediately after use.
695 /// </remarks>
696 protected abstract CompressionEngine CreateCompressionEngine();
697
698 /// <summary>
699 /// Creates a case-insensitive dictionary mapping from one list of
700 /// strings to the other.
701 /// </summary>
702 /// <param name="keys">List of keys.</param>
703 /// <param name="values">List of values that are mapped 1-to-1 to
704 /// the keys.</param>
705 /// <returns>A filled dictionary of the strings.</returns>
706 private static IDictionary<string, string> CreateStringDictionary(
707 IList<string> keys, IList<string> values)
708 {
709 IDictionary<string, string> stringDict =
710 new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
711 for (int i = 0; i < keys.Count; i++)
712 {
713 stringDict.Add(keys[i], values[i]);
714 }
715
716 return stringDict;
717 }
718
719 /// <summary>
720 /// Recursive-descent helper function for
721 /// GetRelativeFilePathsInDirectoryTree.
722 /// </summary>
723 /// <param name="dir">The root directory of the search.</param>
724 /// <param name="relativeDir">The relative directory to be
725 /// processed now.</param>
726 /// <param name="includeSubdirectories">True to descend into
727 /// subdirectories.</param>
728 /// <param name="fileList">List of files found so far.</param>
729 private void RecursiveGetRelativeFilePathsInDirectoryTree(
730 string dir,
731 string relativeDir,
732 bool includeSubdirectories,
733 IList<string> fileList)
734 {
735 foreach (string file in System.IO.Directory.GetFiles(dir))
736 {
737 string fileName = Path.GetFileName(file);
738 fileList.Add(Path.Combine(relativeDir, fileName));
739 }
740
741 if (includeSubdirectories)
742 {
743 foreach (string subDir in System.IO.Directory.GetDirectories(dir))
744 {
745 string subDirName = Path.GetFileName(subDir);
746 this.RecursiveGetRelativeFilePathsInDirectoryTree(
747 Path.Combine(dir, subDirName),
748 Path.Combine(relativeDir, subDirName),
749 includeSubdirectories,
750 fileList);
751 }
752 }
753 }
754
755 /// <summary>
756 /// Uses a CompressionEngine to get ArchiveFileInfo objects from this
757 /// archive, and then associates them with this ArchiveInfo instance.
758 /// </summary>
759 /// <param name="fileFilter">Optional predicate that can determine
760 /// which files to process.</param>
761 /// <returns>A list of <see cref="ArchiveFileInfo"/> objects, each
762 /// containing information about a file in the archive.</returns>
763 private IList<ArchiveFileInfo> InternalGetFiles(Predicate<string> fileFilter)
764 {
765 using (CompressionEngine compressionEngine = this.CreateCompressionEngine())
766 {
767 ArchiveFileStreamContext streamContext =
768 new ArchiveFileStreamContext(this.FullName, null, null);
769 streamContext.EnableOffsetOpen = true;
770 IList<ArchiveFileInfo> files =
771 compressionEngine.GetFileInfo(streamContext, fileFilter);
772 for (int i = 0; i < files.Count; i++)
773 {
774 files[i].Archive = this;
775 }
776
777 return files;
778 }
779 }
780 }
781}
diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs
new file mode 100644
index 00000000..5d96d714
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressEventArgs.cs
@@ -0,0 +1,307 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5using System;
6using System.Collections.Generic;
7using System.Text;
8
9 /// <summary>
10 /// Contains the data reported in an archive progress event.
11 /// </summary>
12 public class ArchiveProgressEventArgs : EventArgs
13 {
14 private ArchiveProgressType progressType;
15
16 private string currentFileName;
17 private int currentFileNumber;
18 private int totalFiles;
19 private long currentFileBytesProcessed;
20 private long currentFileTotalBytes;
21
22 private string currentArchiveName;
23 private short currentArchiveNumber;
24 private short totalArchives;
25 private long currentArchiveBytesProcessed;
26 private long currentArchiveTotalBytes;
27
28 private long fileBytesProcessed;
29 private long totalFileBytes;
30
31 /// <summary>
32 /// Creates a new ArchiveProgressEventArgs object from specified event parameters.
33 /// </summary>
34 /// <param name="progressType">type of status message</param>
35 /// <param name="currentFileName">name of the file being processed</param>
36 /// <param name="currentFileNumber">number of the current file being processed</param>
37 /// <param name="totalFiles">total number of files to be processed</param>
38 /// <param name="currentFileBytesProcessed">number of bytes processed so far when compressing or extracting a file</param>
39 /// <param name="currentFileTotalBytes">total number of bytes in the current file</param>
40 /// <param name="currentArchiveName">name of the current Archive</param>
41 /// <param name="currentArchiveNumber">current Archive number, when processing a chained set of Archives</param>
42 /// <param name="totalArchives">total number of Archives in a chained set</param>
43 /// <param name="currentArchiveBytesProcessed">number of compressed bytes processed so far during an extraction</param>
44 /// <param name="currentArchiveTotalBytes">total number of compressed bytes to be processed during an extraction</param>
45 /// <param name="fileBytesProcessed">number of uncompressed file bytes processed so far</param>
46 /// <param name="totalFileBytes">total number of uncompressed file bytes to be processed</param>
47 public ArchiveProgressEventArgs(
48 ArchiveProgressType progressType,
49 string currentFileName,
50 int currentFileNumber,
51 int totalFiles,
52 long currentFileBytesProcessed,
53 long currentFileTotalBytes,
54 string currentArchiveName,
55 int currentArchiveNumber,
56 int totalArchives,
57 long currentArchiveBytesProcessed,
58 long currentArchiveTotalBytes,
59 long fileBytesProcessed,
60 long totalFileBytes)
61 {
62 this.progressType = progressType;
63 this.currentFileName = currentFileName;
64 this.currentFileNumber = currentFileNumber;
65 this.totalFiles = totalFiles;
66 this.currentFileBytesProcessed = currentFileBytesProcessed;
67 this.currentFileTotalBytes = currentFileTotalBytes;
68 this.currentArchiveName = currentArchiveName;
69 this.currentArchiveNumber = (short) currentArchiveNumber;
70 this.totalArchives = (short) totalArchives;
71 this.currentArchiveBytesProcessed = currentArchiveBytesProcessed;
72 this.currentArchiveTotalBytes = currentArchiveTotalBytes;
73 this.fileBytesProcessed = fileBytesProcessed;
74 this.totalFileBytes = totalFileBytes;
75 }
76
77 /// <summary>
78 /// Gets the type of status message.
79 /// </summary>
80 /// <value>A <see cref="ArchiveProgressType"/> value indicating what type of progress event occurred.</value>
81 /// <remarks>
82 /// The handler may choose to ignore some types of progress events.
83 /// For example, if the handler will only list each file as it is
84 /// compressed/extracted, it can ignore events that
85 /// are not of type <see cref="ArchiveProgressType.FinishFile"/>.
86 /// </remarks>
87 public ArchiveProgressType ProgressType
88 {
89 get
90 {
91 return this.progressType;
92 }
93 }
94
95 /// <summary>
96 /// Gets the name of the file being processed. (The name of the file within the Archive; not the external
97 /// file path.) Also includes the internal path of the file, if any. Valid for
98 /// <see cref="ArchiveProgressType.StartFile"/>, <see cref="ArchiveProgressType.PartialFile"/>,
99 /// and <see cref="ArchiveProgressType.FinishFile"/> messages.
100 /// </summary>
101 /// <value>The name of the file currently being processed, or null if processing
102 /// is currently at the stream or archive level.</value>
103 public string CurrentFileName
104 {
105 get
106 {
107 return this.currentFileName;
108 }
109 }
110
111 /// <summary>
112 /// Gets the number of the current file being processed. The first file is number 0, and the last file
113 /// is <see cref="TotalFiles"/>-1. Valid for <see cref="ArchiveProgressType.StartFile"/>,
114 /// <see cref="ArchiveProgressType.PartialFile"/>, and <see cref="ArchiveProgressType.FinishFile"/> messages.
115 /// </summary>
116 /// <value>The number of the file currently being processed, or the most recent
117 /// file processed if processing is currently at the stream or archive level.</value>
118 public int CurrentFileNumber
119 {
120 get
121 {
122 return this.currentFileNumber;
123 }
124 }
125
126 /// <summary>
127 /// Gets the total number of files to be processed. Valid for all message types.
128 /// </summary>
129 /// <value>The total number of files to be processed that are known so far.</value>
130 public int TotalFiles
131 {
132 get
133 {
134 return this.totalFiles;
135 }
136 }
137
138 /// <summary>
139 /// Gets the number of bytes processed so far when compressing or extracting a file. Valid for
140 /// <see cref="ArchiveProgressType.StartFile"/>, <see cref="ArchiveProgressType.PartialFile"/>,
141 /// and <see cref="ArchiveProgressType.FinishFile"/> messages.
142 /// </summary>
143 /// <value>The number of uncompressed bytes processed so far for the current file,
144 /// or 0 if processing is currently at the stream or archive level.</value>
145 public long CurrentFileBytesProcessed
146 {
147 get
148 {
149 return this.currentFileBytesProcessed;
150 }
151 }
152
153 /// <summary>
154 /// Gets the total number of bytes in the current file. Valid for <see cref="ArchiveProgressType.StartFile"/>,
155 /// <see cref="ArchiveProgressType.PartialFile"/>, and <see cref="ArchiveProgressType.FinishFile"/> messages.
156 /// </summary>
157 /// <value>The uncompressed size of the current file being processed,
158 /// or 0 if processing is currently at the stream or archive level.</value>
159 public long CurrentFileTotalBytes
160 {
161 get
162 {
163 return this.currentFileTotalBytes;
164 }
165 }
166
167 /// <summary>
168 /// Gets the name of the current archive. Not necessarily the name of the archive on disk.
169 /// Valid for all message types.
170 /// </summary>
171 /// <value>The name of the current archive, or an empty string if no name was specified.</value>
172 public string CurrentArchiveName
173 {
174 get
175 {
176 return this.currentArchiveName;
177 }
178 }
179
180 /// <summary>
181 /// Gets the current archive number, when processing a chained set of archives. Valid for all message types.
182 /// </summary>
183 /// <value>The number of the current archive.</value>
184 /// <remarks>The first archive is number 0, and the last archive is
185 /// <see cref="TotalArchives"/>-1.</remarks>
186 public int CurrentArchiveNumber
187 {
188 get
189 {
190 return this.currentArchiveNumber;
191 }
192 }
193
194 /// <summary>
195 /// Gets the total number of known archives in a chained set. Valid for all message types.
196 /// </summary>
197 /// <value>The total number of known archives in a chained set.</value>
198 /// <remarks>
199 /// When using the compression option to auto-split into multiple archives based on data size,
200 /// this value will not be accurate until the end.
201 /// </remarks>
202 public int TotalArchives
203 {
204 get
205 {
206 return this.totalArchives;
207 }
208 }
209
210 /// <summary>
211 /// Gets the number of compressed bytes processed so far during extraction
212 /// of the current archive. Valid for all extraction messages.
213 /// </summary>
214 /// <value>The number of compressed bytes processed so far during extraction
215 /// of the current archive.</value>
216 public long CurrentArchiveBytesProcessed
217 {
218 get
219 {
220 return this.currentArchiveBytesProcessed;
221 }
222 }
223
224 /// <summary>
225 /// Gets the total number of compressed bytes to be processed during extraction
226 /// of the current archive. Valid for all extraction messages.
227 /// </summary>
228 /// <value>The total number of compressed bytes to be processed during extraction
229 /// of the current archive.</value>
230 public long CurrentArchiveTotalBytes
231 {
232 get
233 {
234 return this.currentArchiveTotalBytes;
235 }
236 }
237
238 /// <summary>
239 /// Gets the number of uncompressed bytes processed so far among all files. Valid for all message types.
240 /// </summary>
241 /// <value>The number of uncompressed file bytes processed so far among all files.</value>
242 /// <remarks>
243 /// When compared to <see cref="TotalFileBytes"/>, this can be used as a measure of overall progress.
244 /// </remarks>
245 public long FileBytesProcessed
246 {
247 get
248 {
249 return this.fileBytesProcessed;
250 }
251 }
252
253 /// <summary>
254 /// Gets the total number of uncompressed file bytes to be processed. Valid for all message types.
255 /// </summary>
256 /// <value>The total number of uncompressed bytes to be processed among all files.</value>
257 public long TotalFileBytes
258 {
259 get
260 {
261 return this.totalFileBytes;
262 }
263 }
264
265#if DEBUG
266
267 /// <summary>
268 /// Creates a string representation of the progress event.
269 /// </summary>
270 /// <returns>a listing of all event parameters and values</returns>
271 public override string ToString()
272 {
273 string formatString =
274 "{0}\n" +
275 "\t CurrentFileName = {1}\n" +
276 "\t CurrentFileNumber = {2}\n" +
277 "\t TotalFiles = {3}\n" +
278 "\t CurrentFileBytesProcessed = {4}\n" +
279 "\t CurrentFileTotalBytes = {5}\n" +
280 "\t CurrentArchiveName = {6}\n" +
281 "\t CurrentArchiveNumber = {7}\n" +
282 "\t TotalArchives = {8}\n" +
283 "\t CurrentArchiveBytesProcessed = {9}\n" +
284 "\t CurrentArchiveTotalBytes = {10}\n" +
285 "\t FileBytesProcessed = {11}\n" +
286 "\t TotalFileBytes = {12}\n";
287 return String.Format(
288 System.Globalization.CultureInfo.InvariantCulture,
289 formatString,
290 this.ProgressType,
291 this.CurrentFileName,
292 this.CurrentFileNumber,
293 this.TotalFiles,
294 this.CurrentFileBytesProcessed,
295 this.CurrentFileTotalBytes,
296 this.CurrentArchiveName,
297 this.CurrentArchiveNumber,
298 this.TotalArchives,
299 this.CurrentArchiveBytesProcessed,
300 this.CurrentArchiveTotalBytes,
301 this.FileBytesProcessed,
302 this.TotalFileBytes);
303 }
304
305#endif
306 }
307}
diff --git a/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs b/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs
new file mode 100644
index 00000000..2307c28e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/ArchiveProgressType.cs
@@ -0,0 +1,69 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5using System;
6using System.Collections.Generic;
7using System.Text;
8
9 /// <summary>
10 /// The type of progress event.
11 /// </summary>
12 /// <remarks>
13 /// <p>PACKING EXAMPLE: The following sequence of events might be received when
14 /// extracting a simple archive file with 2 files.</p>
15 /// <list type="table">
16 /// <listheader><term>Message Type</term><description>Description</description></listheader>
17 /// <item><term>StartArchive</term> <description>Begin extracting archive</description></item>
18 /// <item><term>StartFile</term> <description>Begin extracting first file</description></item>
19 /// <item><term>PartialFile</term> <description>Extracting first file</description></item>
20 /// <item><term>PartialFile</term> <description>Extracting first file</description></item>
21 /// <item><term>FinishFile</term> <description>Finished extracting first file</description></item>
22 /// <item><term>StartFile</term> <description>Begin extracting second file</description></item>
23 /// <item><term>PartialFile</term> <description>Extracting second file</description></item>
24 /// <item><term>FinishFile</term> <description>Finished extracting second file</description></item>
25 /// <item><term>FinishArchive</term><description>Finished extracting archive</description></item>
26 /// </list>
27 /// <p></p>
28 /// <p>UNPACKING EXAMPLE: Packing 3 files into 2 archive chunks, where the second file is
29 /// continued to the second archive chunk.</p>
30 /// <list type="table">
31 /// <listheader><term>Message Type</term><description>Description</description></listheader>
32 /// <item><term>StartFile</term> <description>Begin compressing first file</description></item>
33 /// <item><term>FinishFile</term> <description>Finished compressing first file</description></item>
34 /// <item><term>StartFile</term> <description>Begin compressing second file</description></item>
35 /// <item><term>PartialFile</term> <description>Compressing second file</description></item>
36 /// <item><term>PartialFile</term> <description>Compressing second file</description></item>
37 /// <item><term>FinishFile</term> <description>Finished compressing second file</description></item>
38 /// <item><term>StartArchive</term> <description>Begin writing first archive</description></item>
39 /// <item><term>PartialArchive</term><description>Writing first archive</description></item>
40 /// <item><term>FinishArchive</term> <description>Finished writing first archive</description></item>
41 /// <item><term>StartFile</term> <description>Begin compressing third file</description></item>
42 /// <item><term>PartialFile</term> <description>Compressing third file</description></item>
43 /// <item><term>FinishFile</term> <description>Finished compressing third file</description></item>
44 /// <item><term>StartArchive</term> <description>Begin writing second archive</description></item>
45 /// <item><term>PartialArchive</term><description>Writing second archive</description></item>
46 /// <item><term>FinishArchive</term> <description>Finished writing second archive</description></item>
47 /// </list>
48 /// </remarks>
49 public enum ArchiveProgressType : int
50 {
51 /// <summary>Status message before beginning the packing or unpacking an individual file.</summary>
52 StartFile,
53
54 /// <summary>Status message (possibly reported multiple times) during the process of packing or unpacking a file.</summary>
55 PartialFile,
56
57 /// <summary>Status message after completion of the packing or unpacking an individual file.</summary>
58 FinishFile,
59
60 /// <summary>Status message before beginning the packing or unpacking an archive.</summary>
61 StartArchive,
62
63 /// <summary>Status message (possibly reported multiple times) during the process of packing or unpacking an archiv.</summary>
64 PartialArchive,
65
66 /// <summary>Status message after completion of the packing or unpacking of an archive.</summary>
67 FinishArchive,
68 }
69}
diff --git a/src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs b/src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs
new file mode 100644
index 00000000..94d13b9c
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/BasicUnpackStreamContext.cs
@@ -0,0 +1,90 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7 using System.Diagnostics.CodeAnalysis;
8
9 /// <summary>
10 /// Stream context used to extract a single file from an archive into a memory stream.
11 /// </summary>
12 [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")]
13 public class BasicUnpackStreamContext : IUnpackStreamContext
14 {
15 private Stream archiveStream;
16 private Stream fileStream;
17
18 /// <summary>
19 /// Creates a new BasicExtractStreamContext that reads from the specified archive stream.
20 /// </summary>
21 /// <param name="archiveStream">Archive stream to read from.</param>
22 public BasicUnpackStreamContext(Stream archiveStream)
23 {
24 this.archiveStream = archiveStream;
25 }
26
27 /// <summary>
28 /// Gets the stream for the extracted file, or null if no file was extracted.
29 /// </summary>
30 public Stream FileStream
31 {
32 get
33 {
34 return this.fileStream;
35 }
36 }
37
38 /// <summary>
39 /// Opens the archive stream for reading. Returns a DuplicateStream instance,
40 /// so the stream may be virtually opened multiple times.
41 /// </summary>
42 /// <param name="archiveNumber">The archive number to open (ignored; 0 is assumed).</param>
43 /// <param name="archiveName">The name of the archive being opened.</param>
44 /// <param name="compressionEngine">Instance of the compression engine doing the operations.</param>
45 /// <returns>A stream from which archive bytes are read.</returns>
46 public Stream OpenArchiveReadStream(int archiveNumber, string archiveName, CompressionEngine compressionEngine)
47 {
48 return new DuplicateStream(this.archiveStream);
49 }
50
51 /// <summary>
52 /// Does *not* close the stream. The archive stream should be managed by
53 /// the code that invokes the archive extraction.
54 /// </summary>
55 /// <param name="archiveNumber">The archive number of the stream to close.</param>
56 /// <param name="archiveName">The name of the archive being closed.</param>
57 /// <param name="stream">The stream being closed.</param>
58 public void CloseArchiveReadStream(int archiveNumber, string archiveName, Stream stream)
59 {
60 // Do nothing.
61 }
62
63 /// <summary>
64 /// Opens a stream for writing extracted file bytes. The returned stream is a MemoryStream
65 /// instance, so the file is extracted straight into memory.
66 /// </summary>
67 /// <param name="path">Path of the file within the archive.</param>
68 /// <param name="fileSize">The uncompressed size of the file to be extracted.</param>
69 /// <param name="lastWriteTime">The last write time of the file.</param>
70 /// <returns>A stream where extracted file bytes are to be written.</returns>
71 public Stream OpenFileWriteStream(string path, long fileSize, DateTime lastWriteTime)
72 {
73 this.fileStream = new MemoryStream(new byte[fileSize], 0, (int) fileSize, true, true);
74 return this.fileStream;
75 }
76
77 /// <summary>
78 /// Does *not* close the file stream. The file stream is saved in memory so it can
79 /// be read later.
80 /// </summary>
81 /// <param name="path">Path of the file within the archive.</param>
82 /// <param name="stream">The file stream to be closed.</param>
83 /// <param name="attributes">The attributes of the extracted file.</param>
84 /// <param name="lastWriteTime">The last write time of the file.</param>
85 public void CloseFileWriteStream(string path, Stream stream, FileAttributes attributes, DateTime lastWriteTime)
86 {
87 // Do nothing.
88 }
89 }
90}
diff --git a/src/dtf/WixToolset.Dtf.Compression/CargoStream.cs b/src/dtf/WixToolset.Dtf.Compression/CargoStream.cs
new file mode 100644
index 00000000..78798a35
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/CargoStream.cs
@@ -0,0 +1,192 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.Collections.Generic;
7 using System.IO;
8
9 /// <summary>
10 /// Wraps a source stream and carries additional items that are disposed when the stream is closed.
11 /// </summary>
12 public class CargoStream : Stream
13 {
14 private Stream source;
15 private List<IDisposable> cargo;
16
17 /// <summary>
18 /// Creates a new a cargo stream.
19 /// </summary>
20 /// <param name="source">source of the stream</param>
21 /// <param name="cargo">List of additional items that are disposed when the stream is closed.
22 /// The order of the list is the order in which the items are disposed.</param>
23 public CargoStream(Stream source, params IDisposable[] cargo)
24 {
25 if (source == null)
26 {
27 throw new ArgumentNullException("source");
28 }
29
30 this.source = source;
31 this.cargo = new List<IDisposable>(cargo);
32 }
33
34 /// <summary>
35 /// Gets the source stream of the cargo stream.
36 /// </summary>
37 public Stream Source
38 {
39 get
40 {
41 return this.source;
42 }
43 }
44
45 /// <summary>
46 /// Gets the list of additional items that are disposed when the stream is closed.
47 /// The order of the list is the order in which the items are disposed. The contents can be modified any time.
48 /// </summary>
49 public IList<IDisposable> Cargo
50 {
51 get
52 {
53 return this.cargo;
54 }
55 }
56
57 /// <summary>
58 /// Gets a value indicating whether the source stream supports reading.
59 /// </summary>
60 /// <value>true if the stream supports reading; otherwise, false.</value>
61 public override bool CanRead
62 {
63 get
64 {
65 return this.source.CanRead;
66 }
67 }
68
69 /// <summary>
70 /// Gets a value indicating whether the source stream supports writing.
71 /// </summary>
72 /// <value>true if the stream supports writing; otherwise, false.</value>
73 public override bool CanWrite
74 {
75 get
76 {
77 return this.source.CanWrite;
78 }
79 }
80
81 /// <summary>
82 /// Gets a value indicating whether the source stream supports seeking.
83 /// </summary>
84 /// <value>true if the stream supports seeking; otherwise, false.</value>
85 public override bool CanSeek
86 {
87 get
88 {
89 return this.source.CanSeek;
90 }
91 }
92
93 /// <summary>
94 /// Gets the length of the source stream.
95 /// </summary>
96 public override long Length
97 {
98 get
99 {
100 return this.source.Length;
101 }
102 }
103
104 /// <summary>
105 /// Gets or sets the position of the source stream.
106 /// </summary>
107 public override long Position
108 {
109 get
110 {
111 return this.source.Position;
112 }
113
114 set
115 {
116 this.source.Position = value;
117 }
118 }
119
120 /// <summary>
121 /// Flushes the source stream.
122 /// </summary>
123 public override void Flush()
124 {
125 this.source.Flush();
126 }
127
128 /// <summary>
129 /// Sets the length of the source stream.
130 /// </summary>
131 /// <param name="value">The desired length of the stream in bytes.</param>
132 public override void SetLength(long value)
133 {
134 this.source.SetLength(value);
135 }
136
137 /// <summary>
138 /// Closes the source stream and also closes the additional objects that are carried.
139 /// </summary>
140 public override void Close()
141 {
142 this.source.Close();
143
144 foreach (IDisposable cargoObject in this.cargo)
145 {
146 cargoObject.Dispose();
147 }
148 }
149
150 /// <summary>
151 /// Reads from the source stream.
152 /// </summary>
153 /// <param name="buffer">An array of bytes. When this method returns, the buffer
154 /// contains the specified byte array with the values between offset and
155 /// (offset + count - 1) replaced by the bytes read from the source.</param>
156 /// <param name="offset">The zero-based byte offset in buffer at which to begin
157 /// storing the data read from the stream.</param>
158 /// <param name="count">The maximum number of bytes to be read from the stream.</param>
159 /// <returns>The total number of bytes read into the buffer. This can be less
160 /// than the number of bytes requested if that many bytes are not currently available,
161 /// or zero (0) if the end of the stream has been reached.</returns>
162 public override int Read(byte[] buffer, int offset, int count)
163 {
164 return this.source.Read(buffer, offset, count);
165 }
166
167 /// <summary>
168 /// Writes to the source stream.
169 /// </summary>
170 /// <param name="buffer">An array of bytes. This method copies count
171 /// bytes from buffer to the stream.</param>
172 /// <param name="offset">The zero-based byte offset in buffer at which
173 /// to begin copying bytes to the stream.</param>
174 /// <param name="count">The number of bytes to be written to the stream.</param>
175 public override void Write(byte[] buffer, int offset, int count)
176 {
177 this.source.Write(buffer, offset, count);
178 }
179
180 /// <summary>
181 /// Changes the position of the source stream.
182 /// </summary>
183 /// <param name="offset">A byte offset relative to the origin parameter.</param>
184 /// <param name="origin">A value of type SeekOrigin indicating the reference
185 /// point used to obtain the new position.</param>
186 /// <returns>The new position within the stream.</returns>
187 public override long Seek(long offset, SeekOrigin origin)
188 {
189 return this.source.Seek(offset, origin);
190 }
191 }
192}
diff --git a/src/dtf/WixToolset.Dtf.Compression/Compression.cd b/src/dtf/WixToolset.Dtf.Compression/Compression.cd
new file mode 100644
index 00000000..95012be0
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/Compression.cd
@@ -0,0 +1,175 @@
1<?xml version="1.0" encoding="utf-8"?>
2<ClassDiagram MajorVersion="1" MinorVersion="1">
3 <Comment CommentText="File-based classes">
4 <Position X="2.35" Y="1.442" Height="0.408" Width="0.783" />
5 </Comment>
6 <Comment CommentText="Stream-based classes">
7 <Position X="9.649" Y="1.317" Height="0.4" Width="0.996" />
8 </Comment>
9 <Class Name="WixToolset.Dtf.Compression.ArchiveException" Collapsed="true">
10 <Position X="3" Y="4.25" Width="2" />
11 <TypeIdentifier>
12 <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
13 <FileName>ArchiveException.cs</FileName>
14 </TypeIdentifier>
15 </Class>
16 <Class Name="WixToolset.Dtf.Compression.ArchiveFileInfo">
17 <Position X="3" Y="0.5" Width="2" />
18 <Members>
19 <Method Name="ArchiveFileInfo" Hidden="true" />
20 <Field Name="archiveInfo" Hidden="true" />
21 <Field Name="archiveNumber" Hidden="true" />
22 <Field Name="attributes" Hidden="true" />
23 <Field Name="exists" Hidden="true" />
24 <Method Name="GetObjectData" Hidden="true" />
25 <Field Name="initialized" Hidden="true" />
26 <Field Name="lastWriteTime" Hidden="true" />
27 <Field Name="length" Hidden="true" />
28 <Field Name="name" Hidden="true" />
29 <Field Name="path" Hidden="true" />
30 </Members>
31 <TypeIdentifier>
32 <HashCode>AAAgAAAAIRJAAIMEAEACgARwAAEEEAAAASAAAAEAIAA=</HashCode>
33 <FileName>ArchiveFileInfo.cs</FileName>
34 </TypeIdentifier>
35 </Class>
36 <Class Name="WixToolset.Dtf.Compression.ArchiveInfo">
37 <Position X="0.5" Y="0.5" Width="2.25" />
38 <Members>
39 <Method Name="ArchiveInfo" Hidden="true" />
40 <Method Name="CreateStringDictionary" Hidden="true" />
41 <Method Name="GetFile" Hidden="true" />
42 <Method Name="GetRelativeFilePathsInDirectoryTree" Hidden="true" />
43 <Method Name="InternalGetFiles" Hidden="true" />
44 <Method Name="RecursiveGetRelativeFilePathsInDirectoryTree" Hidden="true" />
45 </Members>
46 <TypeIdentifier>
47 <HashCode>AAEAABAAIAAAAgQEAAgBAARAHAEJACAAAABEAAkAMAI=</HashCode>
48 <FileName>ArchiveInfo.cs</FileName>
49 </TypeIdentifier>
50 </Class>
51 <Class Name="WixToolset.Dtf.Compression.ArchiveFileStreamContext">
52 <Position X="12.75" Y="0.75" Width="2.25" />
53 <Members>
54 <Field Name="archiveFiles" Hidden="true" />
55 <Method Name="ArchiveFileStreamContext" Hidden="true" />
56 <Method Name="CloseArchiveReadStream" Hidden="true" />
57 <Method Name="CloseArchiveWriteStream" Hidden="true" />
58 <Method Name="CloseFileReadStream" Hidden="true" />
59 <Method Name="CloseFileWriteStream" Hidden="true" />
60 <Field Name="directory" Hidden="true" />
61 <Field Name="enableOffsetOpen" Hidden="true" />
62 <Field Name="extractOnlyNewerFiles" Hidden="true" />
63 <Field Name="files" Hidden="true" />
64 <Method Name="GetArchiveName" Hidden="true" />
65 <Method Name="GetOption" Hidden="true" />
66 <Method Name="OpenArchiveReadStream" Hidden="true" />
67 <Method Name="OpenArchiveWriteStream" Hidden="true" />
68 <Method Name="OpenFileReadStream" Hidden="true" />
69 <Method Name="OpenFileWriteStream" Hidden="true" />
70 <Method Name="TranslateFilePath" Hidden="true" />
71 </Members>
72 <TypeIdentifier>
73 <HashCode>AEQAABgAAACQAACACACAAgAQAAIgAAAAACAMgAAEAKA=</HashCode>
74 <FileName>ArchiveFileStreamContext.cs</FileName>
75 </TypeIdentifier>
76 <Lollipop Position="0.2" />
77 </Class>
78 <Class Name="WixToolset.Dtf.Compression.ArchiveProgressEventArgs">
79 <Position X="5.25" Y="0.5" Width="2.25" />
80 <Members>
81 <Method Name="ArchiveProgressEventArgs" Hidden="true" />
82 <Field Name="currentArchiveBytesProcessed" Hidden="true" />
83 <Field Name="currentArchiveName" Hidden="true" />
84 <Field Name="currentArchiveNumber" Hidden="true" />
85 <Field Name="currentArchiveTotalBytes" Hidden="true" />
86 <Field Name="currentFileBytesProcessed" Hidden="true" />
87 <Field Name="currentFileName" Hidden="true" />
88 <Field Name="currentFileNumber" Hidden="true" />
89 <Field Name="currentFileTotalBytes" Hidden="true" />
90 <Field Name="fileBytesProcessed" Hidden="true" />
91 <Field Name="progressType" Hidden="true" />
92 <Field Name="totalArchives" Hidden="true" />
93 <Field Name="totalFileBytes" Hidden="true" />
94 <Field Name="totalFiles" Hidden="true" />
95 </Members>
96 <TypeIdentifier>
97 <HashCode>AAMCAQASACAAABBBAAASUAAAQBAAAMAAAAGQAAgBEAA=</HashCode>
98 <FileName>ArchiveProgressEventArgs.cs</FileName>
99 </TypeIdentifier>
100 </Class>
101 <Class Name="WixToolset.Dtf.Compression.BasicUnpackStreamContext">
102 <Position X="12.75" Y="3" Width="2.25" />
103 <Members>
104 <Field Name="archiveStream" Hidden="true" />
105 <Method Name="BasicUnpackStreamContext" Hidden="true" />
106 <Method Name="CloseArchiveReadStream" Hidden="true" />
107 <Method Name="CloseFileWriteStream" Hidden="true" />
108 <Field Name="fileStream" Hidden="true" />
109 <Method Name="OpenArchiveReadStream" Hidden="true" />
110 <Method Name="OpenFileWriteStream" Hidden="true" />
111 </Members>
112 <TypeIdentifier>
113 <HashCode>AAAAAAgAAACEAAAAAAAAAAAAAAAgAAAAIAAMAAAAAAA=</HashCode>
114 <FileName>BasicUnpackStreamContext.cs</FileName>
115 </TypeIdentifier>
116 <Lollipop Position="0.2" />
117 </Class>
118 <Class Name="WixToolset.Dtf.Compression.CompressionEngine">
119 <Position X="8" Y="0.5" Width="2.25" />
120 <Members>
121 <Method Name="~CompressionEngine" Hidden="true" />
122 <Method Name="CompressionEngine" Hidden="true" />
123 <Field Name="compressionLevel" Hidden="true" />
124 <Field Name="dontUseTempFiles" Hidden="true" />
125 </Members>
126 <TypeIdentifier>
127 <HashCode>AAAEAAAABCBAACRgAAAAAAQAAEAAAAAAQAEAAAiAAAI=</HashCode>
128 <FileName>CompressionEngine.cs</FileName>
129 </TypeIdentifier>
130 <Lollipop Position="0.2" />
131 </Class>
132 <Class Name="WixToolset.Dtf.Compression.DuplicateStream" Collapsed="true">
133 <Position X="10.5" Y="4.25" Width="2" />
134 <TypeIdentifier>
135 <HashCode>AAAAAEAAAgAAQAIgGAAAIABgAAAAAAAAAAAAAAGIACA=</HashCode>
136 <FileName>DuplicateStream.cs</FileName>
137 </TypeIdentifier>
138 </Class>
139 <Class Name="WixToolset.Dtf.Compression.OffsetStream" Collapsed="true">
140 <Position X="8" Y="4.25" Width="2" />
141 <TypeIdentifier>
142 <HashCode>AAAAAAAAAgAAQAIgGAAAAABgAAAAAEAgAAAAAAGIwCA=</HashCode>
143 <FileName>OffsetStream.cs</FileName>
144 </TypeIdentifier>
145 </Class>
146 <Interface Name="WixToolset.Dtf.Compression.IPackStreamContext">
147 <Position X="10.5" Y="0.5" Width="2" />
148 <TypeIdentifier>
149 <HashCode>AAAAAAAAAAAAAACAAACAAAAQAAAgAAAAACAIAAAAAAA=</HashCode>
150 <FileName>IPackStreamContext.cs</FileName>
151 </TypeIdentifier>
152 </Interface>
153 <Interface Name="WixToolset.Dtf.Compression.IUnpackStreamContext">
154 <Position X="10.5" Y="2.5" Width="2" />
155 <TypeIdentifier>
156 <HashCode>AAAAAAgAAACAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAA=</HashCode>
157 <FileName>IUnpackStreamContext.cs</FileName>
158 </TypeIdentifier>
159 </Interface>
160 <Enum Name="WixToolset.Dtf.Compression.ArchiveProgressType" Collapsed="true">
161 <Position X="5.25" Y="3.75" Width="2" />
162 <TypeIdentifier>
163 <HashCode>QAAAAAAAAAAAAIAAgAAAAAAAAAQAAAAACIAAAAAAAAA=</HashCode>
164 <FileName>ArchiveProgressType.cs</FileName>
165 </TypeIdentifier>
166 </Enum>
167 <Enum Name="WixToolset.Dtf.Compression.CompressionLevel" Collapsed="true">
168 <Position X="5.25" Y="4.5" Width="2" />
169 <TypeIdentifier>
170 <HashCode>AAAAAAAAABAAAAAAEAAAAAAAAAAIAAAAAAAAAAEAAAA=</HashCode>
171 <FileName>CompressionLevel.cs</FileName>
172 </TypeIdentifier>
173 </Enum>
174 <Font Name="Verdana" Size="8" />
175</ClassDiagram> \ No newline at end of file
diff --git a/src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs b/src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs
new file mode 100644
index 00000000..7758ea98
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/CompressionEngine.cs
@@ -0,0 +1,371 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5using System;
6using System.IO;
7using System.Collections.Generic;
8using System.Globalization;
9
10 /// <summary>
11 /// Base class for an engine capable of packing and unpacking a particular
12 /// compressed file format.
13 /// </summary>
14 public abstract class CompressionEngine : IDisposable
15 {
16 private CompressionLevel compressionLevel;
17 private bool dontUseTempFiles;
18
19 /// <summary>
20 /// Creates a new instance of the compression engine base class.
21 /// </summary>
22 protected CompressionEngine()
23 {
24 this.compressionLevel = CompressionLevel.Normal;
25 }
26
27 /// <summary>
28 /// Disposes the compression engine.
29 /// </summary>
30 ~CompressionEngine()
31 {
32 this.Dispose(false);
33 }
34
35 /// <summary>
36 /// Occurs when the compression engine reports progress in packing
37 /// or unpacking an archive.
38 /// </summary>
39 /// <seealso cref="ArchiveProgressType"/>
40 public event EventHandler<ArchiveProgressEventArgs> Progress;
41
42 /// <summary>
43 /// Gets or sets a flag indicating whether temporary files are created
44 /// and used during compression.
45 /// </summary>
46 /// <value>True if temporary files are used; false if compression is done
47 /// entirely in-memory.</value>
48 /// <remarks>The value of this property is true by default. Using temporary
49 /// files can greatly reduce the memory requirement of compression,
50 /// especially when compressing large archives. However, setting this property
51 /// to false may yield slightly better performance when creating small
52 /// archives. Or it may be necessary if the process does not have sufficient
53 /// privileges to create temporary files.</remarks>
54 public bool UseTempFiles
55 {
56 get
57 {
58 return !this.dontUseTempFiles;
59 }
60
61 set
62 {
63 this.dontUseTempFiles = !value;
64 }
65 }
66
67 /// <summary>
68 /// Compression level to use when compressing files.
69 /// </summary>
70 /// <value>A compression level ranging from minimum to maximum compression,
71 /// or no compression.</value>
72 public CompressionLevel CompressionLevel
73 {
74 get
75 {
76 return this.compressionLevel;
77 }
78
79 set
80 {
81 this.compressionLevel = value;
82 }
83 }
84
85 /// <summary>
86 /// Disposes of resources allocated by the compression engine.
87 /// </summary>
88 public void Dispose()
89 {
90 this.Dispose(true);
91 GC.SuppressFinalize(this);
92 }
93
94 /// <summary>
95 /// Creates an archive.
96 /// </summary>
97 /// <param name="streamContext">A context interface to handle opening
98 /// and closing of archive and file streams.</param>
99 /// <param name="files">The paths of the files in the archive
100 /// (not external file paths).</param>
101 /// <exception cref="ArchiveException">The archive could not be
102 /// created.</exception>
103 /// <remarks>
104 /// The stream context implementation may provide a mapping from the
105 /// file paths within the archive to the external file paths.
106 /// </remarks>
107 public void Pack(IPackStreamContext streamContext, IEnumerable<string> files)
108 {
109 if (files == null)
110 {
111 throw new ArgumentNullException("files");
112 }
113
114 this.Pack(streamContext, files, 0);
115 }
116
117 /// <summary>
118 /// Creates an archive or chain of archives.
119 /// </summary>
120 /// <param name="streamContext">A context interface to handle opening
121 /// and closing of archive and file streams.</param>
122 /// <param name="files">The paths of the files in the archive (not
123 /// external file paths).</param>
124 /// <param name="maxArchiveSize">The maximum number of bytes for one
125 /// archive before the contents are chained to the next archive, or zero
126 /// for unlimited archive size.</param>
127 /// <exception cref="ArchiveException">The archive could not be
128 /// created.</exception>
129 /// <remarks>
130 /// The stream context implementation may provide a mapping from the file
131 /// paths within the archive to the external file paths.
132 /// </remarks>
133 public abstract void Pack(
134 IPackStreamContext streamContext,
135 IEnumerable<string> files,
136 long maxArchiveSize);
137
138 /// <summary>
139 /// Checks whether a Stream begins with a header that indicates
140 /// it is a valid archive.
141 /// </summary>
142 /// <param name="stream">Stream for reading the archive file.</param>
143 /// <returns>True if the stream is a valid archive
144 /// (with no offset); false otherwise.</returns>
145 public abstract bool IsArchive(Stream stream);
146
147 /// <summary>
148 /// Gets the offset of an archive that is positioned 0 or more bytes
149 /// from the start of the Stream.
150 /// </summary>
151 /// <param name="stream">A stream for reading the archive.</param>
152 /// <returns>The offset in bytes of the archive,
153 /// or -1 if no archive is found in the Stream.</returns>
154 /// <remarks>The archive must begin on a 4-byte boundary.</remarks>
155 public virtual long FindArchiveOffset(Stream stream)
156 {
157 if (stream == null)
158 {
159 throw new ArgumentNullException("stream");
160 }
161
162 long sectionSize = 4;
163 long length = stream.Length;
164 for (long offset = 0; offset <= length - sectionSize; offset += sectionSize)
165 {
166 stream.Seek(offset, SeekOrigin.Begin);
167 if (this.IsArchive(stream))
168 {
169 return offset;
170 }
171 }
172
173 return -1;
174 }
175
176 /// <summary>
177 /// Gets information about all files in an archive stream.
178 /// </summary>
179 /// <param name="stream">A stream for reading the archive.</param>
180 /// <returns>Information about all files in the archive stream.</returns>
181 /// <exception cref="ArchiveException">The stream is not a valid
182 /// archive.</exception>
183 public IList<ArchiveFileInfo> GetFileInfo(Stream stream)
184 {
185 return this.GetFileInfo(new BasicUnpackStreamContext(stream), null);
186 }
187
188 /// <summary>
189 /// Gets information about files in an archive or archive chain.
190 /// </summary>
191 /// <param name="streamContext">A context interface to handle opening
192 /// and closing of archive and file streams.</param>
193 /// <param name="fileFilter">A predicate that can determine
194 /// which files to process, optional.</param>
195 /// <returns>Information about files in the archive stream.</returns>
196 /// <exception cref="ArchiveException">The archive provided
197 /// by the stream context is not valid.</exception>
198 /// <remarks>
199 /// The <paramref name="fileFilter"/> predicate takes an internal file
200 /// path and returns true to include the file or false to exclude it.
201 /// </remarks>
202 public abstract IList<ArchiveFileInfo> GetFileInfo(
203 IUnpackStreamContext streamContext,
204 Predicate<string> fileFilter);
205
206 /// <summary>
207 /// Gets the list of files in an archive Stream.
208 /// </summary>
209 /// <param name="stream">A stream for reading the archive.</param>
210 /// <returns>A list of the paths of all files contained in the
211 /// archive.</returns>
212 /// <exception cref="ArchiveException">The stream is not a valid
213 /// archive.</exception>
214 public IList<string> GetFiles(Stream stream)
215 {
216 return this.GetFiles(new BasicUnpackStreamContext(stream), null);
217 }
218
219 /// <summary>
220 /// Gets the list of files in an archive or archive chain.
221 /// </summary>
222 /// <param name="streamContext">A context interface to handle opening
223 /// and closing of archive and file streams.</param>
224 /// <param name="fileFilter">A predicate that can determine
225 /// which files to process, optional.</param>
226 /// <returns>An array containing the names of all files contained in
227 /// the archive or archive chain.</returns>
228 /// <exception cref="ArchiveException">The archive provided
229 /// by the stream context is not valid.</exception>
230 /// <remarks>
231 /// The <paramref name="fileFilter"/> predicate takes an internal file
232 /// path and returns true to include the file or false to exclude it.
233 /// </remarks>
234 public IList<string> GetFiles(
235 IUnpackStreamContext streamContext,
236 Predicate<string> fileFilter)
237 {
238 if (streamContext == null)
239 {
240 throw new ArgumentNullException("streamContext");
241 }
242
243 IList<ArchiveFileInfo> files =
244 this.GetFileInfo(streamContext, fileFilter);
245 IList<string> fileNames = new List<string>(files.Count);
246 for (int i = 0; i < files.Count; i++)
247 {
248 fileNames.Add(files[i].Name);
249 }
250
251 return fileNames;
252 }
253
254 /// <summary>
255 /// Reads a single file from an archive stream.
256 /// </summary>
257 /// <param name="stream">A stream for reading the archive.</param>
258 /// <param name="path">The path of the file within the archive
259 /// (not the external file path).</param>
260 /// <returns>A stream for reading the extracted file, or null
261 /// if the file does not exist in the archive.</returns>
262 /// <exception cref="ArchiveException">The stream is not a valid
263 /// archive.</exception>
264 /// <remarks>The entire extracted file is cached in memory, so this
265 /// method requires enough free memory to hold the file.</remarks>
266 public Stream Unpack(Stream stream, string path)
267 {
268 if (stream == null)
269 {
270 throw new ArgumentNullException("stream");
271 }
272
273 if (path == null)
274 {
275 throw new ArgumentNullException("path");
276 }
277
278 BasicUnpackStreamContext streamContext =
279 new BasicUnpackStreamContext(stream);
280 this.Unpack(
281 streamContext,
282 delegate(string match)
283 {
284 return String.Compare(
285 match, path, true, CultureInfo.InvariantCulture) == 0;
286 });
287
288 Stream extractStream = streamContext.FileStream;
289 if (extractStream != null)
290 {
291 extractStream.Position = 0;
292 }
293
294 return extractStream;
295 }
296
297 /// <summary>
298 /// Extracts files from an archive or archive chain.
299 /// </summary>
300 /// <param name="streamContext">A context interface to handle opening
301 /// and closing of archive and file streams.</param>
302 /// <param name="fileFilter">An optional predicate that can determine
303 /// which files to process.</param>
304 /// <exception cref="ArchiveException">The archive provided
305 /// by the stream context is not valid.</exception>
306 /// <remarks>
307 /// The <paramref name="fileFilter"/> predicate takes an internal file
308 /// path and returns true to include the file or false to exclude it.
309 /// </remarks>
310 public abstract void Unpack(
311 IUnpackStreamContext streamContext,
312 Predicate<string> fileFilter);
313
314 /// <summary>
315 /// Called by sublcasses to distribute a packing or unpacking progress
316 /// event to listeners.
317 /// </summary>
318 /// <param name="e">Event details.</param>
319 protected void OnProgress(ArchiveProgressEventArgs e)
320 {
321 if (this.Progress != null)
322 {
323 this.Progress(this, e);
324 }
325 }
326
327 /// <summary>
328 /// Disposes of resources allocated by the compression engine.
329 /// </summary>
330 /// <param name="disposing">If true, the method has been called
331 /// directly or indirectly by a user's code, so managed and unmanaged
332 /// resources will be disposed. If false, the method has been called by
333 /// the runtime from inside the finalizer, and only unmanaged resources
334 /// will be disposed.</param>
335 protected virtual void Dispose(bool disposing)
336 {
337 }
338
339 /// <summary>
340 /// Compresion utility function for converting old-style
341 /// date and time values to a DateTime structure.
342 /// </summary>
343 public static void DosDateAndTimeToDateTime(
344 short dosDate, short dosTime, out DateTime dateTime)
345 {
346 if (dosDate == 0 && dosTime == 0)
347 {
348 dateTime = DateTime.MinValue;
349 }
350 else
351 {
352 long fileTime;
353 SafeNativeMethods.DosDateTimeToFileTime(dosDate, dosTime, out fileTime);
354 dateTime = DateTime.FromFileTimeUtc(fileTime);
355 dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Local);
356 }
357 }
358
359 /// <summary>
360 /// Compresion utility function for converting a DateTime structure
361 /// to old-style date and time values.
362 /// </summary>
363 public static void DateTimeToDosDateAndTime(
364 DateTime dateTime, out short dosDate, out short dosTime)
365 {
366 dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Utc);
367 long filetime = dateTime.ToFileTimeUtc();
368 SafeNativeMethods.FileTimeToDosDateTime(ref filetime, out dosDate, out dosTime);
369 }
370 }
371}
diff --git a/src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs b/src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs
new file mode 100644
index 00000000..84ec8fc4
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/CompressionLevel.cs
@@ -0,0 +1,31 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5using System;
6using System.Collections.Generic;
7using System.Text;
8
9 /// <summary>
10 /// Specifies the compression level ranging from minimum compresion to
11 /// maximum compression, or no compression at all.
12 /// </summary>
13 /// <remarks>
14 /// Although only four values are enumerated, any integral value between
15 /// <see cref="CompressionLevel.Min"/> and <see cref="CompressionLevel.Max"/> can also be used.
16 /// </remarks>
17 public enum CompressionLevel
18 {
19 /// <summary>Do not compress files, only store.</summary>
20 None = 0,
21
22 /// <summary>Minimum compression; fastest.</summary>
23 Min = 1,
24
25 /// <summary>A compromize between speed and compression efficiency.</summary>
26 Normal = 6,
27
28 /// <summary>Maximum compression; slowest.</summary>
29 Max = 10
30 }
31}
diff --git a/src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs b/src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs
new file mode 100644
index 00000000..50e62e73
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/DuplicateStream.cs
@@ -0,0 +1,212 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7
8 /// <summary>
9 /// Duplicates a source stream by maintaining a separate position.
10 /// </summary>
11 /// <remarks>
12 /// WARNING: duplicate streams are not thread-safe with respect to each other or the original stream.
13 /// If multiple threads use duplicate copies of the same stream, they must synchronize for any operations.
14 /// </remarks>
15 public class DuplicateStream : Stream
16 {
17 private Stream source;
18 private long position;
19
20 /// <summary>
21 /// Creates a new duplicate of a stream.
22 /// </summary>
23 /// <param name="source">source of the duplicate</param>
24 public DuplicateStream(Stream source)
25 {
26 if (source == null)
27 {
28 throw new ArgumentNullException("source");
29 }
30
31 this.source = DuplicateStream.OriginalStream(source);
32 }
33
34 /// <summary>
35 /// Gets the original stream that was used to create the duplicate.
36 /// </summary>
37 public Stream Source
38 {
39 get
40 {
41 return this.source;
42 }
43 }
44
45 /// <summary>
46 /// Gets a value indicating whether the source stream supports reading.
47 /// </summary>
48 /// <value>true if the stream supports reading; otherwise, false.</value>
49 public override bool CanRead
50 {
51 get
52 {
53 return this.source.CanRead;
54 }
55 }
56
57 /// <summary>
58 /// Gets a value indicating whether the source stream supports writing.
59 /// </summary>
60 /// <value>true if the stream supports writing; otherwise, false.</value>
61 public override bool CanWrite
62 {
63 get
64 {
65 return this.source.CanWrite;
66 }
67 }
68
69 /// <summary>
70 /// Gets a value indicating whether the source stream supports seeking.
71 /// </summary>
72 /// <value>true if the stream supports seeking; otherwise, false.</value>
73 public override bool CanSeek
74 {
75 get
76 {
77 return this.source.CanSeek;
78 }
79 }
80
81 /// <summary>
82 /// Gets the length of the source stream.
83 /// </summary>
84 public override long Length
85 {
86 get
87 {
88 return this.source.Length;
89 }
90 }
91
92 /// <summary>
93 /// Gets or sets the position of the current stream,
94 /// ignoring the position of the source stream.
95 /// </summary>
96 public override long Position
97 {
98 get
99 {
100 return this.position;
101 }
102
103 set
104 {
105 this.position = value;
106 }
107 }
108
109 /// <summary>
110 /// Retrieves the original stream from a possible duplicate stream.
111 /// </summary>
112 /// <param name="stream">Possible duplicate stream.</param>
113 /// <returns>If the stream is a DuplicateStream, returns
114 /// the duplicate's source; otherwise returns the same stream.</returns>
115 public static Stream OriginalStream(Stream stream)
116 {
117 DuplicateStream dupStream = stream as DuplicateStream;
118 return dupStream != null ? dupStream.Source : stream;
119 }
120
121 /// <summary>
122 /// Flushes the source stream.
123 /// </summary>
124 public override void Flush()
125 {
126 this.source.Flush();
127 }
128
129 /// <summary>
130 /// Sets the length of the source stream.
131 /// </summary>
132 /// <param name="value">The desired length of the stream in bytes.</param>
133 public override void SetLength(long value)
134 {
135 this.source.SetLength(value);
136 }
137
138 /// <summary>
139 /// Closes the underlying stream, effectively closing ALL duplicates.
140 /// </summary>
141 public override void Close()
142 {
143 this.source.Close();
144 }
145
146 /// <summary>
147 /// Reads from the source stream while maintaining a separate position
148 /// and not impacting the source stream's position.
149 /// </summary>
150 /// <param name="buffer">An array of bytes. When this method returns, the buffer
151 /// contains the specified byte array with the values between offset and
152 /// (offset + count - 1) replaced by the bytes read from the current source.</param>
153 /// <param name="offset">The zero-based byte offset in buffer at which to begin
154 /// storing the data read from the current stream.</param>
155 /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
156 /// <returns>The total number of bytes read into the buffer. This can be less
157 /// than the number of bytes requested if that many bytes are not currently available,
158 /// or zero (0) if the end of the stream has been reached.</returns>
159 public override int Read(byte[] buffer, int offset, int count)
160 {
161 long saveSourcePosition = this.source.Position;
162 this.source.Position = this.position;
163 int read = this.source.Read(buffer, offset, count);
164 this.position = this.source.Position;
165 this.source.Position = saveSourcePosition;
166 return read;
167 }
168
169 /// <summary>
170 /// Writes to the source stream while maintaining a separate position
171 /// and not impacting the source stream's position.
172 /// </summary>
173 /// <param name="buffer">An array of bytes. This method copies count
174 /// bytes from buffer to the current stream.</param>
175 /// <param name="offset">The zero-based byte offset in buffer at which
176 /// to begin copying bytes to the current stream.</param>
177 /// <param name="count">The number of bytes to be written to the
178 /// current stream.</param>
179 public override void Write(byte[] buffer, int offset, int count)
180 {
181 long saveSourcePosition = this.source.Position;
182 this.source.Position = this.position;
183 this.source.Write(buffer, offset, count);
184 this.position = this.source.Position;
185 this.source.Position = saveSourcePosition;
186 }
187
188 /// <summary>
189 /// Changes the position of this stream without impacting the
190 /// source stream's position.
191 /// </summary>
192 /// <param name="offset">A byte offset relative to the origin parameter.</param>
193 /// <param name="origin">A value of type SeekOrigin indicating the reference
194 /// point used to obtain the new position.</param>
195 /// <returns>The new position within the current stream.</returns>
196 public override long Seek(long offset, SeekOrigin origin)
197 {
198 long originPosition = 0;
199 if (origin == SeekOrigin.Current)
200 {
201 originPosition = this.position;
202 }
203 else if (origin == SeekOrigin.End)
204 {
205 originPosition = this.Length;
206 }
207
208 this.position = originPosition + offset;
209 return this.position;
210 }
211 }
212}
diff --git a/src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs b/src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs
new file mode 100644
index 00000000..19d77be5
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/IPackStreamContext.cs
@@ -0,0 +1,117 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7 using System.Diagnostics.CodeAnalysis;
8
9 /// <summary>
10 /// This interface provides the methods necessary for the
11 /// <see cref="CompressionEngine"/> to open and close streams for archives
12 /// and files. The implementor of this interface can use any kind of logic
13 /// to determine what kind of streams to open and where.
14 /// </summary>
15 public interface IPackStreamContext
16 {
17 /// <summary>
18 /// Gets the name of the archive with a specified number.
19 /// </summary>
20 /// <param name="archiveNumber">The 0-based index of the archive
21 /// within the chain.</param>
22 /// <returns>The name of the requested archive. May be an empty string
23 /// for non-chained archives, but may never be null.</returns>
24 /// <remarks>The archive name is the name stored within the archive, used for
25 /// identification of the archive especially among archive chains. That
26 /// name is often, but not necessarily the same as the filename of the
27 /// archive package.</remarks>
28 string GetArchiveName(int archiveNumber);
29
30 /// <summary>
31 /// Opens a stream for writing an archive package.
32 /// </summary>
33 /// <param name="archiveNumber">The 0-based index of the archive within
34 /// the chain.</param>
35 /// <param name="archiveName">The name of the archive that was returned
36 /// by <see cref="GetArchiveName"/>.</param>
37 /// <param name="truncate">True if the stream should be truncated when
38 /// opened (if it already exists); false if an existing stream is being
39 /// re-opened for writing additional data.</param>
40 /// <param name="compressionEngine">Instance of the compression engine
41 /// doing the operations.</param>
42 /// <returns>A writable Stream where the compressed archive bytes will be
43 /// written, or null to cancel the archive creation.</returns>
44 /// <remarks>
45 /// If this method returns null, the archive engine will throw a
46 /// FileNotFoundException.
47 /// </remarks>
48 Stream OpenArchiveWriteStream(
49 int archiveNumber,
50 string archiveName,
51 bool truncate,
52 CompressionEngine compressionEngine);
53
54 /// <summary>
55 /// Closes a stream where an archive package was written.
56 /// </summary>
57 /// <param name="archiveNumber">The 0-based index of the archive within
58 /// the chain.</param>
59 /// <param name="archiveName">The name of the archive that was previously
60 /// returned by
61 /// <see cref="GetArchiveName"/>.</param>
62 /// <param name="stream">A stream that was previously returned by
63 /// <see cref="OpenArchiveWriteStream"/> and is now ready to be closed.</param>
64 /// <remarks>
65 /// If there is another archive package in the chain, then after this stream
66 /// is closed a new stream will be opened.
67 /// </remarks>
68 void CloseArchiveWriteStream(int archiveNumber, string archiveName, Stream stream);
69
70 /// <summary>
71 /// Opens a stream to read a file that is to be included in an archive.
72 /// </summary>
73 /// <param name="path">The path of the file within the archive. This is often,
74 /// but not necessarily, the same as the relative path of the file outside
75 /// the archive.</param>
76 /// <param name="attributes">Returned attributes of the opened file, to be
77 /// stored in the archive.</param>
78 /// <param name="lastWriteTime">Returned last-modified time of the opened file,
79 /// to be stored in the archive.</param>
80 /// <returns>A readable Stream where the file bytes will be read from before
81 /// they are compressed, or null to skip inclusion of the file and continue to
82 /// the next file.</returns>
83 [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
84 Stream OpenFileReadStream(
85 string path,
86 out FileAttributes attributes,
87 out DateTime lastWriteTime);
88
89 /// <summary>
90 /// Closes a stream that has been used to read a file.
91 /// </summary>
92 /// <param name="path">The path of the file within the archive; the same as
93 /// the path provided
94 /// when the stream was opened.</param>
95 /// <param name="stream">A stream that was previously returned by
96 /// <see cref="OpenFileReadStream"/> and is now ready to be closed.</param>
97 void CloseFileReadStream(string path, Stream stream);
98
99 /// <summary>
100 /// Gets extended parameter information specific to the compression
101 /// format being used.
102 /// </summary>
103 /// <param name="optionName">Name of the option being requested.</param>
104 /// <param name="parameters">Parameters for the option; for per-file options,
105 /// the first parameter is typically the internal file path.</param>
106 /// <returns>Option value, or null to use the default behavior.</returns>
107 /// <remarks>
108 /// This method provides a way to set uncommon options during packaging, or a
109 /// way to handle aspects of compression formats not supported by the base library.
110 /// <para>For example, this may be used by the zip compression library to
111 /// specify different compression methods/levels on a per-file basis.</para>
112 /// <para>The available option names, parameters, and expected return values
113 /// should be documented by each compression library.</para>
114 /// </remarks>
115 object GetOption(string optionName, object[] parameters);
116 }
117}
diff --git a/src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs b/src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs
new file mode 100644
index 00000000..f0bc6aad
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/IUnpackStreamContext.cs
@@ -0,0 +1,71 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7
8 /// <summary>
9 /// This interface provides the methods necessary for the <see cref="CompressionEngine"/> to open
10 /// and close streams for archives and files. The implementor of this interface can use any
11 /// kind of logic to determine what kind of streams to open and where
12 /// </summary>
13 public interface IUnpackStreamContext
14 {
15 /// <summary>
16 /// Opens the archive stream for reading.
17 /// </summary>
18 /// <param name="archiveNumber">The zero-based index of the archive to open.</param>
19 /// <param name="archiveName">The name of the archive being opened.</param>
20 /// <param name="compressionEngine">Instance of the compression engine doing the operations.</param>
21 /// <returns>A stream from which archive bytes are read, or null to cancel extraction
22 /// of the archive.</returns>
23 /// <remarks>
24 /// When the first archive in a chain is opened, the name is not yet known, so the
25 /// provided value will be an empty string. When opening further archives, the
26 /// provided value is the next-archive name stored in the previous archive. This
27 /// name is often, but not necessarily, the same as the filename of the archive
28 /// package to be opened.
29 /// <para>If this method returns null, the archive engine will throw a
30 /// FileNotFoundException.</para>
31 /// </remarks>
32 Stream OpenArchiveReadStream(int archiveNumber, string archiveName, CompressionEngine compressionEngine);
33
34 /// <summary>
35 /// Closes a stream where an archive package was read.
36 /// </summary>
37 /// <param name="archiveNumber">The archive number of the stream to close.</param>
38 /// <param name="archiveName">The name of the archive being closed.</param>
39 /// <param name="stream">The stream that was previously returned by
40 /// <see cref="OpenArchiveReadStream"/> and is now ready to be closed.</param>
41 void CloseArchiveReadStream(int archiveNumber, string archiveName, Stream stream);
42
43 /// <summary>
44 /// Opens a stream for writing extracted file bytes.
45 /// </summary>
46 /// <param name="path">The path of the file within the archive. This is often, but
47 /// not necessarily, the same as the relative path of the file outside the archive.</param>
48 /// <param name="fileSize">The uncompressed size of the file to be extracted.</param>
49 /// <param name="lastWriteTime">The last write time of the file to be extracted.</param>
50 /// <returns>A stream where extracted file bytes are to be written, or null to skip
51 /// extraction of the file and continue to the next file.</returns>
52 /// <remarks>
53 /// The implementor may use the path, size and date information to dynamically
54 /// decide whether or not the file should be extracted.
55 /// </remarks>
56 Stream OpenFileWriteStream(string path, long fileSize, DateTime lastWriteTime);
57
58 /// <summary>
59 /// Closes a stream where an extracted file was written.
60 /// </summary>
61 /// <param name="path">The path of the file within the archive.</param>
62 /// <param name="stream">The stream that was previously returned by <see cref="OpenFileWriteStream"/>
63 /// and is now ready to be closed.</param>
64 /// <param name="attributes">The attributes of the extracted file.</param>
65 /// <param name="lastWriteTime">The last write time of the file.</param>
66 /// <remarks>
67 /// The implementor may wish to apply the attributes and date to the newly-extracted file.
68 /// </remarks>
69 void CloseFileWriteStream(string path, Stream stream, FileAttributes attributes, DateTime lastWriteTime);
70 }
71}
diff --git a/src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs b/src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs
new file mode 100644
index 00000000..65562524
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/OffsetStream.cs
@@ -0,0 +1,206 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.IO;
7
8 /// <summary>
9 /// Wraps a source stream and offsets all read/write/seek calls by a given value.
10 /// </summary>
11 /// <remarks>
12 /// This class is used to trick archive an packing or unpacking process
13 /// into reading or writing at an offset into a file, primarily for
14 /// self-extracting packages.
15 /// </remarks>
16 public class OffsetStream : Stream
17 {
18 private Stream source;
19 private long sourceOffset;
20
21 /// <summary>
22 /// Creates a new OffsetStream instance from a source stream
23 /// and using a specified offset.
24 /// </summary>
25 /// <param name="source">Underlying stream for which all calls will be offset.</param>
26 /// <param name="offset">Positive or negative number of bytes to offset.</param>
27 public OffsetStream(Stream source, long offset)
28 {
29 if (source == null)
30 {
31 throw new ArgumentNullException("source");
32 }
33
34 this.source = source;
35 this.sourceOffset = offset;
36
37 this.source.Seek(this.sourceOffset, SeekOrigin.Current);
38 }
39
40 /// <summary>
41 /// Gets the underlying stream that this OffsetStream calls into.
42 /// </summary>
43 public Stream Source
44 {
45 get { return this.source; }
46 }
47
48 /// <summary>
49 /// Gets the number of bytes to offset all calls before
50 /// redirecting to the underlying stream.
51 /// </summary>
52 public long Offset
53 {
54 get { return this.sourceOffset; }
55 }
56
57 /// <summary>
58 /// Gets a value indicating whether the source stream supports reading.
59 /// </summary>
60 /// <value>true if the stream supports reading; otherwise, false.</value>
61 public override bool CanRead
62 {
63 get
64 {
65 return this.source.CanRead;
66 }
67 }
68
69 /// <summary>
70 /// Gets a value indicating whether the source stream supports writing.
71 /// </summary>
72 /// <value>true if the stream supports writing; otherwise, false.</value>
73 public override bool CanWrite
74 {
75 get
76 {
77 return this.source.CanWrite;
78 }
79 }
80
81 /// <summary>
82 /// Gets a value indicating whether the source stream supports seeking.
83 /// </summary>
84 /// <value>true if the stream supports seeking; otherwise, false.</value>
85 public override bool CanSeek
86 {
87 get
88 {
89 return this.source.CanSeek;
90 }
91 }
92
93 /// <summary>
94 /// Gets the effective length of the stream, which is equal to
95 /// the length of the source stream minus the offset.
96 /// </summary>
97 public override long Length
98 {
99 get { return this.source.Length - this.sourceOffset; }
100 }
101
102 /// <summary>
103 /// Gets or sets the effective position of the stream, which
104 /// is equal to the position of the source stream minus the offset.
105 /// </summary>
106 public override long Position
107 {
108 get { return this.source.Position - this.sourceOffset; }
109 set { this.source.Position = value + this.sourceOffset; }
110 }
111
112 /// <summary>
113 /// Reads a sequence of bytes from the source stream and advances
114 /// the position within the stream by the number of bytes read.
115 /// </summary>
116 /// <param name="buffer">An array of bytes. When this method returns, the buffer
117 /// contains the specified byte array with the values between offset and
118 /// (offset + count - 1) replaced by the bytes read from the current source.</param>
119 /// <param name="offset">The zero-based byte offset in buffer at which to begin
120 /// storing the data read from the current stream.</param>
121 /// <param name="count">The maximum number of bytes to be read from the current stream.</param>
122 /// <returns>The total number of bytes read into the buffer. This can be less
123 /// than the number of bytes requested if that many bytes are not currently available,
124 /// or zero (0) if the end of the stream has been reached.</returns>
125 public override int Read(byte[] buffer, int offset, int count)
126 {
127 return this.source.Read(buffer, offset, count);
128 }
129
130 /// <summary>
131 /// Writes a sequence of bytes to the source stream and advances the
132 /// current position within this stream by the number of bytes written.
133 /// </summary>
134 /// <param name="buffer">An array of bytes. This method copies count
135 /// bytes from buffer to the current stream.</param>
136 /// <param name="offset">The zero-based byte offset in buffer at which
137 /// to begin copying bytes to the current stream.</param>
138 /// <param name="count">The number of bytes to be written to the
139 /// current stream.</param>
140 public override void Write(byte[] buffer, int offset, int count)
141 {
142 this.source.Write(buffer, offset, count);
143 }
144
145 /// <summary>
146 /// Reads a byte from the stream and advances the position within the
147 /// source stream by one byte, or returns -1 if at the end of the stream.
148 /// </summary>
149 /// <returns>The unsigned byte cast to an Int32, or -1 if at the
150 /// end of the stream.</returns>
151 public override int ReadByte()
152 {
153 return this.source.ReadByte();
154 }
155
156 /// <summary>
157 /// Writes a byte to the current position in the source stream and
158 /// advances the position within the stream by one byte.
159 /// </summary>
160 /// <param name="value">The byte to write to the stream.</param>
161 public override void WriteByte(byte value)
162 {
163 this.source.WriteByte(value);
164 }
165
166 /// <summary>
167 /// Flushes the source stream.
168 /// </summary>
169 public override void Flush()
170 {
171 this.source.Flush();
172 }
173
174 /// <summary>
175 /// Sets the position within the current stream, which is
176 /// equal to the position within the source stream minus the offset.
177 /// </summary>
178 /// <param name="offset">A byte offset relative to the origin parameter.</param>
179 /// <param name="origin">A value of type SeekOrigin indicating
180 /// the reference point used to obtain the new position.</param>
181 /// <returns>The new position within the current stream.</returns>
182 public override long Seek(long offset, SeekOrigin origin)
183 {
184 return this.source.Seek(offset + (origin == SeekOrigin.Begin ? this.sourceOffset : 0), origin) - this.sourceOffset;
185 }
186
187 /// <summary>
188 /// Sets the effective length of the stream, which is equal to
189 /// the length of the source stream minus the offset.
190 /// </summary>
191 /// <param name="value">The desired length of the
192 /// current stream in bytes.</param>
193 public override void SetLength(long value)
194 {
195 this.source.SetLength(value + this.sourceOffset);
196 }
197
198 /// <summary>
199 /// Closes the underlying stream.
200 /// </summary>
201 public override void Close()
202 {
203 this.source.Close();
204 }
205 }
206}
diff --git a/src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs b/src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs
new file mode 100644
index 00000000..1829ba81
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/SafeNativeMethods.cs
@@ -0,0 +1,22 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Compression
4{
5 using System;
6 using System.Security;
7 using System.Runtime.InteropServices;
8
9 [SuppressUnmanagedCodeSecurity]
10 internal static class SafeNativeMethods
11 {
12 [DllImport("kernel32.dll", SetLastError = true)]
13 [return: MarshalAs(UnmanagedType.Bool)]
14 internal static extern bool DosDateTimeToFileTime(
15 short wFatDate, short wFatTime, out long fileTime);
16
17 [DllImport("kernel32.dll", SetLastError = true)]
18 [return: MarshalAs(UnmanagedType.Bool)]
19 internal static extern bool FileTimeToDosDateTime(
20 ref long fileTime, out short wFatDate, out short wFatTime);
21 }
22}
diff --git a/src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj b/src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj
new file mode 100644
index 00000000..e49a446b
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Compression/WixToolset.Dtf.Compression.csproj
@@ -0,0 +1,21 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.Compression</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.Compression</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Abstract base libraries for archive packing and unpacking</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <None Include="Compression.cd" />
15 </ItemGroup>
16
17 <ItemGroup>
18 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
19 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
20 </ItemGroup>
21</Project>
diff --git a/src/dtf/WixToolset.Dtf.MSBuild/WixToolset.Dtf.MSBuild.csproj b/src/dtf/WixToolset.Dtf.MSBuild/WixToolset.Dtf.MSBuild.csproj
new file mode 100644
index 00000000..b1b3faa7
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.MSBuild/WixToolset.Dtf.MSBuild.csproj
@@ -0,0 +1,44 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5
6 <PropertyGroup>
7 <TargetFramework>netcoreapp3.1</TargetFramework>
8 <IncludeBuildOutput>false</IncludeBuildOutput>
9 <Description>WiX Toolset Dtf MSBuild integration</Description>
10 <NuspecFile>$(MSBuildThisFileName).nuspec</NuspecFile>
11 <NuspecBasePath>$(OutputPath)publish\WixToolset.Dtf.MSBuild\</NuspecBasePath>
12 <NuspecProperties>Id=$(MSBuildThisFileName);Authors=$(Authors);Copyright=$(Copyright);Description=$(Description)</NuspecProperties>
13 </PropertyGroup>
14
15 <ItemGroup>
16 <None Remove="build\WixToolset.Dtf.MSBuild.props" />
17 <None Remove="tools\wix.ca.targets" />
18 </ItemGroup>
19
20 <ItemGroup>
21 <Content Include="build\WixToolset.Dtf.MSBuild.props">
22 <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
23 </Content>
24 <Content Include="tools\wix.ca.targets">
25 <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
26 </Content>
27 </ItemGroup>
28
29 <ItemGroup>
30 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
31 </ItemGroup>
32
33 <PropertyGroup>
34 <GenerateNuspecDependsOn>$(GenerateNuspecDependsOn);SetNuspecVersion</GenerateNuspecDependsOn>
35 </PropertyGroup>
36
37 <Target Name="SetNuspecVersion">
38 <Error Text="Cannot pack $(MSBuildThisFileName) until all projects are published to: '$(NuspecBasePath)'. Run appveyor.cmd to publish projects properly." Condition=" !Exists('$(NuspecBasePath)') " />
39
40 <PropertyGroup>
41 <NuspecProperties>$(NuspecProperties);Version=$(Version);ProjectFolder=$(MSBuildThisFileDirectory)</NuspecProperties>
42 </PropertyGroup>
43 </Target>
44</Project>
diff --git a/src/dtf/WixToolset.Dtf.MSBuild/WixToolset.Dtf.MSBuild.nuspec b/src/dtf/WixToolset.Dtf.MSBuild/WixToolset.Dtf.MSBuild.nuspec
new file mode 100644
index 00000000..7f819cdb
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.MSBuild/WixToolset.Dtf.MSBuild.nuspec
@@ -0,0 +1,18 @@
1<?xml version="1.0" encoding="utf-8"?>
2<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
3 <metadata>
4 <id>$id$</id>
5 <version>$version$</version>
6 <authors>$authors$</authors>
7 <owners>$authors$</owners>
8 <license type="expression">MS-RL</license>
9 <requireLicenseAcceptance>false</requireLicenseAcceptance>
10 <description>$description$</description>
11 <copyright>$copyright$</copyright>
12 </metadata>
13
14 <files>
15 <file src="build\**\*" target="build" />
16 <file src="tools\**\*" target="tools" />
17 </files>
18</package>
diff --git a/src/dtf/WixToolset.Dtf.MSBuild/build/WixToolset.Dtf.MSBuild.props b/src/dtf/WixToolset.Dtf.MSBuild/build/WixToolset.Dtf.MSBuild.props
new file mode 100644
index 00000000..06a98d6e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.MSBuild/build/WixToolset.Dtf.MSBuild.props
@@ -0,0 +1,8 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
5 <PropertyGroup>
6 <WixCATargetsPath Condition=" '$(WixCATargetsPath)' == '' ">$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)..\tools\wix.ca.targets'))</WixCATargetsPath>
7 </PropertyGroup>
8</Project>
diff --git a/src/dtf/WixToolset.Dtf.MSBuild/tools/wix.ca.targets b/src/dtf/WixToolset.Dtf.MSBuild/tools/wix.ca.targets
new file mode 100644
index 00000000..4578c2d8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.MSBuild/tools/wix.ca.targets
@@ -0,0 +1,123 @@
1<?xml version="1.0" encoding="utf-8" ?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4
5<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
6
7 <Import Project="$(CustomBeforeWixCATargets)" Condition=" '$(CustomBeforeWixCATargets)' != '' and Exists('$(CustomBeforeWixCATargets)')" />
8
9 <PropertyGroup>
10 <WixCATargetsImported>true</WixCATargetsImported>
11
12 <TargetCAFileName Condition=" '$(TargetCAFileName)' == '' ">$(TargetName).CA$(TargetExt)</TargetCAFileName>
13
14 <WixSdkPath Condition=" '$(WixSdkPath)' == '' ">$(MSBuildThisFileDirectory)</WixSdkPath>
15 <WixSdkX86Path Condition=" '$(WixSdkX86Path)' == '' ">$(WixSdkPath)x86\</WixSdkX86Path>
16 <WixSdkX64Path Condition=" '$(WixSdkX64Path)' == '' ">$(WixSdkPath)x64\</WixSdkX64Path>
17
18 <MakeSfxCA Condition=" '$(MakeSfxCA)' == '' ">$(WixSdkPath)MakeSfxCA.exe</MakeSfxCA>
19 <SfxCADll Condition=" '$(SfxCADll)' == '' and '$(Platform)' == 'x64' ">$(WixSdkX64Path)SfxCA.dll</SfxCADll>
20 <SfxCADll Condition=" '$(SfxCADll)' == '' ">$(WixSdkX86Path)SfxCA.dll</SfxCADll>
21 </PropertyGroup>
22
23 <!--
24 ==================================================================================================
25 PackCustomAction
26
27 Creates an MSI managed custom action package that includes the custom action assembly,
28 local assembly dependencies, and project content files.
29
30 [IN]
31 @(IntermediateAssembly) - Managed custom action assembly.
32 @(Content) - Project items of type Content will be included in the package.
33 $(CustomActionContents) - Optional space-delimited list of additional files to include.
34
35 [OUT]
36 $(IntermediateOutputPath)$(TargetCAFileName) - Managed custom action package with unmanaged stub.
37 ==================================================================================================
38 -->
39 <Target Name="PackCustomAction"
40 Inputs="@(IntermediateAssembly);@(Content);$(CustomActionContents)"
41 Outputs="$(IntermediateOutputPath)$(TargetCAFileName)">
42
43 <!-- Find all referenced items marked CopyLocal, but exclude non-binary files. -->
44 <ItemGroup>
45 <CustomActionReferenceContents Include="@(ReferenceCopyLocalPaths)"
46 Condition=" '%(Extension)' == '.dll' or '%(Extension)' == '.exe' " />
47 <CustomActionReferenceContents Include="@(ReferenceComWrappersToCopyLocal)"
48 Condition=" '%(Extension)' == '.dll' or '%(Extension)' == '.exe' " />
49
50 <!-- include PDBs for Debug only -->
51 <CustomActionReferenceContents Include="@(IntermediateAssembly->'%(RootDir)%(Directory)%(Filename).pdb')"
52 Condition=" Exists('%(RootDir)%(Directory)%(Filename).pdb') and '$(Configuration)' == 'Debug' " />
53 <CustomActionReferenceContents Include="@(ReferenceCopyLocalPaths)"
54 Condition=" '%(Extension)' == '.pdb' and '$(Configuration)' == 'Debug' " />
55 <CustomActionReferenceContents Include="@(ReferenceComWrappersToCopyLocal)"
56 Condition=" '%(Extension)' == '.pdb' and '$(Configuration)' == 'Debug' " />
57 </ItemGroup>
58
59 <!--
60 Items to include in the CA package:
61 - Reference assemblies marked CopyLocal
62 - Project items of type Content
63 - Additional items in the CustomActionContents property
64 -->
65 <PropertyGroup>
66 <CustomActionContents>@(CustomActionReferenceContents);@(Content->'%(FullPath)');$(CustomActionContents)</CustomActionContents>
67 </PropertyGroup>
68
69 <ItemGroup>
70 <IntermediateCAAssembly Include="@(IntermediateAssembly->'%(FullPath)')" />
71 <IntermediateCAPackage Include="@(IntermediateAssembly->'%(RootDir)%(Directory)$(TargetCAFileName)')" />
72 </ItemGroup>
73
74 <!-- Run the MakeSfxCA.exe CA packaging tool. -->
75 <Exec Command='"$(MakeSfxCA)" "@(IntermediateCAPackage)" "$(SfxCADll)" "@(IntermediateCAAssembly)" "$(CustomActionContents)"'
76 WorkingDirectory="$(ProjectDir)" />
77
78 <!-- Add modules to be copied to output dir. -->
79 <ItemGroup>
80 <AddModules Include="@(IntermediateCAPackage)" />
81 </ItemGroup>
82 </Target>
83
84 <!--
85 ==================================================================================================
86 CleanCustomAction
87
88 Cleans the .CA.dll binary created by the PackCustomAction target.
89
90 ==================================================================================================
91 -->
92 <Target Name="CleanCustomAction">
93 <Delete Files="$(IntermediateOutputPath)$(TargetCAFileName)"
94 TreatErrorsAsWarnings="true" />
95 </Target>
96
97 <!--
98 ==================================================================================================
99 AfterCompile (redefinition)
100
101 Calls the PackCustomAction target after compiling.
102 Overrides the empty AfterCompile target from Microsoft.Common.targets.
103
104 ==================================================================================================
105 -->
106 <Target Name="AfterCompile"
107 DependsOnTargets="PackCustomAction" />
108
109 <!--
110 ==================================================================================================
111 BeforeClean (redefinition)
112
113 Calls the CleanCustomAction target before cleaning.
114 Overrides the empty AfterCompile target from Microsoft.Common.targets.
115
116 ==================================================================================================
117 -->
118 <Target Name="BeforeClean"
119 DependsOnTargets="CleanCustomAction" />
120
121 <Import Project="$(CustomAfterWixCATargets)" Condition=" '$(CustomAfterWixCATargets)' != '' and Exists('$(CustomAfterWixCATargets)')" />
122
123</Project>
diff --git a/src/dtf/WixToolset.Dtf.Resources/AssemblyInfo.cs b/src/dtf/WixToolset.Dtf.Resources/AssemblyInfo.cs
new file mode 100644
index 00000000..77f33f97
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/AssemblyInfo.cs
@@ -0,0 +1,5 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3using System.Diagnostics.CodeAnalysis;
4
5[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "WixToolset.Dtf.Resources.ResourceCollection.#System.Collections.Generic.ICollection`1<WixToolset.Dtf.Resources.Resource>.IsReadOnly")]
diff --git a/src/dtf/WixToolset.Dtf.Resources/BitmapResource.cs b/src/dtf/WixToolset.Dtf.Resources/BitmapResource.cs
new file mode 100644
index 00000000..42c886cb
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/BitmapResource.cs
@@ -0,0 +1,57 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7
8 /// <summary>
9 /// A subclass of Resource which provides specific methods for manipulating the resource data.
10 /// </summary>
11 /// <remarks>
12 /// The resource is of type <see cref="ResourceType.Bitmap"/> (RT_GROUPICON).
13 /// </remarks>
14 public sealed class BitmapResource : Resource
15 {
16 private const int SizeOfBitmapFileHeader = 14; // this is the sizeof(BITMAPFILEHEADER)
17
18 /// <summary>
19 /// Creates a new BitmapResource object without any data. The data can be later loaded from a file.
20 /// </summary>
21 /// <param name="name">Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#".</param>
22 /// <param name="locale">Locale of the resource</param>
23 public BitmapResource(string name, int locale)
24 : this(name, locale, null)
25 {
26 }
27
28 /// <summary>
29 /// Creates a new BitmapResource object with data. The data can be later saved to a file.
30 /// </summary>
31 /// <param name="name">Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#".</param>
32 /// <param name="locale">Locale of the resource</param>
33 /// <param name="data">Raw resource data</param>
34 public BitmapResource(string name, int locale, byte[] data)
35 : base(ResourceType.Bitmap, name, locale, data)
36 {
37 }
38
39 /// <summary>
40 /// Reads the bitmap from a .bmp file.
41 /// </summary>
42 /// <param name="path">Path to a bitmap file (.bmp).</param>
43 public void ReadFromFile(string path)
44 {
45 using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read))
46 {
47 // Move past the BITMAPFILEHEADER, and copy the rest of the bitmap as the resource data. Resource
48 // functions expect only the BITMAPINFO struct which exists just beyond the BITMAPFILEHEADER
49 // struct in bitmap files.
50 fs.Seek(BitmapResource.SizeOfBitmapFileHeader, SeekOrigin.Begin);
51
52 base.Data = new byte[fs.Length - BitmapResource.SizeOfBitmapFileHeader];
53 fs.Read(base.Data, 0, base.Data.Length);
54 }
55 }
56 }
57}
diff --git a/src/dtf/WixToolset.Dtf.Resources/FixedFileVersionInfo.cs b/src/dtf/WixToolset.Dtf.Resources/FixedFileVersionInfo.cs
new file mode 100644
index 00000000..e0d081db
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/FixedFileVersionInfo.cs
@@ -0,0 +1,183 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Globalization;
11 using System.Diagnostics.CodeAnalysis;
12
13 internal class FixedFileVersionInfo
14 {
15 public FixedFileVersionInfo()
16 {
17 // Set reasonable defaults
18 this.signature = 0xFEEF04BD;
19 this.structVersion = 0x00010000; // v1.0
20 this.FileVersion = new Version(0, 0, 0, 0);
21 this.ProductVersion = new Version(0, 0, 0, 0);
22 this.FileFlagsMask = VersionBuildTypes.Debug | VersionBuildTypes.Prerelease;
23 this.FileFlags = VersionBuildTypes.None;
24 this.FileOS = VersionFileOS.NT_WINDOWS32;
25 this.FileType = VersionFileType.Application;
26 this.FileSubtype = VersionFileSubtype.Unknown;
27 this.Timestamp = DateTime.MinValue;
28 }
29
30 private uint signature;
31 private uint structVersion;
32
33 public Version FileVersion
34 {
35 get
36 {
37 return this.fileVersion;
38 }
39
40 set
41 {
42 if (value == null)
43 {
44 throw new InvalidOperationException();
45 }
46
47 this.fileVersion = value;
48 }
49 }
50 private Version fileVersion;
51
52 public Version ProductVersion
53 {
54 get
55 {
56 return this.productVersion;
57 }
58
59 set
60 {
61 if (value == null)
62 {
63 throw new InvalidOperationException();
64 }
65
66 this.productVersion = value;
67 }
68 }
69 private Version productVersion;
70
71 public VersionBuildTypes FileFlagsMask
72 {
73 get { return this.fileFlagsMask; }
74 set { this.fileFlagsMask = value; }
75 }
76 private VersionBuildTypes fileFlagsMask;
77
78 public VersionBuildTypes FileFlags
79 {
80 get { return this.fileFlags; }
81 set { this.fileFlags = value; }
82 }
83 private VersionBuildTypes fileFlags;
84
85 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
86 public VersionFileOS FileOS
87 {
88 get { return this.fileOS; }
89 set { this.fileOS = value; }
90 }
91 private VersionFileOS fileOS;
92
93 public VersionFileType FileType
94 {
95 get { return this.fileType; }
96 set { this.fileType = value; }
97 }
98 private VersionFileType fileType;
99
100 public VersionFileSubtype FileSubtype
101 {
102 get { return this.fileSubtype; }
103 set { this.fileSubtype = value; }
104 }
105 private VersionFileSubtype fileSubtype;
106
107 public DateTime Timestamp
108 {
109 get { return this.timestamp; }
110 set { this.timestamp = value; }
111 }
112 private DateTime timestamp;
113
114 public void Read(BinaryReader reader)
115 {
116 this.signature = reader.ReadUInt32();
117 this.structVersion = reader.ReadUInt32();
118 this.fileVersion = UInt64ToVersion(reader.ReadUInt64());
119 this.productVersion = UInt64ToVersion(reader.ReadUInt64());
120 this.fileFlagsMask = (VersionBuildTypes) reader.ReadInt32();
121 this.fileFlags = (VersionBuildTypes) reader.ReadInt32();
122 this.fileOS = (VersionFileOS) reader.ReadInt32();
123 this.fileType = (VersionFileType) reader.ReadInt32();
124 this.fileSubtype = (VersionFileSubtype) reader.ReadInt32();
125 this.timestamp = UInt64ToDateTime(reader.ReadUInt64());
126 }
127
128 public void Write(BinaryWriter writer)
129 {
130 writer.Write(this.signature);
131 writer.Write(this.structVersion);
132 writer.Write(VersionToUInt64(this.fileVersion));
133 writer.Write(VersionToUInt64(this.productVersion));
134 writer.Write((int) this.fileFlagsMask);
135 writer.Write((int) this.fileFlags);
136 writer.Write((int) this.fileOS);
137 writer.Write((int) this.fileType);
138 writer.Write((int) this.fileSubtype);
139 writer.Write(DateTimeToUInt64(this.timestamp));
140 }
141
142 public static explicit operator FixedFileVersionInfo(byte[] bytesValue)
143 {
144 FixedFileVersionInfo ffviValue = new FixedFileVersionInfo();
145 using (BinaryReader reader = new BinaryReader(new MemoryStream(bytesValue, false)))
146 {
147 ffviValue.Read(reader);
148 }
149 return ffviValue;
150 }
151
152 public static explicit operator byte[](FixedFileVersionInfo ffviValue)
153 {
154 const int FFVI_LENGTH = 52;
155
156 byte[] bytesValue = new byte[FFVI_LENGTH];
157 using (BinaryWriter writer = new BinaryWriter(new MemoryStream(bytesValue, true)))
158 {
159 ffviValue.Write(writer);
160 }
161 return bytesValue;
162 }
163
164 private static Version UInt64ToVersion(ulong version)
165 {
166 return new Version((int) ((version >> 16) & 0xFFFF), (int) (version & 0xFFFF), (int) (version >> 48), (int) ((version >> 32) & 0xFFFF));
167 }
168 private static ulong VersionToUInt64(Version version)
169 {
170 return (((ulong) (ushort) version.Major) << 16) | ((ulong) (ushort) version.Minor)
171 | (((ulong) (ushort) version.Build) << 48) | (((ulong) (ushort) version.Revision) << 32);
172 }
173
174 private static DateTime UInt64ToDateTime(ulong dateTime)
175 {
176 return (dateTime == 0 ? DateTime.MinValue : DateTime.FromFileTime((long) dateTime));
177 }
178 private static ulong DateTimeToUInt64(DateTime dateTime)
179 {
180 return (dateTime == DateTime.MinValue ? 0 : (ulong) dateTime.ToFileTime());
181 }
182 }
183}
diff --git a/src/dtf/WixToolset.Dtf.Resources/GroupIconInfo.cs b/src/dtf/WixToolset.Dtf.Resources/GroupIconInfo.cs
new file mode 100644
index 00000000..0fb56223
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/GroupIconInfo.cs
@@ -0,0 +1,119 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Globalization;
11 using System.Diagnostics.CodeAnalysis;
12
13 internal enum GroupIconType
14 {
15 Unknown,
16 Icon,
17 Cursor,
18 }
19
20 internal struct GroupIconDirectoryInfo
21 {
22 public byte width;
23 public byte height;
24 public byte colors;
25 public byte reserved;
26 public ushort planes;
27 public ushort bitsPerPixel;
28 public uint imageSize;
29 public uint imageOffset; // only valid when icon group is read from .ico file.
30 public ushort imageIndex; // only valid when icon group is read from PE resource.
31 }
32
33 internal class GroupIconInfo
34 {
35 private ushort reserved;
36 private GroupIconType type;
37 private GroupIconDirectoryInfo[] images;
38
39 public GroupIconInfo()
40 {
41 this.images = new GroupIconDirectoryInfo[0];
42 }
43
44 public GroupIconDirectoryInfo[] DirectoryInfo { get { return this.images; } }
45
46 public void ReadFromFile(Stream stream)
47 {
48 BinaryReader reader = new BinaryReader(stream);
49 this.Read(reader, true);
50 }
51
52 public void ReadFromResource(byte[] data)
53 {
54 using (BinaryReader reader = new BinaryReader(new MemoryStream(data, false)))
55 {
56 this.Read(reader, false);
57 }
58 }
59
60 public byte[] GetResourceData()
61 {
62 byte[] data = null;
63
64 using (MemoryStream stream = new MemoryStream())
65 {
66 BinaryWriter writer = new BinaryWriter(stream);
67 writer.Write(this.reserved);
68 writer.Write((ushort)this.type);
69 writer.Write((ushort)this.images.Length);
70 for (int i = 0; i < this.images.Length; ++i)
71 {
72 writer.Write(this.images[i].width);
73 writer.Write(this.images[i].height);
74 writer.Write(this.images[i].colors);
75 writer.Write(this.images[i].reserved);
76 writer.Write(this.images[i].planes);
77 writer.Write(this.images[i].bitsPerPixel);
78 writer.Write(this.images[i].imageSize);
79 writer.Write(this.images[i].imageIndex);
80 }
81
82 data = new byte[stream.Length];
83 stream.Seek(0, SeekOrigin.Begin);
84 stream.Read(data, 0, data.Length);
85 }
86
87 return data;
88 }
89
90 private void Read(BinaryReader reader, bool readFromFile)
91 {
92 this.reserved = reader.ReadUInt16();
93 this.type = (GroupIconType)reader.ReadUInt16();
94
95 int imageCount = reader.ReadUInt16();
96 this.images = new GroupIconDirectoryInfo[imageCount];
97 for (int i = 0; i < imageCount; ++i)
98 {
99 this.images[i].width = reader.ReadByte();
100 this.images[i].height = reader.ReadByte();
101 this.images[i].colors = reader.ReadByte();
102 this.images[i].reserved = reader.ReadByte();
103 this.images[i].planes = reader.ReadUInt16();
104 this.images[i].bitsPerPixel = reader.ReadUInt16();
105 this.images[i].imageSize = reader.ReadUInt32();
106 if (readFromFile)
107 {
108 this.images[i].imageOffset = reader.ReadUInt32();
109 this.images[i].imageIndex = (ushort)(i + 1);
110 }
111 else
112 {
113 this.images[i].imageIndex = reader.ReadUInt16();
114 }
115 }
116 }
117
118 }
119}
diff --git a/src/dtf/WixToolset.Dtf.Resources/GroupIconResource.cs b/src/dtf/WixToolset.Dtf.Resources/GroupIconResource.cs
new file mode 100644
index 00000000..8820ce75
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/GroupIconResource.cs
@@ -0,0 +1,120 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// A subclass of Resource which provides specific methods for manipulating the resource data.
16 /// </summary>
17 /// <remarks>
18 /// The resource is of type <see cref="ResourceType.GroupIcon"/> (RT_GROUPICON).
19 /// </remarks>
20 public sealed class GroupIconResource : Resource
21 {
22 internal bool dirty;
23 private GroupIconInfo rawGroupIconInfo;
24 private List<Resource> icons;
25
26 /// <summary>
27 /// Creates a new GroupIconResource object without any data. The data can be later loaded from a file.
28 /// </summary>
29 /// <param name="name">Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#".</param>
30 /// <param name="locale">Locale of the resource</param>
31 public GroupIconResource(string name, int locale)
32 : this(name, locale, null)
33 {
34 }
35
36 /// <summary>
37 /// Creates a new GroupIconResource object with data. The data can be later saved to a file.
38 /// </summary>
39 /// <param name="name">Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#".</param>
40 /// <param name="locale">Locale of the resource</param>
41 /// <param name="data">Raw resource data</param>
42 public GroupIconResource(string name, int locale, byte[] data)
43 : base(ResourceType.GroupIcon, name, locale, data)
44 {
45 this.RefreshIconGroupInfo(data);
46 }
47
48 /// <summary>
49 /// Gets or sets the raw data of the resource. The data is in the format of the RT_GROUPICON resource structure.
50 /// </summary>
51 public override byte[] Data
52 {
53 get
54 {
55 if (this.dirty)
56 {
57 base.Data = this.rawGroupIconInfo.GetResourceData();
58 this.dirty = false;
59 }
60
61 return base.Data;
62 }
63 set
64 {
65 this.RefreshIconGroupInfo(value);
66
67 base.Data = value;
68 this.dirty = false;
69 }
70 }
71
72 /// <summary>
73 /// Enumerates the the icons in the icon group.
74 /// </summary>
75 public IEnumerable<Resource> Icons { get { return this.icons; } }
76
77 /// <summary>
78 /// Reads the icon group from a .ico file.
79 /// </summary>
80 /// <param name="path">Path to an icon file (.ico).</param>
81 public void ReadFromFile(string path)
82 {
83 this.rawGroupIconInfo = new GroupIconInfo();
84 this.icons = new List<Resource>();
85 using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
86 {
87 this.rawGroupIconInfo.ReadFromFile(fs);
88
89 // After reading the group icon info header from the file, read all the icons.
90 for (int i = 0; i < this.rawGroupIconInfo.DirectoryInfo.Length; ++i)
91 {
92 ushort index = this.rawGroupIconInfo.DirectoryInfo[i].imageIndex;
93 uint offset = this.rawGroupIconInfo.DirectoryInfo[i].imageOffset;
94 uint size = this.rawGroupIconInfo.DirectoryInfo[i].imageSize;
95 byte[] data = new byte[size];
96
97 fs.Seek(offset, SeekOrigin.Begin);
98 fs.Read(data, 0, data.Length);
99
100 Resource resource = new Resource(ResourceType.Icon, String.Concat("#", index), this.Locale, data);
101 this.icons.Add(resource);
102 }
103 }
104
105 this.dirty = true;
106 }
107
108 private void RefreshIconGroupInfo(byte[] refreshData)
109 {
110 this.rawGroupIconInfo = new GroupIconInfo();
111 this.icons = new List<Resource>();
112 if (refreshData != null)
113 {
114 this.rawGroupIconInfo.ReadFromResource(refreshData);
115 }
116
117 this.dirty = true;
118 }
119 }
120}
diff --git a/src/dtf/WixToolset.Dtf.Resources/NativeMethods.cs b/src/dtf/WixToolset.Dtf.Resources/NativeMethods.cs
new file mode 100644
index 00000000..bf1bd1dc
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/NativeMethods.cs
@@ -0,0 +1,55 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Globalization;
11 using System.Runtime.InteropServices;
12
13 internal static class NativeMethods
14 {
15 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
16 internal static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, uint flags);
17 internal const uint LOAD_LIBRARY_AS_DATAFILE = 2;
18 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
19 internal static extern bool FreeLibrary(IntPtr module);
20 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
21 internal static extern bool EnumResourceTypes(IntPtr module, EnumResTypesProc enumFunc, IntPtr param);
22 [return: MarshalAs(UnmanagedType.Bool)]
23 internal delegate bool EnumResTypesProc(IntPtr module, IntPtr type, IntPtr param);
24 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
25 internal static extern bool EnumResourceNames(IntPtr module, IntPtr type, EnumResNamesProc enumFunc, IntPtr param);
26 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
27 internal static extern bool EnumResourceNames(IntPtr module, string type, EnumResNamesProc enumFunc, IntPtr param);
28 [return: MarshalAs(UnmanagedType.Bool)]
29 internal delegate bool EnumResNamesProc(IntPtr module, IntPtr type, IntPtr name, IntPtr param);
30 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
31 internal static extern bool EnumResourceLanguages(IntPtr module, IntPtr type, IntPtr name, EnumResLangsProc enumFunc, IntPtr param);
32 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
33 internal static extern bool EnumResourceLanguages(IntPtr module, string type, string name, EnumResLangsProc enumFunc, IntPtr param);
34 [return: MarshalAs(UnmanagedType.Bool)]
35 internal delegate bool EnumResLangsProc(IntPtr module, IntPtr type, IntPtr name, ushort langId, IntPtr param);
36 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
37 internal static extern IntPtr FindResourceEx(IntPtr module, string type, string name, ushort langId);
38 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
39 internal static extern IntPtr LoadResource(IntPtr module, IntPtr resourceInfo);
40 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
41 internal static extern IntPtr LockResource(IntPtr resourceData);
42 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
43 internal static extern uint SizeofResource(IntPtr module, IntPtr resourceInfo);
44 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
45 internal static extern IntPtr BeginUpdateResource(string fileName, [MarshalAs(UnmanagedType.Bool)] bool deleteExistingResources);
46 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
47 internal static extern bool UpdateResource(IntPtr updateHandle, IntPtr type, IntPtr name, ushort lcid, IntPtr data, uint dataSize);
48 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)][return: MarshalAs(UnmanagedType.Bool)]
49 internal static extern bool UpdateResource(IntPtr updateHandle, IntPtr type, string name, ushort lcid, IntPtr data, uint dataSize);
50 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
51 internal static extern bool UpdateResource(IntPtr updateHandle, string type, string name, ushort lcid, IntPtr data, uint dataSize);
52 [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)]
53 internal static extern bool EndUpdateResource(IntPtr updateHandle, [MarshalAs(UnmanagedType.Bool)] bool discardChanges);
54 }
55}
diff --git a/src/dtf/WixToolset.Dtf.Resources/Resource.cs b/src/dtf/WixToolset.Dtf.Resources/Resource.cs
new file mode 100644
index 00000000..23b4621f
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/Resource.cs
@@ -0,0 +1,225 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Globalization;
11 using System.Runtime.InteropServices;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// Represents a Win32 resource which can be loaded from and saved to a PE file.
16 /// </summary>
17 public class Resource
18 {
19 private ResourceType type;
20 private string name;
21 private int locale;
22 private byte[] data;
23
24 /// <summary>
25 /// Creates a new Resource object without any data. The data can be later loaded from a file.
26 /// </summary>
27 /// <param name="type">Type of the resource; may be one of the ResourceType constants or a user-defined type.</param>
28 /// <param name="name">Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#".</param>
29 /// <param name="locale">Locale of the resource</param>
30 public Resource(ResourceType type, string name, int locale)
31 : this(type, name, locale, null)
32 {
33 }
34
35 /// <summary>
36 /// Creates a new Resource object with data. The data can be later saved to a file.
37 /// </summary>
38 /// <param name="type">Type of the resource; may be one of the ResourceType constants or a user-defined type.</param>
39 /// <param name="name">Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#".</param>
40 /// <param name="locale">Locale of the resource</param>
41 /// <param name="data">Raw resource data</param>
42 public Resource(ResourceType type, string name, int locale, byte[] data)
43 {
44 if (name == null)
45 {
46 throw new ArgumentNullException("name");
47 }
48
49 this.type = type;
50 this.name = name;
51 this.locale = locale;
52 this.data = data;
53 }
54
55 /// <summary>
56 /// Gets or sets the type of the resource. This may be one of the ResourceType constants
57 /// or a user-defined type name.
58 /// </summary>
59 public ResourceType ResourceType
60 {
61 get { return this.type; }
62 set { this.type = value; }
63 }
64
65 /// <summary>
66 /// Gets or sets the name of the resource. For a numeric resource identifier, the decimal number is prefixed with a "#".
67 /// </summary>
68 public string Name
69 {
70 get
71 {
72 return this.name;
73 }
74
75 set
76 {
77 if (value == null)
78 {
79 throw new ArgumentNullException("value");
80 }
81
82 this.name = value;
83 }
84 }
85
86 /// <summary>
87 /// Gets or sets the locale of the resource.
88 /// </summary>
89 public int Locale
90 {
91 get { return this.locale; }
92 set { this.locale = value; }
93 }
94
95 /// <summary>
96 /// Gets or sets the raw data of the resource.
97 /// </summary>
98 [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
99 public virtual byte[] Data
100 {
101 get { return this.data; }
102 set { this.data = value; }
103 }
104
105 /// <summary>
106 /// Loads the resource data from a file. The file is searched for a resource with matching type, name, and locale.
107 /// </summary>
108 /// <param name="file">Win32 PE file containing the resource</param>
109 public void Load(string file)
110 {
111 IntPtr module = NativeMethods.LoadLibraryEx(file, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE);
112 try
113 {
114 this.Load(module);
115 }
116 finally
117 {
118 NativeMethods.FreeLibrary(module);
119 }
120 }
121
122 internal void Load(IntPtr module)
123 {
124 IntPtr resourceInfo = NativeMethods.FindResourceEx(module, (string) this.ResourceType, this.Name, (ushort) this.Locale);
125 if (resourceInfo != IntPtr.Zero)
126 {
127 uint resourceLength = NativeMethods.SizeofResource(module, resourceInfo);
128 IntPtr resourceData = NativeMethods.LoadResource(module, resourceInfo);
129 IntPtr resourcePtr = NativeMethods.LockResource(resourceData);
130 byte[] resourceBytes = new byte[resourceLength];
131 Marshal.Copy(resourcePtr, resourceBytes, 0, resourceBytes.Length);
132 this.Data = resourceBytes;
133 }
134 else
135 {
136 this.Data = null;
137 }
138 }
139
140 /// <summary>
141 /// Saves the resource to a file. Any existing resource data with matching type, name, and locale is overwritten.
142 /// </summary>
143 /// <param name="file">Win32 PE file to contain the resource</param>
144 public void Save(string file)
145 {
146 IntPtr updateHandle = IntPtr.Zero;
147 try
148 {
149 updateHandle = NativeMethods.BeginUpdateResource(file, false);
150 this.Save(updateHandle);
151 if (!NativeMethods.EndUpdateResource(updateHandle, false))
152 {
153 int err = Marshal.GetLastWin32Error();
154 throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to save resource. Error code: {0}", err));
155 }
156 updateHandle = IntPtr.Zero;
157 }
158 finally
159 {
160 if (updateHandle != IntPtr.Zero)
161 {
162 NativeMethods.EndUpdateResource(updateHandle, true);
163 }
164 }
165 }
166
167 internal void Save(IntPtr updateHandle)
168 {
169 IntPtr dataPtr = IntPtr.Zero;
170 try
171 {
172 int dataLength = 0;
173 if (this.Data != null)
174 {
175 dataLength = this.Data.Length;
176 dataPtr = Marshal.AllocHGlobal(dataLength);
177 Marshal.Copy(this.Data, 0, dataPtr, dataLength);
178 }
179 bool updateSuccess;
180 if (this.Name.StartsWith("#", StringComparison.Ordinal))
181 {
182 // A numeric-named resource must be saved via the integer version of UpdateResource.
183 IntPtr intName = new IntPtr(Int32.Parse(this.Name.Substring(1), CultureInfo.InvariantCulture));
184 updateSuccess = NativeMethods.UpdateResource(updateHandle, new IntPtr(this.ResourceType.IntegerValue), intName, (ushort) this.Locale, dataPtr, (uint) dataLength);
185 }
186 else
187 {
188 updateSuccess = NativeMethods.UpdateResource(updateHandle, (string) this.ResourceType, this.Name, (ushort) this.Locale, dataPtr, (uint) dataLength);
189 }
190 if (!updateSuccess)
191 {
192 throw new IOException("Failed to save resource. Error: " + Marshal.GetLastWin32Error());
193 }
194 }
195 finally
196 {
197 if (dataPtr != IntPtr.Zero)
198 {
199 Marshal.FreeHGlobal(dataPtr);
200 }
201 }
202 }
203
204 /// <summary>
205 /// Tests if type, name, and locale of this Resource object match another Resource object.
206 /// </summary>
207 /// <param name="obj">Resource object to be compared</param>
208 /// <returns>True if the objects represent the same resource; false otherwise.</returns>
209 public override bool Equals(object obj)
210 {
211 Resource res = obj as Resource;
212 if (res == null) return false;
213 return this.ResourceType == res.ResourceType && this.Name == res.Name && this.Locale == res.Locale;
214 }
215
216 /// <summary>
217 /// Gets a hash code for this Resource object.
218 /// </summary>
219 /// <returns>Hash code generated from the resource type, name, and locale.</returns>
220 public override int GetHashCode()
221 {
222 return this.ResourceType.GetHashCode() ^ this.Name.GetHashCode() ^ this.Locale.GetHashCode();
223 }
224 }
225}
diff --git a/src/dtf/WixToolset.Dtf.Resources/ResourceCollection.cs b/src/dtf/WixToolset.Dtf.Resources/ResourceCollection.cs
new file mode 100644
index 00000000..5d9e5653
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/ResourceCollection.cs
@@ -0,0 +1,340 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Runtime.InteropServices;
13 using System.Diagnostics.CodeAnalysis;
14
15 /// <summary>
16 /// Allows reading and editing of resource data in a Win32 PE file.
17 /// </summary>
18 /// <remarks>
19 /// To use this class:<list type="number">
20 /// <item>Create a new ResourceCollection</item>
21 /// <item>Locate resources for the collection by calling one of the <see cref="ResourceCollection.Find(string)"/> methods</item>
22 /// <item>Load data of one or more <see cref="Resource"/>s from a file by calling the <see cref="Load"/> method of the
23 /// Resource class, or load them all at once (more efficient) with the <see cref="Load"/> method of the ResourceCollection.</item>
24 /// <item>Read and/or edit data of the individual Resource objects using the methods on that class.</item>
25 /// <item>Save data of one or more <see cref="Resource"/>s to a file by calling the <see cref="Save"/> method of the
26 /// Resource class, or save them all at once (more efficient) with the <see cref="Save"/> method of the ResourceCollection.</item>
27 /// </list>
28 /// </remarks>
29 public class ResourceCollection : ICollection<Resource>
30 {
31 private List<Resource> resources;
32
33 /// <summary>
34 /// Creates a new, empty ResourceCollection.
35 /// </summary>
36 public ResourceCollection()
37 {
38 this.resources = new List<Resource>();
39 }
40
41 /// <summary>
42 /// Locates all resources in a file, including all resource types and languages. For each located resource,
43 /// a <see cref="Resource"/> instance (or subclass) is added to the collection.
44 /// </summary>
45 /// <param name="resFile">The file to be searched for resources.</param>
46 /// <exception cref="IOException">resources could not be read from the file</exception>
47 public void Find(string resFile)
48 {
49 this.Clear();
50
51 IntPtr module = NativeMethods.LoadLibraryEx(resFile, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE);
52 if (module == IntPtr.Zero)
53 {
54 int err = Marshal.GetLastWin32Error();
55 throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to load resource file. Error code: {0}", err));
56 }
57 try
58 {
59 if (!NativeMethods.EnumResourceTypes(module, new NativeMethods.EnumResTypesProc(this.EnumResTypes), IntPtr.Zero))
60 {
61 int err = Marshal.GetLastWin32Error();
62 throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to enumerate resources. Error code: {0}", err));
63 }
64 }
65 finally
66 {
67 NativeMethods.FreeLibrary(module);
68 }
69 }
70
71 /// <summary>
72 /// Locates all resources in a file of a given type, including all languages. For each located resource,
73 /// a <see cref="Resource"/> instance (or subclass) is added to the collection.
74 /// </summary>
75 /// <param name="resFile">The file to be searched for resources.</param>
76 /// <param name="type">The type of resource to search for; may be one of the ResourceType constants or a user-defined type.</param>
77 /// <exception cref="IOException">resources could not be read from the file</exception>
78 public void Find(string resFile, ResourceType type)
79 {
80 this.Clear();
81
82 IntPtr module = NativeMethods.LoadLibraryEx(resFile, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE);
83 try
84 {
85 if (!NativeMethods.EnumResourceNames(module, (string) type, new NativeMethods.EnumResNamesProc(this.EnumResNames), IntPtr.Zero))
86 {
87 int err = Marshal.GetLastWin32Error();
88 throw new IOException(String.Format(CultureInfo.InvariantCulture, "EnumResourceNames error. Error code: {0}", err));
89 }
90 }
91 finally
92 {
93 NativeMethods.FreeLibrary(module);
94 }
95 }
96
97 /// <summary>
98 /// Locates all resources in a file of a given type and language. For each located resource,
99 /// a <see cref="Resource"/> instance (or subclass) is added to the collection.
100 /// </summary>
101 /// <param name="resFile">The file to be searched for resources.</param>
102 /// <param name="type">The type of resource to search for; may be one of the ResourceType constants or a user-defined type.</param>
103 /// <param name="name">The name of the resource to search for.</param>
104 /// <exception cref="IOException">resources could not be read from the file</exception>
105 public void Find(string resFile, ResourceType type, string name)
106 {
107 this.Clear();
108
109 IntPtr module = NativeMethods.LoadLibraryEx(resFile, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE);
110 try
111 {
112 if (!NativeMethods.EnumResourceLanguages(module, (string) type, name, new NativeMethods.EnumResLangsProc(this.EnumResLangs), IntPtr.Zero))
113 {
114 int err = Marshal.GetLastWin32Error();
115 throw new IOException(String.Format(CultureInfo.InvariantCulture, "EnumResourceLanguages error. Error code: {0}", err));
116 }
117 }
118 finally
119 {
120 NativeMethods.FreeLibrary(module);
121 }
122 }
123
124 private bool EnumResTypes(IntPtr module, IntPtr type, IntPtr param)
125 {
126 if (!NativeMethods.EnumResourceNames(module, type, new NativeMethods.EnumResNamesProc(EnumResNames), IntPtr.Zero))
127 {
128 int err = Marshal.GetLastWin32Error();
129 throw new IOException(String.Format(CultureInfo.InvariantCulture, "EnumResourceNames error! Error code: {0}", err));
130 }
131 return true;
132 }
133
134 private bool EnumResNames(IntPtr module, IntPtr type, IntPtr name, IntPtr param)
135 {
136 if (!NativeMethods.EnumResourceLanguages(module, type, name, new NativeMethods.EnumResLangsProc(EnumResLangs), IntPtr.Zero))
137 {
138 int err = Marshal.GetLastWin32Error();
139 throw new IOException(String.Format(CultureInfo.InvariantCulture, "EnumResourceLanguages error. Error code: {0}", err));
140 }
141 return true;
142 }
143
144 private bool EnumResLangs(IntPtr module, IntPtr type, IntPtr name, ushort langId, IntPtr param)
145 {
146 Resource res;
147 if (((int) type) == ResourceType.Version.IntegerValue)
148 {
149 res = new VersionResource(ResourceNameToString(name), langId);
150 }
151 else
152 {
153 res = new Resource(ResourceNameToString(type), ResourceNameToString(name), langId);
154 }
155
156 if (!this.Contains(res))
157 {
158 this.Add(res);
159 }
160
161 return true;
162 }
163
164 private static string ResourceNameToString(IntPtr resName)
165 {
166 if ((resName.ToInt64() >> 16) == 0)
167 {
168 return "#" + resName.ToString();
169 }
170 else
171 {
172 return Marshal.PtrToStringAuto(resName);
173 }
174 }
175
176 /// <summary>
177 /// For all resources in the collection, loads their data from a resource file.
178 /// </summary>
179 /// <param name="file">The file from which resources are loaded.</param>
180 public void Load(string file)
181 {
182 IntPtr module = NativeMethods.LoadLibraryEx(file, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE);
183 try
184 {
185 foreach (Resource res in this)
186 {
187 res.Load(module);
188 }
189 }
190 finally
191 {
192 NativeMethods.FreeLibrary(module);
193 }
194 }
195
196 /// <summary>
197 /// For all resources in the collection, saves their data to a resource file.
198 /// </summary>
199 /// <param name="file">The file to which resources are saved.</param>
200 public void Save(string file)
201 {
202 IntPtr updateHandle = IntPtr.Zero;
203 try
204 {
205 updateHandle = NativeMethods.BeginUpdateResource(file, false);
206 foreach (Resource res in this)
207 {
208 res.Save(updateHandle);
209 }
210 if (!NativeMethods.EndUpdateResource(updateHandle, false))
211 {
212 int err = Marshal.GetLastWin32Error();
213 throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to save resource. Error {0}", err));
214 }
215 updateHandle = IntPtr.Zero;
216 }
217 finally
218 {
219 if (updateHandle != IntPtr.Zero)
220 {
221 NativeMethods.EndUpdateResource(updateHandle, true);
222 }
223 }
224 }
225
226 /// <summary>
227 /// Gets or sets the element at the specified index.
228 /// </summary>
229 public Resource this[int index]
230 {
231 get
232 {
233 return (Resource) this.resources[index];
234 }
235 set
236 {
237 this.resources[index] = value;
238 }
239 }
240
241 /// <summary>
242 /// Adds a new item to the collection.
243 /// </summary>
244 /// <param name="item">The Resource to add.</param>
245 public void Add(Resource item)
246 {
247 this.resources.Add(item);
248 }
249
250 /// <summary>
251 /// Removes an item to the collection.
252 /// </summary>
253 /// <param name="item">The Resource to remove.</param>
254 public bool Remove(Resource item)
255 {
256 return this.resources.Remove(item);
257 }
258
259 /// <summary>
260 /// Gets the index of an item in the collection.
261 /// </summary>
262 /// <param name="item">The Resource to search for.</param>
263 /// <returns>The index of the item, or -1 if not found.</returns>
264 public int IndexOf(Resource item)
265 {
266 return this.resources.IndexOf(item);
267 }
268
269 /// <summary>
270 /// Inserts a item into the collection.
271 /// </summary>
272 /// <param name="index">The insertion index.</param>
273 /// <param name="item">The Resource to insert.</param>
274 public void Insert(int index, Resource item)
275 {
276 this.resources.Insert(index, item);
277 }
278
279 /// <summary>
280 /// Tests if the collection contains an item.
281 /// </summary>
282 /// <param name="item">The Resource to search for.</param>
283 /// <returns>true if the item is found; false otherwise</returns>
284 public bool Contains(Resource item)
285 {
286 return this.resources.Contains(item);
287 }
288
289 /// <summary>
290 /// Copies the collection into an array.
291 /// </summary>
292 /// <param name="array">The array to copy into.</param>
293 /// <param name="arrayIndex">The starting index in the destination array.</param>
294 public void CopyTo(Resource[] array, int arrayIndex)
295 {
296 this.resources.CopyTo(array, arrayIndex);
297 }
298
299 /// <summary>
300 /// Removes all resources from the collection.
301 /// </summary>
302 public void Clear()
303 {
304 this.resources.Clear();
305 }
306
307 /// <summary>
308 /// Gets the number of resources in the collection.
309 /// </summary>
310 public int Count
311 {
312 get
313 {
314 return this.resources.Count;
315 }
316 }
317
318 /// <summary>
319 /// Gets an enumerator over all resources in the collection.
320 /// </summary>
321 /// <returns></returns>
322 public IEnumerator<Resource> GetEnumerator()
323 {
324 return this.resources.GetEnumerator();
325 }
326
327 IEnumerator IEnumerable.GetEnumerator()
328 {
329 return ((IEnumerable) this.resources).GetEnumerator();
330 }
331
332 bool ICollection<Resource>.IsReadOnly
333 {
334 get
335 {
336 return false;
337 }
338 }
339 }
340}
diff --git a/src/dtf/WixToolset.Dtf.Resources/ResourceType.cs b/src/dtf/WixToolset.Dtf.Resources/ResourceType.cs
new file mode 100644
index 00000000..4b3971ab
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/ResourceType.cs
@@ -0,0 +1,198 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.Diagnostics.CodeAnalysis;
7 using System.Globalization;
8
9 /// <summary>
10 /// Represents either a standard integer resource type or a custom resource type name.
11 /// </summary>
12 public class ResourceType
13 {
14 // Silence warnings about doc-comments
15 #pragma warning disable 1591
16
17 public static ResourceType None { get { return "#0"; } }
18 public static ResourceType Cursor { get { return "#1"; } }
19 public static ResourceType Bitmap { get { return "#2"; } }
20 public static ResourceType Icon { get { return "#3"; } }
21 public static ResourceType Menu { get { return "#4"; } }
22 public static ResourceType Dialog { get { return "#5"; } }
23 public static ResourceType String { get { return "#6"; } }
24 public static ResourceType FontDir { get { return "#7"; } }
25 public static ResourceType Font { get { return "#8"; } }
26 public static ResourceType Accelerator { get { return "#9"; } }
27 public static ResourceType RCData { get { return "#10"; } }
28 public static ResourceType MessageTable { get { return "#11"; } }
29 public static ResourceType GroupCursor { get { return "#12"; } }
30 public static ResourceType GroupIcon { get { return "#14"; } }
31 public static ResourceType Version { get { return "#16"; } }
32 public static ResourceType DialogInclude { get { return "#17"; } }
33 public static ResourceType PlugPlay { get { return "#19"; } }
34 public static ResourceType Vxd { get { return "#20"; } }
35 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ani")]
36 public static ResourceType AniCursor { get { return "#21"; } }
37 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ani")]
38 public static ResourceType AniIcon { get { return "#22"; } }
39 public static ResourceType Html { get { return "#23"; } }
40 public static ResourceType Manifest { get { return "#24"; } }
41
42 #pragma warning restore 1591
43
44 private string resourceType;
45
46 /// <summary>
47 /// Creates a new resource type from a string resource name.
48 /// </summary>
49 /// <param name="resourceType">String resource name,
50 /// or an integer resource type prefixed by a #.</param>
51 public ResourceType(string resourceType)
52 {
53 if (string.IsNullOrEmpty(resourceType))
54 {
55 throw new ArgumentNullException("resourceType");
56 }
57
58 this.resourceType = resourceType;
59
60 if (this.IsInteger && this.IntegerValue < 0)
61 {
62 throw new ArgumentOutOfRangeException("Invalid integer resource type value.");
63 }
64 }
65
66 /// <summary>
67 /// Creates a new integer resource type.
68 /// </summary>
69 /// <param name="resourceType">Integer value of a well-known resource type.</param>
70 public ResourceType(int resourceType)
71 : this("#" + resourceType)
72 {
73 }
74
75 /// <summary>
76 /// Gets a flag indicating whether the resource type is an integer type.
77 /// </summary>
78 public bool IsInteger
79 {
80 get
81 {
82 return this.resourceType.StartsWith("#", StringComparison.Ordinal);
83 }
84 }
85
86 /// <summary>
87 /// Gets the integer value of the resource type, or -1 if the resource type is not an integer.
88 /// </summary>
89 public int IntegerValue
90 {
91 get
92 {
93 int value;
94 if (!this.IsInteger ||
95 !Int32.TryParse(this.resourceType.Substring(1), out value))
96 {
97 value = -1;
98 }
99
100 return value;
101 }
102 }
103
104 /// <summary>
105 /// Gets a string representation of the resource type.
106 /// </summary>
107 /// <returns>The custom resource name, or the name of a well-known resource type.</returns>
108 public override string ToString()
109 {
110 if (this.IsInteger)
111 {
112 switch (this.IntegerValue)
113 {
114 case 0: return "None";
115 case 1: return "Cursor";
116 case 2: return "Bitmap";
117 case 3: return "Icon";
118 case 4: return "Menu";
119 case 5: return "Dialog";
120 case 6: return "String";
121 case 7: return "FontDir";
122 case 8: return "Font";
123 case 9: return "Accelerator";
124 case 10: return "RCData";
125 case 11: return "MessageTable";
126 case 12: return "GroupCursor";
127 case 14: return "GroupIcon";
128 case 16: return "Version";
129 case 17: return "DialogInclude";
130 case 19: return "PlugPlay";
131 case 20: return "Vxd";
132 case 21: return "AniCursor";
133 case 22: return "AniIcon";
134 case 23: return "Html";
135 case 24: return "Manifest";
136 }
137 }
138
139 return this.resourceType;
140 }
141
142 /// <summary>
143 /// Tests whether one resource type equals another object.
144 /// </summary>
145 /// <param name="obj">Other object.</param>
146 /// <returns>True if equal, else false.</returns>
147 public override bool Equals(object obj)
148 {
149 return this.Equals(obj as ResourceType);
150 }
151
152 /// <summary>
153 /// Tests whether one resource type equals another.
154 /// </summary>
155 /// <param name="otherType">Other resource type.</param>
156 /// <returns>True if equal, else false.</returns>
157 public bool Equals(ResourceType otherType)
158 {
159 return otherType != null && this.resourceType.Equals(otherType.resourceType, StringComparison.Ordinal);
160 }
161
162 /// <summary>
163 /// Gets a hash code suitable for using the resource type as a dictionary key.
164 /// </summary>
165 /// <returns>Hash code based on the resource type string.</returns>
166 public override int GetHashCode()
167 {
168 return this.resourceType.GetHashCode();
169 }
170
171 /// <summary>
172 /// Implicitly converts a string to a ResourceType.
173 /// </summary>
174 /// <param name="resourceType">String resource type to convert.</param>
175 /// <returns>ResourceType object.</returns>
176 public static implicit operator ResourceType(string resourceType)
177 {
178 return new ResourceType(resourceType);
179 }
180
181 /// <summary>
182 /// Explicitly converts a ResourceType to a string.
183 /// </summary>
184 /// <param name="resourceType">ResourceType object to convert.</param>
185 /// <returns>The resource type string.</returns>
186 /// <remarks>
187 /// Unlike <see cref="ToString" />, this conversion does not return
188 /// the common name of well-known integer resource types. Therefore,
189 /// the returned string is suitable for passing directly to Win32
190 /// resource APIs that accept resource type strings.
191 /// </remarks>
192 public static explicit operator string(ResourceType resourceType)
193 {
194 return resourceType.resourceType;
195 }
196 }
197}
198
diff --git a/src/dtf/WixToolset.Dtf.Resources/VersionEnums.cs b/src/dtf/WixToolset.Dtf.Resources/VersionEnums.cs
new file mode 100644
index 00000000..96e54b51
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/VersionEnums.cs
@@ -0,0 +1,86 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 // Silence warnings about doc-comments
6 #pragma warning disable 1591
7
8 using System;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// Identifies build types of a versioned file.
13 /// </summary>
14 [Flags]
15 public enum VersionBuildTypes : int
16 {
17 None = 0x00,
18 Debug = 0x01,
19 Prerelease = 0x02,
20 Patched = 0x04,
21 PrivateBuild = 0x08,
22 InfoInferred = 0x10,
23 SpecialBuild = 0x20,
24 }
25
26 [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")]
27 internal enum VersionFileOS : int
28 {
29 Unknown = 0x00000000,
30 DOS = 0x00010000,
31 OS216 = 0x00020000,
32 OS232 = 0x00030000,
33 NT = 0x00040000,
34 WINCE = 0x00050000,
35 WINDOWS16 = 0x00000001,
36 PM16 = 0x00000002,
37 PM32 = 0x00000003,
38 WINDOWS32 = 0x00000004,
39 DOS_WINDOWS16 = 0x00010001,
40 DOS_WINDOWS32 = 0x00010004,
41 OS216_PM16 = 0x00020002,
42 OS232_PM32 = 0x00030003,
43 NT_WINDOWS32 = 0x00040004,
44 }
45
46 /// <summary>
47 /// Identifies the type of a versioned file.
48 /// </summary>
49 [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")]
50 public enum VersionFileType : int
51 {
52 Unknown = 0,
53 Application = 1,
54 Dll = 2,
55 Driver = 3,
56 Font = 4,
57 VirtualDevice = 5,
58 StaticLibrary = 7,
59 }
60
61 /// <summary>
62 /// Identifies the sub-type of a versioned file.
63 /// </summary>
64 public enum VersionFileSubtype : int
65 {
66 Unknown = 0,
67 PrinterDriver = 1,
68 KeyboardDriver = 2,
69 LanguageDriver = 3,
70 DisplayDriver = 4,
71 MouseDriver = 5,
72 NetworkDriver = 6,
73 SystemDriver = 7,
74 InstallableDriver = 8,
75 SoundDriver = 9,
76 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Comm")]
77 CommDriver = 10,
78 InputMethodDriver = 11,
79 VersionedPrinterDriver = 12,
80 RasterFont = 1,
81 VectorFont = 2,
82 TrueTypeFont = 3,
83 }
84
85 #pragma warning restore 1591
86}
diff --git a/src/dtf/WixToolset.Dtf.Resources/VersionInfo.cs b/src/dtf/WixToolset.Dtf.Resources/VersionInfo.cs
new file mode 100644
index 00000000..38224d12
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/VersionInfo.cs
@@ -0,0 +1,270 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Collections.Generic;
11 using System.Globalization;
12
13 internal class VersionInfo : ICollection<VersionInfo>
14 {
15 private string key;
16 private bool isString;
17 private byte[] data;
18 private List<VersionInfo> children;
19
20 public VersionInfo(string key)
21 : base()
22 {
23 if (key == null)
24 {
25 throw new ArgumentNullException("key");
26 }
27
28 this.key = key;
29 this.children = new List<VersionInfo>();
30 }
31
32 public string Key
33 {
34 get
35 {
36 return this.key;
37 }
38
39 set
40 {
41 if (value == null)
42 {
43 throw new ArgumentNullException("value");
44 }
45
46 this.key = value;
47 }
48 }
49
50 public bool IsString
51 {
52 get
53 {
54 return this.isString;
55 }
56
57 set
58 {
59 this.isString = value;
60 }
61 }
62
63 public byte[] Data
64 {
65 get
66 {
67 return this.data;
68 }
69
70 set
71 {
72 this.data = value;
73 this.isString = false;
74 }
75 }
76
77 public void Read(BinaryReader reader)
78 {
79 long basePosition = reader.BaseStream.Position;
80 int verInfoSize = (int) reader.ReadUInt16();
81 int valueSize = (int) reader.ReadUInt16();
82 bool dataIsString = (reader.ReadUInt16() != 0);
83 StringBuilder keyStringBuilder = new StringBuilder();
84 char c;
85 while ((c = (char) reader.ReadUInt16()) != 0)
86 {
87 keyStringBuilder.Append(c);
88 }
89 this.Key = keyStringBuilder.ToString();
90 Pad(reader, basePosition);
91 if (valueSize == 0)
92 {
93 this.data = null;
94 }
95 else
96 {
97 if (dataIsString) valueSize *= 2; // Count is # of chars instead of bytes
98 this.data = reader.ReadBytes(valueSize);
99 this.isString = dataIsString;
100 Pad(reader, basePosition);
101 }
102
103 while (reader.BaseStream.Position - basePosition < verInfoSize)
104 {
105 Pad(reader, basePosition);
106 VersionInfo childVerInfo = new VersionInfo("");
107 childVerInfo.Read(reader);
108 this.children.Add(childVerInfo);
109 }
110 }
111
112 public void Write(BinaryWriter writer)
113 {
114 long basePosition = writer.BaseStream.Position;
115 writer.Write((ushort) this.Length);
116 byte[] valueBytes = this.data;
117 writer.Write((ushort) ((valueBytes != null ? valueBytes.Length : 0) / (this.IsString ? 2 : 1)));
118 writer.Write((ushort) (this.IsString ? 1 : 0));
119 byte[] keyBytes = new byte[Encoding.Unicode.GetByteCount(this.Key) + 2];
120 Encoding.Unicode.GetBytes(this.Key, 0, this.Key.Length, keyBytes, 0);
121 writer.Write(keyBytes);
122 Pad(writer, basePosition);
123 if (valueBytes != null)
124 {
125 writer.Write(valueBytes);
126 Pad(writer, basePosition);
127 }
128
129 foreach (VersionInfo childVersionInfo in this.children)
130 {
131 Pad(writer, basePosition);
132 childVersionInfo.Write(writer);
133 }
134 }
135
136 private static void Pad(BinaryReader reader, long basePosition)
137 {
138 long position = reader.BaseStream.Position;
139 int diff = (int) (position - basePosition) % 4;
140 if (diff > 0) while (diff++ < 4 && reader.BaseStream.Position < reader.BaseStream.Length) reader.ReadByte();
141 }
142 private static void Pad(BinaryWriter writer, long basePosition)
143 {
144 long position = writer.BaseStream.Position;
145 int diff = (int) (position - basePosition) % 4;
146 if (diff > 0) while (diff++ < 4) writer.Write((byte) 0);
147 }
148
149 private int Length
150 {
151 get
152 {
153 int len = 6 + Encoding.Unicode.GetByteCount(this.Key) + 2;
154 if (len % 4 > 0) len += (4 - len % 4);
155 len += (this.data != null ? this.data.Length : 0);
156 if (len % 4 > 0) len += (4 - len % 4);
157 foreach (VersionInfo childVersionInfo in this.children)
158 {
159 if (len % 4 > 0) len += (4 - len % 4);
160 len += childVersionInfo.Length;
161 }
162 return len;
163 }
164 }
165
166 public static explicit operator VersionInfo(byte[] bytesValue)
167 {
168 VersionInfo viValue = new VersionInfo("");
169 using (BinaryReader reader = new BinaryReader(new MemoryStream(bytesValue, false)))
170 {
171 viValue.Read(reader);
172 }
173 return viValue;
174 }
175
176 public static explicit operator byte[](VersionInfo viValue)
177 {
178 byte[] bytesValue = new byte[viValue.Length];
179 using (BinaryWriter writer = new BinaryWriter(new MemoryStream(bytesValue, true)))
180 {
181 viValue.Write(writer);
182 }
183 return bytesValue;
184 }
185
186 public VersionInfo this[string itemKey]
187 {
188 get
189 {
190 int index = this.IndexOf(itemKey);
191 if (index < 0) return null;
192 return this.children[index];
193 }
194 }
195
196 public void Add(VersionInfo item)
197 {
198 this.children.Add(item);
199 }
200
201 public bool Remove(VersionInfo item)
202 {
203 return this.children.Remove(item);
204 }
205
206 public bool Remove(string itemKey)
207 {
208 int index = this.IndexOf(itemKey);
209 if (index >= 0)
210 {
211 this.children.RemoveAt(index);
212 return true;
213 }
214 else
215 {
216 return false;
217 }
218 }
219
220 private int IndexOf(string itemKey)
221 {
222 for (int i = 0; i < this.children.Count; i++)
223 {
224 if (this.children[i].Key == itemKey) return i;
225 }
226 return -1;
227 }
228
229 public bool Contains(VersionInfo item)
230 {
231 return this.children.Contains(item);
232 }
233
234 public void CopyTo(VersionInfo[] array, int index)
235 {
236 this.children.CopyTo(array, index);
237 }
238
239 public void Clear()
240 {
241 this.children.Clear();
242 }
243
244 public int Count
245 {
246 get
247 {
248 return this.children.Count;
249 }
250 }
251
252 public bool IsReadOnly
253 {
254 get
255 {
256 return false;
257 }
258 }
259
260 public IEnumerator<VersionInfo> GetEnumerator()
261 {
262 return this.children.GetEnumerator();
263 }
264
265 IEnumerator IEnumerable.GetEnumerator()
266 {
267 return this.GetEnumerator();
268 }
269 }
270}
diff --git a/src/dtf/WixToolset.Dtf.Resources/VersionResource.cs b/src/dtf/WixToolset.Dtf.Resources/VersionResource.cs
new file mode 100644
index 00000000..9955fa03
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/VersionResource.cs
@@ -0,0 +1,415 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// A subclass of Resource which provides specific methods for manipulating the resource data.
16 /// </summary>
17 /// <remarks>
18 /// The resource is of type <see cref="ResourceType.Version"/> (RT_VERSION).
19 /// </remarks>
20 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
21 public sealed class VersionResource : Resource, ICollection<VersionStringTable>
22 {
23 internal bool dirty;
24 private VersionInfo rawVersionInfo;
25 private FixedFileVersionInfo rawFileVersionInfo;
26
27 /// <summary>
28 /// Creates a new VersionResource object without any data. The data can be later loaded from a file.
29 /// </summary>
30 /// <param name="name">Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#".</param>
31 /// <param name="locale">Locale of the resource</param>
32 public VersionResource(string name, int locale)
33 : this(name, locale, null)
34 {
35 }
36
37 /// <summary>
38 /// Creates a new VersionResource object with data. The data can be later saved to a file.
39 /// </summary>
40 /// <param name="name">Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#".</param>
41 /// <param name="locale">Locale of the resource</param>
42 /// <param name="data">Raw resource data</param>
43 public VersionResource(string name, int locale, byte[] data)
44 : base(ResourceType.Version, name, locale, data)
45 {
46 this.RefreshVersionInfo(data);
47 }
48
49 /// <summary>
50 /// Gets or sets the raw data of the resource. The data is in the format of the VS_VERSIONINFO structure.
51 /// </summary>
52 public override byte[] Data
53 {
54 get
55 {
56 if (this.dirty)
57 {
58 this.rawVersionInfo.Data = (byte[]) this.rawFileVersionInfo;
59 base.Data = (byte[]) this.rawVersionInfo;
60 this.dirty = false;
61 }
62
63 return base.Data;
64 }
65 set
66 {
67 base.Data = value;
68 this.RefreshVersionInfo(value);
69 this.dirty = false;
70 }
71 }
72
73 private void RefreshVersionInfo(byte[] refreshData)
74 {
75 if (refreshData == null)
76 {
77 this.rawVersionInfo = new VersionInfo("VS_VERSION_INFO");
78 this.rawFileVersionInfo = new FixedFileVersionInfo();
79 }
80 else
81 {
82 this.rawVersionInfo = (VersionInfo) refreshData;
83 this.rawFileVersionInfo = (FixedFileVersionInfo) this.rawVersionInfo.Data;
84 }
85 }
86
87 /// <summary>
88 /// Gets or sets the binary locale-independent file version of the version resource.
89 /// </summary>
90 public Version FileVersion
91 {
92 get
93 {
94 return this.rawFileVersionInfo.FileVersion;
95 }
96 set
97 {
98 this.rawFileVersionInfo.FileVersion = value;
99 this.dirty = true;
100 }
101 }
102
103 /// <summary>
104 /// Gets or sets the binary locale-independent product version of the version resource.
105 /// </summary>
106 public Version ProductVersion
107 {
108 get
109 {
110 return this.rawFileVersionInfo.ProductVersion;
111 }
112 set
113 {
114 this.rawFileVersionInfo.ProductVersion = value;
115 this.dirty = true;
116 }
117 }
118
119 /// <summary>
120 /// Gets or sets a bitmask that specifies the build types of the file.
121 /// </summary>
122 public VersionBuildTypes BuildTypes
123 {
124 get
125 {
126 return this.rawFileVersionInfo.FileFlags &
127 this.rawFileVersionInfo.FileFlagsMask;
128 }
129 set
130 {
131 this.rawFileVersionInfo.FileFlags = value;
132 this.rawFileVersionInfo.FileFlagsMask = value;
133 this.dirty = true;
134 }
135 }
136
137 /// <summary>
138 /// Gets or sets the general type of the file.
139 /// </summary>
140 public VersionFileType FileType
141 {
142 get
143 {
144 return this.rawFileVersionInfo.FileType;
145 }
146 set
147 {
148 this.rawFileVersionInfo.FileType = value;
149 this.dirty = true;
150 }
151 }
152
153 /// <summary>
154 /// Gets or sets the specific type of the file.
155 /// </summary>
156 public VersionFileSubtype FileSubtype
157 {
158 get
159 {
160 return this.rawFileVersionInfo.FileSubtype;
161 }
162 set
163 {
164 this.rawFileVersionInfo.FileSubtype = value;
165 this.dirty = true;
166 }
167 }
168
169 /// <summary>
170 /// Gets or sets the binary creation date and time.
171 /// </summary>
172 public DateTime Timestamp
173 {
174 get
175 {
176 return this.rawFileVersionInfo.Timestamp;
177 }
178 set
179 {
180 this.rawFileVersionInfo.Timestamp = value;
181 this.dirty = true;
182 }
183 }
184
185 /// <summary>
186 /// Gets the string table for a specific locale, or null if there is no table for that locale.
187 /// </summary>
188 /// <seealso cref="Add(int)"/>
189 /// <seealso cref="Remove(int)"/>
190 public VersionStringTable this[int locale]
191 {
192 get
193 {
194 VersionInfo svi = this.rawVersionInfo["StringFileInfo"];
195 if (svi != null)
196 {
197 foreach (VersionInfo strings in svi)
198 {
199 int stringsLocale = UInt16.Parse(strings.Key.Substring(0, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
200 if (stringsLocale == locale)
201 {
202 return new VersionStringTable(this, strings);
203 }
204 }
205 }
206 return null;
207 }
208 }
209
210 /// <summary>
211 /// Adds a new version string table for a locale.
212 /// </summary>
213 /// <param name="locale">Locale of the table</param>
214 /// <returns>The new string table, or the existing table if the locale already existed.</returns>
215 public VersionStringTable Add(int locale)
216 {
217 VersionInfo svi = this.rawVersionInfo["StringFileInfo"];
218 if (svi == null)
219 {
220 svi = new VersionInfo("StringFileInfo");
221 this.rawVersionInfo.Add(svi);
222 }
223 foreach (VersionInfo strings in svi)
224 {
225 int stringsLocale = UInt16.Parse(strings.Key.Substring(0, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
226 if (stringsLocale == locale)
227 {
228 return new VersionStringTable(this, strings);
229 }
230 }
231
232 VersionInfo newStrings = new VersionInfo(
233 ((ushort) locale).ToString("x4", CultureInfo.InvariantCulture) + ((ushort) 1200).ToString("x4", CultureInfo.InvariantCulture));
234 svi.Add(newStrings);
235 this.dirty = true;
236
237 VersionInfo vvi = this.rawVersionInfo["VarFileInfo"];
238 if (vvi == null)
239 {
240 vvi = new VersionInfo("VarFileInfo");
241 vvi.Add(new VersionInfo("Translation"));
242 this.rawVersionInfo.Add(vvi);
243 }
244 VersionInfo tVerInfo = vvi["Translation"];
245 if (tVerInfo != null)
246 {
247 byte[] oldValue = tVerInfo.Data;
248 if (oldValue == null) oldValue = new byte[0];
249 tVerInfo.Data = new byte[oldValue.Length + 4];
250 Array.Copy(oldValue, tVerInfo.Data, oldValue.Length);
251 using (BinaryWriter bw = new BinaryWriter(new MemoryStream(tVerInfo.Data, oldValue.Length, 4, true)))
252 {
253 bw.Write((ushort) locale);
254 bw.Write((ushort) 1200);
255 }
256 }
257
258 return new VersionStringTable(this, newStrings);
259 }
260
261 /// <summary>
262 /// Removes a version string table for a locale.
263 /// </summary>
264 /// <param name="locale">Locale of the table</param>
265 public void Remove(int locale)
266 {
267 VersionInfo svi = this.rawVersionInfo["StringFileInfo"];
268 if (svi != null)
269 {
270 foreach (VersionInfo strings in svi)
271 {
272 int stringsLocale = UInt16.Parse(strings.Key.Substring(0, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
273 if (stringsLocale == locale)
274 {
275 svi.Remove(strings);
276 this.dirty = true;
277 break;
278 }
279 }
280
281 }
282
283 VersionInfo vvi = this.rawVersionInfo["VarFileInfo"];
284 if (vvi != null)
285 {
286 VersionInfo tVerInfo = vvi["Translation"];
287 if (tVerInfo != null)
288 {
289 byte[] newValue = new byte[tVerInfo.Data.Length];
290 int j = 0;
291 using (BinaryWriter bw = new BinaryWriter(new MemoryStream(newValue, 0, newValue.Length, true)))
292 {
293 using (BinaryReader br = new BinaryReader(new MemoryStream(tVerInfo.Data)))
294 {
295 for (int i = tVerInfo.Data.Length / 4; i > 0; i--)
296 {
297 ushort tLocale = br.ReadUInt16();
298 ushort cp = br.ReadUInt16();
299 if (tLocale != locale)
300 {
301 bw.Write((ushort) tLocale);
302 bw.Write((ushort) cp);
303 j++;
304 }
305 }
306 }
307 }
308 tVerInfo.Data = new byte[j * 4];
309 Array.Copy(newValue, tVerInfo.Data, tVerInfo.Data.Length);
310 }
311 }
312 }
313
314 /// <summary>
315 /// Checks if a version string table exists for a given locale.
316 /// </summary>
317 /// <param name="locale">Locale to search for</param>
318 /// <returns>True if a string table was found for the locale; false otherwise.</returns>
319 public bool Contains(int locale)
320 {
321 return this[locale] != null;
322 }
323
324 /// <summary>
325 /// Gets the number string tables in the version resource.
326 /// </summary>
327 public int Count
328 {
329 get
330 {
331 VersionInfo svi = this.rawVersionInfo["StringFileInfo"];
332 return svi != null ? svi.Count : 0;
333 }
334 }
335
336 /// <summary>
337 /// Removes all string tables from the version resource.
338 /// </summary>
339 public void Clear()
340 {
341 VersionInfo svi = this.rawVersionInfo["StringFileInfo"];
342 if (svi != null)
343 {
344 svi.Clear();
345 this.dirty = true;
346 }
347 }
348
349 bool ICollection<VersionStringTable>.IsReadOnly
350 {
351 get
352 {
353 return false;
354 }
355 }
356
357 void ICollection<VersionStringTable>.Add(VersionStringTable item)
358 {
359 throw new NotSupportedException();
360 }
361
362 bool ICollection<VersionStringTable>.Remove(VersionStringTable item)
363 {
364 throw new NotSupportedException();
365 }
366
367 bool ICollection<VersionStringTable>.Contains(VersionStringTable item)
368 {
369 throw new NotSupportedException();
370 }
371
372 /// <summary>
373 /// Copies the version string tables to an array, starting at a particular array index.
374 /// </summary>
375 /// <param name="array">The one-dimensional Array that is the destination of the elements copied
376 /// from the collection. The Array must have zero-based indexing.</param>
377 /// <param name="arrayIndex">The zero-based index in array at which copying begins.</param>
378 public void CopyTo(VersionStringTable[] array, int arrayIndex)
379 {
380 VersionInfo svi = this.rawVersionInfo["StringFileInfo"];
381 if (svi != null)
382 {
383 foreach (VersionInfo strings in svi)
384 {
385 array[arrayIndex++] = new VersionStringTable(this, strings);
386 }
387 }
388 }
389
390 /// <summary>
391 /// Gets an enumerator that can iterate over the version string tables in the collection.
392 /// </summary>
393 /// <returns>An enumerator that returns <see cref="VersionStringTable"/> objects.</returns>
394 public IEnumerator<VersionStringTable> GetEnumerator()
395 {
396 VersionInfo svi = this.rawVersionInfo["StringFileInfo"];
397 if (svi != null)
398 {
399 foreach (VersionInfo strings in svi)
400 {
401 yield return new VersionStringTable(this, strings);
402 }
403 }
404 }
405
406 /// <summary>
407 /// Gets an enumerator that can iterate over the version string tables in the collection.
408 /// </summary>
409 /// <returns>An enumerator that returns <see cref="VersionStringTable"/> objects.</returns>
410 IEnumerator IEnumerable.GetEnumerator()
411 {
412 return this.GetEnumerator();
413 }
414 }
415}
diff --git a/src/dtf/WixToolset.Dtf.Resources/VersionStringTable.cs b/src/dtf/WixToolset.Dtf.Resources/VersionStringTable.cs
new file mode 100644
index 00000000..6aad68c6
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/VersionStringTable.cs
@@ -0,0 +1,231 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Resources
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Collections;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// Represents a string table of a file version resource.
16 /// </summary>
17 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
18 public sealed class VersionStringTable : IDictionary<string, string>
19 {
20 private VersionResource parent;
21 private VersionInfo rawStringVersionInfo;
22
23 internal VersionStringTable(VersionResource parent, VersionInfo rawStringVersionInfo)
24 {
25 this.parent = parent;
26 this.rawStringVersionInfo = rawStringVersionInfo;
27 }
28
29 /// <summary>
30 /// Gets the locale (LCID) of the string table.
31 /// </summary>
32 public int Locale
33 {
34 get
35 {
36 return UInt16.Parse(rawStringVersionInfo.Key.Substring(0, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
37 }
38 set
39 {
40 rawStringVersionInfo.Key = ((ushort) value).ToString("x4", CultureInfo.InvariantCulture) + rawStringVersionInfo.Key.Substring(4, 4);
41 this.parent.dirty = true;
42 }
43 }
44
45 /// <summary>
46 /// Gets or sets a string value.
47 /// </summary>
48 /// <param name="key">Name of the string.</param>
49 public string this[string key]
50 {
51 get
52 {
53 VersionInfo verValue = this.rawStringVersionInfo[key];
54 if (verValue == null)
55 {
56 return null;
57 }
58 else
59 {
60 return Encoding.Unicode.GetString(verValue.Data, 0, verValue.Data.Length - 2);
61 }
62 }
63 set
64 {
65 if (value == null)
66 {
67 rawStringVersionInfo.Remove(key);
68 }
69 else
70 {
71 VersionInfo verValue = rawStringVersionInfo[key];
72 if (verValue == null)
73 {
74 verValue = new VersionInfo(key);
75 verValue.IsString = true;
76 rawStringVersionInfo.Add(verValue);
77 }
78 verValue.Data = new byte[Encoding.Unicode.GetByteCount(value) + 2];
79 Encoding.Unicode.GetBytes(value, 0, value.Length, verValue.Data, 0);
80 }
81 this.parent.dirty = true;
82 }
83 }
84
85 bool ICollection<KeyValuePair<string, string>>.IsReadOnly
86 {
87 get
88 {
89 return false;
90 }
91 }
92
93 bool IDictionary<string, string>.TryGetValue(string key, out string value)
94 {
95 value = this[key];
96 return value != null;
97 }
98
99
100 void ICollection<KeyValuePair<string, string>>.Add(KeyValuePair<string, string> item)
101 {
102 this[item.Key] = item.Value;
103 }
104
105 bool ICollection<KeyValuePair<string, string>>.Remove(KeyValuePair<string, string> item)
106 {
107 string value = this[item.Key];
108 if (value == item.Value)
109 {
110 this[item.Key] = null;
111 return true;
112 }
113 else
114 {
115 return false;
116 }
117 }
118
119 bool ICollection<KeyValuePair<string, string>>.Contains(KeyValuePair<string, string> item)
120 {
121 string value = this[item.Key];
122 if (value == item.Value)
123 {
124 return true;
125 }
126 else
127 {
128 return false;
129 }
130 }
131
132 bool IDictionary<string, string>.ContainsKey(string key)
133 {
134 return this[key] != null;
135 }
136
137 void IDictionary<string, string>.Add(string key, string value)
138 {
139 this[key] = value;
140 }
141
142 bool IDictionary<string, string>.Remove(string key)
143 {
144 if (this[key] != null)
145 {
146 this[key] = null;
147 return true;
148 }
149 else
150 {
151 return false;
152 }
153 }
154
155 /// <summary>
156 /// Removes all strings from the string table.
157 /// </summary>
158 public void Clear()
159 {
160 this.rawStringVersionInfo.Clear();
161 }
162
163 /// <summary>
164 /// Gets a collection of all the names of the strings in the table.
165 /// </summary>
166 public ICollection<string> Keys
167 {
168 get
169 {
170 List<string> keys = new List<string>(this.rawStringVersionInfo.Count);
171 foreach (VersionInfo verValue in this.rawStringVersionInfo)
172 {
173 keys.Add(verValue.Key);
174 }
175 return keys;
176 }
177 }
178
179 /// <summary>
180 /// Gets a collection of all the values in the table.
181 /// </summary>
182 public ICollection<string> Values
183 {
184 get
185 {
186 List<string> values = new List<string>(this.rawStringVersionInfo.Count);
187 foreach (VersionInfo verValue in this.rawStringVersionInfo)
188 {
189 values.Add(Encoding.Unicode.GetString(verValue.Data, 0, verValue.Data.Length - 2));
190 }
191 return values;
192 }
193 }
194
195 /// <summary>
196 /// Gets the number of strings in the table.
197 /// </summary>
198 public int Count
199 {
200 get
201 {
202 return this.rawStringVersionInfo.Count;
203 }
204 }
205
206 void ICollection<KeyValuePair<string, string>>.CopyTo(KeyValuePair<string, string>[] array, int index)
207 {
208 foreach (VersionInfo verValue in this.rawStringVersionInfo)
209 {
210 array[index++] = new KeyValuePair<string, string>(verValue.Key, Encoding.Unicode.GetString(verValue.Data, 0, verValue.Data.Length - 2));
211 }
212 }
213
214 /// <summary>
215 /// Gets an enumeration over all strings in the table.
216 /// </summary>
217 /// <returns>Enumeration of string name and value pairs</returns>
218 public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
219 {
220 foreach (VersionInfo verValue in this.rawStringVersionInfo)
221 {
222 yield return new KeyValuePair<string, string>(verValue.Key, Encoding.Unicode.GetString(verValue.Data, 0, verValue.Data.Length - 2));
223 }
224 }
225
226 IEnumerator IEnumerable.GetEnumerator()
227 {
228 return this.GetEnumerator();
229 }
230 }
231}
diff --git a/src/dtf/WixToolset.Dtf.Resources/WixToolset.Dtf.Resources.csproj b/src/dtf/WixToolset.Dtf.Resources/WixToolset.Dtf.Resources.csproj
new file mode 100644
index 00000000..b5f5e9a2
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.Resources/WixToolset.Dtf.Resources.csproj
@@ -0,0 +1,17 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.Resources</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.Resources</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Classes for reading and writing resource data in executable files</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
15 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
16 </ItemGroup>
17</Project>
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs
new file mode 100644
index 00000000..94abf1dc
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs
@@ -0,0 +1,6 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3using System.Diagnostics.CodeAnalysis;
4
5[assembly: SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "WixToolset.Dtf.WindowsInstaller.Linq.QTable`1.System.Linq.IQueryable<TRecord>.CreateQuery(System.Linq.Expressions.Expression):System.Linq.IQueryable`1<TElement>")]
6[assembly: SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "WixToolset.Dtf.WindowsInstaller.Linq.QTable`1.System.Linq.IQueryable<TRecord>.Execute(System.Linq.Expressions.Expression):TResult")]
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs
new file mode 100644
index 00000000..60008bc8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs
@@ -0,0 +1,60 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6
7 /// <summary>
8 /// Apply to a subclass of QRecord to indicate the name of
9 /// the table the record type is to be used with.
10 /// </summary>
11 /// <remarks>
12 /// If this attribute is not used on a record type, the default
13 /// table name will be derived from the record type name. (An
14 /// optional underscore suffix is stripped.)
15 /// </remarks>
16 [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
17 public class DatabaseTableAttribute : Attribute
18 {
19 /// <summary>
20 /// Creates a new DatabaseTableAttribute for the specified table.
21 /// </summary>
22 /// <param name="table">name of the table associated with the record type</param>
23 public DatabaseTableAttribute(string table)
24 {
25 this.Table = table;
26 }
27
28 /// <summary>
29 /// Gets or sets the table associated with the record type.
30 /// </summary>
31 public string Table { get; set; }
32 }
33
34 /// <summary>
35 /// Apply to a property on a subclass of QRecord to indicate
36 /// the name of the column the property is to be associated with.
37 /// </summary>
38 /// <remarks>
39 /// If this attribute is not used on a property, the default
40 /// column name will be the same as the property name.
41 /// </remarks>
42 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
43 public class DatabaseColumnAttribute : Attribute
44 {
45 /// <summary>
46 /// Creates a new DatabaseColumnAttribute which maps a
47 /// record property to a column.
48 /// </summary>
49 /// <param name="column">name of the column associated with the property</param>
50 public DatabaseColumnAttribute(string column)
51 {
52 this.Column = column;
53 }
54
55 /// <summary>
56 /// Gets or sets the column associated with the record property.
57 /// </summary>
58 public string Column { get; set; }
59 }
60}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs
new file mode 100644
index 00000000..1c51b861
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs
@@ -0,0 +1,150 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Linq.Entities
4{
5 // Silence warnings about style and doc-comments
6 #if !CODE_ANALYSIS
7 #pragma warning disable 1591
8 #region Generated code
9
10 public class Component_ : QRecord
11 {
12 public string Component { get { return this[0]; } set { this[0] = value; } }
13 public string ComponentId { get { return this[1]; } set { this[1] = value; } }
14 public string Directory_ { get { return this[2]; } set { this[2] = value; } }
15 public string Condition { get { return this[4]; } set { this[4] = value; } }
16 public string KeyPath { get { return this[5]; } set { this[5] = value; } }
17 public ComponentAttributes Attributes
18 { get { return (ComponentAttributes) this.I(3); } set { this[3] = ((int) value).ToString(); } }
19 }
20
21 public class CreateFolder_ : QRecord
22 {
23 public string Directory_ { get { return this[0]; } set { this[0] = value; } }
24 public string Component_ { get { return this[1]; } set { this[1] = value; } }
25 }
26
27 public class CustomAction_ : QRecord
28 {
29 public string Action { get { return this[0]; } set { this[0] = value; } }
30 public string Source { get { return this[2]; } set { this[2] = value; } }
31 public string Target { get { return this[3]; } set { this[3] = value; } }
32 public CustomActionTypes Type
33 { get { return (CustomActionTypes) this.I(1); } set { this[1] = ((int) value).ToString(); } }
34 }
35
36 public class Directory_ : QRecord
37 {
38 public string Directory { get { return this[0]; } set { this[0] = value; } }
39 public string Directory_Parent { get { return this[1]; } set { this[1] = value; } }
40 public string DefaultDir { get { return this[2]; } set { this[2] = value; } }
41 }
42
43 public class DuplicateFile_ : QRecord
44 {
45 public string FileKey { get { return this[0]; } set { this[0] = value; } }
46 public string Component_ { get { return this[1]; } set { this[1] = value; } }
47 public string File_ { get { return this[2]; } set { this[2] = value; } }
48 public string DestName { get { return this[4]; } set { this[4] = value; } }
49 public string DestFolder { get { return this[5]; } set { this[5] = value; } }
50 }
51
52 public class Feature_ : QRecord
53 {
54 public string Feature { get { return this[0]; } set { this[0] = value; } }
55 public string Feature_Parent { get { return this[1]; } set { this[1] = value; } }
56 public string Title { get { return this[2]; } set { this[2] = value; } }
57 public string Description { get { return this[3]; } set { this[3] = value; } }
58 public int? Display { get { return this.NI(4); } set { this[4] = value.ToString(); } }
59 public int Level { get { return this.I(5); } set { this[5] = value.ToString(); } }
60 public string Directory_ { get { return this[6]; } set { this[6] = value; } }
61 public FeatureAttributes Attributes
62 { get { return (FeatureAttributes) this.I(7); } set { this[7] = ((int) value).ToString(); } }
63 }
64
65 [DatabaseTable("FeatureComponents")]
66 public class FeatureComponent_ : QRecord
67 {
68 public string Feature_ { get { return this[0]; } set { this[0] = value; } }
69 public string Component_ { get { return this[1]; } set { this[1] = value; } }
70 }
71
72 public class File_ : QRecord
73 {
74 public string File { get { return this[0]; } set { this[0] = value; } }
75 public string Component_ { get { return this[1]; } set { this[1] = value; } }
76 public string FileName { get { return this[2]; } set { this[2] = value; } }
77 public int FileSize { get { return this.I(3); } set { this[3] = value.ToString(); } }
78 public string Version { get { return this[4]; } set { this[4] = value; } }
79 public string Language { get { return this[5]; } set { this[5] = value; } }
80 public int Sequence { get { return this.I(7); } set { this[7] = value.ToString(); } }
81 public FileAttributes Attributes
82 { get { return (FileAttributes) this.I(6); } set { this[6] = ((int) value).ToString(); } }
83 }
84
85 [DatabaseTable("MsiFileHash")]
86 public class FileHash_ : QRecord
87 {
88 public string File_ { get { return this[0]; } set { this[0] = value; } }
89 public int Options { get { return this.I(1); } set { this[1] = value.ToString(); } }
90 public int HashPart1 { get { return this.I(2); } set { this[2] = value.ToString(); } }
91 public int HashPart2 { get { return this.I(3); } set { this[3] = value.ToString(); } }
92 public int HashPart3 { get { return this.I(4); } set { this[4] = value.ToString(); } }
93 public int HashPart4 { get { return this.I(5); } set { this[5] = value.ToString(); } }
94 }
95
96 [DatabaseTable("InstallExecuteSequence")]
97 public class InstallSequence_ : QRecord
98 {
99 public string Action { get { return this[0]; } set { this[0] = value; } }
100 public string Condition { get { return this[1]; } set { this[1] = value; } }
101 public int Sequence { get { return this.I(2); } set { this[2] = value.ToString(); } }
102 }
103
104 public class LaunchCondition_ : QRecord
105 {
106 public string Condition { get { return this[0]; } set { this[0] = value; } }
107 public string Description { get { return this[1]; } set { this[1] = value; } }
108 }
109
110 public class Media_ : QRecord
111 {
112 public int DiskId { get { return this.I(0); } set { this[0] = value.ToString(); } }
113 public int LastSequence { get { return this.I(1); } set { this[1] = value.ToString(); } }
114 public string DiskPrompt { get { return this[2]; } set { this[2] = value; } }
115 public string Cabinet { get { return this[3]; } set { this[3] = value; } }
116 public string VolumeLabel { get { return this[4]; } set { this[4] = value; } }
117 public string Source { get { return this[5]; } set { this[5] = value; } }
118 }
119
120 public class Property_ : QRecord
121 {
122 public string Property { get { return this[0]; } set { this[0] = value; } }
123 public string Value { get { return this[1]; } set { this[1] = value; } }
124 }
125
126 public class Registry_ : QRecord
127 {
128 public string Registry { get { return this[0]; } set { this[0] = value; } }
129 public string Key { get { return this[2]; } set { this[2] = value; } }
130 public string Name { get { return this[3]; } set { this[3] = value; } }
131 public string Value { get { return this[4]; } set { this[4] = value; } }
132 public string Component_ { get { return this[5]; } set { this[5] = value; } }
133 public RegistryRoot Root
134 { get { return (RegistryRoot) this.I(1); } set { this[0] = ((int) value).ToString(); } }
135 }
136
137 public class RemoveFile_ : QRecord
138 {
139 public string FileKey { get { return this[0]; } set { this[0] = value; } }
140 public string Component_ { get { return this[2]; } set { this[2] = value; } }
141 public string FileName { get { return this[3]; } set { this[3] = value; } }
142 public string DirProperty { get { return this[4]; } set { this[4] = value; } }
143 public RemoveFileModes InstallMode
144 { get { return (RemoveFileModes) this.I(5); } set { this[5] = ((int) value).ToString(); } }
145 }
146
147 #endregion // Generated code
148 #pragma warning restore 1591
149 #endif // !CODE_ANALYSIS
150}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs
new file mode 100644
index 00000000..b4de2f60
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs
@@ -0,0 +1,214 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6 using System.IO;
7 using WixToolset.Dtf.WindowsInstaller.Linq.Entities;
8
9 /// <summary>
10 /// Allows any Database instance to be converted into a queryable database.
11 /// </summary>
12 public static class Queryable
13 {
14 /// <summary>
15 /// Converts any Database instance into a queryable database.
16 /// </summary>
17 /// <param name="db"></param>
18 /// <returns>Queryable database instance that operates on the same
19 /// MSI handle.</returns>
20 /// <remarks>
21 /// This extension method is meant for convenient on-the-fly conversion.
22 /// If the existing database instance already happens to be a QDatabase,
23 /// then it is returned unchanged. Otherwise since the new database
24 /// carries the same MSI handle, only one of the instances needs to be
25 /// closed, not both.
26 /// </remarks>
27 public static QDatabase AsQueryable(this Database db)
28 {
29 QDatabase qdb = db as QDatabase;
30 if (qdb == null && db != null)
31 {
32 qdb = new QDatabase(db.Handle, true, db.FilePath, db.OpenMode);
33 }
34 return qdb;
35 }
36 }
37
38 /// <summary>
39 /// Queryable MSI database - extends the base Database class with
40 /// LINQ query functionality along with predefined entity types
41 /// for common tables.
42 /// </summary>
43 public class QDatabase : Database
44 {
45 /// <summary>
46 /// Opens an existing database in read-only mode.
47 /// </summary>
48 /// <param name="filePath">Path to the database file.</param>
49 /// <exception cref="InstallerException">the database could not be created/opened</exception>
50 /// <remarks>
51 /// Because this constructor initiates database access, it cannot be used with a
52 /// running installation.
53 /// <para>The Database object should be <see cref="InstallerHandle.Close"/>d after use.
54 /// The finalizer will close the handle if it is still open, however due to the nondeterministic
55 /// nature of finalization it is best that the handle be closed manually as soon as it is no
56 /// longer needed, as leaving lots of unused handles open can degrade performance.</para>
57 /// </remarks>
58 public QDatabase(string filePath)
59 : base(filePath)
60 {
61 }
62
63 /// <summary>
64 /// Opens an existing database with another database as output.
65 /// </summary>
66 /// <param name="filePath">Path to the database to be read.</param>
67 /// <param name="outputPath">Open mode for the database</param>
68 /// <returns>Database object representing the created or opened database</returns>
69 /// <exception cref="InstallerException">the database could not be created/opened</exception>
70 /// <remarks>
71 /// When a database is opened as the output of another database, the summary information stream
72 /// of the output database is actually a read-only mirror of the original database and thus cannot
73 /// be changed. Additionally, it is not persisted with the database. To create or modify the
74 /// summary information for the output database it must be closed and re-opened.
75 /// <para>The returned Database object should be <see cref="InstallerHandle.Close"/>d after use.
76 /// The finalizer will close the handle if it is still open, however due to the nondeterministic
77 /// nature of finalization it is best that the handle be closed manually as soon as it is no
78 /// longer needed, as leaving lots of unused handles open can degrade performance.</para>
79 /// </remarks>
80 public QDatabase(string filePath, string outputPath)
81 : base(filePath, outputPath)
82 {
83 }
84
85 /// <summary>
86 /// Opens an existing database or creates a new one.
87 /// </summary>
88 /// <param name="filePath">Path to the database file. If an empty string
89 /// is supplied, a temporary database is created that is not persisted.</param>
90 /// <param name="mode">Open mode for the database</param>
91 /// <exception cref="InstallerException">the database could not be created/opened</exception>
92 /// <remarks>
93 /// To make and save changes to a database first open the database in transaction,
94 /// create or, or direct mode. After making the changes, always call the Commit method
95 /// before closing the database handle. The Commit method flushes all buffers.
96 /// <para>Always call the Commit method on a database that has been opened in direct
97 /// mode before closing the database. Failure to do this may corrupt the database.</para>
98 /// <para>Because this constructor initiates database access, it cannot be used with a
99 /// running installation.</para>
100 /// <para>The Database object should be <see cref="InstallerHandle.Close"/>d after use.
101 /// The finalizer will close the handle if it is still open, however due to the nondeterministic
102 /// nature of finalization it is best that the handle be closed manually as soon as it is no
103 /// longer needed, as leaving lots of unused handles open can degrade performance.</para>
104 /// </remarks>
105 public QDatabase(string filePath, DatabaseOpenMode mode)
106 : base(filePath, mode)
107 {
108 }
109
110 /// <summary>
111 /// Creates a new database from an MSI handle.
112 /// </summary>
113 /// <param name="handle">Native MSI database handle.</param>
114 /// <param name="ownsHandle">True if the handle should be closed
115 /// when the database object is disposed</param>
116 /// <param name="filePath">Path of the database file, if known</param>
117 /// <param name="openMode">Mode the handle was originally opened in</param>
118 protected internal QDatabase(
119 IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode)
120 : base(handle, ownsHandle, filePath, openMode)
121 {
122 }
123
124 /// <summary>
125 /// Gets or sets a log where all MSI SQL queries are written.
126 /// </summary>
127 /// <remarks>
128 /// The log can be useful for debugging, or simply to watch the LINQ magic in action.
129 /// </remarks>
130 public TextWriter Log { get; set; }
131
132 /// <summary>
133 /// Gets a queryable table from the datbaase.
134 /// </summary>
135 /// <param name="table">name of the table</param>
136 public QTable<QRecord> this[string table]
137 {
138 get
139 {
140 return new QTable<QRecord>(this, table);
141 }
142 }
143
144 #if !CODE_ANALYSIS
145 #region Queryable tables
146
147 /// <summary>Queryable standard table with predefined specialized record type.</summary>
148 public QTable<Component_> Components
149 { get { return new QTable<Component_>(this); } }
150
151 /// <summary>Queryable standard table with predefined specialized record type.</summary>
152 public QTable<CreateFolder_> CreateFolders
153 { get { return new QTable<CreateFolder_>(this); } }
154
155 /// <summary>Queryable standard table with predefined specialized record type.</summary>
156 public QTable<CustomAction_> CustomActions
157 { get { return new QTable<CustomAction_>(this); } }
158
159 /// <summary>Queryable standard table with predefined specialized record type.</summary>
160 public QTable<Directory_> Directories
161 { get { return new QTable<Directory_>(this); } }
162
163 /// <summary>Queryable standard table with predefined specialized record type.</summary>
164 public QTable<DuplicateFile_> DuplicateFiles
165 { get { return new QTable<DuplicateFile_>(this); } }
166
167 /// <summary>Queryable standard table with predefined specialized record type.</summary>
168 public QTable<Feature_> Features
169 { get { return new QTable<Feature_>(this); } }
170
171 /// <summary>Queryable standard table with predefined specialized record type.</summary>
172 public QTable<FeatureComponent_> FeatureComponents
173 { get { return new QTable<FeatureComponent_>(this); } }
174
175 /// <summary>Queryable standard table with predefined specialized record type.</summary>
176 public QTable<File_> Files
177 { get { return new QTable<File_>(this); } }
178
179 /// <summary>Queryable standard table with predefined specialized record type.</summary>
180 public QTable<FileHash_> FileHashes
181 { get { return new QTable<FileHash_>(this); } }
182
183 /// <summary>Queryable standard table with predefined specialized record type.</summary>
184 public QTable<InstallSequence_> InstallExecuteSequences
185 { get { return new QTable<InstallSequence_>(this, "InstallExecuteSequence"); } }
186
187 /// <summary>Queryable standard table with predefined specialized record type.</summary>
188 public QTable<InstallSequence_> InstallUISequences
189 { get { return new QTable<InstallSequence_>(this, "InstallUISequence"); } }
190
191 /// <summary>Queryable standard table with predefined specialized record type.</summary>
192 public QTable<LaunchCondition_> LaunchConditions
193 { get { return new QTable<LaunchCondition_>(this); } }
194
195 /// <summary>Queryable standard table with predefined specialized record type.</summary>
196 public QTable<Media_> Medias
197 { get { return new QTable<Media_>(this); } }
198
199 /// <summary>Queryable standard table with predefined specialized record type.</summary>
200 public QTable<Property_> Properties
201 { get { return new QTable<Property_>(this); } }
202
203 /// <summary>Queryable standard table with predefined specialized record type.</summary>
204 public QTable<Registry_> Registries
205 { get { return new QTable<Registry_>(this); } }
206
207 /// <summary>Queryable standard table with predefined specialized record type.</summary>
208 public QTable<RemoveFile_> RemoveFiles
209 { get { return new QTable<RemoveFile_>(this); } }
210
211 #endregion // Queryable tables
212 #endif // !CODE_ANALYSIS
213 }
214}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs
new file mode 100644
index 00000000..4b3145fd
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs
@@ -0,0 +1,501 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Globalization;
9 using System.Collections.Generic;
10 using System.Diagnostics.CodeAnalysis;
11
12 /// <summary>
13 /// Generic record entity for queryable databases,
14 /// and base for strongly-typed entity subclasses.
15 /// </summary>
16 /// <remarks>
17 /// Several predefined specialized subclasses are provided for common
18 /// standard tables. Subclasses for additional standard tables
19 /// or custom tables are not necessary, but they are easy to create
20 /// and make the coding experience much nicer.
21 /// <para>When creating subclasses, the following attributes may be
22 /// useful: <see cref="DatabaseTableAttribute"/>,
23 /// <see cref="DatabaseColumnAttribute"/></para>
24 /// </remarks>
25 public class QRecord
26 {
27 /// <summary>
28 /// Do not call. Use QTable.NewRecord() instead.
29 /// </summary>
30 /// <remarks>
31 /// Subclasses must also provide a public parameterless constructor.
32 /// <para>QRecord constructors are only public due to implementation
33 /// reasons (to satisfy the new() constraint on the QTable generic
34 /// class). They are not intended to be called by user code other than
35 /// a subclass constructor. If the constructor is invoked directly,
36 /// the record instance will not be properly initialized (associated
37 /// with a database table) and calls to methods on the instance
38 /// will throw a NullReferenceException.</para>
39 /// </remarks>
40 /// <seealso cref="QTable&lt;TRecord&gt;.NewRecord()"/>
41 public QRecord()
42 {
43 }
44
45 internal QDatabase Database { get; set; }
46
47 internal TableInfo TableInfo { get; set; }
48
49 internal IList<string> Values { get; set; }
50
51 internal bool Exists { get; set; }
52
53 /// <summary>
54 /// Gets the number of fields in the record.
55 /// </summary>
56 public int FieldCount
57 {
58 get
59 {
60 return this.Values.Count;
61 }
62 }
63
64 /// <summary>
65 /// Gets or sets a record field.
66 /// </summary>
67 /// <param name="field">column name of the field</param>
68 /// <remarks>
69 /// Setting a field value will automatically update the database.
70 /// </remarks>
71 public string this[string field]
72 {
73 get
74 {
75 if (field == null)
76 {
77 throw new ArgumentNullException("field");
78 }
79
80 int index = this.TableInfo.Columns.IndexOf(field);
81 if (index < 0)
82 {
83 throw new ArgumentOutOfRangeException("field");
84 }
85
86 return this[index];
87 }
88
89 set
90 {
91 if (field == null)
92 {
93 throw new ArgumentNullException("field");
94 }
95
96 this.Update(new string[] { field }, new string[] { value });
97 }
98 }
99
100 /// <summary>
101 /// Gets or sets a record field.
102 /// </summary>
103 /// <param name="index">zero-based column index of the field</param>
104 /// <remarks>
105 /// Setting a field value will automatically update the database.
106 /// </remarks>
107 public string this[int index]
108 {
109 get
110 {
111 if (index < 0 || index >= this.FieldCount)
112 {
113 throw new ArgumentOutOfRangeException("index");
114 }
115
116 return this.Values[index];
117 }
118
119 set
120 {
121 if (index < 0 || index >= this.FieldCount)
122 {
123 throw new ArgumentOutOfRangeException("index");
124 }
125
126 this.Update(new int[] { index }, new string[] { value });
127 }
128 }
129
130 /// <summary>
131 /// Used by subclasses to get a field as an integer.
132 /// </summary>
133 /// <param name="index">zero-based column index of the field</param>
134 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "I")]
135 protected int I(int index)
136 {
137 string value = this[index];
138 return value.Length > 0 ?
139 Int32.Parse(value, CultureInfo.InvariantCulture) : 0;
140 }
141
142 /// <summary>
143 /// Used by subclasses to get a field as a nullable integer.
144 /// </summary>
145 /// <param name="index">zero-based column index of the field</param>
146 protected int? NI(int index)
147 {
148 string value = this[index];
149 return value.Length > 0 ?
150 new int?(Int32.Parse(value, CultureInfo.InvariantCulture)) : null;
151 }
152
153 /// <summary>
154 /// Dumps all record fields to a string.
155 /// </summary>
156 public override string ToString()
157 {
158 StringBuilder buf = new StringBuilder(this.GetType().Name);
159 buf.Append(" {");
160 for (int i = 0; i < this.FieldCount; i++)
161 {
162 buf.AppendFormat("{0} {1} = {2}",
163 (i > 0 ? "," : String.Empty),
164 this.TableInfo.Columns[i].Name,
165 this[i]);
166 }
167 buf.Append(" }");
168 return buf.ToString();
169 }
170
171 /// <summary>
172 /// Update multiple fields in the record (and the database).
173 /// </summary>
174 /// <param name="fields">column names of fields to update</param>
175 /// <param name="values">new values for each field being updated</param>
176 public void Update(IList<string> fields, IList<string> values)
177 {
178 if (fields == null)
179 {
180 throw new ArgumentNullException("fields");
181 }
182
183 if (values == null)
184 {
185 throw new ArgumentNullException("values");
186 }
187
188 if (fields.Count == 0 || values.Count == 0 ||
189 fields.Count > this.FieldCount ||
190 values.Count != fields.Count)
191 {
192 throw new ArgumentOutOfRangeException("fields");
193 }
194
195 int[] indexes = new int[fields.Count];
196 for (int i = 0; i < indexes.Length; i++)
197 {
198 if (fields[i] == null)
199 {
200 throw new ArgumentNullException("fields[" + i + "]");
201 }
202
203 indexes[i] = this.TableInfo.Columns.IndexOf(fields[i]);
204
205 if (indexes[i] < 0)
206 {
207 throw new ArgumentOutOfRangeException("fields[" + i + "]");
208 }
209 }
210
211 this.Update(indexes, values);
212 }
213
214 /// <summary>
215 /// Update multiple fields in the record (and the database).
216 /// </summary>
217 /// <param name="indexes">column indexes of fields to update</param>
218 /// <param name="values">new values for each field being updated</param>
219 /// <remarks>
220 /// The record (primary keys) must already exist in the table.
221 /// <para>Updating primary key fields is not yet implemented; use Delete()
222 /// and Insert() instead.</para>
223 /// </remarks>
224 public void Update(IList<int> indexes, IList<string> values)
225 {
226 if (indexes == null)
227 {
228 throw new ArgumentNullException("indexes");
229 }
230
231 if (values == null)
232 {
233 throw new ArgumentNullException("values");
234 }
235
236 if (indexes.Count == 0 || values.Count == 0 ||
237 indexes.Count > this.FieldCount ||
238 values.Count != indexes.Count)
239 {
240 throw new ArgumentOutOfRangeException("indexes");
241 }
242
243 bool primaryKeyChanged = false;
244 for (int i = 0; i < indexes.Count; i++)
245 {
246 int index = indexes[i];
247 if (index < 0 || index >= this.FieldCount)
248 {
249 throw new ArgumentOutOfRangeException("index[" + i + "]");
250 }
251
252 ColumnInfo col = this.TableInfo.Columns[index];
253 if (this.TableInfo.PrimaryKeys.Contains(col.Name))
254 {
255 if (values[i] == null)
256 {
257 throw new ArgumentNullException("values[" + i + "]");
258 }
259
260 primaryKeyChanged = true;
261 }
262 else if (values[i] == null)
263 {
264 if (col.IsRequired)
265 {
266 throw new ArgumentNullException("values[" + i + "]");
267 }
268 }
269
270 this.Values[index] = values[i];
271 }
272
273 if (this.Exists)
274 {
275 if (!primaryKeyChanged)
276 {
277 int updateRecSize = indexes.Count + this.TableInfo.PrimaryKeys.Count;
278 using (Record updateRec = this.Database.CreateRecord(updateRecSize))
279 {
280 StringBuilder s = new StringBuilder("UPDATE `");
281 s.Append(this.TableInfo.Name);
282 s.Append("` SET");
283
284 for (int i = 0; i < indexes.Count; i++)
285 {
286 ColumnInfo col = this.TableInfo.Columns[indexes[i]];
287 if (col.Type == typeof(Stream))
288 {
289 throw new NotSupportedException(
290 "Cannot update stream columns via QRecord.");
291 }
292
293 int index = indexes[i];
294 s.AppendFormat("{0} `{1}` = ?",
295 (i > 0 ? "," : String.Empty),
296 col.Name);
297
298 if (values[i] != null)
299 {
300 updateRec[i + 1] = values[i];
301 }
302 }
303
304 for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++)
305 {
306 string key = this.TableInfo.PrimaryKeys[i];
307 s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key);
308 int index = this.TableInfo.Columns.IndexOf(key);
309 updateRec[indexes.Count + i + 1] = this.Values[index];
310
311 }
312
313 string updateSql = s.ToString();
314 TextWriter log = this.Database.Log;
315 if (log != null)
316 {
317 log.WriteLine();
318 log.WriteLine(updateSql);
319 for (int field = 1; field <= updateRecSize; field++)
320 {
321 log.WriteLine(" ? = " + updateRec.GetString(field));
322 }
323 }
324
325 this.Database.Execute(updateSql, updateRec);
326 }
327 }
328 else
329 {
330 throw new NotImplementedException(
331 "Update() cannot handle changed primary keys yet.");
332 // TODO:
333 // query using old values
334 // update values
335 // View.Replace
336 }
337 }
338 }
339
340 /// <summary>
341 /// Inserts the record in the database.
342 /// </summary>
343 /// <remarks>
344 /// The record (primary keys) may not already exist in the table.
345 /// <para>Use <see cref="QTable&lt;TRecord&gt;.NewRecord()"/> to get a new
346 /// record. Prmary keys and all required fields
347 /// must be filled in before insertion.</para>
348 /// </remarks>
349 public void Insert()
350 {
351 this.Insert(false);
352 }
353
354 /// <summary>
355 /// Inserts the record into the table.
356 /// </summary>
357 /// <param name="temporary">true if the record is temporarily
358 /// inserted, to be visible only as long as the database is open</param>
359 /// <remarks>
360 /// The record (primary keys) may not already exist in the table.
361 /// <para>Use <see cref="QTable&lt;TRecord&gt;.NewRecord()"/> to get a new
362 /// record. Prmary keys and all required fields
363 /// must be filled in before insertion.</para>
364 /// </remarks>
365 public void Insert(bool temporary)
366 {
367 using (Record updateRec = this.Database.CreateRecord(this.FieldCount))
368 {
369 string insertSql = this.TableInfo.SqlInsertString;
370 if (temporary)
371 {
372 insertSql += " TEMPORARY";
373 }
374
375 TextWriter log = this.Database.Log;
376 if (log != null)
377 {
378 log.WriteLine();
379 log.WriteLine(insertSql);
380 }
381
382 for (int index = 0; index < this.FieldCount; index++)
383 {
384 ColumnInfo col = this.TableInfo.Columns[index];
385 if (col.Type == typeof(Stream))
386 {
387 throw new NotSupportedException(
388 "Cannot insert stream columns via QRecord.");
389 }
390
391 if (this.Values[index] != null)
392 {
393 updateRec[index + 1] = this.Values[index];
394 }
395
396 if (log != null)
397 {
398 log.WriteLine(" ? = " + this.Values[index]);
399 }
400 }
401
402 this.Database.Execute(insertSql, updateRec);
403 this.Exists = true;
404 }
405 }
406
407 /// <summary>
408 /// Deletes the record from the table if it exists.
409 /// </summary>
410 public void Delete()
411 {
412 using (Record keyRec = this.Database.CreateRecord(this.TableInfo.PrimaryKeys.Count))
413 {
414 StringBuilder s = new StringBuilder("DELETE FROM `");
415 s.Append(this.TableInfo.Name);
416 s.Append("`");
417 for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++)
418 {
419 string key = this.TableInfo.PrimaryKeys[i];
420 s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key);
421 int index = this.TableInfo.Columns.IndexOf(key);
422 keyRec[i + 1] = this.Values[index];
423 }
424
425 string deleteSql = s.ToString();
426
427 TextWriter log = this.Database.Log;
428 if (log != null)
429 {
430 log.WriteLine();
431 log.WriteLine(deleteSql);
432
433 for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++)
434 {
435 log.WriteLine(" ? = " + keyRec.GetString(i + 1));
436 }
437 }
438
439 this.Database.Execute(deleteSql, keyRec);
440 this.Exists = false;
441 }
442 }
443
444 /// <summary>
445 /// Not yet implemented.
446 /// </summary>
447 public void Refresh()
448 {
449 throw new NotImplementedException();
450 }
451
452 /// <summary>
453 /// Not yet implemented.
454 /// </summary>
455 public void Assign()
456 {
457 throw new NotImplementedException();
458 }
459
460 /// <summary>
461 /// Not yet implemented.
462 /// </summary>
463 public bool Merge()
464 {
465 throw new NotImplementedException();
466 }
467
468 /// <summary>
469 /// Not yet implemented.
470 /// </summary>
471 public ICollection<ValidationErrorInfo> Validate()
472 {
473 throw new NotImplementedException();
474 }
475
476 /// <summary>
477 /// Not yet implemented.
478 /// </summary>
479 [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
480 public ICollection<ValidationErrorInfo> ValidateNew()
481 {
482 throw new NotImplementedException();
483 }
484
485 /// <summary>
486 /// Not yet implemented.
487 /// </summary>
488 public ICollection<ValidationErrorInfo> ValidateFields()
489 {
490 throw new NotImplementedException();
491 }
492
493 /// <summary>
494 /// Not yet implemented.
495 /// </summary>
496 public ICollection<ValidationErrorInfo> ValidateDelete()
497 {
498 throw new NotImplementedException();
499 }
500 }
501}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs
new file mode 100644
index 00000000..e0e1c154
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs
@@ -0,0 +1,296 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6 using System.IO;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.Reflection;
10 using System.Linq;
11 using System.Linq.Expressions;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// Represents one table in a LINQ-queryable Database.
16 /// </summary>
17 /// <typeparam name="TRecord">type that represents one record in the table</typeparam>
18 /// <remarks>
19 /// This class is the primary gateway to all LINQ to MSI query functionality.
20 /// <para>The TRecord generic parameter may be the general <see cref="QRecord" />
21 /// class, or a specialized subclass of QRecord.</para>
22 /// </remarks>
23 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
24 public sealed class QTable<TRecord> : IOrderedQueryable<TRecord>, IQueryProvider
25 where TRecord : QRecord, new()
26 {
27 private QDatabase db;
28 private TableInfo tableInfo;
29
30 /// <summary>
31 /// Infers the name of the table this instance will be
32 /// associated with.
33 /// </summary>
34 /// <returns>table name</returns>
35 /// <remarks>
36 /// The table name is retrieved from a DatabaseTableAttribute
37 /// on the record type if it exists; otherwise the name is
38 /// derived from the name of the record type itself.
39 /// (An optional underscore suffix on the record type name is dropped.)
40 /// </remarks>
41 private static string InferTableName()
42 {
43 foreach (DatabaseTableAttribute attr in typeof(TRecord).GetCustomAttributes(
44 typeof(DatabaseTableAttribute), false))
45 {
46 string tableName = attr.Table;
47 if (!String.IsNullOrEmpty(tableName))
48 {
49 return tableName;
50 }
51 }
52
53 string recordTypeName = typeof(TRecord).Name;
54 if (recordTypeName[recordTypeName.Length - 1] == '_')
55 {
56 return recordTypeName.Substring(0, recordTypeName.Length - 1);
57 }
58 else
59 {
60 return recordTypeName;
61 }
62 }
63
64 /// <summary>
65 /// Creates a new QTable, inferring the table name
66 /// from the name of the record type parameter.
67 /// </summary>
68 /// <param name="db">database that contains the table</param>
69 public QTable(QDatabase db)
70 : this(db, InferTableName())
71 {
72 }
73
74 /// <summary>
75 /// Creates a new QTable with an explicit table name.
76 /// </summary>
77 /// <param name="db">database that contains the table</param>
78 /// <param name="table">name of the table</param>
79 public QTable(QDatabase db, string table)
80 {
81 if (db == null)
82 {
83 throw new ArgumentNullException("db");
84 }
85
86 if (String.IsNullOrEmpty(table))
87 {
88 throw new ArgumentNullException("table");
89 }
90
91 this.db = db;
92 this.tableInfo = db.Tables[table];
93 if (this.tableInfo == null)
94 {
95 throw new ArgumentException(
96 "Table does not exist in database: " + table);
97 }
98 }
99
100 /// <summary>
101 /// Gets schema information about the table.
102 /// </summary>
103 public TableInfo TableInfo
104 {
105 get
106 {
107 return this.tableInfo;
108 }
109 }
110
111 /// <summary>
112 /// Gets the database this table is associated with.
113 /// </summary>
114 public QDatabase Database
115 {
116 get
117 {
118 return this.db;
119 }
120 }
121
122 /// <summary>
123 /// Enumerates over all records in the table.
124 /// </summary>
125 /// <returns></returns>
126 public IEnumerator<TRecord> GetEnumerator()
127 {
128 string query = this.tableInfo.SqlSelectString;
129
130 TextWriter log = this.db.Log;
131 if (log != null)
132 {
133 log.WriteLine();
134 log.WriteLine(query);
135 }
136
137 using (View view = db.OpenView(query))
138 {
139 view.Execute();
140
141 ColumnCollection columns = this.tableInfo.Columns;
142 int columnCount = columns.Count;
143 bool[] isBinary = new bool[columnCount];
144
145 for (int i = 0; i < isBinary.Length; i++)
146 {
147 isBinary[i] = columns[i].Type == typeof(System.IO.Stream);
148 }
149
150 foreach (Record rec in view) using (rec)
151 {
152 string[] values = new string[columnCount];
153 for (int i = 0; i < values.Length; i++)
154 {
155 values[i] = isBinary[i] ? "[Binary Data]" : rec.GetString(i + 1);
156 }
157
158 TRecord trec = new TRecord();
159 trec.Database = this.Database;
160 trec.TableInfo = this.TableInfo;
161 trec.Values = values;
162 trec.Exists = true;
163 yield return trec;
164 }
165 }
166 }
167
168 IEnumerator IEnumerable.GetEnumerator()
169 {
170 return ((IEnumerable<TRecord>) this).GetEnumerator();
171 }
172
173 IQueryable<TElement> IQueryProvider.CreateQuery<TElement>(Expression expression)
174 {
175 if (expression == null)
176 {
177 throw new ArgumentNullException("expression");
178 }
179
180 Query<TElement> q = new Query<TElement>(this.Database, expression);
181
182 MethodCallExpression methodCallExpression = (MethodCallExpression) expression;
183 string methodName = methodCallExpression.Method.Name;
184 if (methodName == "Where")
185 {
186 LambdaExpression argumentExpression = (LambdaExpression)
187 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
188 q.BuildQuery(this.TableInfo, argumentExpression);
189 }
190 else if (methodName == "OrderBy")
191 {
192 LambdaExpression argumentExpression = (LambdaExpression)
193 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
194 q.BuildSequence(this.TableInfo, argumentExpression);
195 }
196 else if (methodName == "Select")
197 {
198 LambdaExpression argumentExpression = (LambdaExpression)
199 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
200 q.BuildNullQuery(this.TableInfo, typeof(TRecord), argumentExpression);
201 q.BuildProjection(null, argumentExpression);
202 }
203 else if (methodName == "Join")
204 {
205 ConstantExpression constantExpression = (ConstantExpression)
206 methodCallExpression.Arguments[1];
207 IQueryable inner = (IQueryable) constantExpression.Value;
208 q.PerformJoin(
209 this.TableInfo,
210 typeof(TRecord),
211 inner,
212 GetJoinLambda(methodCallExpression.Arguments[2]),
213 GetJoinLambda(methodCallExpression.Arguments[3]),
214 GetJoinLambda(methodCallExpression.Arguments[4]));
215 }
216 else
217 {
218 throw new NotSupportedException(
219 "Query operation not supported: " + methodName);
220 }
221
222 return q;
223 }
224
225 private static LambdaExpression GetJoinLambda(Expression expresion)
226 {
227 UnaryExpression unaryExpression = (UnaryExpression) expresion;
228 return (LambdaExpression) unaryExpression.Operand;
229 }
230
231 IQueryable IQueryProvider.CreateQuery(Expression expression)
232 {
233 return ((IQueryProvider) this).CreateQuery<TRecord>(expression);
234 }
235
236 TResult IQueryProvider.Execute<TResult>(Expression expression)
237 {
238 throw new NotSupportedException(
239 "Direct method calls not supported -- use AsEnumerable() instead.");
240 }
241
242 object IQueryProvider.Execute(Expression expression)
243 {
244 throw new NotSupportedException(
245 "Direct method calls not supported -- use AsEnumerable() instead.");
246 }
247
248 IQueryProvider IQueryable.Provider
249 {
250 get
251 {
252 return this;
253 }
254 }
255
256 Type IQueryable.ElementType
257 {
258 get
259 {
260 return typeof(TRecord);
261 }
262 }
263
264 Expression IQueryable.Expression
265 {
266 get
267 {
268 return Expression.Constant(this);
269 }
270 }
271
272 /// <summary>
273 /// Creates a new record that can be inserted into this table.
274 /// </summary>
275 /// <returns>a record with all fields initialized to null</returns>
276 /// <remarks>
277 /// Primary keys and required fields must be filled in with
278 /// non-null values before the record can be inserted.
279 /// <para>The record is tied to this table in this database;
280 /// it cannot be inserted into another table or database.</para>
281 /// </remarks>
282 public TRecord NewRecord()
283 {
284 TRecord rec = new TRecord();
285 rec.Database = this.Database;
286 rec.TableInfo = this.TableInfo;
287 IList<string> values = new List<string>(this.TableInfo.Columns.Count);
288 for (int i = 0; i < this.TableInfo.Columns.Count; i++)
289 {
290 values.Add(null);
291 }
292 rec.Values = values;
293 return rec;
294 }
295 }
296}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs
new file mode 100644
index 00000000..ea58757c
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs
@@ -0,0 +1,992 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Linq
4{
5 using System;
6 using System.IO;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.Linq;
10 using System.Linq.Expressions;
11 using System.Text;
12 using System.Reflection;
13 using System.Globalization;
14 using System.Diagnostics.CodeAnalysis;
15
16 /// <summary>
17 /// Implements the LINQ to MSI query functionality.
18 /// </summary>
19 /// <typeparam name="T">the result type of the current query --
20 /// either some kind of QRecord, or some projection of record data</typeparam>
21 internal sealed class Query<T> : IOrderedQueryable<T>, IQueryProvider
22 {
23 private QDatabase db;
24 private Expression queryableExpression;
25 private List<TableInfo> tables;
26 private List<Type> recordTypes;
27 private List<string> selectors;
28 private string where;
29 private List<object> whereParameters;
30 private List<TableColumn> orderbyColumns;
31 private List<TableColumn> selectColumns;
32 private List<TableColumn> joinColumns;
33 private List<Delegate> projectionDelegates;
34
35 internal Query(QDatabase db, Expression expression)
36 {
37 if (expression == null)
38 {
39 throw new ArgumentNullException("expression");
40 }
41
42 this.db = db;
43 this.queryableExpression = expression;
44 this.tables = new List<TableInfo>();
45 this.recordTypes = new List<Type>();
46 this.selectors = new List<string>();
47 this.whereParameters = new List<object>();
48 this.orderbyColumns = new List<TableColumn>();
49 this.selectColumns = new List<TableColumn>();
50 this.joinColumns = new List<TableColumn>();
51 this.projectionDelegates = new List<Delegate>();
52 }
53
54 public IEnumerator<T> GetEnumerator()
55 {
56 if (this.selectColumns.Count == 0)
57 {
58 AddAllColumns(this.tables[0], this.selectColumns);
59 }
60
61 string query = this.CompileQuery();
62 return this.InvokeQuery(query);
63 }
64
65 private string CompileQuery()
66 {
67 bool explicitTables = this.tables.Count > 1;
68
69 StringBuilder queryBuilder = new StringBuilder("SELECT");
70
71 for (int i = 0; i < this.selectColumns.Count; i++)
72 {
73 queryBuilder.AppendFormat(
74 CultureInfo.InvariantCulture,
75 (explicitTables ? "{0} `{1}`.`{2}`" : "{0} `{2}`"),
76 (i > 0 ? "," : String.Empty),
77 this.selectColumns[i].Table.Name,
78 this.selectColumns[i].Column.Name);
79 }
80
81 for (int i = 0; i < this.tables.Count; i++)
82 {
83 queryBuilder.AppendFormat(
84 CultureInfo.InvariantCulture,
85 "{0} `{1}`",
86 (i == 0 ? " FROM" : ","),
87 this.tables[i].Name);
88 }
89
90 bool startedWhere = false;
91 for (int i = 0; i < this.joinColumns.Count - 1; i += 2)
92 {
93 queryBuilder.AppendFormat(
94 CultureInfo.InvariantCulture,
95 "{0} `{1}`.`{2}` = `{3}`.`{4}` ",
96 (i == 0 ? " WHERE" : "AND"),
97 this.joinColumns[i].Table,
98 this.joinColumns[i].Column,
99 this.joinColumns[i + 1].Table,
100 this.joinColumns[i + 1].Column);
101 startedWhere = true;
102 }
103
104 if (this.where != null)
105 {
106 queryBuilder.Append(startedWhere ? "AND " : " WHERE");
107 queryBuilder.Append(this.where);
108 }
109
110 for (int i = 0; i < this.orderbyColumns.Count; i++)
111 {
112 VerifyOrderByColumn(this.orderbyColumns[i]);
113
114 queryBuilder.AppendFormat(
115 CultureInfo.InvariantCulture,
116 (explicitTables ? "{0} `{1}`.`{2}`" : "{0} `{2}`"),
117 (i == 0 ? " ORDER BY" : ","),
118 this.orderbyColumns[i].Table.Name,
119 this.orderbyColumns[i].Column.Name);
120 }
121
122 return queryBuilder.ToString();
123 }
124
125 private static void VerifyOrderByColumn(TableColumn tableColumn)
126 {
127 if (tableColumn.Column.Type != typeof(int) &&
128 tableColumn.Column.Type != typeof(short))
129 {
130 throw new NotSupportedException(
131 "Cannot orderby column: " + tableColumn.Column.Name +
132 "; orderby is only supported on integer fields");
133 }
134 }
135
136 private IEnumerator<T> InvokeQuery(string query)
137 {
138 TextWriter log = this.db.Log;
139 if (log != null)
140 {
141 log.WriteLine();
142 log.WriteLine(query);
143 }
144
145 using (View queryView = this.db.OpenView(query))
146 {
147 if (this.whereParameters != null && this.whereParameters.Count > 0)
148 {
149 using (Record paramsRec = this.db.CreateRecord(this.whereParameters.Count))
150 {
151 for (int i = 0; i < this.whereParameters.Count; i++)
152 {
153 paramsRec[i + 1] = this.whereParameters[i];
154
155 if (log != null)
156 {
157 log.WriteLine(" ? = " + this.whereParameters[i]);
158 }
159 }
160
161 queryView.Execute(paramsRec);
162 }
163 }
164 else
165 {
166 queryView.Execute();
167 }
168
169 foreach (Record resultRec in queryView) using (resultRec)
170 {
171 yield return this.GetResult(resultRec);
172 }
173 }
174 }
175
176 private T GetResult(Record resultRec)
177 {
178 object[] results = new object[this.tables.Count];
179
180 for (int i = 0; i < this.tables.Count; i++)
181 {
182 string[] values = new string[this.tables[i].Columns.Count];
183 for (int j = 0; j < this.selectColumns.Count; j++)
184 {
185 TableColumn col = this.selectColumns[j];
186 if (col.Table.Name == this.tables[i].Name)
187 {
188 int index = this.tables[i].Columns.IndexOf(
189 col.Column.Name);
190 if (index >= 0)
191 {
192 if (col.Column.Type == typeof(Stream))
193 {
194 values[index] = "[Binary Data]";
195 }
196 else
197 {
198 values[index] = resultRec.GetString(j + 1);
199 }
200 }
201 }
202 }
203
204 QRecord result = (QRecord) this.recordTypes[i]
205 .GetConstructor(Type.EmptyTypes).Invoke(null);
206 result.Database = this.db;
207 result.TableInfo = this.tables[i];
208 result.Values = values;
209 result.Exists = true;
210 results[i] = result;
211 }
212
213 if (this.projectionDelegates.Count > 0)
214 {
215 object resultsProjection = results[0];
216 for (int i = 1; i <= results.Length; i++)
217 {
218 if (i < results.Length)
219 {
220 resultsProjection = this.projectionDelegates[i - 1]
221 .DynamicInvoke(new object[] { resultsProjection, results[i] });
222 }
223 else
224 {
225 resultsProjection = this.projectionDelegates[i - 1]
226 .DynamicInvoke(resultsProjection);
227 }
228 }
229
230 return (T) resultsProjection;
231 }
232 else
233 {
234 return (T) (object) results[0];
235 }
236 }
237
238 IEnumerator IEnumerable.GetEnumerator()
239 {
240 return ((IEnumerable<T>) this).GetEnumerator();
241 }
242
243 public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
244 {
245 if (expression == null)
246 {
247 throw new ArgumentNullException("expression");
248 }
249
250 Query<TElement> q = new Query<TElement>(this.db, expression);
251 q.tables.AddRange(this.tables);
252 q.recordTypes.AddRange(this.recordTypes);
253 q.selectors.AddRange(this.selectors);
254 q.where = this.where;
255 q.whereParameters.AddRange(this.whereParameters);
256 q.orderbyColumns.AddRange(this.orderbyColumns);
257 q.selectColumns.AddRange(this.selectColumns);
258 q.joinColumns.AddRange(this.joinColumns);
259 q.projectionDelegates.AddRange(this.projectionDelegates);
260
261 MethodCallExpression methodCallExpression = (MethodCallExpression) expression;
262 string methodName = methodCallExpression.Method.Name;
263 if (methodName == "Select")
264 {
265 LambdaExpression argumentExpression = (LambdaExpression)
266 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
267 q.BuildProjection(null, argumentExpression);
268 }
269 else if (methodName == "Where")
270 {
271 LambdaExpression argumentExpression = (LambdaExpression)
272 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
273 q.BuildQuery(null, argumentExpression);
274 }
275 else if (methodName == "ThenBy")
276 {
277 LambdaExpression argumentExpression = (LambdaExpression)
278 ((UnaryExpression) methodCallExpression.Arguments[1]).Operand;
279 q.BuildSequence(null, argumentExpression);
280 }
281 else if (methodName == "Join")
282 {
283 ConstantExpression constantExpression = (ConstantExpression)
284 methodCallExpression.Arguments[1];
285 IQueryable inner = (IQueryable) constantExpression.Value;
286 q.PerformJoin(
287 null,
288 null,
289 inner,
290 GetJoinLambda(methodCallExpression.Arguments[2]),
291 GetJoinLambda(methodCallExpression.Arguments[3]),
292 GetJoinLambda(methodCallExpression.Arguments[4]));
293 }
294 else
295 {
296 throw new NotSupportedException(
297 "Query operation not supported: " + methodName);
298 }
299
300 return q;
301 }
302
303 public IQueryable CreateQuery(Expression expression)
304 {
305 return this.CreateQuery<T>(expression);
306 }
307
308 private static LambdaExpression GetJoinLambda(Expression expresion)
309 {
310 UnaryExpression unaryExpression = (UnaryExpression) expresion;
311 return (LambdaExpression) unaryExpression.Operand;
312 }
313
314 public TResult Execute<TResult>(Expression expression)
315 {
316 throw new NotSupportedException(
317 "Direct method calls not supported -- use AsEnumerable() instead.");
318 }
319
320 object IQueryProvider.Execute(Expression expression)
321 {
322 throw new NotSupportedException(
323 "Direct method calls not supported -- use AsEnumerable() instead.");
324 }
325
326 public IQueryProvider Provider
327 {
328 get
329 {
330 return this;
331 }
332 }
333
334 public Type ElementType
335 {
336 get
337 {
338 return typeof(T);
339 }
340 }
341
342 public Expression Expression
343 {
344 get
345 {
346 return this.queryableExpression;
347 }
348 }
349
350 internal void BuildQuery(TableInfo tableInfo, LambdaExpression expression)
351 {
352 if (tableInfo != null)
353 {
354 this.tables.Add(tableInfo);
355 this.recordTypes.Add(typeof(T));
356 this.selectors.Add(expression.Parameters[0].Name);
357 }
358
359 StringBuilder queryBuilder = new StringBuilder();
360
361 this.ParseQuery(expression.Body, queryBuilder);
362
363 this.where = queryBuilder.ToString();
364 }
365
366 internal void BuildNullQuery(TableInfo tableInfo, Type recordType, LambdaExpression expression)
367 {
368 this.tables.Add(tableInfo);
369 this.recordTypes.Add(recordType);
370 this.selectors.Add(expression.Parameters[0].Name);
371 }
372
373 private void ParseQuery(Expression expression, StringBuilder queryBuilder)
374 {
375 queryBuilder.Append("(");
376
377 BinaryExpression binaryExpression;
378 UnaryExpression unaryExpression;
379 MethodCallExpression methodCallExpression;
380
381 if ((binaryExpression = expression as BinaryExpression) != null)
382 {
383 switch (binaryExpression.NodeType)
384 {
385 case ExpressionType.AndAlso:
386 this.ParseQuery(binaryExpression.Left, queryBuilder);
387 queryBuilder.Append(" AND ");
388 this.ParseQuery(binaryExpression.Right, queryBuilder);
389 break;
390
391 case ExpressionType.OrElse:
392 this.ParseQuery(binaryExpression.Left, queryBuilder);
393 queryBuilder.Append(" OR ");
394 this.ParseQuery(binaryExpression.Right, queryBuilder);
395 break;
396
397 case ExpressionType.Equal:
398 case ExpressionType.NotEqual:
399 case ExpressionType.GreaterThan:
400 case ExpressionType.LessThan:
401 case ExpressionType.GreaterThanOrEqual:
402 case ExpressionType.LessThanOrEqual:
403 this.ParseQueryCondition(binaryExpression, queryBuilder);
404 break;
405
406 default:
407 throw new NotSupportedException(
408 "Expression type not supported: " + binaryExpression.NodeType );
409 }
410 }
411 else if ((unaryExpression = expression as UnaryExpression) != null)
412 {
413 throw new NotSupportedException(
414 "Expression type not supported: " + unaryExpression.NodeType);
415 }
416 else if ((methodCallExpression = expression as MethodCallExpression) != null)
417 {
418 throw new NotSupportedException(
419 "Method call not supported: " + methodCallExpression.Method.Name + "()");
420 }
421 else
422 {
423 throw new NotSupportedException(
424 "Query filter expression not supported: " + expression);
425 }
426
427 queryBuilder.Append(")");
428 }
429
430 private static ExpressionType OppositeExpression(ExpressionType e)
431 {
432 switch (e)
433 {
434 case ExpressionType.LessThan:
435 return ExpressionType.GreaterThan;
436 case ExpressionType.LessThanOrEqual:
437 return ExpressionType.GreaterThanOrEqual;
438 case ExpressionType.GreaterThan:
439 return ExpressionType.LessThan;
440 case ExpressionType.GreaterThanOrEqual:
441 return ExpressionType.LessThanOrEqual;
442 default:
443 return e;
444 }
445 }
446
447 private static bool IsIntegerType(Type t)
448 {
449 return
450 t == typeof(sbyte) ||
451 t == typeof(byte) ||
452 t == typeof(short) ||
453 t == typeof(ushort) ||
454 t == typeof(int) ||
455 t == typeof(uint) ||
456 t == typeof(long) ||
457 t == typeof(ulong);
458 }
459
460 private void ParseQueryCondition(
461 BinaryExpression binaryExpression, StringBuilder queryBuilder)
462 {
463 bool swap;
464 string column = this.GetConditionColumn(binaryExpression, out swap);
465 queryBuilder.Append(column);
466
467 ExpressionType expressionType = binaryExpression.NodeType;
468 if (swap)
469 {
470 expressionType = OppositeExpression(expressionType);
471 }
472
473 LambdaExpression valueExpression = Expression.Lambda(
474 swap ? binaryExpression.Left : binaryExpression.Right);
475 object value = valueExpression.Compile().DynamicInvoke();
476
477 bool valueIsInt = false;
478 if (value != null)
479 {
480 if (IsIntegerType(value.GetType()))
481 {
482 valueIsInt = true;
483 }
484 else
485 {
486 value = value.ToString();
487 }
488 }
489
490 switch (expressionType)
491 {
492 case ExpressionType.Equal:
493 if (value == null)
494 {
495 queryBuilder.Append(" IS NULL");
496 }
497 else if (valueIsInt)
498 {
499 queryBuilder.Append(" = ");
500 queryBuilder.Append(value);
501 }
502 else
503 {
504 queryBuilder.Append(" = ?");
505 this.whereParameters.Add(value);
506 }
507 return;
508
509 case ExpressionType.NotEqual:
510 if (value == null)
511 {
512 queryBuilder.Append(" IS NOT NULL");
513 }
514 else if (valueIsInt)
515 {
516 queryBuilder.Append(" <> ");
517 queryBuilder.Append(value);
518 }
519 else
520 {
521 queryBuilder.Append(" <> ?");
522 this.whereParameters.Add(value);
523 }
524 return;
525 }
526
527 if (value == null)
528 {
529 throw new InvalidOperationException(
530 "A null value was used in a greater-than/less-than operation.");
531 }
532
533 if (!valueIsInt)
534 {
535 throw new NotSupportedException(
536 "Greater-than/less-than operators not supported on strings.");
537 }
538
539 switch (expressionType)
540 {
541 case ExpressionType.LessThan:
542 queryBuilder.Append(" < ");
543 break;
544
545 case ExpressionType.LessThanOrEqual:
546 queryBuilder.Append(" <= ");
547 break;
548
549 case ExpressionType.GreaterThan:
550 queryBuilder.Append(" > ");
551 break;
552
553 case ExpressionType.GreaterThanOrEqual:
554 queryBuilder.Append(" >= ");
555 break;
556
557 default:
558 throw new NotSupportedException(
559 "Unsupported query expression type: " + expressionType);
560 }
561
562 queryBuilder.Append(value);
563 }
564
565 private string GetConditionColumn(
566 BinaryExpression binaryExpression, out bool swap)
567 {
568 MemberExpression memberExpression;
569 MethodCallExpression methodCallExpression;
570
571 if (((memberExpression = binaryExpression.Left as MemberExpression) != null) ||
572 ((binaryExpression.Left.NodeType == ExpressionType.Convert ||
573 binaryExpression.Left.NodeType == ExpressionType.ConvertChecked) &&
574 (memberExpression = ((UnaryExpression) binaryExpression.Left).Operand
575 as MemberExpression) != null))
576 {
577 string column = this.GetConditionColumn(memberExpression);
578 if (column != null)
579 {
580 swap = false;
581 return column;
582 }
583 }
584 else if (((memberExpression = binaryExpression.Right as MemberExpression) != null) ||
585 ((binaryExpression.Right.NodeType == ExpressionType.Convert ||
586 binaryExpression.Right.NodeType == ExpressionType.ConvertChecked) &&
587 (memberExpression = ((UnaryExpression) binaryExpression.Right).Operand
588 as MemberExpression) != null))
589 {
590 string column = this.GetConditionColumn(memberExpression);
591 if (column != null)
592 {
593 swap = true;
594 return column;
595 }
596 }
597 else if ((methodCallExpression = binaryExpression.Left as MethodCallExpression) != null)
598 {
599 string column = this.GetConditionColumn(methodCallExpression);
600 if (column != null)
601 {
602 swap = false;
603 return column;
604 }
605 }
606 else if ((methodCallExpression = binaryExpression.Right as MethodCallExpression) != null)
607 {
608 string column = this.GetConditionColumn(methodCallExpression);
609 if (column != null)
610 {
611 swap = true;
612 return column;
613 }
614 }
615
616 throw new NotSupportedException(
617 "Unsupported binary expression: " + binaryExpression);
618 }
619
620 private string GetConditionColumn(MemberExpression memberExpression)
621 {
622 string columnName = GetColumnName(memberExpression.Member);
623 string selectorName = GetConditionSelectorName(memberExpression.Expression);
624 string tableName = this.GetConditionTable(selectorName, columnName);
625 return this.FormatColumn(tableName, columnName);
626 }
627
628 private string GetConditionColumn(MethodCallExpression methodCallExpression)
629 {
630 LambdaExpression argumentExpression =
631 Expression.Lambda(methodCallExpression.Arguments[0]);
632 string columnName = (string) argumentExpression.Compile().DynamicInvoke();
633 string selectorName = GetConditionSelectorName(methodCallExpression.Object);
634 string tableName = this.GetConditionTable(selectorName, columnName);
635 return this.FormatColumn(tableName, columnName);
636 }
637
638 private static string GetConditionSelectorName(Expression expression)
639 {
640 ParameterExpression parameterExpression;
641 MemberExpression memberExpression;
642 if ((parameterExpression = expression as ParameterExpression) != null)
643 {
644 return parameterExpression.Name;
645 }
646 else if ((memberExpression = expression as MemberExpression) != null)
647 {
648 return memberExpression.Member.Name;
649 }
650 else
651 {
652 throw new NotSupportedException(
653 "Unsupported conditional selector expression: " + expression);
654 }
655 }
656
657 private string GetConditionTable(string selectorName, string columnName)
658 {
659 string tableName = null;
660
661 for (int i = 0; i < this.tables.Count; i++)
662 {
663 if (this.selectors[i] == selectorName)
664 {
665 tableName = this.tables[i].Name;
666 break;
667 }
668 }
669
670 if (tableName == null)
671 {
672 throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture,
673 "Conditional expression contains column {0}.{1} " +
674 "from a table that is not in the query.",
675 selectorName,
676 columnName));
677 }
678
679 return tableName;
680 }
681
682 private string FormatColumn(string tableName, string columnName)
683 {
684 if (tableName != null && this.tables.Count > 1)
685 {
686 return String.Format(CultureInfo.InvariantCulture, "`{0}`.`{1}`", tableName, columnName);
687 }
688 else
689 {
690 return String.Format(CultureInfo.InvariantCulture, "`{0}`", columnName);
691 }
692 }
693
694 private static string GetColumnName(MemberInfo memberInfo)
695 {
696 foreach (var attr in memberInfo.GetCustomAttributes(
697 typeof(DatabaseColumnAttribute), false))
698 {
699 return ((DatabaseColumnAttribute) attr).Column;
700 }
701
702 return memberInfo.Name;
703 }
704
705 internal void BuildProjection(TableInfo tableInfo, LambdaExpression expression)
706 {
707 if (tableInfo != null)
708 {
709 this.tables.Add(tableInfo);
710 this.recordTypes.Add(typeof(T));
711 this.selectors.Add(expression.Parameters[0].Name);
712 }
713
714 this.FindColumns(expression, this.selectColumns);
715 this.projectionDelegates.Add(expression.Compile());
716 }
717
718 internal void BuildSequence(TableInfo tableInfo, LambdaExpression expression)
719 {
720 if (tableInfo != null)
721 {
722 this.tables.Add(tableInfo);
723 this.recordTypes.Add(typeof(T));
724 this.selectors.Add(expression.Parameters[0].Name);
725 }
726
727 this.FindColumns(expression.Body, this.orderbyColumns);
728 }
729
730 private static void AddAllColumns(TableInfo tableInfo, IList<TableColumn> columnList)
731 {
732 foreach (ColumnInfo column in tableInfo.Columns)
733 {
734 columnList.Add(new TableColumn(tableInfo, column));
735 }
736 }
737
738 [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
739 private void FindColumns(Expression expression, IList<TableColumn> columnList)
740 {
741 if (expression is ParameterExpression)
742 {
743 ParameterExpression e = expression as ParameterExpression;
744 string selector = e.Name;
745 for (int i = 0; i < this.tables.Count; i++)
746 {
747 if (this.selectors[i] == selector)
748 {
749 AddAllColumns(this.tables[i], columnList);
750 break;
751 }
752 }
753 }
754 else if (expression.NodeType == ExpressionType.MemberAccess)
755 {
756 this.FindColumns(expression as MemberExpression, columnList);
757 }
758 else if (expression is MethodCallExpression)
759 {
760 this.FindColumns(expression as MethodCallExpression, columnList);
761 }
762 else if (expression is BinaryExpression)
763 {
764 BinaryExpression e = expression as BinaryExpression;
765 this.FindColumns(e.Left, columnList);
766 this.FindColumns(e.Right, columnList);
767 }
768 else if (expression is UnaryExpression)
769 {
770 UnaryExpression e = expression as UnaryExpression;
771 this.FindColumns(e.Operand, columnList);
772 }
773 else if (expression is ConditionalExpression)
774 {
775 ConditionalExpression e = expression as ConditionalExpression;
776 this.FindColumns(e.Test, columnList);
777 this.FindColumns(e.IfTrue, columnList);
778 this.FindColumns(e.IfFalse, columnList);
779 }
780 else if (expression is InvocationExpression)
781 {
782 InvocationExpression e = expression as InvocationExpression;
783 this.FindColumns(e.Expression, columnList);
784 this.FindColumns(e.Arguments, columnList);
785 }
786 else if (expression is LambdaExpression)
787 {
788 LambdaExpression e = expression as LambdaExpression;
789 this.FindColumns(e.Body, columnList);
790 }
791 else if (expression is ListInitExpression)
792 {
793 ListInitExpression e = expression as ListInitExpression;
794 this.FindColumns(e.NewExpression, columnList);
795 foreach (ElementInit ei in e.Initializers)
796 {
797 this.FindColumns(ei.Arguments, columnList);
798 }
799 }
800 else if (expression is MemberInitExpression)
801 {
802 MemberInitExpression e = expression as MemberInitExpression;
803 this.FindColumns(e.NewExpression, columnList);
804 foreach (MemberAssignment b in e.Bindings)
805 {
806 this.FindColumns(b.Expression, columnList);
807 }
808 }
809 else if (expression is NewExpression)
810 {
811 NewExpression e = expression as NewExpression;
812 this.FindColumns(e.Arguments, columnList);
813 }
814 else if (expression is NewArrayExpression)
815 {
816 NewArrayExpression e = expression as NewArrayExpression;
817 this.FindColumns(e.Expressions, columnList);
818 }
819 else if (expression is TypeBinaryExpression)
820 {
821 TypeBinaryExpression e = expression as TypeBinaryExpression;
822 this.FindColumns(e.Expression, columnList);
823 }
824 }
825
826 private void FindColumns(IEnumerable<Expression> expressions, IList<TableColumn> columnList)
827 {
828 foreach (Expression expression in expressions)
829 {
830 this.FindColumns(expression, columnList);
831 }
832 }
833
834 private void FindColumns(MemberExpression memberExpression, IList<TableColumn> columnList)
835 {
836 string selector = null;
837 MemberExpression objectMemberExpression;
838 ParameterExpression objectParameterExpression;
839 if ((objectParameterExpression = memberExpression.Expression as
840 ParameterExpression) != null)
841 {
842 selector = objectParameterExpression.Name;
843 }
844 else if ((objectMemberExpression = memberExpression.Expression as
845 MemberExpression) != null)
846 {
847 selector = objectMemberExpression.Member.Name;
848 }
849
850 if (selector != null)
851 {
852 for (int i = 0; i < this.tables.Count; i++)
853 {
854 if (this.selectors[i] == selector)
855 {
856 string columnName = GetColumnName(memberExpression.Member);
857 ColumnInfo column = this.tables[i].Columns[columnName];
858 columnList.Add(new TableColumn(this.tables[i], column));
859 break;
860 }
861 }
862 }
863
864 selector = memberExpression.Member.Name;
865 for (int i = 0; i < this.tables.Count; i++)
866 {
867 if (this.selectors[i] == selector)
868 {
869 AddAllColumns(this.tables[i], columnList);
870 break;
871 }
872 }
873 }
874
875 private void FindColumns(MethodCallExpression methodCallExpression, IList<TableColumn> columnList)
876 {
877 if (methodCallExpression.Method.Name == "get_Item" &&
878 methodCallExpression.Arguments.Count == 1 &&
879 methodCallExpression.Arguments[0].Type == typeof(string))
880 {
881 string selector = null;
882 MemberExpression objectMemberExpression;
883 ParameterExpression objectParameterExpression;
884 if ((objectParameterExpression = methodCallExpression.Object as ParameterExpression) != null)
885 {
886 selector = objectParameterExpression.Name;
887 }
888 else if ((objectMemberExpression = methodCallExpression.Object as MemberExpression) != null)
889 {
890 selector = objectMemberExpression.Member.Name;
891 }
892
893 if (selector != null)
894 {
895 for (int i = 0; i < this.tables.Count; i++)
896 {
897 if (this.selectors[i] == selector)
898 {
899 LambdaExpression argumentExpression =
900 Expression.Lambda(methodCallExpression.Arguments[0]);
901 string columnName = (string)
902 argumentExpression.Compile().DynamicInvoke();
903 ColumnInfo column = this.tables[i].Columns[columnName];
904 columnList.Add(new TableColumn(this.tables[i], column));
905 break;
906 }
907 }
908 }
909 }
910
911 if (methodCallExpression.Object != null && methodCallExpression.Object.NodeType != ExpressionType.Parameter)
912 {
913 this.FindColumns(methodCallExpression.Object, columnList);
914 }
915 }
916
917 internal void PerformJoin(
918 TableInfo tableInfo,
919 Type recordType,
920 IQueryable joinTable,
921 LambdaExpression outerKeySelector,
922 LambdaExpression innerKeySelector,
923 LambdaExpression resultSelector)
924 {
925 if (joinTable == null)
926 {
927 throw new ArgumentNullException("joinTable");
928 }
929
930 if (tableInfo != null)
931 {
932 this.tables.Add(tableInfo);
933 this.recordTypes.Add(recordType);
934 this.selectors.Add(outerKeySelector.Parameters[0].Name);
935 }
936
937 PropertyInfo tableInfoProp = joinTable.GetType().GetProperty("TableInfo");
938 if (tableInfoProp == null)
939 {
940 throw new NotSupportedException(
941 "Cannot join with object: " + joinTable.GetType().Name +
942 "; join is only supported on another QTable.");
943 }
944
945 TableInfo joinTableInfo = (TableInfo) tableInfoProp.GetValue(joinTable, null);
946 if (joinTableInfo == null)
947 {
948 throw new InvalidOperationException("Missing join table info.");
949 }
950
951 this.tables.Add(joinTableInfo);
952 this.recordTypes.Add(joinTable.ElementType);
953 this.selectors.Add(innerKeySelector.Parameters[0].Name);
954 this.projectionDelegates.Add(resultSelector.Compile());
955
956 int joinColumnCount = this.joinColumns.Count;
957 this.FindColumns(outerKeySelector.Body, this.joinColumns);
958 if (this.joinColumns.Count > joinColumnCount + 1)
959 {
960 throw new NotSupportedException("Join operations involving " +
961 "multiple columns are not supported.");
962 }
963 else if (this.joinColumns.Count != joinColumnCount + 1)
964 {
965 throw new InvalidOperationException("Bad outer key selector for join.");
966 }
967
968 this.FindColumns(innerKeySelector.Body, this.joinColumns);
969 if (this.joinColumns.Count > joinColumnCount + 2)
970 {
971 throw new NotSupportedException("Join operations involving " +
972 "multiple columns not are supported.");
973 }
974 if (this.joinColumns.Count != joinColumnCount + 2)
975 {
976 throw new InvalidOperationException("Bad inner key selector for join.");
977 }
978 }
979 }
980
981 internal class TableColumn
982 {
983 public TableColumn(TableInfo table, ColumnInfo column)
984 {
985 this.Table = table;
986 this.Column = column;
987 }
988
989 public TableInfo Table { get; set; }
990 public ColumnInfo Column { get; set; }
991 }
992}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj
new file mode 100644
index 00000000..b4587071
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj
@@ -0,0 +1,21 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.WindowsInstaller.Linq</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.WindowsInstaller.Linq</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net35</TargetFrameworks>
9 <Description>LINQ extensions for Windows Installer classes</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <ProjectReference Include="..\WixToolset.Dtf.WindowsInstaller\WixToolset.Dtf.WindowsInstaller.csproj" />
15 </ItemGroup>
16
17 <ItemGroup>
18 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
19 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
20 </ItemGroup>
21</Project>
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs
new file mode 100644
index 00000000..276732b7
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPackage.cs
@@ -0,0 +1,1169 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Package
4{
5using System;
6using System.IO;
7using System.Text;
8using System.Collections;
9using System.Collections.Generic;
10using System.Diagnostics.CodeAnalysis;
11using System.Globalization;
12using System.Text.RegularExpressions;
13using WixToolset.Dtf.Compression;
14using WixToolset.Dtf.Compression.Cab;
15
16/// <summary>
17/// Handles status messages generated when operations are performed on an
18/// <see cref="InstallPackage"/> or <see cref="PatchPackage"/>.
19/// </summary>
20/// <example>
21/// <c>installPackage.Message += new InstallPackageMessageHandler(Console.WriteLine);</c>
22/// </example>
23public delegate void InstallPackageMessageHandler(string format, params object[] args);
24
25/// <summary>
26/// Provides access to powerful build, maintenance, and analysis operations on an
27/// installation package (.MSI or .MSM).
28/// </summary>
29public class InstallPackage : Database
30{
31 private string cabName;
32 private string cabMsg;
33
34 /// <summary>
35 /// Creates a new InstallPackage object. The file source directory and working
36 /// directory are the same as the location as the package file.
37 /// </summary>
38 /// <param name="packagePath">Path to the install package to be created or opened</param>
39 /// <param name="openMode">Open mode for the database</param>
40 public InstallPackage(string packagePath, DatabaseOpenMode openMode)
41 : this(packagePath, openMode, null, null)
42 {
43 }
44 /// <summary>
45 /// Creates a new InstallPackage object, specifying an alternate file source
46 /// directory and/or working directory.
47 /// </summary>
48 /// <param name="packagePath">Path to the install package to be created or opened</param>
49 /// <param name="openMode">Open mode for the database</param>
50 /// <param name="sourceDir">Location to obtain source files and cabinets when extracting
51 /// or updating files in the working directory. This is often the location of an original
52 /// copy of the package that is not meant to be modified. If this parameter is null, it
53 /// defaults to the directory of <paramref name="packagePath"/>.</param>
54 /// <param name="workingDir">Location where files will be extracted to/updated from. Also
55 /// the location where a temporary folder is created during some operations. If this
56 /// parameter is null, it defaults to the directory of <paramref name="packagePath"/>.</param>
57 /// <remarks>If the source location is different than the working directory, then
58 /// no files will be modified at the source location.
59 /// </remarks>
60 public InstallPackage(string packagePath, DatabaseOpenMode openMode,
61 string sourceDir, string workingDir) : base(packagePath, openMode)
62 {
63 this.sourceDir = (sourceDir != null ? sourceDir : Path.GetDirectoryName(packagePath));
64 this.workingDir = (workingDir != null ? workingDir : Path.GetDirectoryName(packagePath));
65 this.compressionLevel = CompressionLevel.Normal;
66
67 this.DeleteOnClose(this.TempDirectory);
68 }
69
70 /// <summary>
71 /// Handle this event to receive status messages when operations are performed
72 /// on the install package.
73 /// </summary>
74 /// <example>
75 /// <c>installPackage.Message += new InstallPackageMessageHandler(Console.WriteLine);</c>
76 /// </example>
77 public event InstallPackageMessageHandler Message;
78
79 /// <summary>
80 /// Sends a message to the <see cref="Message"/> event-handler.
81 /// </summary>
82 /// <param name="format">Message string, containing 0 or more format items</param>
83 /// <param name="args">Items to be formatted</param>
84 protected void LogMessage(string format, params object[] args)
85 {
86 if(this.Message != null)
87 {
88 this.Message(format, args);
89 }
90 }
91
92 /// <summary>
93 /// Gets or sets the location to obtain source files and cabinets when
94 /// extracting or updating files in the working directory. This is often
95 /// the location of an original copy of the package that is not meant
96 /// to be modified.
97 /// </summary>
98 public string SourceDirectory
99 {
100 get { return this.sourceDir; }
101 set { this.sourceDir = value; }
102 }
103 private string sourceDir;
104
105 /// <summary>
106 /// Gets or sets the location where files will be extracted to/updated from. Also
107 /// the location where a temporary folder is created during some operations.
108 /// </summary>
109 public string WorkingDirectory
110 {
111 get { return this.workingDir; }
112 set { this.workingDir = value; }
113 }
114 private string workingDir;
115
116 private const string TEMP_DIR_NAME = "WITEMP";
117
118 private string TempDirectory
119 {
120 get { return Path.Combine(this.WorkingDirectory, TEMP_DIR_NAME); }
121 }
122
123 /// <summary>
124 /// Gets the list of file keys that have the specified long file name.
125 /// </summary>
126 /// <param name="longFileName">File name to search for (case-insensitive)</param>
127 /// <returns>Array of file keys, or a 0-length array if none are found</returns>
128 [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
129 public string[] FindFiles(string longFileName)
130 {
131 longFileName = longFileName.ToLowerInvariant();
132 ArrayList fileList = new ArrayList();
133 foreach(KeyValuePair<string, InstallPath> entry in this.Files)
134 {
135 if(((InstallPath) entry.Value).TargetName.ToLowerInvariant()
136 == longFileName)
137 {
138 fileList.Add(entry.Key);
139 }
140 }
141 return (string[]) fileList.ToArray(typeof(string));
142 }
143
144 /// <summary>
145 /// Gets the list of file keys whose long file names match a specified
146 /// regular-expression search pattern.
147 /// </summary>
148 /// <param name="pattern">Regular expression search pattern</param>
149 /// <returns>Array of file keys, or a 0-length array if none are found</returns>
150 public string[] FindFiles(Regex pattern)
151 {
152 ArrayList fileList = new ArrayList();
153 foreach (KeyValuePair<string, InstallPath> entry in this.Files)
154 {
155 if(pattern.IsMatch(((InstallPath) entry.Value).TargetName))
156 {
157 fileList.Add(entry.Key);
158 }
159 }
160 return (string[]) fileList.ToArray(typeof(string));
161 }
162
163 /// <summary>
164 /// Extracts all files to the <see cref="WorkingDirectory"/>. The files are extracted
165 /// to the relative directory matching their <see cref="InstallPath.SourcePath"/>.
166 /// </summary>
167 /// <remarks>If any files have the uncompressed attribute, they will be copied
168 /// from the <see cref="SourceDirectory"/>.</remarks>
169 public void ExtractFiles()
170 {
171 this.ExtractFiles(null);
172 }
173 /// <summary>
174 /// Extracts a specified list of files to the <see cref="WorkingDirectory"/>. The files
175 /// are extracted to the relative directory matching their <see cref="InstallPath.SourcePath"/>.
176 /// </summary>
177 /// <param name="fileKeys">List of file key strings to extract</param>
178 /// <remarks>If any files have the uncompressed attribute, they will be copied
179 /// from the <see cref="SourceDirectory"/>.</remarks>
180 public void ExtractFiles(ICollection<string> fileKeys)
181 {
182 this.ProcessFilesByMediaDisk(fileKeys,
183 new ProcessFilesOnOneMediaDiskHandler(this.ExtractFilesOnOneMediaDisk));
184 }
185
186 private bool IsMergeModule()
187 {
188 return this.CountRows("Media", "`LastSequence` >= 0") == 0 &&
189 this.CountRows("_Streams", "`Name` = 'MergeModule.CABinet'") != 0;
190 }
191
192 private delegate void ProcessFilesOnOneMediaDiskHandler(string mediaCab,
193 InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap);
194
195 private void ProcessFilesByMediaDisk(ICollection<string> fileKeys,
196 ProcessFilesOnOneMediaDiskHandler diskHandler)
197 {
198 if(this.IsMergeModule())
199 {
200 InstallPathMap files = new InstallPathMap();
201 foreach(string fileKey in this.Files.Keys)
202 {
203 if(fileKeys == null || fileKeys.Contains(fileKey))
204 {
205 files[fileKey] = this.Files[fileKey];
206 }
207 }
208 diskHandler("#MergeModule.CABinet", files, new InstallPathMap());
209 }
210 else
211 {
212 bool defaultCompressed = ((this.SummaryInfo.WordCount & 0x2) != 0);
213
214 View fileView = null, mediaView = null;
215 Record fileRec = null;
216 try
217 {
218 fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " +
219 "FROM `File` ORDER BY `Sequence`");
220 mediaView = this.OpenView("SELECT `DiskId`, `LastSequence`, `Cabinet` " +
221 "FROM `Media` ORDER BY `DiskId`");
222 fileView.Execute();
223 mediaView.Execute();
224
225 int currentMediaDiskId = -1;
226 int currentMediaMaxSequence = -1;
227 string currentMediaCab = null;
228 InstallPathMap compressedFileMap = new InstallPathMap();
229 InstallPathMap uncompressedFileMap = new InstallPathMap();
230
231 while((fileRec = fileView.Fetch()) != null)
232 {
233 string fileKey = (string) fileRec[1];
234
235 if(fileKeys == null || fileKeys.Contains(fileKey))
236 {
237 int fileAttributes = fileRec.GetInteger(2);
238 int fileSequence = fileRec.GetInteger(3);
239
240 InstallPath fileInstallPath = this.Files[fileKey];
241 if(fileInstallPath == null)
242 {
243 this.LogMessage("Could not get install path for source file: {0}", fileKey);
244 throw new InstallerException("Could not get install path for source file: " + fileKey);
245 }
246
247 if(fileSequence > currentMediaMaxSequence)
248 {
249 if(currentMediaDiskId != -1)
250 {
251 diskHandler(currentMediaCab,
252 compressedFileMap, uncompressedFileMap);
253 compressedFileMap.Clear();
254 uncompressedFileMap.Clear();
255 }
256
257 while(fileSequence > currentMediaMaxSequence)
258 {
259 Record mediaRec = mediaView.Fetch();
260 if(mediaRec == null)
261 {
262 currentMediaDiskId = -1;
263 break;
264 }
265 using(mediaRec)
266 {
267 currentMediaDiskId = mediaRec.GetInteger(1);
268 currentMediaMaxSequence = mediaRec.GetInteger(2);
269 currentMediaCab = (string) mediaRec[3];
270 }
271 }
272 if(fileSequence > currentMediaMaxSequence) break;
273 }
274
275 if((fileAttributes & (int) WixToolset.Dtf.WindowsInstaller.FileAttributes.Compressed) != 0)
276 {
277 compressedFileMap[fileKey] = fileInstallPath;
278 }
279 else if ((fileAttributes & (int) WixToolset.Dtf.WindowsInstaller.FileAttributes.NonCompressed) != 0)
280 {
281 // Non-compressed files are located
282 // in the same directory as the MSI, without any path.
283 uncompressedFileMap[fileKey] = new InstallPath(fileInstallPath.SourceName);
284 }
285 else if(defaultCompressed)
286 {
287 compressedFileMap[fileKey] = fileInstallPath;
288 }
289 else
290 {
291 uncompressedFileMap[fileKey] = fileInstallPath;
292 }
293 }
294 fileRec.Close();
295 fileRec = null;
296 }
297 if(currentMediaDiskId != -1)
298 {
299 diskHandler(currentMediaCab,
300 compressedFileMap, uncompressedFileMap);
301 }
302 }
303 finally
304 {
305 if (fileRec != null) fileRec.Close();
306 if (fileView != null) fileView.Close();
307 if (mediaView != null) mediaView.Close();
308 }
309 }
310 }
311
312 [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
313 private void ExtractFilesOnOneMediaDisk(string mediaCab,
314 InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap)
315 {
316 if(compressedFileMap.Count > 0)
317 {
318 string cabFile = null;
319 if(mediaCab.StartsWith("#", StringComparison.Ordinal))
320 {
321 mediaCab = mediaCab.Substring(1);
322
323 using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
324 "WHERE `Name` = '{0}'", mediaCab))
325 {
326 streamView.Execute();
327 Record streamRec = streamView.Fetch();
328 if(streamRec == null)
329 {
330 this.LogMessage("Stream not found: {0}", mediaCab);
331 throw new InstallerException("Stream not found: " + mediaCab);
332 }
333 using(streamRec)
334 {
335 this.LogMessage("extract cab {0}", mediaCab);
336 Directory.CreateDirectory(this.TempDirectory);
337 cabFile = Path.Combine(this.TempDirectory,
338 Path.GetFileNameWithoutExtension(mediaCab) + ".cab");
339 streamRec.GetStream("Data", cabFile);
340 }
341 }
342 }
343 else
344 {
345 cabFile = Path.Combine(this.SourceDirectory, mediaCab);
346 }
347
348 this.cabName = mediaCab;
349 this.cabMsg = "extract {0}\\{1} {2}";
350 new CabInfo(cabFile).UnpackFileSet(compressedFileMap.SourcePaths, this.WorkingDirectory,
351 this.CabinetProgress);
352 ClearReadOnlyAttribute(this.WorkingDirectory, compressedFileMap.Values);
353 }
354 foreach(InstallPath fileInstallPath in uncompressedFileMap.Values)
355 {
356 string sourcePath = Path.Combine(this.SourceDirectory, fileInstallPath.SourcePath);
357 string extractPath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath);
358 if(Path.GetFullPath(sourcePath).ToLowerInvariant() !=
359 Path.GetFullPath(extractPath).ToLowerInvariant())
360 {
361 if(!File.Exists(sourcePath))
362 {
363 this.LogMessage("Error: Uncompressed file not found: {0}", sourcePath);
364 throw new FileNotFoundException("Uncompressed file not found.", sourcePath);
365 }
366 else
367 {
368 this.LogMessage("copy {0} {1}", sourcePath, extractPath);
369 Directory.CreateDirectory(Path.GetDirectoryName(extractPath));
370 File.Copy(sourcePath, extractPath, true);
371 }
372 }
373 else
374 {
375 if(!File.Exists(extractPath))
376 {
377 this.LogMessage("Error: Uncompressed file not found: {0}", extractPath);
378 throw new FileNotFoundException("Uncompressed file not found.", extractPath);
379 }
380 }
381 }
382 }
383
384 private void CabinetProgress(object sender, ArchiveProgressEventArgs e)
385 {
386 switch(e.ProgressType)
387 {
388 case ArchiveProgressType.StartFile:
389 {
390 string filePath = e.CurrentFileName;
391 if(this.filePathMap != null)
392 {
393 InstallPath fileInstallPath = this.Files[e.CurrentFileName];
394 if(fileInstallPath != null)
395 {
396 filePath = fileInstallPath.SourcePath;
397 }
398 }
399 this.LogMessage(this.cabMsg, this.cabName, e.CurrentFileName,
400 Path.Combine(this.WorkingDirectory, filePath));
401 }
402 break;
403 }
404 }
405
406 /// <summary>
407 /// Updates the install package with new files from the <see cref="WorkingDirectory"/>. The
408 /// files must be in the relative directory matching their <see cref="InstallPath.SourcePath"/>.
409 /// This method re-compresses and packages the files if necessary, and also updates the
410 /// following data: File.FileSize, File.Version, File.Language, MsiFileHash.HashPart*
411 /// </summary>
412 /// <remarks>
413 /// The cabinet compression level used during re-cabbing can be configured with the
414 /// <see cref="CompressionLevel"/> property.
415 /// </remarks>
416 public void UpdateFiles()
417 {
418 this.UpdateFiles(null);
419 }
420 /// <summary>
421 /// Updates the install package with new files from the <see cref="WorkingDirectory"/>. The
422 /// files must be in the relative directory matching their <see cref="InstallPath.SourcePath"/>.
423 /// This method re-compresses and packages the files if necessary, and also updates the
424 /// following data: File.FileSize, File.Version, File.Language, MsiFileHash.HashPart?.
425 /// </summary>
426 /// <param name="fileKeys">List of file key strings to update</param>
427 /// <remarks>
428 /// This method does not change the media structure of the package, so it may require extracting
429 /// and re-compressing a large cabinet just to update one file.
430 /// <p>The cabinet compression level used during re-cabbing can be configured with the
431 /// <see cref="CompressionLevel"/> property.</p>
432 /// </remarks>
433 public void UpdateFiles(ICollection<string> fileKeys)
434 {
435 this.ProcessFilesByMediaDisk(fileKeys,
436 new ProcessFilesOnOneMediaDiskHandler(this.UpdateFilesOnOneMediaDisk));
437 }
438
439 private void UpdateFilesOnOneMediaDisk(string mediaCab,
440 InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap)
441 {
442 if(compressedFileMap.Count > 0)
443 {
444 string cabFile = null;
445 bool cabFileIsTemp = false;
446 if(mediaCab.StartsWith("#", StringComparison.Ordinal))
447 {
448 cabFileIsTemp = true;
449 mediaCab = mediaCab.Substring(1);
450
451 using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
452 "WHERE `Name` = '{0}'", mediaCab))
453 {
454 streamView.Execute();
455 Record streamRec = streamView.Fetch();
456 if(streamRec == null)
457 {
458 this.LogMessage("Stream not found: {0}", mediaCab);
459 throw new InstallerException("Stream not found: " + mediaCab);
460 }
461 using(streamRec)
462 {
463 this.LogMessage("extract cab {0}", mediaCab);
464 Directory.CreateDirectory(this.TempDirectory);
465 cabFile = Path.Combine(this.TempDirectory,
466 Path.GetFileNameWithoutExtension(mediaCab) + ".cab");
467 streamRec.GetStream("Data", cabFile);
468 }
469 }
470 }
471 else
472 {
473 cabFile = Path.Combine(this.SourceDirectory, mediaCab);
474 }
475
476 CabInfo cab = new CabInfo(cabFile);
477 ArrayList fileKeyList = new ArrayList();
478 foreach (CabFileInfo fileInCab in cab.GetFiles())
479 {
480 string fileKey = fileInCab.Name;
481 if(this.Files[fileKey] != null)
482 {
483 fileKeyList.Add(fileKey);
484 }
485 }
486 string[] fileKeys = (string[]) fileKeyList.ToArray(typeof(string));
487
488 Directory.CreateDirectory(this.TempDirectory);
489
490 ArrayList remainingFileKeys = new ArrayList(fileKeys);
491 foreach(string fileKey in fileKeys)
492 {
493 InstallPath fileInstallPath = compressedFileMap[fileKey];
494 if(fileInstallPath != null)
495 {
496 UpdateFileStats(fileKey, fileInstallPath);
497
498 string filePath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath);
499 this.LogMessage("copy {0} {1}", filePath, fileKey);
500 File.Copy(filePath, Path.Combine(this.TempDirectory, fileKey), true);
501 remainingFileKeys.Remove(fileKey);
502 }
503 }
504
505 if(remainingFileKeys.Count > 0)
506 {
507 this.cabName = mediaCab;
508 this.cabMsg = "extract {0}\\{1}";
509 string[] remainingFileKeysArray = (string[]) remainingFileKeys.ToArray(typeof(string));
510 cab.UnpackFiles(remainingFileKeysArray, this.TempDirectory, remainingFileKeysArray,
511 this.CabinetProgress);
512 }
513
514 ClearReadOnlyAttribute(this.TempDirectory, fileKeys);
515
516 if(!cabFileIsTemp)
517 {
518 cab = new CabInfo(Path.Combine(this.WorkingDirectory, mediaCab));
519 }
520 this.cabName = mediaCab;
521 this.cabMsg = "compress {0}\\{1}";
522 cab.PackFiles(this.TempDirectory, fileKeys, fileKeys,
523 this.CompressionLevel, this.CabinetProgress);
524
525 if(cabFileIsTemp)
526 {
527 using (Record streamRec = new Record(1))
528 {
529 streamRec.SetStream(1, cabFile);
530 this.Execute(String.Format(
531 "UPDATE `_Streams` SET `Data` = ? WHERE `Name` = '{0}'", mediaCab),
532 streamRec);
533 }
534 }
535 }
536
537 foreach (KeyValuePair<string, InstallPath> entry in uncompressedFileMap)
538 {
539 UpdateFileStats((string) entry.Key, (InstallPath) entry.Value);
540 }
541 }
542
543 private void UpdateFileStats(string fileKey, InstallPath fileInstallPath)
544 {
545 string filePath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath);
546 if(!File.Exists(filePath))
547 {
548 this.LogMessage("Updated source file not found: {0}", filePath);
549 throw new FileNotFoundException("Updated source file not found: " + filePath);
550 }
551
552 this.LogMessage("updatestats {0}", fileKey);
553
554 string version = Installer.GetFileVersion(filePath);
555 string language = Installer.GetFileLanguage(filePath);
556 long size = new FileInfo(filePath).Length;
557
558 this.Execute("UPDATE `File` SET `Version` = '{0}', `Language` = '{1}', " +
559 "`FileSize` = {2} WHERE `File` = '{3}'", version, language, size, fileKey);
560
561 if ((version == null || version.Length == 0) && this.Tables.Contains("MsiFileHash"))
562 {
563 int[] hash = new int[4];
564 Installer.GetFileHash(filePath, hash);
565 this.Execute("DELETE FROM `MsiFileHash` WHERE `File_` = '{0}'", fileKey);
566 this.Execute("INSERT INTO `MsiFileHash` (`File_`, `Options`, `HashPart1`, `HashPart2`, " +
567 "`HashPart3`, `HashPart4`) VALUES ('" + fileKey + "', 0, {0}, {1}, {2}, {3})",
568 hash[0], hash[1], hash[2], hash[3]);
569 }
570 }
571
572 /// <summary>
573 /// Consolidates a package by combining and re-compressing all files into a single
574 /// internal or external cabinet.
575 /// </summary>
576 /// <param name="mediaCabinet"></param>
577 /// <remarks>If an installation package was built from many merge modules, this
578 /// method can somewhat decrease package size, complexity, and installation time.
579 /// <p>This method will also convert a package with all or mostly uncompressed
580 /// files into a package where all files are compressed.</p>
581 /// <p>If the package contains any not-yet-applied binary file patches (for
582 /// example, a package generated by a call to <see cref="ApplyPatch"/>) then
583 /// this method will apply the patches before compressing the updated files.</p>
584 /// <p>This method edits the database summary information and the File, Media
585 /// and Patch tables as necessary to maintain a valid installation package.</p>
586 /// <p>The cabinet compression level used during re-cabbing can be configured with the
587 /// <see cref="CompressionLevel"/> property.</p>
588 /// </remarks>
589 public void Consolidate(string mediaCabinet)
590 {
591 this.LogMessage("Consolidating package");
592
593 Directory.CreateDirectory(this.TempDirectory);
594
595 this.LogMessage("Extracting/preparing files");
596 this.ProcessFilesByMediaDisk(null,
597 new ProcessFilesOnOneMediaDiskHandler(this.PrepareOneMediaDiskForConsolidation));
598
599 this.LogMessage("Applying any file patches");
600 ApplyFilePatchesForConsolidation();
601
602 this.LogMessage("Clearing PatchPackage, Patch, MsiPatchHeaders tables");
603 if (this.Tables.Contains("PatchPackage"))
604 {
605 this.Execute("DELETE FROM `PatchPackage` WHERE `PatchId` <> ''");
606 }
607 if (this.Tables.Contains("Patch"))
608 {
609 this.Execute("DELETE FROM `Patch` WHERE `File_` <> ''");
610 }
611 if (this.Tables.Contains("MsiPatchHeaders"))
612 {
613 this.Execute("DELETE FROM `MsiPatchHeaders` WHERE `StreamRef` <> ''");
614 }
615
616 this.LogMessage("Resequencing files");
617 ArrayList files = new ArrayList();
618 using(View fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " +
619 "FROM `File` ORDER BY `Sequence`"))
620 {
621 fileView.Execute();
622
623 foreach (Record fileRec in fileView) using(fileRec)
624 {
625 files.Add(fileRec[1]);
626 int fileAttributes = fileRec.GetInteger(2);
627 fileAttributes &= ~(int) (WixToolset.Dtf.WindowsInstaller.FileAttributes.Compressed
628 | WixToolset.Dtf.WindowsInstaller.FileAttributes.NonCompressed | WixToolset.Dtf.WindowsInstaller.FileAttributes.PatchAdded);
629 fileRec[2] = fileAttributes;
630 fileRec[3] = files.Count;
631 fileView.Update(fileRec);
632 }
633 }
634
635 bool internalCab = false;
636 if(mediaCabinet.StartsWith("#", StringComparison.Ordinal))
637 {
638 internalCab = true;
639 mediaCabinet = mediaCabinet.Substring(1);
640 }
641
642 this.LogMessage("Cabbing files");
643 string[] fileKeys = (string[]) files.ToArray(typeof(string));
644 string cabPath = Path.Combine(internalCab ? this.TempDirectory
645 : this.WorkingDirectory, mediaCabinet);
646 this.cabName = mediaCabinet;
647 this.cabMsg = "compress {0}\\{1}";
648 new CabInfo(cabPath).PackFiles(this.TempDirectory, fileKeys,
649 fileKeys, this.CompressionLevel, this.CabinetProgress);
650
651 this.DeleteEmbeddedCabs();
652
653 if(internalCab)
654 {
655 this.LogMessage("Inserting cab stream into MSI");
656 Record cabRec = new Record(1);
657 cabRec.SetStream(1, cabPath);
658 this.Execute("INSERT INTO `_Streams` (`Name`, `Data`) VALUES ('" + mediaCabinet + "', ?)", cabRec);
659 }
660
661 this.LogMessage("Inserting cab media record into MSI");
662 this.Execute("DELETE FROM `Media` WHERE `DiskId` <> 0");
663 this.Execute("INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) " +
664 "VALUES (1, " + files.Count + ", '" + (internalCab ? "#" : "") + mediaCabinet + "')");
665
666
667 this.LogMessage("Setting compressed flag on package summary info");
668 this.SummaryInfo.WordCount = this.SummaryInfo.WordCount | 2;
669 this.SummaryInfo.Persist();
670 }
671
672 private void DeleteEmbeddedCabs()
673 {
674 using (View view = this.OpenView("SELECT `Cabinet` FROM `Media` WHERE `Cabinet` <> ''"))
675 {
676 view.Execute();
677
678 foreach (Record rec in view) using(rec)
679 {
680 string cab = rec.GetString(1);
681 if(cab.StartsWith("#", StringComparison.Ordinal))
682 {
683 cab = cab.Substring(1);
684 this.LogMessage("Deleting embedded cab stream: {0}", cab);
685 this.Execute("DELETE FROM `_Streams` WHERE `Name` = '{0}'", cab);
686 }
687 }
688 }
689 }
690
691 private void PrepareOneMediaDiskForConsolidation(string mediaCab,
692 InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap)
693 {
694 if(compressedFileMap.Count > 0)
695 {
696 string cabFile = null;
697 if(mediaCab.StartsWith("#", StringComparison.Ordinal))
698 {
699 mediaCab = mediaCab.Substring(1);
700
701 using (View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
702 "WHERE `Name` = '{0}'", mediaCab))
703 {
704 streamView.Execute();
705 Record streamRec = streamView.Fetch();
706 if(streamRec == null)
707 {
708 this.LogMessage("Stream not found: {0}", mediaCab);
709 throw new InstallerException("Stream not found: " + mediaCab);
710 }
711 using(streamRec)
712 {
713 this.LogMessage("extract cab {0}", mediaCab);
714 cabFile = Path.Combine(this.TempDirectory,
715 Path.GetFileNameWithoutExtension(mediaCab) + ".cab");
716 streamRec.GetStream("Data", cabFile);
717 }
718 }
719 }
720 else
721 {
722 cabFile = Path.Combine(this.SourceDirectory, mediaCab);
723 }
724 string[] fileKeys = new string[compressedFileMap.Keys.Count];
725 compressedFileMap.Keys.CopyTo(fileKeys, 0);
726 this.cabName = mediaCab;
727 this.cabMsg = "extract {0}\\{1}";
728 new CabInfo(cabFile).UnpackFiles(fileKeys, this.TempDirectory, fileKeys,
729 this.CabinetProgress);
730 ClearReadOnlyAttribute(this.TempDirectory, fileKeys);
731 }
732 foreach (KeyValuePair<string, InstallPath> entry in uncompressedFileMap)
733 {
734 string fileKey = (string) entry.Key;
735 InstallPath fileInstallPath = (InstallPath) entry.Value;
736
737 string filePath = Path.Combine(this.SourceDirectory, fileInstallPath.SourcePath);
738 this.LogMessage("copy {0} {1}", filePath, fileKey);
739 File.Copy(filePath, Path.Combine(this.TempDirectory, fileKey));
740 }
741 }
742
743 private void ClearReadOnlyAttribute(string baseDirectory, IEnumerable filePaths)
744 {
745 foreach(object filePath in filePaths)
746 {
747 string fullFilePath = Path.Combine(baseDirectory, filePath.ToString());
748 if (File.Exists(fullFilePath))
749 {
750 System.IO.FileAttributes fileAttributes = File.GetAttributes(fullFilePath);
751 if ((fileAttributes & System.IO.FileAttributes.ReadOnly) != 0)
752 {
753 fileAttributes &= ~System.IO.FileAttributes.ReadOnly;
754 File.SetAttributes(fullFilePath, fileAttributes);
755 }
756 }
757 }
758 }
759
760 private void ApplyFilePatchesForConsolidation()
761 {
762 if(this.Tables.Contains("Patch"))
763 {
764 using(View patchView = this.OpenView("SELECT `File_`, `Sequence` " +
765 "FROM `Patch` ORDER BY `Sequence`"))
766 {
767 patchView.Execute();
768 Hashtable extractedPatchCabs = new Hashtable();
769
770 foreach (Record patchRec in patchView) using(patchRec)
771 {
772 string fileKey = (string) patchRec[1];
773 int sequence = patchRec.GetInteger(2);
774 this.LogMessage("patch {0}", fileKey);
775
776 string tempPatchFile = Path.Combine(this.TempDirectory, fileKey + ".pat");
777 ExtractFilePatch(fileKey, sequence, tempPatchFile, extractedPatchCabs);
778 string filePath = Path.Combine(this.TempDirectory, fileKey);
779 string oldFilePath = filePath + ".old";
780 if(File.Exists(oldFilePath)) File.Delete(oldFilePath);
781 File.Move(filePath, oldFilePath);
782 Type.GetType("WixToolset.Dtf.WindowsInstaller.FilePatch")
783 .GetMethod("ApplyPatchToFile",
784 new Type[] { typeof(string), typeof(string), typeof(string) })
785 .Invoke(null, new object[] { tempPatchFile, oldFilePath, filePath });
786 }
787 }
788 }
789 }
790
791 private void ExtractFilePatch(string fileKey, int sequence, string extractPath,
792 IDictionary extractedCabs)
793 {
794 string mediaCab = null;
795 using(View mediaView = this.OpenView("SELECT `DiskId`, `LastSequence`, `Cabinet` " +
796 "FROM `Media` ORDER BY `DiskId`"))
797 {
798 mediaView.Execute();
799
800 foreach (Record mediaRec in mediaView) using(mediaRec)
801 {
802 int mediaMaxSequence = mediaRec.GetInteger(2);
803 if(mediaMaxSequence >= sequence)
804 {
805 mediaCab = mediaRec.GetString(3);
806 break;
807 }
808 }
809 }
810
811 if(mediaCab == null || mediaCab.Length == 0)
812 {
813 this.LogMessage("Could not find cabinet for file patch: {0}", fileKey);
814 throw new InstallerException("Could not find cabinet for file patch: " + fileKey);
815 }
816
817 if(!mediaCab.StartsWith("#", StringComparison.Ordinal))
818 {
819 this.LogMessage("Error: Patch cabinet {0} must be embedded", mediaCab);
820 throw new InstallerException("Patch cabinet " + mediaCab + " must be embedded.");
821 }
822 mediaCab = mediaCab.Substring(1);
823
824 string cabFile = (string) extractedCabs[mediaCab];
825 if(cabFile == null)
826 {
827 using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
828 "WHERE `Name` = '{0}'", mediaCab))
829 {
830 streamView.Execute();
831 Record streamRec = streamView.Fetch();
832 if(streamRec == null)
833 {
834 this.LogMessage("Stream not found: {0}", mediaCab);
835 throw new InstallerException("Stream not found: " + mediaCab);
836 }
837 using(streamRec)
838 {
839 this.LogMessage("extract cab {0}", mediaCab);
840 Directory.CreateDirectory(this.TempDirectory);
841 cabFile = Path.Combine(this.TempDirectory,
842 Path.GetFileNameWithoutExtension(mediaCab) + ".cab");
843 streamRec.GetStream("Data", cabFile);
844 }
845 }
846 extractedCabs[mediaCab] = cabFile;
847 }
848
849 this.LogMessage("extract patch {0}\\{1}", mediaCab, fileKey);
850 new CabInfo(cabFile).UnpackFile(fileKey, extractPath);
851 }
852
853 /// <summary>
854 /// Rebuilds the cached directory structure information accessed by the
855 /// <see cref="Directories"/> and <see cref="Files"/> properties. This
856 /// should be done after modifying the File, Component, or Directory
857 /// tables, or else the cached information may no longer be accurate.
858 /// </summary>
859 public void UpdateDirectories()
860 {
861 this.dirPathMap = null;
862 this.filePathMap = InstallPathMap.BuildFilePathMap(this,
863 InstallPathMap.BuildComponentPathMap(this, this.Directories), false);
864 }
865
866 /// <summary>
867 /// Gets a mapping from Directory keys to source/target paths.
868 /// </summary>
869 /// <remarks>
870 /// If the Directory table is modified, this mapping
871 /// will be outdated until you call <see cref="UpdateDirectories"/>.
872 /// </remarks>
873 public InstallPathMap Directories
874 {
875 get
876 {
877 if(this.dirPathMap == null)
878 {
879 this.dirPathMap = InstallPathMap.BuildDirectoryPathMap(this, false);
880 }
881 return this.dirPathMap;
882 }
883 }
884 private InstallPathMap dirPathMap;
885
886 /// <summary>
887 /// Gets a mapping from File keys to source/target paths.
888 /// </summary>
889 /// <remarks>
890 /// If the File, Component, or Directory tables are modified, this mapping
891 /// may be outdated until you call <see cref="UpdateDirectories"/>.
892 /// </remarks>
893 public InstallPathMap Files
894 {
895 get
896 {
897 if(this.filePathMap == null)
898 {
899 this.filePathMap = InstallPathMap.BuildFilePathMap(this,
900 InstallPathMap.BuildComponentPathMap(this, this.Directories), false);
901 }
902 return this.filePathMap;
903 }
904 }
905 private InstallPathMap filePathMap;
906
907 /// <summary>
908 /// Gets or sets the compression level used by <see cref="UpdateFiles()"/>
909 /// and <see cref="Consolidate"/>.
910 /// </summary>
911 /// <remarks>
912 /// If the Directory table is modified, this mapping will be outdated
913 /// until you close and reopen the install package.
914 /// </remarks>
915 public CompressionLevel CompressionLevel
916 {
917 get { return this.compressionLevel; }
918 set { this.compressionLevel = value; }
919 }
920 private CompressionLevel compressionLevel;
921
922 /// <summary>
923 /// Applies a patch package to the database, resulting in an installation package that
924 /// has the patch built-in.
925 /// </summary>
926 /// <param name="patchPackage">The patch package to be applied</param>
927 /// <param name="transform">Optional name of the specific transform to apply.
928 /// This parameter is usually left null, which causes the patch to be searched for
929 /// a transform that is valid to apply to this database.</param>
930 /// <remarks>
931 /// If the patch contains any binary file patches, they will not immediately be applied
932 /// to the target files, though they will at installation time.
933 /// <p>After calling this method you can use <see cref="Consolidate"/> to apply
934 /// the file patches immediately and also discard any outdated files from the package.</p>
935 /// </remarks>
936 public void ApplyPatch(PatchPackage patchPackage, string transform)
937 {
938 if(patchPackage == null) throw new ArgumentNullException("patchPackage");
939
940 this.LogMessage("Applying patch file {0} to database {1}",
941 patchPackage.FilePath, this.FilePath);
942
943 if(transform == null)
944 {
945 this.LogMessage("No transform specified; searching for valid patch transform");
946 string[] validTransforms = patchPackage.GetValidTransforms(this);
947 if(validTransforms.Length == 0)
948 {
949 this.LogMessage("No valid patch transform was found");
950 throw new InvalidOperationException("No valid patch transform was found.");
951 }
952 transform = validTransforms[0];
953 }
954 this.LogMessage("Patch transform = {0}", transform);
955
956 string patchPrefix = Path.GetFileNameWithoutExtension(patchPackage.FilePath) + "_";
957
958 string specialTransform = "#" + transform;
959 Directory.CreateDirectory(this.TempDirectory);
960 this.LogMessage("Extracting substorage {0}", transform);
961 string transformFile = Path.Combine(this.TempDirectory,
962 patchPrefix + Path.GetFileNameWithoutExtension(transform) + ".mst");
963 patchPackage.ExtractTransform(transform, transformFile);
964 this.LogMessage("Extracting substorage {0}", specialTransform);
965 string specialTransformFile = Path.Combine(this.TempDirectory,
966 patchPrefix + Path.GetFileNameWithoutExtension(specialTransform) + ".mst");
967 patchPackage.ExtractTransform(specialTransform, specialTransformFile);
968
969 if (this.Tables.Contains("Patch") && !this.Tables["Patch"].Columns.Contains("_StreamRef"))
970 {
971 if(this.CountRows("Patch") > 0)
972 {
973 this.LogMessage("Warning: non-empty Patch table exists without StreamRef_ column; " +
974 "patch transform may fail");
975 }
976 else
977 {
978 this.Execute("DROP TABLE `Patch`");
979 this.Execute("CREATE TABLE `Patch` (`File_` CHAR(72) NOT NULL, " +
980 "`Sequence` INTEGER NOT NULL, `PatchSize` LONG NOT NULL, " +
981 "`Attributes` INTEGER NOT NULL, `Header` OBJECT, `StreamRef_` CHAR(72) " +
982 "PRIMARY KEY `File_`, `Sequence`)");
983 }
984 }
985
986 this.LogMessage("Applying transform {0} to database", transform);
987 this.ApplyTransform(transformFile);
988 this.LogMessage("Applying transform {0} to database", specialTransform);
989 this.ApplyTransform(specialTransformFile);
990
991 if (this.Tables.Contains("MsiPatchHeaders") && this.CountRows("MsiPatchHeaders") > 0 &&
992 (!this.Tables.Contains("Patch") || this.CountRows("Patch", "`StreamRef_` <> ''") == 0))
993 {
994 this.LogMessage("Error: patch transform failed because of missing Patch.StreamRef_ column");
995 throw new InstallerException("Patch transform failed because of missing Patch.StreamRef_ column");
996 }
997
998 IList<int> mediaIds = this.ExecuteIntegerQuery("SELECT `Media_` FROM `PatchPackage` " +
999 "WHERE `PatchId` = '{0}'", patchPackage.PatchCode);
1000 if (mediaIds.Count == 0)
1001 {
1002 this.LogMessage("Warning: PatchPackage Media record not found -- " +
1003 "skipping inclusion of patch cabinet");
1004 }
1005 else
1006 {
1007 int patchMediaDiskId = mediaIds[0];
1008 IList<string> patchCabinets = this.ExecuteStringQuery("SELECT `Cabinet` FROM `Media` " +
1009 "WHERE `DiskId` = {0}", patchMediaDiskId);
1010 if(patchCabinets.Count == 0)
1011 {
1012 this.LogMessage("Patch cabinet record not found");
1013 throw new InstallerException("Patch cabinet record not found.");
1014 }
1015 string patchCabinet = patchCabinets[0];
1016 this.LogMessage("Patch cabinet = {0}", patchCabinet);
1017 if(!patchCabinet.StartsWith("#", StringComparison.Ordinal))
1018 {
1019 this.LogMessage("Error: Patch cabinet must be embedded");
1020 throw new InstallerException("Patch cabinet must be embedded.");
1021 }
1022 patchCabinet = patchCabinet.Substring(1);
1023
1024 string renamePatchCabinet = patchPrefix + patchCabinet;
1025
1026 const int HIGH_DISKID = 30000; // Must not collide with other patch media DiskIDs
1027 int renamePatchMediaDiskId = HIGH_DISKID;
1028 while (this.CountRows("Media", "`DiskId` = " + renamePatchMediaDiskId) > 0) renamePatchMediaDiskId++;
1029
1030 // Since the patch cab is now embedded in the MSI, it shouldn't have a separate disk prompt/source
1031 this.LogMessage("Renaming the patch media record");
1032 int lastSeq = Convert.ToInt32(this.ExecuteScalar("SELECT `LastSequence` FROM `Media` WHERE `DiskId` = {0}", patchMediaDiskId));
1033 this.Execute("DELETE FROM `Media` WHERE `DiskId` = {0}", patchMediaDiskId);
1034 this.Execute("INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) VALUES ({0}, '{1}', '#{2}')",
1035 renamePatchMediaDiskId, lastSeq, renamePatchCabinet);
1036 this.Execute("UPDATE `PatchPackage` SET `Media_` = {0} WHERE `PatchId` = '{1}'", renamePatchMediaDiskId, patchPackage.PatchCode);
1037
1038 this.LogMessage("Copying patch cabinet: {0}", patchCabinet);
1039 string patchCabFile = Path.Combine(this.TempDirectory,
1040 Path.GetFileNameWithoutExtension(patchCabinet) + ".cab");
1041 using(View streamView = patchPackage.OpenView("SELECT `Name`, `Data` FROM `_Streams` " +
1042 "WHERE `Name` = '{0}'", patchCabinet))
1043 {
1044 streamView.Execute();
1045 Record streamRec = streamView.Fetch();
1046 if(streamRec == null)
1047 {
1048 this.LogMessage("Error: Patch cabinet not found");
1049 throw new InstallerException("Patch cabinet not found.");
1050 }
1051 using(streamRec)
1052 {
1053 streamRec.GetStream(2, patchCabFile);
1054 }
1055 }
1056 using(Record patchCabRec = new Record(2))
1057 {
1058 patchCabRec[1] = patchCabinet;
1059 patchCabRec.SetStream(2, patchCabFile);
1060 this.Execute("INSERT INTO `_Streams` (`Name`, `Data`) VALUES (?, ?)", patchCabRec);
1061 }
1062
1063 this.LogMessage("Ensuring PatchFiles action exists in InstallExecuteSequence table");
1064 if (this.Tables.Contains("InstallExecuteSequence"))
1065 {
1066 if(this.CountRows("InstallExecuteSequence", "`Action` = 'PatchFiles'") == 0)
1067 {
1068 IList<int> installFilesSeqList = this.ExecuteIntegerQuery("SELECT `Sequence` " +
1069 "FROM `InstallExecuteSequence` WHERE `Action` = 'InstallFiles'");
1070 short installFilesSeq = (short) (installFilesSeqList.Count != 0 ?
1071 installFilesSeqList[0] : 0);
1072 this.Execute("INSERT INTO `InstallExecuteSequence` (`Action`, `Sequence`) " +
1073 "VALUES ('PatchFiles', {0})", installFilesSeq + 1);
1074 }
1075 }
1076
1077 // Patch-added files need to be marked always-compressed
1078 this.LogMessage("Adjusting attributes of patch-added files");
1079 using(View fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " +
1080 "FROM `File` ORDER BY `Sequence`"))
1081 {
1082 fileView.Execute();
1083
1084 foreach (Record fileRec in fileView) using(fileRec)
1085 {
1086 int fileAttributes = fileRec.GetInteger(2);
1087 if ((fileAttributes & (int) WixToolset.Dtf.WindowsInstaller.FileAttributes.PatchAdded) != 0)
1088 {
1089 fileAttributes = (fileAttributes | (int) WixToolset.Dtf.WindowsInstaller.FileAttributes.Compressed)
1090 & ~(int) WixToolset.Dtf.WindowsInstaller.FileAttributes.NonCompressed
1091 & ~(int) WixToolset.Dtf.WindowsInstaller.FileAttributes.PatchAdded;
1092 fileRec[2] = fileAttributes;
1093 fileView.Update(fileRec);
1094 }
1095 }
1096 }
1097 }
1098
1099 this.LogMessage("Applying new summary info from patch package");
1100 this.SummaryInfo.RevisionNumber = this.Property["PATCHNEWPACKAGECODE"];
1101 this.SummaryInfo.Subject = this.Property["PATCHNEWSUMMARYSUBJECT"];
1102 this.SummaryInfo.Comments = this.Property["PATCHNEWSUMMARYCOMMENTS"];
1103 this.SummaryInfo.Persist();
1104 this.Property["PATCHNEWPACKAGECODE" ] = null;
1105 this.Property["PATCHNEWSUMMARYSUBJECT" ] = null;
1106 this.Property["PATCHNEWSUMMARYCOMMENTS"] = null;
1107
1108 this.LogMessage("Patch application finished");
1109 }
1110
1111 /// <summary>
1112 /// Accessor for getting and setting properties of the InstallPackage database.
1113 /// </summary>
1114 public InstallPackageProperties Property
1115 {
1116 get
1117 {
1118 if(this.properties == null)
1119 {
1120 this.properties = new InstallPackageProperties(this);
1121 }
1122 return this.properties;
1123 }
1124 }
1125 private InstallPackageProperties properties = null;
1126}
1127
1128/// <summary>
1129/// Accessor for getting and setting properties of the <see cref="InstallPackage"/> database.
1130/// </summary>
1131public class InstallPackageProperties
1132{
1133 internal InstallPackageProperties(InstallPackage installPackage)
1134 {
1135 this.installPackage = installPackage;
1136 }
1137 private InstallPackage installPackage;
1138
1139 /// <summary>
1140 /// Gets or sets a property in the database. When getting a property
1141 /// that does not exist in the database, an empty string is returned.
1142 /// To remove a property from the database, set it to an empty string.
1143 /// </summary>
1144 /// <remarks>
1145 /// This has the same results as direct SQL queries on the Property table; it's only
1146 /// meant to be a more convenient way of access.
1147 /// </remarks>
1148 public string this[string name]
1149 {
1150 get
1151 {
1152 IList<string> values = installPackage.ExecuteStringQuery(
1153 "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", name);
1154 return (values.Count != 0 ? values[0] : "");
1155 }
1156 set
1157 {
1158 Record propRec = new Record(name, (value != null ? value : ""));
1159 installPackage.Execute("DELETE FROM `Property` WHERE `Property` = ?", propRec);
1160 if(value != null && value.Length != 0)
1161 {
1162 installPackage.Execute("INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)",
1163 propRec);
1164 }
1165 }
1166 }
1167}
1168
1169}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPath.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPath.cs
new file mode 100644
index 00000000..e3ba81b5
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/InstallPath.cs
@@ -0,0 +1,1073 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Package
4{
5 using System;
6 using System.IO;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.Globalization;
10
11 /// <summary>
12 /// Represents the installation path of a file or directory from an installer product database.
13 /// </summary>
14 public class InstallPath
15 {
16 /// <summary>
17 /// Creates a new InstallPath, specifying a filename.
18 /// </summary>
19 /// <param name="name">The name of the file or directory. Not a full path.</param>
20 public InstallPath(string name) : this(name, false) { }
21
22 /// <summary>
23 /// Creates a new InstallPath, parsing out either the short or long filename.
24 /// </summary>
25 /// <param name="name">The name of the file or directory, in short|long syntax for a filename
26 /// or targetshort|targetlong:sourceshort|sourcelong syntax for a directory.</param>
27 /// <param name="useShortNames">true to parse the short part of the combined filename; false to parse the long part</param>
28 public InstallPath(string name, bool useShortNames)
29 {
30 if(name == null)
31 {
32 throw new ArgumentNullException();
33 }
34 this.parentPath = null;
35 ParseName(name, useShortNames);
36 }
37
38 private void ParseName(string name, bool useShortNames)
39 {
40 string[] parse = name.Split(new char[] { ':' }, 3);
41 if(parse.Length == 3)
42 {
43 // Syntax was targetshort:sourceshort|targetlong:sourcelong.
44 // Chnage it to targetshort|targetlong:sourceshort|sourcelong.
45 parse = name.Split(new char[] { ':', '|' }, 4);
46 if(parse.Length == 4)
47 parse = new string[] { parse[0] + '|' + parse[2], parse[1] + '|' + parse[3] };
48 else
49 parse = new string[] { parse[0] + '|' + parse[1], parse[1] + '|' + parse[2] };
50 }
51 string targetName = parse[0];
52 string sourceName = (parse.Length == 2 ? parse[1] : parse[0]);
53 parse = targetName.Split(new char[] { '|' }, 2);
54 if(parse.Length == 2) targetName = (useShortNames ? parse[0] : parse[1]);
55 parse = sourceName.Split(new char[] { '|' }, 2);
56 if(parse.Length == 2) sourceName = (useShortNames ? parse[0] : parse[1]);
57
58 this.SourceName = sourceName;
59 this.TargetName = targetName;
60 }
61
62 /// <summary>
63 /// Gets the path of the parent directory.
64 /// </summary>
65 public InstallPath ParentPath
66 {
67 get
68 {
69 return parentPath;
70 }
71 }
72 internal void SetParentPath(InstallPath value)
73 {
74 parentPath = value;
75 ResetSourcePath();
76 ResetTargetPath();
77 }
78 private InstallPath parentPath;
79
80 /// <summary>
81 /// Gets the set of child paths if this InstallPath object represents a a directory.
82 /// </summary>
83 public InstallPathCollection ChildPaths
84 {
85 get
86 {
87 if(childPaths == null)
88 {
89 childPaths = new InstallPathCollection(this);
90 }
91 return childPaths;
92 }
93 }
94 private InstallPathCollection childPaths;
95
96 /// <summary>
97 /// Gets or sets the source name of the InstallPath.
98 /// </summary>
99 public string SourceName
100 {
101 get
102 {
103 return sourceName;
104 }
105 set
106 {
107 if(value == null)
108 {
109 throw new ArgumentNullException();
110 }
111 sourceName = value;
112 ResetSourcePath();
113 }
114 }
115 private string sourceName;
116
117 /// <summary>
118 /// Gets or sets the target name of the install path.
119 /// </summary>
120 public string TargetName
121 {
122 get
123 {
124 return targetName;
125 }
126 set
127 {
128 if(value == null)
129 {
130 throw new ArgumentNullException();
131 }
132 targetName = value;
133 ResetTargetPath();
134 }
135 }
136 private string targetName;
137
138 /// <summary>
139 /// Gets the full source path.
140 /// </summary>
141 public string SourcePath
142 {
143 get
144 {
145 if(sourcePath == null)
146 {
147 if(parentPath != null)
148 {
149 sourcePath = (sourceName.Equals(".") ? parentPath.SourcePath
150 : Path.Combine(parentPath.SourcePath, sourceName));
151 }
152 else
153 {
154 sourcePath = sourceName;
155 }
156 }
157 return sourcePath;
158 }
159 set
160 {
161 ResetSourcePath();
162 sourcePath = value;
163 }
164 }
165 private string sourcePath;
166
167 /// <summary>
168 /// Gets the full target path.
169 /// </summary>
170 public string TargetPath
171 {
172 get
173 {
174 if(targetPath == null)
175 {
176 if(parentPath != null)
177 {
178 targetPath = (targetName.Equals(".") ? parentPath.TargetPath
179 : Path.Combine(parentPath.TargetPath, targetName));
180 }
181 else
182 {
183 targetPath = targetName;
184 }
185 }
186 return targetPath;
187 }
188 set
189 {
190 ResetTargetPath();
191 targetPath = value;
192 }
193 }
194 private string targetPath;
195
196 private void ResetSourcePath()
197 {
198 if(sourcePath != null)
199 {
200 sourcePath = null;
201 if(childPaths != null)
202 {
203 foreach(InstallPath ip in childPaths)
204 {
205 ip.ResetSourcePath();
206 }
207 }
208 }
209 }
210
211 private void ResetTargetPath()
212 {
213 if(targetPath != null)
214 {
215 targetPath = null;
216 if(childPaths != null)
217 {
218 foreach(InstallPath ip in childPaths)
219 {
220 ip.ResetTargetPath();
221 }
222 }
223 }
224 }
225
226 /// <summary>
227 /// Gets the full source path.
228 /// </summary>
229 /// <returns><see cref="SourcePath"/></returns>
230 public override String ToString()
231 {
232 return SourcePath;
233 }
234 }
235
236 /// <summary>
237 /// Represents a collection of InstallPaths that are the child paths of the same parent directory.
238 /// </summary>
239 public class InstallPathCollection : IList<InstallPath>
240 {
241 private InstallPath parentPath;
242 private List<InstallPath> items;
243
244 internal InstallPathCollection(InstallPath parentPath)
245 {
246 this.parentPath = parentPath;
247 this.items = new List<InstallPath>();
248 }
249
250 /// <summary>
251 /// Gets or sets the element at the specified index.
252 /// </summary>
253 public InstallPath this[int index]
254 {
255 get
256 {
257 return this.items[index];
258 }
259 set
260 {
261 this.OnSet(this.items[index], value);
262 this.items[index] = value;
263 }
264 }
265
266 /// <summary>
267 /// Adds a new child path to the collection.
268 /// </summary>
269 /// <param name="item">The InstallPath to add.</param>
270 public void Add(InstallPath item)
271 {
272 this.OnInsert(item);
273 this.items.Add(item);
274 }
275
276 /// <summary>
277 /// Removes a child path to the collection.
278 /// </summary>
279 /// <param name="item">The InstallPath to remove.</param>
280 public bool Remove(InstallPath item)
281 {
282 int index = this.items.IndexOf(item);
283 if (index >= 0)
284 {
285 this.OnRemove(item);
286 this.items.RemoveAt(index);
287 return true;
288 }
289 else
290 {
291 return false;
292 }
293 }
294
295 /// <summary>
296 /// Gets the index of a child path in the collection.
297 /// </summary>
298 /// <param name="item">The InstallPath to search for.</param>
299 /// <returns>The index of the item, or -1 if not found.</returns>
300 public int IndexOf(InstallPath item)
301 {
302 return this.items.IndexOf(item);
303 }
304
305 /// <summary>
306 /// Inserts a child path into the collection.
307 /// </summary>
308 /// <param name="index">The insertion index.</param>
309 /// <param name="item">The InstallPath to insert.</param>
310 public void Insert(int index, InstallPath item)
311 {
312 this.OnInsert(item);
313 this.items.Insert(index, item);
314 }
315
316 /// <summary>
317 /// Tests if the collection contains a child path.
318 /// </summary>
319 /// <param name="item">The InstallPath to search for.</param>
320 /// <returns>true if the item is found; false otherwise</returns>
321 public bool Contains(InstallPath item)
322 {
323 return this.items.Contains(item);
324 }
325
326 /// <summary>
327 /// Copies the collection into an array.
328 /// </summary>
329 /// <param name="array">The array to copy into.</param>
330 /// <param name="index">The starting index in the destination array.</param>
331 public void CopyTo(InstallPath[] array, int index)
332 {
333 this.items.CopyTo(array, index);
334 }
335
336 private void OnInsert(InstallPath item)
337 {
338 if (item.ParentPath != null)
339 {
340 item.ParentPath.ChildPaths.Remove(item);
341 }
342
343 item.SetParentPath(this.parentPath);
344 }
345
346 private void OnRemove(InstallPath item)
347 {
348 item.SetParentPath(null);
349 }
350
351 private void OnSet(InstallPath oldItem, InstallPath newItem)
352 {
353 this.OnRemove(oldItem);
354 this.OnInsert(newItem);
355 }
356
357 /// <summary>
358 /// Removes an item from the collection.
359 /// </summary>
360 /// <param name="index">The index of the item to remove.</param>
361 public void RemoveAt(int index)
362 {
363 this.OnRemove(this[index]);
364 this.items.RemoveAt(index);
365 }
366
367 /// <summary>
368 /// Removes all items from the collection.
369 /// </summary>
370 public void Clear()
371 {
372 foreach (InstallPath item in this)
373 {
374 this.OnRemove(item);
375 }
376
377 this.items.Clear();
378 }
379
380 /// <summary>
381 /// Gets the number of items in the collection.
382 /// </summary>
383 public int Count
384 {
385 get
386 {
387 return this.items.Count;
388 }
389 }
390
391 bool ICollection<InstallPath>.IsReadOnly
392 {
393 get
394 {
395 return false;
396 }
397 }
398
399 /// <summary>
400 /// Gets an enumerator over all items in the collection.
401 /// </summary>
402 /// <returns>An enumerator for the collection.</returns>
403 public IEnumerator<InstallPath> GetEnumerator()
404 {
405 return this.items.GetEnumerator();
406 }
407
408 IEnumerator IEnumerable.GetEnumerator()
409 {
410 return ((IEnumerable<InstallPath>) this).GetEnumerator();
411 }
412 }
413
414 /// <summary>
415 /// Represents a mapping of install paths for all directories, components, or files in
416 /// an installation database.
417 /// </summary>
418 public class InstallPathMap : IDictionary<string, InstallPath>
419 {
420 /// <summary>
421 /// Builds a mapping from File keys to installation paths.
422 /// </summary>
423 /// <param name="db">Installation database.</param>
424 /// <param name="componentPathMap">Component mapping returned by <see cref="BuildComponentPathMap"/>.</param>
425 /// <param name="useShortNames">true to use short file names; false to use long names</param>
426 /// <returns>An InstallPathMap with the described mapping.</returns>
427 public static InstallPathMap BuildFilePathMap(Database db, InstallPathMap componentPathMap,
428 bool useShortNames)
429 {
430 if(db == null)
431 {
432 throw new ArgumentNullException("db");
433 }
434
435 if(componentPathMap == null)
436 {
437 componentPathMap = BuildComponentPathMap(db, BuildDirectoryPathMap(db, useShortNames));
438 }
439
440 InstallPathMap filePathMap = new InstallPathMap();
441
442 using (View fileView = db.OpenView("SELECT `File`, `Component_`, `FileName` FROM `File`"))
443 {
444 fileView.Execute();
445
446 foreach (Record fileRec in fileView)
447 {
448 using (fileRec)
449 {
450 string file = (string) fileRec[1];
451 string comp = (string) fileRec[2];
452 string fileName = (string) fileRec[3];
453
454 InstallPath compPath = (InstallPath) componentPathMap[comp];
455 if(compPath != null)
456 {
457 InstallPath filePath = new InstallPath(fileName, useShortNames);
458 compPath.ChildPaths.Add(filePath);
459 filePathMap[file] = filePath;
460 }
461 }
462 }
463 }
464
465 return filePathMap;
466 }
467
468 /// <summary>
469 /// Builds a mapping from Component keys to installation paths.
470 /// </summary>
471 /// <param name="db">Installation database.</param>
472 /// <param name="directoryPathMap">Directory mapping returned by
473 /// <see cref="BuildDirectoryPathMap(Database,bool)"/>.</param>
474 /// <returns>An InstallPathMap with the described mapping.</returns>
475 public static InstallPathMap BuildComponentPathMap(Database db, InstallPathMap directoryPathMap)
476 {
477 if(db == null)
478 {
479 throw new ArgumentNullException("db");
480 }
481
482 InstallPathMap compPathMap = new InstallPathMap();
483
484 using (View compView = db.OpenView("SELECT `Component`, `Directory_` FROM `Component`"))
485 {
486 compView.Execute();
487
488 foreach (Record compRec in compView)
489 {
490 using (compRec)
491 {
492 string comp = (string) compRec[1];
493 InstallPath dirPath = (InstallPath) directoryPathMap[(string) compRec[2]];
494
495 if (dirPath != null)
496 {
497 compPathMap[comp] = dirPath;
498 }
499 }
500 }
501 }
502
503 return compPathMap;
504 }
505
506 /// <summary>
507 /// Builds a mapping from Directory keys to installation paths.
508 /// </summary>
509 /// <param name="db">Installation database.</param>
510 /// <param name="useShortNames">true to use short directory names; false to use long names</param>
511 /// <returns>An InstallPathMap with the described mapping.</returns>
512 public static InstallPathMap BuildDirectoryPathMap(Database db, bool useShortNames)
513 {
514 return BuildDirectoryPathMap(db, useShortNames, null, null);
515 }
516
517 /// <summary>
518 /// Builds a mapping of Directory keys to directory paths, specifying root directories
519 /// for the source and target paths.
520 /// </summary>
521 /// <param name="db">Database containing the Directory table.</param>
522 /// <param name="useShortNames">true to use short directory names; false to use long names</param>
523 /// <param name="sourceRootDir">The root directory path of all source paths, or null to leave them relative.</param>
524 /// <param name="targetRootDir">The root directory path of all source paths, or null to leave them relative.</param>
525 /// <returns>An InstallPathMap with the described mapping.</returns>
526 public static InstallPathMap BuildDirectoryPathMap(Database db, bool useShortNames,
527 string sourceRootDir, string targetRootDir)
528 {
529 if(db == null)
530 {
531 throw new ArgumentNullException("db");
532 }
533
534 if(sourceRootDir == null) sourceRootDir = "";
535 if(targetRootDir == null) targetRootDir = "";
536
537 InstallPathMap dirMap = new InstallPathMap();
538 IDictionary dirTreeMap = new Hashtable();
539
540 using (View dirView = db.OpenView("SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`"))
541 {
542 dirView.Execute();
543
544 foreach (Record dirRec in dirView) using (dirRec)
545 {
546 string key = (string) dirRec[1];
547 string parentKey = (string) dirRec[2];
548 InstallPath dir = new InstallPath((string) dirRec[3], useShortNames);
549
550 dirMap[key] = dir;
551
552 InstallPathMap siblingDirs = (InstallPathMap) dirTreeMap[parentKey];
553 if (siblingDirs == null)
554 {
555 siblingDirs = new InstallPathMap();
556 dirTreeMap[parentKey] = siblingDirs;
557 }
558 siblingDirs.Add(key, dir);
559 }
560 }
561
562 foreach (KeyValuePair<string, InstallPath> entry in (InstallPathMap) dirTreeMap[""])
563 {
564 string key = (string) entry.Key;
565 InstallPath dir = (InstallPath) entry.Value;
566 LinkSubdirectories(key, dir, dirTreeMap);
567 }
568
569 InstallPath targetDirPath = (InstallPath) dirMap["TARGETDIR"];
570 if(targetDirPath != null)
571 {
572 targetDirPath.SourcePath = sourceRootDir;
573 targetDirPath.TargetPath = targetRootDir;
574 }
575
576 return dirMap;
577 }
578
579 private static void LinkSubdirectories(string key, InstallPath dir, IDictionary dirTreeMap)
580 {
581 InstallPathMap subDirs = (InstallPathMap) dirTreeMap[key];
582 if(subDirs != null)
583 {
584 foreach (KeyValuePair<string, InstallPath> entry in subDirs)
585 {
586 string subKey = (string) entry.Key;
587 InstallPath subDir = (InstallPath) entry.Value;
588 dir.ChildPaths.Add(subDir);
589 LinkSubdirectories(subKey, subDir, dirTreeMap);
590 }
591 }
592 }
593
594 private Dictionary<string, InstallPath> items;
595
596 /// <summary>
597 /// Creates a new empty InstallPathMap.
598 /// </summary>
599 public InstallPathMap()
600 {
601 this.items = new Dictionary<string,InstallPath>(StringComparer.Ordinal);
602 }
603
604 /// <summary>
605 /// Gets a mapping from keys to source paths.
606 /// </summary>
607 public IDictionary<string, string> SourcePaths
608 {
609 get
610 {
611 return new SourcePathMap(this);
612 }
613 }
614
615 /// <summary>
616 /// Gets a mapping from keys to target paths.
617 /// </summary>
618 public IDictionary<string, string> TargetPaths
619 {
620 get
621 {
622 return new TargetPathMap(this);
623 }
624 }
625
626 /// <summary>
627 /// Gets or sets an install path for a direcotry, component, or file key.
628 /// </summary>
629 /// <param name="key">Depending on the type of InstallPathMap, this is the primary key from the
630 /// either the Directory, Component, or File table.</param>
631 /// <remarks>
632 /// Changing an install path does not modify the Database used to generate this InstallPathMap.
633 /// </remarks>
634 public InstallPath this[string key]
635 {
636 get
637 {
638 InstallPath value = null;
639 this.items.TryGetValue(key, out value);
640 return value;
641 }
642 set
643 {
644 this.items[key] = value;
645 }
646 }
647
648 /// <summary>
649 /// Gets the collection of keys in the InstallPathMap. Depending on the type of InstallPathMap,
650 /// they are all directory, component, or file key strings.
651 /// </summary>
652 public ICollection<string> Keys
653 {
654 get
655 {
656 return this.items.Keys;
657 }
658 }
659
660 /// <summary>
661 /// Gets the collection of InstallPath values in the InstallPathMap.
662 /// </summary>
663 public ICollection<InstallPath> Values
664 {
665 get
666 {
667 return this.items.Values;
668 }
669 }
670
671 /// <summary>
672 /// Sets an install path for a direcotry, component, or file key.
673 /// </summary>
674 /// <param name="key">Depending on the type of InstallPathMap, this is the primary key from the
675 /// either the Directory, Component, or File table.</param>
676 /// <param name="installPath">The install path of the key item.</param>
677 /// <remarks>
678 /// Changing an install path does not modify the Database used to generate this InstallPathMap.
679 /// </remarks>
680 public void Add(string key, InstallPath installPath)
681 {
682 this.items.Add(key, installPath);
683 }
684
685 /// <summary>
686 /// Removes an install path from the map.
687 /// </summary>
688 /// <param name="key">Depending on the type of InstallPathMap, this is the primary key from the
689 /// either the Directory, Component, or File table.</param>
690 /// <returns>true if the item was removed, false if it did not exist</returns>
691 /// <remarks>
692 /// Changing an install path does not modify the Database used to generate this InstallPathMap.
693 /// </remarks>
694 public bool Remove(string key)
695 {
696 return this.items.Remove(key);
697 }
698
699 /// <summary>
700 /// Tests whether a direcotry, component, or file key exists in the map.
701 /// </summary>
702 /// <param name="key">Depending on the type of InstallPathMap, this is the primary key from the
703 /// either the Directory, Component, or File table.</param>
704 /// <returns>true if the key is found; false otherwise</returns>
705 public bool ContainsKey(string key)
706 {
707 return this.items.ContainsKey(key);
708 }
709
710 /*
711 public override string ToString()
712 {
713 System.Text.StringBuilder buf = new System.Text.StringBuilder();
714 foreach(KeyValuePair<string, InstallPath> entry in this)
715 {
716 buf.AppendFormat("{0}={1}", entry.Key, entry.Value);
717 buf.Append("\n");
718 }
719 return buf.ToString();
720 }
721 */
722
723 /// <summary>
724 /// Attempts to get a value from the dictionary.
725 /// </summary>
726 /// <param name="key">The key to lookup.</param>
727 /// <param name="value">Receives the value, or null if they key was not found.</param>
728 /// <returns>True if the value was found, else false.</returns>
729 public bool TryGetValue(string key, out InstallPath value)
730 {
731 return this.items.TryGetValue(key, out value);
732 }
733
734 void ICollection<KeyValuePair<string, InstallPath>>.Add(KeyValuePair<string, InstallPath> item)
735 {
736 ((ICollection<KeyValuePair<string, InstallPath>>) this.items).Add(item);
737 }
738
739 /// <summary>
740 /// Removes all entries from the dictionary.
741 /// </summary>
742 public void Clear()
743 {
744 this.items.Clear();
745 }
746
747 bool ICollection<KeyValuePair<string, InstallPath>>.Contains(KeyValuePair<string, InstallPath> item)
748 {
749 return ((ICollection<KeyValuePair<string, InstallPath>>) this.items).Contains(item);
750 }
751
752 void ICollection<KeyValuePair<string, InstallPath>>.CopyTo(KeyValuePair<string, InstallPath>[] array, int arrayIndex)
753 {
754 ((ICollection<KeyValuePair<string, InstallPath>>) this.items).CopyTo(array, arrayIndex);
755 }
756
757 /// <summary>
758 /// Gets the number of entries in the dictionary.
759 /// </summary>
760 public int Count
761 {
762 get
763 {
764 return this.items.Count;
765 }
766 }
767
768 bool ICollection<KeyValuePair<string, InstallPath>>.IsReadOnly
769 {
770 get
771 {
772 return false;
773 }
774 }
775
776 bool ICollection<KeyValuePair<string, InstallPath>>.Remove(KeyValuePair<string, InstallPath> item)
777 {
778 return ((ICollection<KeyValuePair<string, InstallPath>>) this.items).Remove(item);
779 }
780
781 /// <summary>
782 /// Gets an enumerator over all entries in the dictionary.
783 /// </summary>
784 /// <returns>An enumerator for the dictionary.</returns>
785 public IEnumerator<KeyValuePair<string, InstallPath>> GetEnumerator()
786 {
787 return this.items.GetEnumerator();
788 }
789
790 IEnumerator IEnumerable.GetEnumerator()
791 {
792 return this.items.GetEnumerator();
793 }
794 }
795
796 internal class SourcePathMap : IDictionary<string, string>
797 {
798 private const string RO_MSG =
799 "The SourcePathMap collection is read-only. " +
800 "Modify the InstallPathMap instead.";
801
802 private InstallPathMap map;
803
804 internal SourcePathMap(InstallPathMap map)
805 {
806 this.map = map;
807 }
808
809 public void Add(string key, string value)
810 {
811 throw new InvalidOperationException(RO_MSG);
812 }
813
814 public bool ContainsKey(string key)
815 {
816 return this.map.ContainsKey(key);
817 }
818
819 public ICollection<string> Keys
820 {
821 get
822 {
823 return this.map.Keys;
824 }
825 }
826
827 public bool Remove(string key)
828 {
829 throw new InvalidOperationException(RO_MSG);
830 }
831
832 public bool TryGetValue(string key, out string value)
833 {
834 InstallPath installPath;
835 if (this.map.TryGetValue(key, out installPath))
836 {
837 value = installPath.SourcePath;
838 return true;
839 }
840 else
841 {
842 value = null;
843 return false;
844 }
845 }
846
847 public ICollection<string> Values
848 {
849 get
850 {
851 List<string> values = new List<string>(this.Count);
852 foreach (KeyValuePair<string, InstallPath> entry in this.map)
853 {
854 values.Add(entry.Value.SourcePath);
855 }
856 return values;
857 }
858 }
859
860 public string this[string key]
861 {
862 get
863 {
864 string value = null;
865 this.TryGetValue(key, out value);
866 return value;
867 }
868 set
869 {
870 throw new InvalidOperationException(RO_MSG);
871 }
872 }
873
874 public void Add(KeyValuePair<string, string> item)
875 {
876 throw new InvalidOperationException(RO_MSG);
877 }
878
879 public void Clear()
880 {
881 throw new InvalidOperationException(RO_MSG);
882 }
883
884 public bool Contains(KeyValuePair<string, string> item)
885 {
886 string value = this[item.Key];
887 return value == item.Value;
888 }
889
890 public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
891 {
892 foreach (KeyValuePair<string, string> entry in this)
893 {
894 array[arrayIndex] = entry;
895 arrayIndex++;
896 }
897 }
898
899 public int Count
900 {
901 get
902 {
903 return this.map.Count;
904 }
905 }
906
907 public bool IsReadOnly
908 {
909 get
910 {
911 return true;
912 }
913 }
914
915 public bool Remove(KeyValuePair<string, string> item)
916 {
917 throw new InvalidOperationException(RO_MSG);
918 }
919
920 public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
921 {
922 foreach (KeyValuePair<string, InstallPath> entry in this.map)
923 {
924 yield return new KeyValuePair<string, string>(
925 entry.Key, entry.Value.SourcePath);
926 }
927 }
928
929 IEnumerator IEnumerable.GetEnumerator()
930 {
931 return this.GetEnumerator();
932 }
933 }
934
935 internal class TargetPathMap : IDictionary<string, string>
936 {
937 private const string RO_MSG =
938 "The TargetPathMap collection is read-only. " +
939 "Modify the InstallPathMap instead.";
940
941 private InstallPathMap map;
942
943 internal TargetPathMap(InstallPathMap map)
944 {
945 this.map = map;
946 }
947
948 public void Add(string key, string value)
949 {
950 throw new InvalidOperationException(RO_MSG);
951 }
952
953 public bool ContainsKey(string key)
954 {
955 return this.map.ContainsKey(key);
956 }
957
958 public ICollection<string> Keys
959 {
960 get
961 {
962 return this.map.Keys;
963 }
964 }
965
966 public bool Remove(string key)
967 {
968 throw new InvalidOperationException(RO_MSG);
969 }
970
971 public bool TryGetValue(string key, out string value)
972 {
973 InstallPath installPath;
974 if (this.map.TryGetValue(key, out installPath))
975 {
976 value = installPath.TargetPath;
977 return true;
978 }
979 else
980 {
981 value = null;
982 return false;
983 }
984 }
985
986 public ICollection<string> Values
987 {
988 get
989 {
990 List<string> values = new List<string>(this.Count);
991 foreach (KeyValuePair<string, InstallPath> entry in this.map)
992 {
993 values.Add(entry.Value.TargetPath);
994 }
995 return values;
996 }
997 }
998
999 public string this[string key]
1000 {
1001 get
1002 {
1003 string value = null;
1004 this.TryGetValue(key, out value);
1005 return value;
1006 }
1007 set
1008 {
1009 throw new InvalidOperationException(RO_MSG);
1010 }
1011 }
1012
1013 public void Add(KeyValuePair<string, string> item)
1014 {
1015 throw new InvalidOperationException(RO_MSG);
1016 }
1017
1018 public void Clear()
1019 {
1020 throw new InvalidOperationException(RO_MSG);
1021 }
1022
1023 public bool Contains(KeyValuePair<string, string> item)
1024 {
1025 string value = this[item.Key];
1026 return value == item.Value;
1027 }
1028
1029 public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
1030 {
1031 foreach (KeyValuePair<string, string> entry in this)
1032 {
1033 array[arrayIndex] = entry;
1034 arrayIndex++;
1035 }
1036 }
1037
1038 public int Count
1039 {
1040 get
1041 {
1042 return this.map.Count;
1043 }
1044 }
1045
1046 public bool IsReadOnly
1047 {
1048 get
1049 {
1050 return true;
1051 }
1052 }
1053
1054 public bool Remove(KeyValuePair<string, string> item)
1055 {
1056 throw new InvalidOperationException(RO_MSG);
1057 }
1058
1059 public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
1060 {
1061 foreach (KeyValuePair<string, InstallPath> entry in this.map)
1062 {
1063 yield return new KeyValuePair<string, string>(
1064 entry.Key, entry.Value.TargetPath);
1065 }
1066 }
1067
1068 IEnumerator IEnumerable.GetEnumerator()
1069 {
1070 return this.GetEnumerator();
1071 }
1072 }
1073}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/PatchPackage.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/PatchPackage.cs
new file mode 100644
index 00000000..54bd2b93
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/PatchPackage.cs
@@ -0,0 +1,259 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Package
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections;
9 using System.Globalization;
10 using System.Runtime.InteropServices;
11
12 /// <summary>
13 /// Provides access to convenient properties and operations on a patch package (.MSP).
14 /// </summary>
15 public class PatchPackage : Database
16 {
17 /// <summary>
18 /// Creates a new patch package object; opening the patch database in read-only mode.
19 /// </summary>
20 /// <param name="packagePath">Path to the patch package (.MSP)</param>
21 /// <remarks>The PatchPackage object only opens the patch database in read-only mode, because
22 /// transforms (sub-storages) cannot be read if the database is open in read-write mode.</remarks>
23 public PatchPackage(string packagePath)
24 : base(packagePath, (DatabaseOpenMode) ((int) DatabaseOpenMode.ReadOnly | 32))
25 // TODO: figure out what to do about DatabaseOpenMode.Patch
26 {
27 }
28
29 /// <summary>
30 /// Handle this event to receive status messages when operations are performed on the patch package.
31 /// </summary>
32 /// <example>
33 /// <c>patchPackage.Message += new InstallPackageMessageHandler(Console.WriteLine);</c>
34 /// </example>
35 public event InstallPackageMessageHandler Message;
36
37 /// <summary>
38 /// Sends a message to the <see cref="Message"/> event-handler.
39 /// </summary>
40 /// <param name="format">Message string, containing 0 or more format items</param>
41 /// <param name="args">Items to be formatted</param>
42 protected void LogMessage(string format, params object[] args)
43 {
44 if(this.Message != null)
45 {
46 this.Message(format, args);
47 }
48 }
49
50 /// <summary>
51 /// Gets the patch code (GUID) of the patch package.
52 /// </summary>
53 /// <remarks>
54 /// The patch code is stored in the RevisionNumber field of the patch summary information.
55 /// </remarks>
56 public string PatchCode
57 {
58 get
59 {
60 string guids = this.SummaryInfo.RevisionNumber;
61 return guids.Substring(0, guids.IndexOf('}') + 1);
62 }
63 }
64
65 /// <summary>
66 /// Gets the list of patch codes that are replaced by this patch package.
67 /// </summary>
68 /// <returns>Array of replaced patch codes (GUIDs)</returns>
69 /// <remarks>
70 /// The list of replaced patch codes is stored in the RevisionNumber field of the patch summary information.
71 /// </remarks>
72 public string[] GetReplacedPatchCodes()
73 {
74 ArrayList patchCodeList = new ArrayList();
75 string guids = this.SummaryInfo.RevisionNumber;
76 int thisGuid = guids.IndexOf('}') + 1;
77 int nextGuid = guids.IndexOf('}', thisGuid) + 1;
78 while(nextGuid > 0)
79 {
80 patchCodeList.Add(guids.Substring(thisGuid, (nextGuid - thisGuid)));
81 thisGuid = nextGuid;
82 nextGuid = guids.IndexOf('}', thisGuid) + 1;
83 }
84 return (string[]) patchCodeList.ToArray(typeof(string));
85 }
86
87 /// <summary>
88 /// Gets the list of product codes of products targeted by this patch package.
89 /// </summary>
90 /// <returns>Array of product codes (GUIDs)</returns>
91 /// <remarks>
92 /// The list of target product codes is stored in the Template field of the patch summary information.
93 /// </remarks>
94 public string[] GetTargetProductCodes()
95 {
96 string productList = this.SummaryInfo.Template;
97 return productList.Split(';');
98 }
99
100 /// <summary>
101 /// Gets the names of the transforms included in the patch package.
102 /// </summary>
103 /// <returns>Array of transform names</returns>
104 /// <remarks>
105 /// The returned list does not include the &quot;patch special transforms&quot; that are prefixed with &quot;#&quot;
106 /// <p>The list of transform names is stored in the LastSavedBy field of the patch summary information.</p>
107 /// </remarks>
108 public string[] GetTransforms()
109 {
110 return this.GetTransforms(false);
111 }
112 /// <summary>
113 /// Gets the names of the transforms included in the patch package.
114 /// </summary>
115 /// <param name="includeSpecialTransforms">Specifies whether to include the
116 /// &quot;patch special transforms&quot; that are prefixed with &quot;#&quot;</param>
117 /// <returns>Array of transform names</returns>
118 /// <remarks>
119 /// The list of transform names is stored in the LastSavedBy field of the patch summary information.
120 /// </remarks>
121 public string[] GetTransforms(bool includeSpecialTransforms)
122 {
123 ArrayList transformArray = new ArrayList();
124 string transformList = this.SummaryInfo.LastSavedBy;
125 foreach(string transform in transformList.Split(';', ':'))
126 {
127 if(transform.Length != 0 && (includeSpecialTransforms || !transform.StartsWith("#", StringComparison.Ordinal)))
128 {
129 transformArray.Add(transform);
130 }
131 }
132 return (string[]) transformArray.ToArray(typeof(string));
133 }
134
135 /// <summary>
136 /// Gets information about the transforms included in the patch package.
137 /// </summary>
138 /// <returns>Array containing information about each transform</returns>
139 /// <remarks>
140 /// The returned info does not include the &quot;patch special transforms&quot; that are prefixed with &quot;#&quot;
141 /// </remarks>
142 public TransformInfo[] GetTransformsInfo()
143 {
144 return this.GetTransformsInfo(false);
145 }
146
147 /// <summary>
148 /// Gets information about the transforms included in the patch package.
149 /// </summary>
150 /// <param name="includeSpecialTransforms">Specifies whether to include the
151 /// &quot;patch special transforms&quot; that are prefixed with &quot;#&quot;</param>
152 /// <returns>Array containing information about each transform</returns>
153 public TransformInfo[] GetTransformsInfo(bool includeSpecialTransforms)
154 {
155 string[] transforms = this.GetTransforms(includeSpecialTransforms);
156 ArrayList transformInfoArray = new ArrayList(transforms.Length);
157 foreach(string transform in transforms)
158 {
159 transformInfoArray.Add(this.GetTransformInfo(transform));
160 }
161 return (TransformInfo[]) transformInfoArray.ToArray(typeof(TransformInfo));
162 }
163
164 /// <summary>
165 /// Gets information about a transforms included in the patch package.
166 /// </summary>
167 /// <param name="transform">Name of the transform to extract; this may optionally be a
168 /// special transform prefixed by &quot;#&quot;</param>
169 /// <returns>Information about the transform</returns>
170 public TransformInfo GetTransformInfo(string transform)
171 {
172 string tempTransformFile = null;
173 try
174 {
175 tempTransformFile = Path.GetTempFileName();
176 this.ExtractTransform(transform, tempTransformFile);
177 using(SummaryInfo transformSummInfo = new SummaryInfo(tempTransformFile, false))
178 {
179 return new TransformInfo(transform, transformSummInfo);
180 }
181 }
182 finally
183 {
184 if(tempTransformFile != null && File.Exists(tempTransformFile))
185 {
186 File.Delete(tempTransformFile);
187 }
188 }
189 }
190
191 /// <summary>
192 /// Analyzes the transforms included in the patch package to find the ones that
193 /// are applicable to an install package.
194 /// </summary>
195 /// <param name="installPackage">The install package to validate the transforms against</param>
196 /// <returns>Array of valid transform names</returns>
197 /// <remarks>
198 /// The returned list does not include the &quot;patch special transforms&quot; that
199 /// are prefixed with &quot;#&quot; If a transform is valid, then its corresponding
200 /// special transform is assumed to be valid as well.
201 /// </remarks>
202 public string[] GetValidTransforms(InstallPackage installPackage)
203 {
204 ArrayList transformArray = new ArrayList();
205 string transformList = this.SummaryInfo.LastSavedBy;
206 foreach(string transform in transformList.Split(';', ':'))
207 {
208 if(transform.Length != 0 && !transform.StartsWith("#", StringComparison.Ordinal))
209 {
210 this.LogMessage("Checking validity of transform {0}", transform);
211 string tempTransformFile = null;
212 try
213 {
214 tempTransformFile = Path.GetTempFileName();
215 this.ExtractTransform(transform, tempTransformFile);
216 if(installPackage.IsTransformValid(tempTransformFile))
217 {
218 this.LogMessage("Found valid transform: {0}", transform);
219 transformArray.Add(transform);
220 }
221 }
222 finally
223 {
224 if(tempTransformFile != null && File.Exists(tempTransformFile))
225 {
226 try { File.Delete(tempTransformFile); }
227 catch(IOException) { }
228 }
229 }
230 }
231 }
232 return (string[]) transformArray.ToArray(typeof(string));
233 }
234
235 /// <summary>
236 /// Extracts a transform (.MST) from a patch package.
237 /// </summary>
238 /// <param name="transform">Name of the transform to extract; this may optionally be a
239 /// special transform prefixed by &quot;#&quot;</param>
240 /// <param name="extractFile">Location where the transform will be extracted</param>
241 public void ExtractTransform(string transform, string extractFile)
242 {
243 using(View stgView = this.OpenView("SELECT `Name`, `Data` FROM `_Storages` WHERE `Name` = '{0}'", transform))
244 {
245 stgView.Execute();
246 Record stgRec = stgView.Fetch();
247 if(stgRec == null)
248 {
249 this.LogMessage("Transform not found: {0}", transform);
250 throw new InstallerException("Transform not found: " + transform);
251 }
252 using(stgRec)
253 {
254 stgRec.GetStream("Data", extractFile);
255 }
256 }
257 }
258 }
259}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/TransformInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/TransformInfo.cs
new file mode 100644
index 00000000..0bf3d3f9
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/TransformInfo.cs
@@ -0,0 +1,154 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller.Package
4{
5 using System;
6 using System.IO;
7 using System.Globalization;
8
9 /// <summary>
10 /// Contains properties of a transform package (.MST).
11 /// </summary>
12 public class TransformInfo
13 {
14 /// <summary>
15 /// Reads transform information from a transform package.
16 /// </summary>
17 /// <param name="mstFile">Path to a transform package (.MST file).</param>
18 public TransformInfo(string mstFile)
19 {
20 this.name = Path.GetFileName(mstFile);
21 using (SummaryInfo transformSummInfo = new SummaryInfo(mstFile, false))
22 {
23 this.DecodeSummaryInfo(transformSummInfo);
24 }
25 }
26
27 /// <summary>
28 /// Reads transform information from the summary information of a transform package.
29 /// </summary>
30 /// <param name="name">Filename of the transform (optional).</param>
31 /// <param name="transformSummaryInfo">Handle to the summary information of a transform package (.MST file).</param>
32 public TransformInfo(string name, SummaryInfo transformSummaryInfo)
33 {
34 this.name = name;
35 this.DecodeSummaryInfo(transformSummaryInfo);
36 }
37
38 private void DecodeSummaryInfo(SummaryInfo transformSummaryInfo)
39 {
40 try
41 {
42 string[] rev = transformSummaryInfo.RevisionNumber.Split(new char[] { ';' }, 3);
43 this.targetProductCode = rev[0].Substring(0, 38);
44 this.targetProductVersion = rev[0].Substring(38);
45 this.upgradeProductCode = rev[1].Substring(0, 38);
46 this.upgradeProductVersion = rev[1].Substring(38);
47 this.upgradeCode = rev[2];
48
49 string[] templ = transformSummaryInfo.Template.Split(new Char[] { ';' }, 2);
50 this.targetPlatform = templ[0];
51 this.targetLanguage = 0;
52 if (templ.Length >= 2 && templ[1].Length > 0)
53 {
54 this.targetLanguage = Int32.Parse(templ[1], CultureInfo.InvariantCulture.NumberFormat);
55 }
56
57 this.validateFlags = (TransformValidations) transformSummaryInfo.CharacterCount;
58 }
59 catch (Exception ex)
60 {
61 throw new InstallerException("Invalid transform summary info", ex);
62 }
63 }
64
65 /// <summary>
66 /// Gets the filename of the transform.
67 /// </summary>
68 public string Name
69 {
70 get { return this.name; }
71 }
72 private string name;
73
74 /// <summary>
75 /// Gets the target product code of the transform.
76 /// </summary>
77 public string TargetProductCode
78 {
79 get { return this.targetProductCode; }
80 }
81 private string targetProductCode;
82
83 /// <summary>
84 /// Gets the target product version of the transform.
85 /// </summary>
86 public string TargetProductVersion
87 {
88 get { return this.targetProductVersion; }
89 }
90 private string targetProductVersion;
91
92 /// <summary>
93 /// Gets the upgrade product code of the transform.
94 /// </summary>
95 public string UpgradeProductCode
96 {
97 get { return this.upgradeProductCode; }
98 }
99 private string upgradeProductCode;
100
101 /// <summary>
102 /// Gets the upgrade product version of the transform.
103 /// </summary>
104 public string UpgradeProductVersion
105 {
106 get { return this.upgradeProductVersion; }
107 }
108 private string upgradeProductVersion;
109
110 /// <summary>
111 /// Gets the upgrade code of the transform.
112 /// </summary>
113 public string UpgradeCode
114 {
115 get { return this.upgradeCode; }
116 }
117 private string upgradeCode;
118
119 /// <summary>
120 /// Gets the target platform of the transform.
121 /// </summary>
122 public string TargetPlatform
123 {
124 get { return this.targetPlatform; }
125 }
126 private string targetPlatform;
127
128 /// <summary>
129 /// Gets the target language of the transform, or 0 if the transform is language-neutral.
130 /// </summary>
131 public int TargetLanguage
132 {
133 get { return this.targetLanguage; }
134 }
135 private int targetLanguage;
136
137 /// <summary>
138 /// Gets the validation flags specified when the transform was generated.
139 /// </summary>
140 public TransformValidations Validations
141 {
142 get { return this.validateFlags; }
143 }
144 private TransformValidations validateFlags;
145
146 /// <summary>
147 /// Returns the name of the transform.
148 /// </summary>
149 public override string ToString()
150 {
151 return (this.Name != null ? this.Name : "MST");
152 }
153 }
154}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/WixToolset.Dtf.WindowsInstaller.Package.csproj b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/WixToolset.Dtf.WindowsInstaller.Package.csproj
new file mode 100644
index 00000000..28ded687
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Package/WixToolset.Dtf.WindowsInstaller.Package.csproj
@@ -0,0 +1,23 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.WindowsInstaller</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.WindowsInstaller.Package</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Extended managed libraries for Windows Installer</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <ProjectReference Include="..\WixToolset.Dtf.WindowsInstaller\WixToolset.Dtf.WindowsInstaller.csproj" />
15 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
16 <ProjectReference Include="..\WixToolset.Dtf.Compression.Cab\WixToolset.Dtf.Compression.Cab.csproj" />
17 </ItemGroup>
18
19 <ItemGroup>
20 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
21 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
22 </ItemGroup>
23</Project>
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnCollection.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnCollection.cs
new file mode 100644
index 00000000..9a452da1
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnCollection.cs
@@ -0,0 +1,333 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Collections;
8 using System.Collections.Generic;
9 using System.Text;
10 using System.Diagnostics.CodeAnalysis;
11
12 /// <summary>
13 /// Collection of column information related to a <see cref="TableInfo"/> or
14 /// <see cref="View"/>.
15 /// </summary>
16 public sealed class ColumnCollection : ICollection<ColumnInfo>
17 {
18 private IList<ColumnInfo> columns;
19 private string formatString;
20
21 /// <summary>
22 /// Creates a new ColumnCollection based on a specified list of columns.
23 /// </summary>
24 /// <param name="columns">columns to be added to the new collection</param>
25 public ColumnCollection(ICollection<ColumnInfo> columns)
26 {
27 if (columns == null)
28 {
29 throw new ArgumentNullException("columns");
30 }
31
32 this.columns = new List<ColumnInfo>(columns);
33 }
34
35 /// <summary>
36 /// Creates a new ColumnCollection that is associated with a database table.
37 /// </summary>
38 /// <param name="view">view that contains the columns</param>
39 internal ColumnCollection(View view)
40 {
41 if (view == null)
42 {
43 throw new ArgumentNullException("view");
44 }
45
46 this.columns = ColumnCollection.GetViewColumns(view);
47 }
48
49 /// <summary>
50 /// Gets the number of columns in the collection.
51 /// </summary>
52 /// <value>number of columns in the collection</value>
53 public int Count
54 {
55 get
56 {
57 return this.columns.Count;
58 }
59 }
60
61 /// <summary>
62 /// Gets a boolean value indicating whether the collection is read-only.
63 /// A ColumnCollection is read-only if it is associated with a <see cref="View"/>
64 /// or a read-only <see cref="Database"/>.
65 /// </summary>
66 /// <value>read-only status of the collection</value>
67 public bool IsReadOnly
68 {
69 get
70 {
71 return true;
72 }
73 }
74
75 /// <summary>
76 /// Gets information about a specific column in the collection.
77 /// </summary>
78 /// <param name="columnIndex">1-based index into the column collection</param>
79 /// <exception cref="ArgumentOutOfRangeException"><paramref name="columnIndex"/> is less
80 /// than 1 or greater than the number of columns in the collection</exception>
81 public ColumnInfo this[int columnIndex]
82 {
83 get
84 {
85 if (columnIndex >= 0 && columnIndex < this.columns.Count)
86 {
87 return this.columns[columnIndex];
88 }
89 else
90 {
91 throw new ArgumentOutOfRangeException("columnIndex");
92 }
93 }
94 }
95
96 /// <summary>
97 /// Gets information about a specific column in the collection.
98 /// </summary>
99 /// <param name="columnName">case-sensitive name of a column collection</param>
100 /// <exception cref="ArgumentOutOfRangeException"><paramref name="columnName"/> does
101 /// not exist in the collection</exception>
102 public ColumnInfo this[string columnName]
103 {
104 get
105 {
106 if (String.IsNullOrEmpty(columnName))
107 {
108 throw new ArgumentNullException("columnName");
109 }
110
111 foreach (ColumnInfo colInfo in this.columns)
112 {
113 if (colInfo.Name == columnName)
114 {
115 return colInfo;
116 }
117 }
118
119 throw new ArgumentOutOfRangeException("columnName");
120 }
121 }
122
123 /// <summary>
124 /// Not supported because the collection is read-only.
125 /// </summary>
126 /// <param name="item">information about the column being added</param>
127 /// <exception cref="InvalidOperationException">the collection is read-only</exception>
128 public void Add(ColumnInfo item)
129 {
130 throw new InvalidOperationException();
131 }
132
133 /// <summary>
134 /// Not supported because the collection is read-only.
135 /// </summary>
136 /// <exception cref="InvalidOperationException">the collection is read-only</exception>
137 public void Clear()
138 {
139 throw new InvalidOperationException();
140 }
141
142 /// <summary>
143 /// Checks if a column with a given name exists in the collection.
144 /// </summary>
145 /// <param name="columnName">case-sensitive name of the column to look for</param>
146 /// <returns>true if the column exists in the collection, false otherwise</returns>
147 public bool Contains(string columnName)
148 {
149 return this.IndexOf(columnName) >= 0;
150 }
151
152 /// <summary>
153 /// Checks if a column with a given name exists in the collection.
154 /// </summary>
155 /// <param name="column">column to look for, with case-sensitive name</param>
156 /// <returns>true if the column exists in the collection, false otherwise</returns>
157 bool ICollection<ColumnInfo>.Contains(ColumnInfo column)
158 {
159 return this.Contains(column.Name);
160 }
161
162 /// <summary>
163 /// Gets the index of a column within the collection.
164 /// </summary>
165 /// <param name="columnName">case-sensitive name of the column to look for</param>
166 /// <returns>0-based index of the column, or -1 if not found</returns>
167 public int IndexOf(string columnName)
168 {
169 if (String.IsNullOrEmpty(columnName))
170 {
171 throw new ArgumentNullException("columnName");
172 }
173
174 for (int index = 0; index < this.columns.Count; index++)
175 {
176 if (this.columns[index].Name == columnName)
177 {
178 return index;
179 }
180 }
181 return -1;
182 }
183
184 /// <summary>
185 /// Copies the columns from this collection into an array.
186 /// </summary>
187 /// <param name="array">destination array to be filed</param>
188 /// <param name="arrayIndex">offset into the destination array where copying begins</param>
189 public void CopyTo(ColumnInfo[] array, int arrayIndex)
190 {
191 if (array == null)
192 {
193 throw new ArgumentNullException("array");
194 }
195
196 this.columns.CopyTo(array, arrayIndex);
197 }
198
199 /// <summary>
200 /// Not supported because the collection is read-only.
201 /// </summary>
202 /// <param name="column">column to remove</param>
203 /// <returns>true if the column was removed, false if it was not found</returns>
204 /// <exception cref="InvalidOperationException">the collection is read-only</exception>
205 [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "column")]
206 [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
207 bool ICollection<ColumnInfo>.Remove(ColumnInfo column)
208 {
209 throw new InvalidOperationException();
210 }
211
212 /// <summary>
213 /// Gets an enumerator over the columns in the collection.
214 /// </summary>
215 /// <returns>An enumerator of ColumnInfo objects.</returns>
216 public IEnumerator<ColumnInfo> GetEnumerator()
217 {
218 return this.columns.GetEnumerator();
219 }
220
221 /// <summary>
222 /// Gets a string suitable for printing all the values of a record containing these columns.
223 /// </summary>
224 public string FormatString
225 {
226 get
227 {
228 if (this.formatString == null)
229 {
230 this.formatString = CreateFormatString(this.columns);
231 }
232 return this.formatString;
233 }
234 }
235
236 private static string CreateFormatString(IList<ColumnInfo> columns)
237 {
238 StringBuilder sb = new StringBuilder();
239 for (int i = 0; i < columns.Count; i++)
240 {
241 if (columns[i].Type == typeof(Stream))
242 {
243 sb.AppendFormat("{0} = [Binary Data]", columns[i].Name);
244 }
245 else
246 {
247 sb.AppendFormat("{0} = [{1}]", columns[i].Name, i + 1);
248 }
249
250 if (i < columns.Count - 1)
251 {
252 sb.Append(", ");
253 }
254 }
255 return sb.ToString();
256 }
257
258 /// <summary>
259 /// Gets an enumerator over the columns in the collection.
260 /// </summary>
261 /// <returns>An enumerator of ColumnInfo objects.</returns>
262 IEnumerator IEnumerable.GetEnumerator()
263 {
264 return this.GetEnumerator();
265 }
266
267 /// <summary>
268 /// Creates ColumnInfo objects for the associated view.
269 /// </summary>
270 /// <returns>dynamically-generated list of columns</returns>
271 private static IList<ColumnInfo> GetViewColumns(View view)
272 {
273 IList<string> columnNames = ColumnCollection.GetViewColumns(view, false);
274 IList<string> columnTypes = ColumnCollection.GetViewColumns(view, true);
275
276 int count = columnNames.Count;
277 if (columnTypes[count - 1] == "O0")
278 {
279 // Weird.. the "_Tables" table returns a second column with type "O0" -- ignore it.
280 count--;
281 }
282
283 IList<ColumnInfo> columnsList = new List<ColumnInfo>(count);
284 for (int i = 0; i < count; i++)
285 {
286 columnsList.Add(new ColumnInfo(columnNames[i], columnTypes[i]));
287 }
288
289 return columnsList;
290 }
291
292 /// <summary>
293 /// Gets a list of column names or column-definition-strings for the
294 /// associated view.
295 /// </summary>
296 /// <param name="view">the view to that defines the columns</param>
297 /// <param name="types">true to return types (column definition strings),
298 /// false to return names</param>
299 /// <returns>list of column names or types</returns>
300 private static IList<string> GetViewColumns(View view, bool types)
301 {
302 int recordHandle;
303 int typesFlag = types ? 1 : 0;
304 uint ret = RemotableNativeMethods.MsiViewGetColumnInfo(
305 (int) view.Handle, (uint) typesFlag, out recordHandle);
306 if (ret != 0)
307 {
308 throw InstallerException.ExceptionFromReturnCode(ret);
309 }
310
311 using (Record rec = new Record((IntPtr) recordHandle, true, null))
312 {
313 int count = rec.FieldCount;
314 IList<string> columnsList = new List<string>(count);
315
316 // Since we must be getting all strings of limited length,
317 // this code is faster than calling rec.GetString(field).
318 for (int field = 1; field <= count; field++)
319 {
320 uint bufSize = 256;
321 StringBuilder buf = new StringBuilder((int) bufSize);
322 ret = RemotableNativeMethods.MsiRecordGetString((int) rec.Handle, (uint) field, buf, ref bufSize);
323 if (ret != 0)
324 {
325 throw InstallerException.ExceptionFromReturnCode(ret);
326 }
327 columnsList.Add(buf.ToString());
328 }
329 return columnsList;
330 }
331 }
332 }
333}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnEnums.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnEnums.cs
new file mode 100644
index 00000000..ad0a945b
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnEnums.cs
@@ -0,0 +1,689 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Diagnostics.CodeAnalysis;
7
8 // Enumerations are in alphabetical order.
9
10 /// <summary>
11 /// Available values for the Attributes column of the Component table.
12 /// </summary>
13 [Flags]
14 public enum ComponentAttributes : int
15 {
16 /// <summary>
17 /// Local only - Component cannot be run from source.
18 /// </summary>
19 /// <remarks><p>
20 /// Set this value for all components belonging to a feature to prevent the feature from being run-from-network or
21 /// run-from-source. Note that if a feature has no components, the feature always shows run-from-source and
22 /// run-from-my-computer as valid options.
23 /// </p></remarks>
24 None = 0x0000,
25
26 /// <summary>
27 /// Component can only be run from source.
28 /// </summary>
29 /// <remarks><p>
30 /// Set this bit for all components belonging to a feature to prevent the feature from being run-from-my-computer.
31 /// Note that if a feature has no components, the feature always shows run-from-source and run-from-my-computer
32 /// as valid options.
33 /// </p></remarks>
34 SourceOnly = 0x0001,
35
36 /// <summary>
37 /// Component can run locally or from source.
38 /// </summary>
39 Optional = 0x0002,
40
41 /// <summary>
42 /// If this bit is set, the value in the KeyPath column is used as a key into the Registry table.
43 /// </summary>
44 /// <remarks><p>
45 /// If the Value field of the corresponding record in the Registry table is null, the Name field in that record
46 /// must not contain "+", "-", or "*". For more information, see the description of the Name field in Registry
47 /// table.
48 /// <p>Setting this bit is recommended for registry entries written to the HKCU hive. This ensures the installer
49 /// writes the necessary HKCU registry entries when there are multiple users on the same machine.</p>
50 /// </p></remarks>
51 RegistryKeyPath = 0x0004,
52
53 /// <summary>
54 /// If this bit is set, the installer increments the reference count in the shared DLL registry of the component's
55 /// key file. If this bit is not set, the installer increments the reference count only if the reference count
56 /// already exists.
57 /// </summary>
58 SharedDllRefCount = 0x0008,
59
60 /// <summary>
61 /// If this bit is set, the installer does not remove the component during an uninstall. The installer registers
62 /// an extra system client for the component in the Windows Installer registry settings.
63 /// </summary>
64 Permanent = 0x0010,
65
66 /// <summary>
67 /// If this bit is set, the value in the KeyPath column is a key into the ODBCDataSource table.
68 /// </summary>
69 OdbcDataSource = 0x0020,
70
71 /// <summary>
72 /// If this bit is set, the installer reevaluates the value of the statement in the Condition column upon a reinstall.
73 /// If the value was previously False and has changed to true, the installer installs the component. If the value
74 /// was previously true and has changed to false, the installer removes the component even if the component has
75 /// other products as clients.
76 /// </summary>
77 Transitive = 0x0040,
78
79 /// <summary>
80 /// If this bit is set, the installer does not install or reinstall the component if a key path file or a key path
81 /// registry entry for the component already exists. The application does register itself as a client of the component.
82 /// </summary>
83 /// <remarks><p>
84 /// Use this flag only for components that are being registered by the Registry table. Do not use this flag for
85 /// components registered by the AppId, Class, Extension, ProgId, MIME, and Verb tables.
86 /// </p></remarks>
87 NeverOverwrite = 0x0080,
88
89 /// <summary>
90 /// Set this bit to mark this as a 64-bit component. This attribute facilitates the installation of packages that
91 /// include both 32-bit and 64-bit components. If this bit is not set, the component is registered as a 32-bit component.
92 /// </summary>
93 /// <remarks><p>
94 /// If this is a 64-bit component replacing a 32-bit component, set this bit and assign a new GUID in the
95 /// ComponentId column.
96 /// </p></remarks>
97 SixtyFourBit = 0x0100,
98
99 /// <summary>
100 /// Set this bit to disable registry reflection on all existing and new registry keys affected by this component.
101 /// </summary>
102 /// <remarks><p>
103 /// If this bit is set, the Windows Installer calls the RegDisableReflectionKey on each key being accessed by the component.
104 /// This bit is available with Windows Installer version 4.0 and is ignored on 32-bit systems.
105 /// </p></remarks>
106 DisableRegistryReflection = 0x0200,
107
108 /// <summary>
109 /// [MSI 4.5] Set this bit for a component in a patch package to prevent leaving orphan components on the computer.
110 /// </summary>
111 /// <remarks><p>
112 /// If a subsequent patch is installed, marked with the SupersedeEarlier flag in its MsiPatchSequence
113 /// table to supersede the first patch, Windows Installer 4.5 can unregister and uninstall components marked with the
114 /// UninstallOnSupersedence value. If the component is not marked with this bit, installation of a superseding patch can leave
115 /// behind an unused component on the computer.
116 /// </p></remarks>
117 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Supersedence")]
118 UninstallOnSupersedence = 0x0400,
119
120 /// <summary>
121 /// [MSI 4.5] If a component is marked with this attribute value in at least one package installed on the system,
122 /// the installer treats the component as marked in all packages. If a package that shares the marked component
123 /// is uninstalled, Windows Installer 4.5 can continue to share the highest version of the component on the system,
124 /// even if that highest version was installed by the package that is being uninstalled.
125 /// </summary>
126 Shared = 0x0800,
127 }
128
129 /// <summary>
130 /// Defines flags for the Attributes column of the Control table.
131 /// </summary>
132 [Flags]
133 public enum ControlAttributes : int
134 {
135 /// <summary>If this bit is set, the control is visible on the dialog box.</summary>
136 Visible = 0x00000001,
137
138 /// <summary>specifies if the given control is enabled or disabled. Most controls appear gray when disabled.</summary>
139 Enabled = 0x00000002,
140
141 /// <summary>If this bit is set, the control is displayed with a sunken, three dimensional look.</summary>
142 Sunken = 0x00000004,
143
144 /// <summary>The Indirect control attribute specifies whether the value displayed or changed by this control is referenced indirectly.</summary>
145 Indirect = 0x00000008,
146
147 /// <summary>If this bit is set on a control, the associated property specified in the Property column of the Control table is an integer.</summary>
148 Integer = 0x00000010,
149
150 /// <summary>If this bit is set the text in the control is displayed in a right-to-left reading order.</summary>
151 RightToLeftReadingOrder = 0x00000020,
152
153 /// <summary>If this style bit is set, text in the control is aligned to the right.</summary>
154 RightAligned = 0x00000040,
155
156 /// <summary>If this bit is set, the scroll bar is located on the left side of the control, otherwise it is on the right.</summary>
157 LeftScroll = 0x00000080,
158
159 /// <summary>This is a combination of the RightToLeftReadingOrder, RightAligned, and LeftScroll attributes.</summary>
160 Bidirectional = RightToLeftReadingOrder | RightAligned | LeftScroll,
161
162 /// <summary>If this bit is set on a text control, the control is displayed transparently with the background showing through the control where there are no characters.</summary>
163 Transparent = 0x00010000,
164
165 /// <summary>If this bit is set on a text control, the occurrence of the character "&amp;" in a text string is displayed as itself.</summary>
166 NoPrefix = 0x00020000,
167
168 /// <summary>If this bit is set the text in the control is displayed on a single line.</summary>
169 NoWrap = 0x00040000,
170
171 /// <summary>If this bit is set for a text control, the control will automatically attempt to format the displayed text as a number representing a count of bytes.</summary>
172 FormatSize = 0x00080000,
173
174 /// <summary>If this bit is set, fonts are created using the user's default UI code page. Otherwise it is created using the database code page.</summary>
175 UsersLanguage = 0x00100000,
176
177 /// <summary>If this bit is set on an Edit control, the installer creates a multiple line edit control with a vertical scroll bar.</summary>
178 Multiline = 0x00010000,
179
180 /// <summary>This attribute creates an edit control for entering passwords. The control displays each character as an asterisk (*) as they are typed into the control.</summary>
181 PasswordInput = 0x00200000,
182
183 /// <summary>If this bit is set on a ProgressBar control, the bar is drawn as a series of small rectangles in Microsoft Windows 95-style. Otherwise it is drawn as a single continuous rectangle.</summary>
184 Progress95 = 0x00010000,
185
186 /// <summary>If this bit is set, the control shows removable volumes.</summary>
187 RemovableVolume = 0x00010000,
188
189 /// <summary>If this bit is set, the control shows fixed internal hard drives.</summary>
190 FixedVolume = 0x00020000,
191
192 /// <summary>If this bit is set, the control shows remote volumes.</summary>
193 RemoteVolume = 0x00040000,
194
195 /// <summary>If this bit is set, the control shows CD-ROM volumes.</summary>
196 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Cdrom")]
197 CdromVolume = 0x00080000,
198
199 /// <summary>If this bit is set, the control shows RAM disk volumes.</summary>
200 RamDiskVolume = 0x00100000,
201
202 /// <summary>If this bit is set, the control shows floppy volumes.</summary>
203 FloppyVolume = 0x00200000,
204
205 /// <summary>Specifies whether or not the rollback backup files are included in the costs displayed by the VolumeCostList control.</summary>
206 ShowRollbackCost = 0x00400000,
207
208 /// <summary>If this bit is set, the items listed in the control are displayed in a specified order. Otherwise, items are displayed in alphabetical order.</summary>
209 Sorted = 0x00010000,
210
211 /// <summary>If this bit is set on a combo box, the edit field is replaced by a static text field. This prevents a user from entering a new value and requires the user to choose only one of the predefined values.</summary>
212 ComboList = 0x00020000,
213
214 //ImageHandle = 0x00010000,
215
216 /// <summary>If this bit is set on a check box or a radio button group, the button is drawn with the appearance of a push button, but its logic stays the same.</summary>
217 PushLike = 0x00020000,
218
219 /// <summary>If this bit is set, the text in the control is replaced by a bitmap image. The Text column in the Control table is a foreign key into the Binary table.</summary>
220 Bitmap = 0x00040000,
221
222 /// <summary>If this bit is set, text is replaced by an icon image and the Text column in the Control table is a foreign key into the Binary table.</summary>
223 Icon = 0x00080000,
224
225 /// <summary>If this bit is set, the picture is cropped or centered in the control without changing its shape or size.</summary>
226 FixedSize = 0x00100000,
227
228 /// <summary>Specifies which size of the icon image to load. If none of the bits are set, the first image is loaded.</summary>
229 IconSize16 = 0x00200000,
230
231 /// <summary>Specifies which size of the icon image to load. If none of the bits are set, the first image is loaded.</summary>
232 IconSize32 = 0x00400000,
233
234 /// <summary>Specifies which size of the icon image to load. If none of the bits are set, the first image is loaded.</summary>
235 IconSize48 = 0x00600000,
236
237 /// <summary>If this bit is set, and the installation is not yet running with elevated privileges, the control is created with a UAC icon.</summary>
238 ElevationShield = 0x00800000,
239
240 /// <summary>If this bit is set, the RadioButtonGroup has text and a border displayed around it.</summary>
241 HasBorder = 0x01000000,
242 }
243
244 /// <summary>
245 /// Defines flags for the Type column of the CustomAction table.
246 /// </summary>
247 [SuppressMessage("Microsoft.Usage", "CA2217:DoNotMarkEnumsWithFlags")]
248 [Flags]
249 public enum CustomActionTypes : int
250 {
251 /// <summary>Unspecified custom action type.</summary>
252 None = 0x0000,
253
254 /// <summary>Target = entry point name</summary>
255 Dll = 0x0001,
256
257 /// <summary>Target = command line args</summary>
258 Exe = 0x0002,
259
260 /// <summary>Target = text string to be formatted and set into property</summary>
261 TextData = 0x0003,
262
263 /// <summary>Target = entry point name, null if none to call</summary>
264 JScript = 0x0005,
265
266 /// <summary>Target = entry point name, null if none to call</summary>
267 VBScript = 0x0006,
268
269 /// <summary>Target = property list for nested engine initialization</summary>
270 Install = 0x0007,
271
272 /// <summary>Source = File.File, file part of installation</summary>
273 SourceFile = 0x0010,
274
275 /// <summary>Source = Directory.Directory, folder containing existing file</summary>
276 Directory = 0x0020,
277
278 /// <summary>Source = Property.Property, full path to executable</summary>
279 Property = 0x0030,
280
281 /// <summary>Ignore action return status, continue running</summary>
282 Continue = 0x0040,
283
284 /// <summary>Run asynchronously</summary>
285 Async = 0x0080,
286
287 /// <summary>Skip if UI sequence already run</summary>
288 FirstSequence = 0x0100,
289
290 /// <summary>Skip if UI sequence already run in same process</summary>
291 OncePerProcess = 0x0200,
292
293 /// <summary>Run on client only if UI already run on client</summary>
294 ClientRepeat = 0x0300,
295
296 /// <summary>Queue for execution within script</summary>
297 InScript = 0x0400,
298
299 /// <summary>In conjunction with InScript: queue in Rollback script</summary>
300 Rollback = 0x0100,
301
302 /// <summary>In conjunction with InScript: run Commit ops from script on success</summary>
303 Commit = 0x0200,
304
305 /// <summary>No impersonation, run in system context</summary>
306 NoImpersonate = 0x0800,
307
308 /// <summary>Impersonate for per-machine installs on TS machines</summary>
309 TSAware = 0x4000,
310
311 /// <summary>Script requires 64bit process</summary>
312 SixtyFourBitScript = 0x1000,
313
314 /// <summary>Don't record the contents of the Target field in the log file</summary>
315 HideTarget = 0x2000,
316
317 /// <summary>The custom action runs only when a patch is being uninstalled</summary>
318 PatchUninstall = 0x8000,
319 }
320
321 /// <summary>
322 /// Defines flags for the Attributes column of the Dialog table.
323 /// </summary>
324 [Flags]
325 public enum DialogAttributes : int
326 {
327 /// <summary>If this bit is set, the dialog is originally created as visible, otherwise it is hidden.</summary>
328 Visible = 0x00000001,
329
330 /// <summary>If this bit is set, the dialog box is modal, other dialogs of the same application cannot be put on top of it, and the dialog keeps the control while it is running.</summary>
331 Modal = 0x00000002,
332
333 /// <summary>If this bit is set, the dialog box can be minimized. This bit is ignored for modal dialog boxes, which cannot be minimized.</summary>
334 Minimize = 0x00000004,
335
336 /// <summary>If this style bit is set, the dialog box will stop all other applications and no other applications can take the focus.</summary>
337 SysModal = 0x00000008,
338
339 /// <summary>If this bit is set, the other dialogs stay alive when this dialog box is created.</summary>
340 KeepModeless = 0x00000010,
341
342 /// <summary>If this bit is set, the dialog box periodically calls the installer. If the property changes, it notifies the controls on the dialog.</summary>
343 TrackDiskSpace = 0x00000020,
344
345 /// <summary>If this bit is set, the pictures on the dialog box are created with the custom palette (one per dialog received from the first control created).</summary>
346 UseCustomPalette = 0x00000040,
347
348 /// <summary>If this style bit is set the text in the dialog box is displayed in right-to-left-reading order.</summary>
349 RightToLeftReadingOrder = 0x00000080,
350
351 /// <summary>If this style bit is set, the text is aligned on the right side of the dialog box.</summary>
352 RightAligned = 0x00000100,
353
354 /// <summary>If this style bit is set, the scroll bar is located on the left side of the dialog box.</summary>
355 LeftScroll = 0x00000200,
356
357 /// <summary>This is a combination of the RightToLeftReadingOrder, RightAligned, and the LeftScroll dialog style bits.</summary>
358 Bidirectional = RightToLeftReadingOrder | RightAligned | LeftScroll,
359
360 /// <summary>If this bit is set, the dialog box is an error dialog.</summary>
361 Error = 0x00010000,
362 }
363
364 /// <summary>
365 /// Available values for the Attributes column of the Feature table.
366 /// </summary>
367 [Flags]
368 public enum FeatureAttributes : int
369 {
370 /// <summary>
371 /// Favor local - Components of this feature that are not marked for installation from source are installed locally.
372 /// </summary>
373 /// <remarks><p>
374 /// A component shared by two or more features, some of which are set to FavorLocal and some to FavorSource,
375 /// is installed locally. Components marked <see cref="ComponentAttributes.SourceOnly"/> in the Component
376 /// table are always run from the source CD/server. The bits FavorLocal and FavorSource work with features not
377 /// listed by the ADVERTISE property.
378 /// </p></remarks>
379 None = 0x0000,
380
381 /// <summary>
382 /// Components of this feature not marked for local installation are installed to run from the source
383 /// CD-ROM or server.
384 /// </summary>
385 /// <remarks><p>
386 /// A component shared by two or more features, some of which are set to FavorLocal and some to FavorSource,
387 /// is installed to run locally. Components marked <see cref="ComponentAttributes.None"/> (local-only) in the
388 /// Component table are always installed locally. The bits FavorLocal and FavorSource work with features
389 /// not listed by the ADVERTISE property.
390 /// </p></remarks>
391 FavorSource = 0x0001,
392
393 /// <summary>
394 /// Set this attribute and the state of the feature is the same as the state of the feature's parent.
395 /// You cannot use this option if the feature is located at the root of a feature tree.
396 /// </summary>
397 /// <remarks><p>
398 /// Omit this attribute and the feature state is determined according to DisallowAdvertise and
399 /// FavorLocal and FavorSource.
400 /// <p>To guarantee that the child feature's state always follows the state of its parent, even when the
401 /// child and parent are initially set to absent in the SelectionTree control, you must include both
402 /// FollowParent and UIDisallowAbsent in the attributes of the child feature.</p>
403 /// <p>Note that if you set FollowParent without setting UIDisallowAbsent, the installer cannot force
404 /// the child feature out of the absent state. In this case, the child feature matches the parent's
405 /// installation state only if the child is set to something other than absent.</p>
406 /// <p>Set FollowParent and UIDisallowAbsent to ensure a child feature follows the state of the parent feature.</p>
407 /// </p></remarks>
408 FollowParent = 0x0002,
409
410 /// <summary>
411 /// Set this attribute and the feature state is Advertise.
412 /// </summary>
413 /// <remarks><p>
414 /// If the feature is listed by the ADDDEFAULT property this bit is ignored and the feature state is determined
415 /// according to FavorLocal and FavorSource.
416 /// <p>Omit this attribute and the feature state is determined according to DisallowAdvertise and FavorLocal
417 /// and FavorSource.</p>
418 /// </p></remarks>
419 FavorAdvertise = 0x0004,
420
421 /// <summary>
422 /// Set this attribute to prevent the feature from being advertised.
423 /// </summary>
424 /// <remarks><p>
425 /// Note that this bit works only with features that are listed by the ADVERTISE property.
426 /// <p>Set this attribute and if the listed feature is not a parent or child, the feature is installed according to
427 /// FavorLocal and FavorSource.</p>
428 /// <p>Set this attribute for the parent of a listed feature and the parent is installed.</p>
429 /// <p>Set this attribute for the child of a listed feature and the state of the child is Absent.</p>
430 /// <p>Omit this attribute and if the listed feature is not a parent or child, the feature state is Advertise.</p>
431 /// <p>Omit this attribute and if the listed feature is a parent or child, the state of both features is Advertise.</p>
432 /// </p></remarks>
433 DisallowAdvertise = 0x0008,
434
435 /// <summary>
436 /// Set this attribute and the user interface does not display an option to change the feature state
437 /// to Absent. Setting this attribute forces the feature to the installation state, whether or not the
438 /// feature is visible in the UI.
439 /// </summary>
440 /// <remarks><p>
441 /// Omit this attribute and the user interface displays an option to change the feature state to Absent.
442 /// <p>Set FollowParent and UIDisallowAbsent to ensure a child feature follows the state of the parent feature.</p>
443 /// <p>Setting this attribute not only affects the UI, but also forces the feature to the install state whether
444 /// the feature is visible in the UI or not.</p>
445 /// </p></remarks>
446 UIDisallowAbsent = 0x0010,
447
448 /// <summary>
449 /// Set this attribute and advertising is disabled for the feature if the operating system shell does not
450 /// support Windows Installer descriptors.
451 /// </summary>
452 NoUnsupportedAdvertise = 0x0020,
453 }
454
455 /// <summary>
456 /// Available values for the Attributes column of the File table.
457 /// </summary>
458 [Flags]
459 public enum FileAttributes : int
460 {
461 /// <summary>No attributes.</summary>
462 None = 0x0000,
463
464 /// <summary>Read-only.</summary>
465 ReadOnly = 0x0001,
466
467 /// <summary>Hidden.</summary>
468 Hidden = 0x0002,
469
470 /// <summary>System.</summary>
471 System = 0x0004,
472
473 /// <summary>The file is vital for the proper operation of the component to which it belongs.</summary>
474 Vital = 0x0200,
475
476 /// <summary>The file contains a valid checksum. A checksum is required to repair a file that has become corrupted.</summary>
477 Checksum = 0x0400,
478
479 /// <summary>This bit must only be added by a patch and if the file is being added by the patch.</summary>
480 PatchAdded = 0x1000,
481
482 /// <summary>
483 /// The file's source type is uncompressed. If set, ignore the WordCount summary information property. If neither
484 /// Noncompressed nor Compressed are set, the compression state of the file is specified by the WordCount summary
485 /// information property. Do not set both Noncompressed and Compressed.
486 /// </summary>
487 NonCompressed = 0x2000,
488
489 /// <summary>
490 /// The file's source type is compressed. If set, ignore the WordCount summary information property. If neither
491 /// Noncompressed or Compressed are set, the compression state of the file is specified by the WordCount summary
492 /// information property. Do not set both Noncompressed and Compressed.
493 /// </summary>
494 Compressed = 0x4000,
495 }
496
497 /// <summary>
498 /// Defines values for the Action column of the IniFile and RemoveIniFile tables.
499 /// </summary>
500 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ini")]
501 public enum IniFileAction : int
502 {
503 /// <summary>Creates or updates a .ini entry.</summary>
504 AddLine = 0,
505
506 /// <summary>Creates a .ini entry only if the entry does not already exist.</summary>
507 CreateLine = 1,
508
509 /// <summary>Deletes .ini entry.</summary>
510 RemoveLine = 2,
511
512 /// <summary>Creates a new entry or appends a new comma-separated value to an existing entry.</summary>
513 AddTag = 3,
514
515 /// <summary>Deletes a tag from a .ini entry.</summary>
516 RemoveTag = 4,
517 }
518
519 /// <summary>
520 /// Defines values for the Type column of the CompLocator, IniLocator, and RegLocator tables.
521 /// </summary>
522 [Flags]
523 [SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue")]
524 public enum LocatorTypes : int
525 {
526 /// <summary>Key path is a directory.</summary>
527 Directory = 0x00000000,
528
529 /// <summary>Key path is a file name.</summary>
530 FileName = 0x00000001,
531
532 /// <summary>Key path is a registry value.</summary>
533 RawValue = 0x00000002,
534
535 /// <summary>Set this bit to have the installer search the 64-bit portion of the registry.</summary>
536 SixtyFourBit = 0x00000010,
537 }
538
539 /// <summary>
540 /// Defines values for the Root column of the Registry, RemoveRegistry, and RegLocator tables.
541 /// </summary>
542 public enum RegistryRoot : int
543 {
544 /// <summary>HKEY_CURRENT_USER for a per-user installation,
545 /// or HKEY_LOCAL_MACHINE for a per-machine installation.</summary>
546 UserOrMachine = -1,
547
548 /// <summary>HKEY_CLASSES_ROOT</summary>
549 ClassesRoot = 0,
550
551 /// <summary>HKEY_CURRENT_USER</summary>
552 CurrentUser = 1,
553
554 /// <summary>HKEY_LOCAL_MACHINE</summary>
555 LocalMachine = 2,
556
557 /// <summary>HKEY_USERS</summary>
558 Users = 3,
559 }
560
561 /// <summary>
562 /// Defines values for the InstallMode column of the RemoveFile table.
563 /// </summary>
564 [Flags]
565 public enum RemoveFileModes : int
566 {
567 /// <summary>Never remove.</summary>
568 None = 0,
569
570 /// <summary>Remove when the associated component is being installed (install state = local or source).</summary>
571 OnInstall = 1,
572
573 /// <summary>Remove when the associated component is being removed (install state = absent).</summary>
574 OnRemove = 2,
575 }
576
577 /// <summary>
578 /// Defines values for the ServiceType, StartType, and ErrorControl columns of the ServiceInstall table.
579 /// </summary>
580 [Flags]
581 public enum ServiceAttributes : int
582 {
583 /// <summary>No flags.</summary>
584 None = 0,
585
586 /// <summary>A Win32 service that runs its own process.</summary>
587 OwnProcess = 0x0010,
588
589 /// <summary>A Win32 service that shares a process.</summary>
590 ShareProcess = 0x0020,
591
592 /// <summary>A Win32 service that interacts with the desktop.
593 /// This value cannot be used alone and must be added to either
594 /// <see cref="OwnProcess"/> or <see cref="ShareProcess"/>.</summary>
595 Interactive = 0x0100,
596
597 /// <summary>Service starts during startup of the system.</summary>
598 AutoStart = 0x0002,
599
600 /// <summary>Service starts when the service control manager calls the StartService function.</summary>
601 DemandStart = 0x0003,
602
603 /// <summary>Specifies a service that can no longer be started.</summary>
604 Disabled = 0x0004,
605
606 /// <summary>Logs the error, displays a message box and continues the startup operation.</summary>
607 ErrorMessage = 0x0001,
608
609 /// <summary>Logs the error if it is possible and the system is restarted with the last configuration
610 /// known to be good. If the last-known-good configuration is being started, the startup operation fails.</summary>
611 ErrorCritical = 0x0003,
612
613 /// <summary>When combined with other error flags, specifies that the overall install should fail if
614 /// the service cannot be installed into the system.</summary>
615 ErrorControlVital = 0x8000,
616 }
617
618 /// <summary>
619 /// Defines values for the Event column of the ServiceControl table.
620 /// </summary>
621 [Flags]
622 public enum ServiceControlEvents : int
623 {
624 /// <summary>No control events.</summary>
625 None = 0x0000,
626
627 /// <summary>During an install, starts the service during the StartServices action.</summary>
628 Start = 0x0001,
629
630 /// <summary>During an install, stops the service during the StopServices action.</summary>
631 Stop = 0x0002,
632
633 /// <summary>During an install, deletes the service during the DeleteServices action.</summary>
634 Delete = 0x0008,
635
636 /// <summary>During an uninstall, starts the service during the StartServices action.</summary>
637 UninstallStart = 0x0010,
638
639 /// <summary>During an uninstall, stops the service during the StopServices action.</summary>
640 UninstallStop = 0x0020,
641
642 /// <summary>During an uninstall, deletes the service during the DeleteServices action.</summary>
643 UninstallDelete = 0x0080,
644 }
645
646 /// <summary>
647 /// Defines values for the StyleBits column of the TextStyle table.
648 /// </summary>
649 [Flags]
650 public enum TextStyles : int
651 {
652 /// <summary>Bold</summary>
653 Bold = 0x0001,
654
655 /// <summary>Italic</summary>
656 Italic = 0x0002,
657
658 /// <summary>Underline</summary>
659 Underline = 0x0004,
660
661 /// <summary>Strike out</summary>
662 Strike = 0x0008,
663 }
664
665 /// <summary>
666 /// Defines values for the Attributes column of the Upgrade table.
667 /// </summary>
668 [Flags]
669 public enum UpgradeAttributes : int
670 {
671 /// <summary>Migrates feature states by enabling the logic in the MigrateFeatureStates action.</summary>
672 MigrateFeatures = 0x0001,
673
674 /// <summary>Detects products and applications but does not remove.</summary>
675 OnlyDetect = 0x0002,
676
677 /// <summary>Continues installation upon failure to remove a product or application.</summary>
678 IgnoreRemoveFailure = 0x0004,
679
680 /// <summary>Detects the range of versions including the value in VersionMin.</summary>
681 VersionMinInclusive = 0x0100,
682
683 /// <summary>Dectects the range of versions including the value in VersionMax.</summary>
684 VersionMaxInclusive = 0x0200,
685
686 /// <summary>Detects all languages, excluding the languages listed in the Language column.</summary>
687 LanguagesExclusive = 0x0400,
688 }
689}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnInfo.cs
new file mode 100644
index 00000000..43363230
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ColumnInfo.cs
@@ -0,0 +1,297 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// Defines a single column of a table in an installer database.
13 /// </summary>
14 /// <remarks>Once created, a ColumnInfo object is immutable.</remarks>
15 public class ColumnInfo
16 {
17 private string name;
18 private Type type;
19 private int size;
20 private bool isRequired;
21 private bool isTemporary;
22 private bool isLocalizable;
23
24 /// <summary>
25 /// Creates a new ColumnInfo object from a column definition.
26 /// </summary>
27 /// <param name="name">name of the column</param>
28 /// <param name="columnDefinition">column definition string</param>
29 /// <seealso cref="ColumnDefinitionString"/>
30 public ColumnInfo(string name, string columnDefinition)
31 : this(name, typeof(String), 0, false, false, false)
32 {
33 if (name == null)
34 {
35 throw new ArgumentNullException("name");
36 }
37
38 if (columnDefinition == null)
39 {
40 throw new ArgumentNullException("columnDefinition");
41 }
42
43 switch (Char.ToLower(columnDefinition[0], CultureInfo.InvariantCulture))
44 {
45 case 'i': this.type = typeof(Int32);
46 break;
47 case 'j': this.type = typeof(Int32); this.isTemporary = true;
48 break;
49 case 'g': this.type = typeof(String); this.isTemporary = true;
50 break;
51 case 'l': this.type = typeof(String); this.isLocalizable = true;
52 break;
53 case 'o': this.type = typeof(Stream); this.isTemporary = true;
54 break;
55 case 's': this.type = typeof(String);
56 break;
57 case 'v': this.type = typeof(Stream);
58 break;
59 default: throw new InstallerException();
60 }
61
62 this.isRequired = Char.IsLower(columnDefinition[0]);
63 this.size = Int32.Parse(
64 columnDefinition.Substring(1),
65 CultureInfo.InvariantCulture.NumberFormat);
66 if (this.type == typeof(Int32) && this.size <= 2)
67 {
68 this.type = typeof(Int16);
69 }
70 }
71
72 /// <summary>
73 /// Creates a new ColumnInfo object from a list of parameters.
74 /// </summary>
75 /// <param name="name">name of the column</param>
76 /// <param name="type">type of the column; must be one of the following:
77 /// Int16, Int32, String, or Stream</param>
78 /// <param name="size">the maximum number of characters for String columns;
79 /// ignored for other column types</param>
80 /// <param name="isRequired">true if the column is required to have a non-null value</param>
81 public ColumnInfo(string name, Type type, int size, bool isRequired)
82 : this(name, type, size, isRequired, false, false)
83 {
84 }
85
86 /// <summary>
87 /// Creates a new ColumnInfo object from a list of parameters.
88 /// </summary>
89 /// <param name="name">name of the column</param>
90 /// <param name="type">type of the column; must be one of the following:
91 /// Int16, Int32, String, or Stream</param>
92 /// <param name="size">the maximum number of characters for String columns;
93 /// ignored for other column types</param>
94 /// <param name="isRequired">true if the column is required to have a non-null value</param>
95 /// <param name="isTemporary">true to if the column is only in-memory and
96 /// not persisted with the database</param>
97 /// <param name="isLocalizable">for String columns, indicates the column
98 /// is localizable; ignored for other column types</param>
99 public ColumnInfo(string name, Type type, int size, bool isRequired, bool isTemporary, bool isLocalizable)
100 {
101 if (name == null)
102 {
103 throw new ArgumentNullException("name");
104 }
105
106 if (type == typeof(Int32))
107 {
108 size = 4;
109 isLocalizable = false;
110 }
111 else if (type == typeof(Int16))
112 {
113 size = 2;
114 isLocalizable = false;
115 }
116 else if (type == typeof(String))
117 {
118 }
119 else if (type == typeof(Stream))
120 {
121 isLocalizable = false;
122 }
123 else
124 {
125 throw new ArgumentOutOfRangeException("type");
126 }
127
128 this.name = name;
129 this.type = type;
130 this.size = size;
131 this.isRequired = isRequired;
132 this.isTemporary = isTemporary;
133 this.isLocalizable = isLocalizable;
134 }
135
136 /// <summary>
137 /// Gets the name of the column.
138 /// </summary>
139 /// <value>name of the column</value>
140 public string Name
141 {
142 get { return this.name; }
143 }
144
145 /// <summary>
146 /// Gets the type of the column as a System.Type. This is one of the following: Int16, Int32, String, or Stream
147 /// </summary>
148 /// <value>type of the column</value>
149 [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")]
150 public Type Type
151 {
152 get { return this.type; }
153 }
154
155 /// <summary>
156 /// Gets the type of the column as an integer that can be cast to a System.Data.DbType. This is one of the following: Int16, Int32, String, or Binary
157 /// </summary>
158 /// <value>equivalent DbType of the column as an integer</value>
159 public int DBType
160 {
161 get
162 {
163 if (this.type == typeof(Int16)) return 10;
164 else if (this.type == typeof(Int32)) return 11;
165 else if (this.type == typeof(Stream)) return 1;
166 else return 16;
167 }
168 }
169
170 /// <summary>
171 /// Gets the size of the column.
172 /// </summary>
173 /// <value>The size of integer columns this is either 2 or 4. For string columns this is the maximum
174 /// recommended length of the string, or 0 for unlimited length. For stream columns, 0 is returned.</value>
175 public int Size
176 {
177 get { return this.size; }
178 }
179
180 /// <summary>
181 /// Gets a value indicating whether the column must be non-null when inserting a record.
182 /// </summary>
183 /// <value>required status of the column</value>
184 public bool IsRequired
185 {
186 get { return this.isRequired; }
187 }
188
189 /// <summary>
190 /// Gets a value indicating whether the column is temporary. Temporary columns are not persisted
191 /// when the database is saved to disk.
192 /// </summary>
193 /// <value>temporary status of the column</value>
194 public bool IsTemporary
195 {
196 get { return this.isTemporary; }
197 }
198
199 /// <summary>
200 /// Gets a value indicating whether the column is a string column that is localizable.
201 /// </summary>
202 /// <value>localizable status of the column</value>
203 public bool IsLocalizable
204 {
205 get { return this.isLocalizable; }
206 }
207
208 /// <summary>
209 /// Gets an SQL fragment that can be used to create this column within a CREATE TABLE statement.
210 /// </summary>
211 /// <value>SQL fragment to be used for creating the column</value>
212 /// <remarks><p>
213 /// Examples:
214 /// <list type="bullet">
215 /// <item>LONG</item>
216 /// <item>SHORT TEMPORARY</item>
217 /// <item>CHAR(0) LOCALIZABLE</item>
218 /// <item>CHAR(72) NOT NULL LOCALIZABLE</item>
219 /// <item>OBJECT</item>
220 /// </list>
221 /// </p></remarks>
222 public string SqlCreateString
223 {
224 get
225 {
226 StringBuilder s = new StringBuilder();
227 s.AppendFormat("`{0}` ", this.name);
228 if (this.type == typeof(Int16)) s.Append("SHORT");
229 else if (this.type == typeof(Int32)) s.Append("LONG");
230 else if (this.type == typeof(String)) s.AppendFormat("CHAR({0})", this.size);
231 else s.Append("OBJECT");
232 if (this.isRequired) s.Append(" NOT NULL");
233 if (this.isTemporary) s.Append(" TEMPORARY");
234 if (this.isLocalizable) s.Append(" LOCALIZABLE");
235 return s.ToString();
236 }
237 }
238
239 /// <summary>
240 /// Gets a short string defining the type and size of the column.
241 /// </summary>
242 /// <value>
243 /// The definition string consists
244 /// of a single letter representing the data type followed by the width of the column (in characters
245 /// when applicable, bytes otherwise). A width of zero designates an unbounded width (for example,
246 /// long text fields and streams). An uppercase letter indicates that null values are allowed in
247 /// the column.
248 /// </value>
249 /// <remarks><p>
250 /// <list>
251 /// <item>s? - String, variable length (?=1-255)</item>
252 /// <item>s0 - String, variable length</item>
253 /// <item>i2 - Short integer</item>
254 /// <item>i4 - Long integer</item>
255 /// <item>v0 - Binary Stream</item>
256 /// <item>g? - Temporary string (?=0-255)</item>
257 /// <item>j? - Temporary integer (?=0,1,2,4)</item>
258 /// <item>O0 - Temporary object (stream)</item>
259 /// <item>l? - Localizable string, variable length (?=1-255)</item>
260 /// <item>l0 - Localizable string, variable length</item>
261 /// </list>
262 /// </p></remarks>
263 public string ColumnDefinitionString
264 {
265 get
266 {
267 char t;
268 if (this.type == typeof(Int16) || this.type == typeof(Int32))
269 {
270 t = (this.isTemporary ? 'j' : 'i');
271 }
272 else if (this.type == typeof(String))
273 {
274 t = (this.isTemporary ? 'g' : this.isLocalizable ? 'l' : 's');
275 }
276 else
277 {
278 t = (this.isTemporary ? 'O' : 'v');
279 }
280 return String.Format(
281 CultureInfo.InvariantCulture,
282 "{0}{1}",
283 (this.isRequired ? t : Char.ToUpper(t, CultureInfo.InvariantCulture)),
284 this.size);
285 }
286 }
287
288 /// <summary>
289 /// Gets the name of the column.
290 /// </summary>
291 /// <returns>Name of the column.</returns>
292 public override string ToString()
293 {
294 return this.Name;
295 }
296 }
297}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ComponentInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ComponentInfo.cs
new file mode 100644
index 00000000..af27fd1d
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ComponentInfo.cs
@@ -0,0 +1,276 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections;
8 using System.Collections.Generic;
9
10 /// <summary>
11 /// Accessor for information about components within the context of an installation session.
12 /// </summary>
13 public sealed class ComponentInfoCollection : ICollection<ComponentInfo>
14 {
15 private Session session;
16
17 internal ComponentInfoCollection(Session session)
18 {
19 this.session = session;
20 }
21
22 /// <summary>
23 /// Gets information about a component within the context of an installation session.
24 /// </summary>
25 /// <param name="component">name of the component</param>
26 /// <returns>component object</returns>
27 public ComponentInfo this[string component]
28 {
29 get
30 {
31 return new ComponentInfo(this.session, component);
32 }
33 }
34
35 void ICollection<ComponentInfo>.Add(ComponentInfo item)
36 {
37 throw new InvalidOperationException();
38 }
39
40 void ICollection<ComponentInfo>.Clear()
41 {
42 throw new InvalidOperationException();
43 }
44
45 /// <summary>
46 /// Checks if the collection contains a component.
47 /// </summary>
48 /// <param name="component">name of the component</param>
49 /// <returns>true if the component is in the collection, else false</returns>
50 public bool Contains(string component)
51 {
52 return this.session.Database.CountRows(
53 "Component", "`Component` = '" + component + "'") == 1;
54 }
55
56 bool ICollection<ComponentInfo>.Contains(ComponentInfo item)
57 {
58 return item != null && this.Contains(item.Name);
59 }
60
61 /// <summary>
62 /// Copies the features into an array.
63 /// </summary>
64 /// <param name="array">array that receives the features</param>
65 /// <param name="arrayIndex">offset into the array</param>
66 public void CopyTo(ComponentInfo[] array, int arrayIndex)
67 {
68 foreach (ComponentInfo component in this)
69 {
70 array[arrayIndex++] = component;
71 }
72 }
73
74 /// <summary>
75 /// Gets the number of components defined for the product.
76 /// </summary>
77 public int Count
78 {
79 get
80 {
81 return this.session.Database.CountRows("Component");
82 }
83 }
84
85 bool ICollection<ComponentInfo>.IsReadOnly
86 {
87 get
88 {
89 return true;
90 }
91 }
92
93 bool ICollection<ComponentInfo>.Remove(ComponentInfo item)
94 {
95 throw new InvalidOperationException();
96 }
97
98 /// <summary>
99 /// Enumerates the components in the collection.
100 /// </summary>
101 /// <returns>an enumerator over all features in the collection</returns>
102 public IEnumerator<ComponentInfo> GetEnumerator()
103 {
104 using (View compView = this.session.Database.OpenView(
105 "SELECT `Component` FROM `Component`"))
106 {
107 compView.Execute();
108
109 foreach (Record compRec in compView) using (compRec)
110 {
111 string comp = compRec.GetString(1);
112 yield return new ComponentInfo(this.session, comp);
113 }
114 }
115 }
116
117 IEnumerator IEnumerable.GetEnumerator()
118 {
119 return this.GetEnumerator();
120 }
121 }
122
123 /// <summary>
124 /// Provides access to information about a component within the context of an installation session.
125 /// </summary>
126 public class ComponentInfo
127 {
128 private Session session;
129 private string name;
130
131 internal ComponentInfo(Session session, string name)
132 {
133 this.session = session;
134 this.name = name;
135 }
136
137 /// <summary>
138 /// Gets the name of the component (primary key in the Component table).
139 /// </summary>
140 public string Name
141 {
142 get
143 {
144 return this.name;
145 }
146 }
147
148 /// <summary>
149 /// Gets the current install state of the designated Component.
150 /// </summary>
151 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
152 /// <exception cref="ArgumentException">an unknown Component was requested</exception>
153 /// <remarks><p>
154 /// Win32 MSI API:
155 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetcomponentstate.asp">MsiGetComponentState</a>
156 /// </p></remarks>
157 public InstallState CurrentState
158 {
159 get
160 {
161 int installedState, actionState;
162 uint ret = RemotableNativeMethods.MsiGetComponentState((int) this.session.Handle, this.name, out installedState, out actionState);
163 if (ret != 0)
164 {
165 if (ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT)
166 {
167 throw InstallerException.ExceptionFromReturnCode(ret, this.name);
168 }
169 else
170 {
171 throw InstallerException.ExceptionFromReturnCode(ret);
172 }
173 }
174 return (InstallState) installedState;
175 }
176 }
177
178 /// <summary>
179 /// Gets or sets the action state of the designated Component.
180 /// </summary>
181 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
182 /// <exception cref="ArgumentException">an unknown Component was requested</exception>
183 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
184 /// <remarks><p>
185 /// Win32 MSI APIs:
186 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetcomponentstate.asp">MsiGetComponentState</a>,
187 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetcomponentstate.asp">MsiSetComponentState</a>
188 /// </p></remarks>
189 public InstallState RequestState
190 {
191 get
192 {
193 int installedState, actionState;
194 uint ret = RemotableNativeMethods.MsiGetComponentState((int) this.session.Handle, this.name, out installedState, out actionState);
195 if (ret != 0)
196 {
197 if (ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT)
198 {
199 throw InstallerException.ExceptionFromReturnCode(ret, this.name);
200 }
201 else
202 {
203 throw InstallerException.ExceptionFromReturnCode(ret);
204 }
205 }
206 return (InstallState) actionState;
207 }
208
209 set
210 {
211 uint ret = RemotableNativeMethods.MsiSetComponentState((int) this.session.Handle, this.name, (int) value);
212 if (ret != 0)
213 {
214 if (ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT)
215 {
216 throw InstallerException.ExceptionFromReturnCode(ret, this.name);
217 }
218 else
219 {
220 throw InstallerException.ExceptionFromReturnCode(ret);
221 }
222 }
223 }
224 }
225
226 /// <summary>
227 /// Gets disk space per drive required to install a component.
228 /// </summary>
229 /// <param name="installState">Requested component state</param>
230 /// <returns>A list of InstallCost structures, specifying the cost for each drive for the component</returns>
231 /// <remarks><p>
232 /// Win32 MSI API:
233 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumcomponentcosts.asp">MsiEnumComponentCosts</a>
234 /// </p></remarks>
235 public IList<InstallCost> GetCost(InstallState installState)
236 {
237 IList<InstallCost> costs = new List<InstallCost>();
238 StringBuilder driveBuf = new StringBuilder(20);
239 for (uint i = 0; true; i++)
240 {
241 int cost, tempCost;
242 uint driveBufSize = (uint) driveBuf.Capacity;
243 uint ret = RemotableNativeMethods.MsiEnumComponentCosts(
244 (int) this.session.Handle,
245 this.name,
246 i,
247 (int) installState,
248 driveBuf,
249 ref driveBufSize,
250 out cost,
251 out tempCost);
252 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break;
253 if (ret == (uint) NativeMethods.Error.MORE_DATA)
254 {
255 driveBuf.Capacity = (int) ++driveBufSize;
256 ret = RemotableNativeMethods.MsiEnumComponentCosts(
257 (int) this.session.Handle,
258 this.name,
259 i,
260 (int) installState,
261 driveBuf,
262 ref driveBufSize,
263 out cost,
264 out tempCost);
265 }
266
267 if (ret != 0)
268 {
269 throw InstallerException.ExceptionFromReturnCode(ret);
270 }
271 costs.Add(new InstallCost(driveBuf.ToString(), cost * 512L, tempCost * 512L));
272 }
273 return costs;
274 }
275 }
276}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ComponentInstallation.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ComponentInstallation.cs
new file mode 100644
index 00000000..6d368899
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ComponentInstallation.cs
@@ -0,0 +1,382 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections.Generic;
8 using System.Diagnostics.CodeAnalysis;
9
10 /// <summary>
11 /// Represents an instance of a registered component.
12 /// </summary>
13 public class ComponentInstallation : InstallationPart
14 {
15 /// <summary>
16 /// Gets the set of installed components for all products.
17 /// </summary>
18 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
19 /// <remarks><p>
20 /// Win32 MSI API:
21 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumcomponents.asp">MsiEnumComponents</a>
22 /// </p></remarks>
23 public static IEnumerable<ComponentInstallation> AllComponents
24 {
25 get
26 {
27 StringBuilder buf = new StringBuilder(40);
28 for (uint i = 0; true; i++)
29 {
30 uint ret = NativeMethods.MsiEnumComponents(i, buf);
31 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break;
32 if (ret != 0)
33 {
34 throw InstallerException.ExceptionFromReturnCode(ret);
35 }
36 yield return new ComponentInstallation(buf.ToString());
37 }
38 }
39 }
40
41 /// <summary>
42 /// Gets the set of installed components for products in the indicated context.
43 /// </summary>
44 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
45 /// <remarks><p>
46 /// Win32 MSI API:
47 /// <a href="http://msdn.microsoft.com/library/dd407947.aspx">MsiEnumComponentsEx</a>
48 /// </p></remarks>
49 public static IEnumerable<ComponentInstallation> Components(string szUserSid, UserContexts dwContext)
50 {
51 uint pcchSid = 32;
52 StringBuilder szSid = new StringBuilder((int)pcchSid);
53 StringBuilder buf = new StringBuilder(40);
54 UserContexts installedContext;
55 for (uint i = 0; true; i++)
56 {
57 uint ret = NativeMethods.MsiEnumComponentsEx(szUserSid, dwContext, i, buf, out installedContext, szSid, ref pcchSid);
58 if (ret == (uint) NativeMethods.Error.MORE_DATA)
59 {
60 szSid.EnsureCapacity((int) ++pcchSid);
61 ret = NativeMethods.MsiEnumComponentsEx(szUserSid, dwContext, i, buf, out installedContext, szSid, ref pcchSid);
62 }
63 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break;
64 if (ret != 0)
65 {
66 throw InstallerException.ExceptionFromReturnCode(ret);
67 }
68 yield return new ComponentInstallation(buf.ToString(), szSid.ToString(), installedContext);
69 }
70 }
71 private static string GetProductCode(string component)
72 {
73 StringBuilder buf = new StringBuilder(40);
74 uint ret = NativeMethods.MsiGetProductCode(component, buf);
75 if (ret != 0)
76 {
77 return null;
78 }
79
80 return buf.ToString();
81 }
82
83 private static string GetProductCode(string component, string szUserSid, UserContexts dwContext)
84 {
85 // TODO: We really need what would be MsiGetProductCodeEx, or at least a reasonable facimile thereof (something that restricts the search to the context defined by szUserSid & dwContext)
86 return GetProductCode(component);
87 }
88
89 /// <summary>
90 /// Creates a new ComponentInstallation, automatically detecting the
91 /// product that the component is a part of.
92 /// </summary>
93 /// <param name="componentCode">component GUID</param>
94 /// <remarks><p>
95 /// Win32 MSI API:
96 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetproductcode.asp">MsiGetProductCode</a>
97 /// </p></remarks>
98 public ComponentInstallation(string componentCode)
99 : this(componentCode, ComponentInstallation.GetProductCode(componentCode))
100 {
101 }
102
103 /// <summary>
104 /// Creates a new ComponentInstallation, automatically detecting the
105 /// product that the component is a part of.
106 /// </summary>
107 /// <param name="componentCode">component GUID</param>
108 /// <param name="szUserSid">context user SID</param>
109 /// <param name="dwContext">user contexts</param>
110 public ComponentInstallation(string componentCode, string szUserSid, UserContexts dwContext)
111 : this(componentCode, ComponentInstallation.GetProductCode(componentCode, szUserSid, dwContext), szUserSid, dwContext)
112 {
113 }
114
115 /// <summary>
116 /// Creates a new ComponentInstallation for a component installed by a
117 /// specific product.
118 /// </summary>
119 /// <param name="componentCode">component GUID</param>
120 /// <param name="productCode">ProductCode GUID</param>
121 public ComponentInstallation(string componentCode, string productCode)
122 : this(componentCode, productCode, null, UserContexts.None)
123 {
124 }
125
126 /// <summary>
127 /// Creates a new ComponentInstallation for a component installed by a
128 /// specific product.
129 /// </summary>
130 /// <param name="componentCode">component GUID</param>
131 /// <param name="productCode">ProductCode GUID</param>
132 /// <param name="szUserSid">context user SID</param>
133 /// <param name="dwContext">user contexts</param>
134 public ComponentInstallation(string componentCode, string productCode, string szUserSid, UserContexts dwContext)
135 : base(componentCode, productCode, szUserSid, dwContext)
136 {
137 if (string.IsNullOrEmpty(componentCode))
138 {
139 throw new ArgumentNullException("componentCode");
140 }
141 }
142
143 /// <summary>
144 /// Gets the component code (GUID) of the component.
145 /// </summary>
146 public string ComponentCode
147 {
148 get
149 {
150 return this.Id;
151 }
152 }
153
154 /// <summary>
155 /// Gets all client products of a specified component.
156 /// </summary>
157 /// <returns>enumeration over all client products of the component</returns>
158 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
159 /// <remarks><p>
160 /// Because clients are not ordered, any new component has an arbitrary index.
161 /// This means that the property may return clients in any order.
162 /// </p><p>
163 /// Win32 MSI API:
164 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumclients.asp">MsiEnumClients</a>,
165 /// <a href="http://msdn.microsoft.com/library/dd407946.aspx">MsiEnumClientsEx</a>
166 /// </p></remarks>
167 public IEnumerable<ProductInstallation> ClientProducts
168 {
169 get
170 {
171 StringBuilder buf = new StringBuilder(40);
172 for (uint i = 0; true; i++)
173 {
174 uint chSid = 0;
175 UserContexts installedContext;
176 uint ret = this.Context == UserContexts.None ?
177 NativeMethods.MsiEnumClients(this.ComponentCode, i, buf) :
178 NativeMethods.MsiEnumClientsEx(this.ComponentCode, this.UserSid, this.Context, i, buf, out installedContext, null, ref chSid);
179 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break;
180 else if (ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT) break;
181 if (ret != 0)
182 {
183 throw InstallerException.ExceptionFromReturnCode(ret);
184 }
185 yield return new ProductInstallation(buf.ToString());
186 }
187 }
188 }
189
190 /// <summary>
191 /// Gets the installed state of a component.
192 /// </summary>
193 /// <returns>the installed state of the component, or InstallState.Unknown
194 /// if this component is not part of a product</returns>
195 /// <remarks><p>
196 /// Win32 MSI API:
197 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetcomponentpath.asp">MsiGetComponentPath</a>,
198 /// <a href="http://msdn.microsoft.com/library/dd408006.aspx">MsiGetComponentPathEx</a>
199 /// </p></remarks>
200 public override InstallState State
201 {
202 get
203 {
204 if (this.ProductCode != null)
205 {
206 uint bufSize = 0;
207 int installState = this.Context == UserContexts.None ?
208 NativeMethods.MsiGetComponentPath(
209 this.ProductCode, this.ComponentCode, null, ref bufSize) :
210 NativeMethods.MsiGetComponentPathEx(
211 this.ProductCode, this.ComponentCode, this.UserSid, this.Context, null, ref bufSize);
212 return (InstallState) installState;
213 }
214 else
215 {
216 return InstallState.Unknown;
217 }
218 }
219 }
220
221 /// <summary>
222 /// Gets the full path to an installed component. If the key path for the component is a
223 /// registry key then the registry key is returned.
224 /// </summary>
225 /// <returns>The file or registry keypath to the component, or null if the component is not available.</returns>
226 /// <exception cref="ArgumentException">An unknown product or component was specified</exception>
227 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
228 /// <remarks><p>
229 /// If the component is a registry key, the registry roots are represented numerically.
230 /// For example, a registry path of "HKEY_CURRENT_USER\SOFTWARE\Microsoft" would be returned
231 /// as "01:\SOFTWARE\Microsoft". The registry roots returned are defined as follows:
232 /// HKEY_CLASSES_ROOT=00, HKEY_CURRENT_USER=01, HKEY_LOCAL_MACHINE=02, HKEY_USERS=03,
233 /// HKEY_PERFORMANCE_DATA=04
234 /// </p><p>
235 /// Win32 MSI APIs:
236 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetcomponentpath.asp">MsiGetComponentPath</a>,
237 /// <a href="http://msdn.microsoft.com/library/dd408006.aspx">MsiGetComponentPathEx</a>,
238 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msilocatecomponent.asp">MsiLocateComponent</a>
239 /// </p></remarks>
240 public string Path
241 {
242 get
243 {
244 StringBuilder buf = new StringBuilder(256);
245 uint bufSize = (uint) buf.Capacity;
246 InstallState installState;
247
248 if (this.ProductCode != null)
249 {
250 installState = (this.Context == UserContexts.None) ?
251 (InstallState) NativeMethods.MsiGetComponentPath(
252 this.ProductCode, this.ComponentCode, buf, ref bufSize) :
253 (InstallState) NativeMethods.MsiGetComponentPathEx(
254 this.ProductCode, this.ComponentCode, this.UserSid, this.Context, buf, ref bufSize);
255 if (installState == InstallState.MoreData)
256 {
257 buf.Capacity = (int) ++bufSize;
258 installState = (this.Context == UserContexts.None) ?
259 (InstallState) NativeMethods.MsiGetComponentPath(
260 this.ProductCode, this.ComponentCode, buf, ref bufSize) :
261 (InstallState) NativeMethods.MsiGetComponentPathEx(
262 this.ProductCode, this.ComponentCode, this.UserSid, this.Context, buf, ref bufSize);
263 }
264 }
265 else
266 {
267 installState = (InstallState) NativeMethods.MsiLocateComponent(
268 this.ComponentCode, buf, ref bufSize);
269 if (installState == InstallState.MoreData)
270 {
271 buf.Capacity = (int) ++bufSize;
272 installState = (InstallState) NativeMethods.MsiLocateComponent(
273 this.ComponentCode, buf, ref bufSize);
274 }
275 }
276
277 switch (installState)
278 {
279 case InstallState.Local:
280 case InstallState.Source:
281 case InstallState.SourceAbsent:
282 return buf.ToString();
283
284 default:
285 return null;
286 }
287 }
288 }
289
290 /// <summary>
291 /// Gets the set of registered qualifiers for the component.
292 /// </summary>
293 /// <returns>Enumeration of the qulifiers for the component.</returns>
294 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
295 /// <remarks><p>
296 /// Because qualifiers are not ordered, any new qualifier has an arbitrary index,
297 /// meaning the function can return qualifiers in any order.
298 /// </p><p>
299 /// Win32 MSI API:
300 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumcomponentqualifiers.asp">MsiEnumComponentQualifiers</a>
301 /// </p></remarks>
302 public IEnumerable<ComponentInstallation.Qualifier> Qualifiers
303 {
304 get
305 {
306 StringBuilder qualBuf = new StringBuilder(255);
307 StringBuilder dataBuf = new StringBuilder(255);
308 for (uint i = 0; ; i++)
309 {
310 uint qualBufSize = (uint) qualBuf.Capacity;
311 uint dataBufSize = (uint) dataBuf.Capacity;
312 uint ret = NativeMethods.MsiEnumComponentQualifiers(
313 this.ComponentCode, i, qualBuf, ref qualBufSize, dataBuf, ref dataBufSize);
314 if (ret == (uint) NativeMethods.Error.MORE_DATA)
315 {
316 qualBuf.Capacity = (int) ++qualBufSize;
317 dataBuf.Capacity = (int) ++dataBufSize;
318 ret = NativeMethods.MsiEnumComponentQualifiers(
319 this.ComponentCode, i, qualBuf, ref qualBufSize, dataBuf, ref dataBufSize);
320 }
321
322 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS ||
323 ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT)
324 {
325 break;
326 }
327
328 if (ret != 0)
329 {
330 throw InstallerException.ExceptionFromReturnCode(ret);
331 }
332
333 yield return new ComponentInstallation.Qualifier(
334 qualBuf.ToString(), dataBuf.ToString());
335 }
336 }
337 }
338
339 /// <summary>
340 /// Holds data about a component qualifier.
341 /// </summary>
342 /// <remarks><p>
343 /// Win32 MSI API:
344 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumcomponentqualifiers.asp">MsiEnumComponentQualifiers</a>
345 /// </p></remarks>
346 [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
347 [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
348 public struct Qualifier
349 {
350 private string qualifierCode;
351 private string data;
352
353 internal Qualifier(string qualifierCode, string data)
354 {
355 this.qualifierCode = qualifierCode;
356 this.data = data;
357 }
358
359 /// <summary>
360 /// Gets the qualifier code.
361 /// </summary>
362 public string QualifierCode
363 {
364 get
365 {
366 return this.qualifierCode;
367 }
368 }
369
370 /// <summary>
371 /// Gets the qualifier data.
372 /// </summary>
373 public string Data
374 {
375 get
376 {
377 return this.data;
378 }
379 }
380 }
381 }
382}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/CustomActionAttribute.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/CustomActionAttribute.cs
new file mode 100644
index 00000000..d9bdb71b
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/CustomActionAttribute.cs
@@ -0,0 +1,55 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Reflection;
7
8 /// <summary>
9 /// Marks a method as a custom action entry point.
10 /// </summary>
11 /// <remarks><p>
12 /// A custom action method must be defined as public and static,
13 /// take a single <see cref="Session"/> object as a parameter,
14 /// and return an <see cref="ActionResult"/> enumeration value.
15 /// </p></remarks>
16 [Serializable, AttributeUsage(AttributeTargets.Method)]
17 public sealed class CustomActionAttribute : Attribute
18 {
19 /// <summary>
20 /// Name of the custom action entrypoint, or null if the same as the method name.
21 /// </summary>
22 private string name;
23
24 /// <summary>
25 /// Marks a method as a custom action entry point.
26 /// </summary>
27 public CustomActionAttribute()
28 : this(null)
29 {
30 }
31
32 /// <summary>
33 /// Marks a method as a custom action entry point.
34 /// </summary>
35 /// <param name="name">Name of the function to be exported,
36 /// defaults to the name of this method</param>
37 public CustomActionAttribute(string name)
38 {
39 this.name = name;
40 }
41
42 /// <summary>
43 /// Gets or sets the name of the custom action entrypoint. A null
44 /// value defaults to the name of the method.
45 /// </summary>
46 /// <value>name of the custom action entrypoint, or null if none was specified</value>
47 public string Name
48 {
49 get
50 {
51 return this.name;
52 }
53 }
54 }
55}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/CustomActionProxy.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/CustomActionProxy.cs
new file mode 100644
index 00000000..d3fd7d1b
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/CustomActionProxy.cs
@@ -0,0 +1,321 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Security;
9 using System.Reflection;
10 using System.Collections;
11 using System.Configuration;
12 using System.Runtime.InteropServices;
13 using System.Diagnostics.CodeAnalysis;
14
15 /// <summary>
16 /// Managed-code portion of the custom action proxy.
17 /// </summary>
18 internal static class CustomActionProxy
19 {
20 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
21 public static int InvokeCustomAction32(int sessionHandle, string entryPoint,
22 int remotingDelegatePtr)
23 {
24 return CustomActionProxy.InvokeCustomAction(sessionHandle, entryPoint, new IntPtr(remotingDelegatePtr));
25 }
26
27 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
28 public static int InvokeCustomAction64(int sessionHandle, string entryPoint,
29 long remotingDelegatePtr)
30 {
31 return CustomActionProxy.InvokeCustomAction(sessionHandle, entryPoint, new IntPtr(remotingDelegatePtr));
32 }
33
34 /// <summary>
35 /// Invokes a managed custom action method.
36 /// </summary>
37 /// <param name="sessionHandle">Integer handle to the installer session.</param>
38 /// <param name="entryPoint">Name of the custom action entrypoint. This must
39 /// either map to an entrypoint definition in the <c>customActions</c>
40 /// config section, or be an explicit entrypoint of the form:
41 /// &quot;AssemblyName!Namespace.Class.Method&quot;</param>
42 /// <param name="remotingDelegatePtr">Pointer to a delegate used to
43 /// make remote API calls, if this custom action is running out-of-proc.</param>
44 /// <returns>The value returned by the custom action method,
45 /// or ERROR_INSTALL_FAILURE if the custom action could not be invoked.</returns>
46 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
47 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
48 public static int InvokeCustomAction(int sessionHandle, string entryPoint,
49 IntPtr remotingDelegatePtr)
50 {
51 Session session = null;
52 string assemblyName, className, methodName;
53 MethodInfo method;
54
55 try
56 {
57 MsiRemoteInvoke remotingDelegate = (MsiRemoteInvoke)
58 Marshal.GetDelegateForFunctionPointer(
59 remotingDelegatePtr, typeof(MsiRemoteInvoke));
60 RemotableNativeMethods.RemotingDelegate = remotingDelegate;
61
62 sessionHandle = RemotableNativeMethods.MakeRemoteHandle(sessionHandle);
63 session = new Session((IntPtr) sessionHandle, false);
64 if (String.IsNullOrEmpty(entryPoint))
65 {
66 throw new ArgumentNullException("entryPoint");
67 }
68
69 if (!CustomActionProxy.FindEntryPoint(
70 session,
71 entryPoint,
72 out assemblyName,
73 out className,
74 out methodName))
75 {
76 return (int) ActionResult.Failure;
77 }
78 session.Log("Calling custom action {0}!{1}.{2}", assemblyName, className, methodName);
79
80 method = CustomActionProxy.GetCustomActionMethod(
81 session,
82 assemblyName,
83 className,
84 methodName);
85 if (method == null)
86 {
87 return (int) ActionResult.Failure;
88 }
89 }
90 catch (Exception ex)
91 {
92 if (session != null)
93 {
94 try
95 {
96 session.Log("Exception while loading custom action:");
97 session.Log(ex.ToString());
98 }
99 catch (Exception) { }
100 }
101 return (int) ActionResult.Failure;
102 }
103
104 try
105 {
106 // Set the current directory to the location of the extracted files.
107 Environment.CurrentDirectory =
108 AppDomain.CurrentDomain.BaseDirectory;
109
110 object[] args = new object[] { session };
111 if (DebugBreakEnabled(new string[] { entryPoint, methodName }))
112 {
113 string message = String.Format(
114 "To debug your custom action, attach to process ID {0} (0x{0:x}) and click OK; otherwise, click Cancel to fail the custom action.",
115 System.Diagnostics.Process.GetCurrentProcess().Id
116 );
117
118 MessageResult button = NativeMethods.MessageBox(
119 IntPtr.Zero,
120 message,
121 "Custom Action Breakpoint",
122 (int)MessageButtons.OKCancel | (int)MessageIcon.Asterisk | (int)(MessageBoxStyles.TopMost | MessageBoxStyles.ServiceNotification)
123 );
124
125 if (MessageResult.Cancel == button)
126 {
127 return (int)ActionResult.UserExit;
128 }
129 }
130
131 ActionResult result = (ActionResult) method.Invoke(null, args);
132 session.Close();
133 return (int) result;
134 }
135 catch (InstallCanceledException)
136 {
137 return (int) ActionResult.UserExit;
138 }
139 catch (Exception ex)
140 {
141 session.Log("Exception thrown by custom action:");
142 session.Log(ex.ToString());
143 return (int) ActionResult.Failure;
144 }
145 }
146
147 /// <summary>
148 /// Checks the "MMsiBreak" environment variable for any matching custom action names.
149 /// </summary>
150 /// <param name="names">List of names to search for in the environment
151 /// variable string.</param>
152 /// <returns>True if a match was found, else false.</returns>
153 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
154 internal static bool DebugBreakEnabled(string[] names)
155 {
156 string mmsibreak = Environment.GetEnvironmentVariable("MMsiBreak");
157 if (mmsibreak != null)
158 {
159 foreach (string breakName in mmsibreak.Split(',', ';'))
160 {
161 foreach (string name in names)
162 {
163 if (breakName == name)
164 {
165 return true;
166 }
167 }
168 }
169 }
170 return false;
171 }
172
173 /// <summary>
174 /// Locates and parses an entrypoint mapping in CustomAction.config.
175 /// </summary>
176 /// <param name="session">Installer session handle, just used for logging.</param>
177 /// <param name="entryPoint">Custom action entrypoint name: the key value
178 /// in an item in the <c>customActions</c> section of the config file.</param>
179 /// <param name="assemblyName">Returned display name of the assembly from
180 /// the entrypoint mapping.</param>
181 /// <param name="className">Returned class name of the entrypoint mapping.</param>
182 /// <param name="methodName">Returned method name of the entrypoint mapping.</param>
183 /// <returns>True if the entrypoint was found, false if not or if some error
184 /// occurred.</returns>
185 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
186 private static bool FindEntryPoint(
187 Session session,
188 string entryPoint,
189 out string assemblyName,
190 out string className,
191 out string methodName)
192 {
193 assemblyName = null;
194 className = null;
195 methodName = null;
196
197 string fullEntryPoint;
198 if (entryPoint.IndexOf('!') > 0)
199 {
200 fullEntryPoint = entryPoint;
201 }
202 else
203 {
204#if NET20
205 IDictionary config;
206 try
207 {
208 config = (IDictionary) ConfigurationManager.GetSection("customActions");
209 }
210 catch (ConfigurationException cex)
211 {
212 session.Log("Error: missing or invalid customActions config section.");
213 session.Log(cex.ToString());
214 return false;
215 }
216 fullEntryPoint = (string) config[entryPoint];
217 if (fullEntryPoint == null)
218 {
219 session.Log(
220 "Error: custom action entry point '{0}' not found " +
221 "in customActions config section.",
222 entryPoint);
223 return false;
224 }
225#else
226 throw new NotImplementedException();
227#endif
228 }
229
230 int assemblySplit = fullEntryPoint.IndexOf('!');
231 int methodSplit = fullEntryPoint.LastIndexOf('.');
232 if (assemblySplit < 0 || methodSplit < 0 || methodSplit < assemblySplit)
233 {
234 session.Log("Error: invalid custom action entry point:" + entryPoint);
235 return false;
236 }
237
238 assemblyName = fullEntryPoint.Substring(0, assemblySplit);
239 className = fullEntryPoint.Substring(assemblySplit + 1, methodSplit - assemblySplit - 1);
240 methodName = fullEntryPoint.Substring(methodSplit + 1);
241 return true;
242 }
243
244 /// <summary>
245 /// Uses reflection to load the assembly and class and find the method.
246 /// </summary>
247 /// <param name="session">Installer session handle, just used for logging.</param>
248 /// <param name="assemblyName">Display name of the assembly containing the
249 /// custom action method.</param>
250 /// <param name="className">Fully-qualified name of the class containing the
251 /// custom action method.</param>
252 /// <param name="methodName">Name of the custom action method.</param>
253 /// <returns>The method, or null if not found.</returns>
254 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
255 private static MethodInfo GetCustomActionMethod(
256 Session session,
257 string assemblyName,
258 string className,
259 string methodName)
260 {
261 Assembly customActionAssembly;
262 Type customActionClass = null;
263 Exception caughtEx = null;
264 try
265 {
266 customActionAssembly = AppDomain.CurrentDomain.Load(assemblyName);
267 customActionClass = customActionAssembly.GetType(className, true, true);
268 }
269 catch (IOException ex) { caughtEx = ex; }
270 catch (BadImageFormatException ex) { caughtEx = ex; }
271 catch (TypeLoadException ex) { caughtEx = ex; }
272 catch (ReflectionTypeLoadException ex) { caughtEx = ex; }
273 catch (SecurityException ex) { caughtEx = ex; }
274 if (caughtEx != null)
275 {
276 session.Log("Error: could not load custom action class " + className + " from assembly: " + assemblyName);
277 session.Log(caughtEx.ToString());
278 return null;
279 }
280
281 MethodInfo[] methods = customActionClass.GetMethods(
282 BindingFlags.Public | BindingFlags.Static);
283 foreach (MethodInfo method in methods)
284 {
285 if (method.Name == methodName &&
286 CustomActionProxy.MethodHasCustomActionSignature(method))
287 {
288 return method;
289 }
290 }
291 session.Log("Error: custom action method \"" + methodName +
292 "\" is missing or has the wrong signature.");
293 return null;
294 }
295
296 /// <summary>
297 /// Checks if a method has the right return and paramater types
298 /// for a custom action, and that it is marked by a CustomActionAttribute.
299 /// </summary>
300 /// <param name="method">Method to be checked.</param>
301 /// <returns>True if the method is a valid custom action, else false.</returns>
302 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
303 private static bool MethodHasCustomActionSignature(MethodInfo method)
304 {
305 if (method.ReturnType == typeof(ActionResult) &&
306 method.GetParameters().Length == 1 &&
307 method.GetParameters()[0].ParameterType == typeof(Session))
308 {
309 object[] methodAttribs = method.GetCustomAttributes(false);
310 foreach (object attrib in methodAttribs)
311 {
312 if (attrib is CustomActionAttribute)
313 {
314 return true;
315 }
316 }
317 }
318 return false;
319 }
320 }
321}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs
new file mode 100644
index 00000000..09627f4b
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Database.cs
@@ -0,0 +1,933 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// Accesses a Windows Installer database.
13 /// </summary>
14 /// <remarks><p>
15 /// The <see cref="Commit"/> method must be called before the Database is closed to write out all
16 /// persistent changes. If the Commit method is not called, the installer performs an implicit
17 /// rollback upon object destruction.
18 /// </p><p>
19 /// The client can use the following procedure for data access:<list type="number">
20 /// <item><description>Obtain a Database object using one of the Database constructors.</description></item>
21 /// <item><description>Initiate a query using a SQL string by calling the <see cref="OpenView"/>
22 /// method of the Database.</description></item>
23 /// <item><description>Set query parameters in a <see cref="Record"/> and execute the database
24 /// query by calling the <see cref="View.Execute(Record)"/> method of the <see cref="View"/>. This
25 /// produces a result that can be fetched or updated.</description></item>
26 /// <item><description>Call the <see cref="View.Fetch"/> method of the View repeatedly to return
27 /// Records.</description></item>
28 /// <item><description>Update database rows of a Record object obtained by the Fetch method using
29 /// one of the <see cref="View.Modify"/> methods of the View.</description></item>
30 /// <item><description>Release the query and any unfetched records by calling the <see cref="InstallerHandle.Close"/>
31 /// method of the View.</description></item>
32 /// <item><description>Persist any database updates by calling the Commit method of the Database.
33 /// </description></item>
34 /// </list>
35 /// </p></remarks>
36 public partial class Database : InstallerHandle
37 {
38 private string filePath;
39 private DatabaseOpenMode openMode;
40 private SummaryInfo summaryInfo;
41 private TableCollection tables;
42 private IList<string> deleteOnClose;
43
44 /// <summary>
45 /// Opens an existing database in read-only mode.
46 /// </summary>
47 /// <param name="filePath">Path to the database file.</param>
48 /// <exception cref="InstallerException">the database could not be created/opened</exception>
49 /// <remarks><p>
50 /// Because this constructor initiates database access, it cannot be used with a
51 /// running installation.
52 /// </p><p>
53 /// The Database object should be <see cref="InstallerHandle.Close"/>d after use.
54 /// It is best that the handle be closed manually as soon as it is no longer
55 /// needed, as leaving lots of unused handles open can degrade performance.
56 /// </p><p>
57 /// Win32 MSI API:
58 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a>
59 /// </p></remarks>
60 public Database(string filePath)
61 : this(filePath, DatabaseOpenMode.ReadOnly)
62 {
63 }
64
65 /// <summary>
66 /// Opens an existing database with another database as output.
67 /// </summary>
68 /// <param name="filePath">Path to the database to be read.</param>
69 /// <param name="outputPath">Open mode for the database</param>
70 /// <returns>Database object representing the created or opened database</returns>
71 /// <exception cref="InstallerException">the database could not be created/opened</exception>
72 /// <remarks><p>
73 /// When a database is opened as the output of another database, the summary information stream
74 /// of the output database is actually a read-only mirror of the original database and thus cannot
75 /// be changed. Additionally, it is not persisted with the database. To create or modify the
76 /// summary information for the output database it must be closed and re-opened.
77 /// </p><p>
78 /// The Database object should be <see cref="InstallerHandle.Close"/>d after use.
79 /// It is best that the handle be closed manually as soon as it is no longer
80 /// needed, as leaving lots of unused handles open can degrade performance.
81 /// </p><p>
82 /// The database is opened in <see cref="DatabaseOpenMode.CreateDirect" /> mode, and will be
83 /// automatically commited when it is closed.
84 /// </p><p>
85 /// Win32 MSI API:
86 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a>
87 /// </p></remarks>
88 public Database(string filePath, string outputPath)
89 : this((IntPtr) Database.Open(filePath, outputPath), true, outputPath, DatabaseOpenMode.CreateDirect)
90 {
91 }
92
93 /// <summary>
94 /// Opens an existing database or creates a new one.
95 /// </summary>
96 /// <param name="filePath">Path to the database file. If an empty string
97 /// is supplied, a temporary database is created that is not persisted.</param>
98 /// <param name="mode">Open mode for the database</param>
99 /// <exception cref="InstallerException">the database could not be created/opened</exception>
100 /// <remarks><p>
101 /// Because this constructor initiates database access, it cannot be used with a
102 /// running installation.
103 /// </p><p>
104 /// The database object should be <see cref="InstallerHandle.Close"/>d after use.
105 /// The finalizer will close the handle if it is still open, however due to the nondeterministic
106 /// nature of finalization it is best that the handle be closed manually as soon as it is no
107 /// longer needed, as leaving lots of unused handles open can degrade performance.
108 /// </p><p>
109 /// A database opened in <see cref="DatabaseOpenMode.CreateDirect" /> or
110 /// <see cref="DatabaseOpenMode.Direct" /> mode will be automatically commited when it is
111 /// closed. However a database opened in <see cref="DatabaseOpenMode.Create" /> or
112 /// <see cref="DatabaseOpenMode.Transact" /> mode must have the <see cref="Commit" /> method
113 /// called before it is closed, otherwise no changes will be persisted.
114 /// </p><p>
115 /// Win32 MSI API:
116 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopendatabase.asp">MsiOpenDatabase</a>
117 /// </p></remarks>
118 public Database(string filePath, DatabaseOpenMode mode)
119 : this((IntPtr) Database.Open(filePath, mode), true, filePath, mode)
120 {
121 }
122
123 /// <summary>
124 /// Creates a new database from an MSI handle.
125 /// </summary>
126 /// <param name="handle">Native MSI database handle.</param>
127 /// <param name="ownsHandle">True if the handle should be closed
128 /// when the database object is disposed</param>
129 /// <param name="filePath">Path of the database file, if known</param>
130 /// <param name="openMode">Mode the handle was originally opened in</param>
131 protected internal Database(
132 IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode)
133 : base(handle, ownsHandle)
134 {
135 this.filePath = filePath;
136 this.openMode = openMode;
137 }
138
139 /// <summary>
140 /// Gets the file path the Database was originally opened from, or null if not known.
141 /// </summary>
142 public String FilePath
143 {
144 get
145 {
146 return this.filePath;
147 }
148 }
149
150 /// <summary>
151 /// Gets the open mode for the database.
152 /// </summary>
153 public DatabaseOpenMode OpenMode
154 {
155 get
156 {
157 return this.openMode;
158 }
159 }
160
161 /// <summary>
162 /// Gets a boolean value indicating whether this database was opened in read-only mode.
163 /// </summary>
164 /// <remarks><p>
165 /// Win32 MSI API:
166 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetdatabasestate.asp">MsiGetDatabaseState</a>
167 /// </p></remarks>
168 public bool IsReadOnly
169 {
170 get
171 {
172 if (RemotableNativeMethods.RemotingEnabled)
173 {
174 return true;
175 }
176
177 int state = NativeMethods.MsiGetDatabaseState((int) this.Handle);
178 return state != 1;
179 }
180 }
181
182 /// <summary>
183 /// Gets the collection of tables in the Database.
184 /// </summary>
185 public TableCollection Tables
186 {
187 get
188 {
189 if (this.tables == null)
190 {
191 this.tables = new TableCollection(this);
192 }
193 return this.tables;
194 }
195 }
196
197 /// <summary>
198 /// Gets or sets the code page of the Database.
199 /// </summary>
200 /// <exception cref="IOException">error exporting/importing the codepage data</exception>
201 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
202 /// <remarks><p>
203 /// Getting or setting the code page is a slow operation because it involves an export or import
204 /// of the codepage data to/from a temporary file.
205 /// </p></remarks>
206 public int CodePage
207 {
208 get
209 {
210 string tempFile = Path.GetTempFileName();
211 StreamReader reader = null;
212 try
213 {
214 this.Export("_ForceCodepage", tempFile);
215 reader = File.OpenText(tempFile);
216 reader.ReadLine(); // Skip column name record.
217 reader.ReadLine(); // Skip column defn record.
218 string codePageLine = reader.ReadLine();
219 return Int32.Parse(codePageLine.Split('\t')[0], CultureInfo.InvariantCulture.NumberFormat);
220 }
221 finally
222 {
223 if (reader != null) reader.Close();
224 File.Delete(tempFile);
225 }
226 }
227
228 set
229 {
230 string tempFile = Path.GetTempFileName();
231 StreamWriter writer = null;
232 try
233 {
234 writer = File.AppendText(tempFile);
235 writer.WriteLine("");
236 writer.WriteLine("");
237 writer.WriteLine("{0}\t_ForceCodepage", value);
238 writer.Close();
239 writer = null;
240 this.Import(tempFile);
241 }
242 finally
243 {
244 if (writer != null) writer.Close();
245 File.Delete(tempFile);
246 }
247 }
248 }
249
250 /// <summary>
251 /// Gets the SummaryInfo object for this database that can be used to examine and modify properties
252 /// to the summary information stream.
253 /// </summary>
254 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
255 /// <remarks><p>
256 /// The object returned from this property does not need to be explicitly persisted or closed.
257 /// Any modifications will be automatically saved when the database is committed.
258 /// </p><p>
259 /// Win32 MSI API:
260 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetsummaryinformation.asp">MsiGetSummaryInformation</a>
261 /// </p></remarks>
262 public SummaryInfo SummaryInfo
263 {
264 get
265 {
266 if (this.summaryInfo == null || this.summaryInfo.IsClosed)
267 {
268 lock (this.Sync)
269 {
270 if (this.summaryInfo == null || this.summaryInfo.IsClosed)
271 {
272 int summaryInfoHandle;
273 int maxProperties = this.IsReadOnly ? 0 : SummaryInfo.MAX_PROPERTIES;
274 uint ret = RemotableNativeMethods.MsiGetSummaryInformation((int) this.Handle, null, (uint) maxProperties, out summaryInfoHandle);
275 if (ret != 0)
276 {
277 throw InstallerException.ExceptionFromReturnCode(ret);
278 }
279 this.summaryInfo = new SummaryInfo((IntPtr) summaryInfoHandle, true);
280 }
281 }
282 }
283 return this.summaryInfo;
284 }
285 }
286
287 /// <summary>
288 /// Creates a new Database object from an integer database handle.
289 /// </summary>
290 /// <remarks><p>
291 /// This method is only provided for interop purposes. A Database object
292 /// should normally be obtained from <see cref="Session.Database"/> or
293 /// a public Database constructor.
294 /// </p></remarks>
295 /// <param name="handle">Integer database handle</param>
296 /// <param name="ownsHandle">true to close the handle when this object is disposed</param>
297 public static Database FromHandle(IntPtr handle, bool ownsHandle)
298 {
299 return new Database(
300 handle,
301 ownsHandle,
302 null,
303 NativeMethods.MsiGetDatabaseState((int) handle) == 1 ? DatabaseOpenMode.Direct : DatabaseOpenMode.ReadOnly);
304 }
305
306 /// <summary>
307 /// Schedules a file or directory for deletion after the database handle is closed.
308 /// </summary>
309 /// <param name="path">File or directory path to be deleted. All files and subdirectories
310 /// under a directory are deleted.</param>
311 /// <remarks><p>
312 /// Once an item is scheduled, it cannot be unscheduled.
313 /// </p><p>
314 /// The items cannot be deleted if the Database object is auto-disposed by the
315 /// garbage collector; the handle must be explicitly closed.
316 /// </p><p>
317 /// Files which are read-only or otherwise locked cannot be deleted,
318 /// but they will not cause an exception to be thrown.
319 /// </p></remarks>
320 public void DeleteOnClose(string path)
321 {
322 if (this.deleteOnClose == null)
323 {
324 this.deleteOnClose = new List<string>();
325 }
326 this.deleteOnClose.Add(path);
327 }
328
329 /// <summary>
330 /// Merges another database with this database.
331 /// </summary>
332 /// <param name="otherDatabase">The database to be merged into this database</param>
333 /// <param name="errorTable">Optional name of table to contain the names of the tables containing
334 /// merge conflicts, the number of conflicting rows within the table, and a reference to the table
335 /// with the merge conflict.</param>
336 /// <exception cref="MergeException">merge failed due to a schema difference or data conflict</exception>
337 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
338 /// <remarks><p>
339 /// Merge does not copy over embedded cabinet files or embedded transforms from the
340 /// reference database into the target database. Embedded data streams that are listed in the
341 /// Binary table or Icon table are copied from the reference database to the target database.
342 /// Storage embedded in the reference database are not copied to the target database.
343 /// </p><p>
344 /// The Merge method merges the data of two databases. These databases must have the same
345 /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists
346 /// if the data in any row in the first database differs from the data in the corresponding row
347 /// of the second database. Corresponding rows are in the same table of both databases and have
348 /// the same primary key in both databases. The tables of non-conflicting databases must have
349 /// the same number of primary keys, same number of columns, same column types, same column names,
350 /// and the same data in rows with identical primary keys. Temporary columns however don't matter
351 /// in the column count and corresponding tables can have a different number of temporary columns
352 /// without creating conflict as long as the persistent columns match.
353 /// </p><p>
354 /// If the number, type, or name of columns in corresponding tables are different, the
355 /// schema of the two databases are incompatible and the installer will stop processing tables
356 /// and the merge fails. The installer checks that the two databases have the same schema before
357 /// checking for row merge conflicts. If the schemas are incompatible, the databases have be
358 /// modified.
359 /// </p><p>
360 /// If the data in particular rows differ, this is a row merge conflict, the merge fails
361 /// and creates a new table with the specified name. The first column of this table is the name
362 /// of the table having the conflict. The second column gives the number of rows in the table
363 /// having the conflict.
364 /// </p><p>
365 /// Win32 MSI API:
366 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasemerge.asp">MsiDatabaseMerge</a>
367 /// </p></remarks>
368 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
369 public void Merge(Database otherDatabase, string errorTable)
370 {
371 if (otherDatabase == null)
372 {
373 throw new ArgumentNullException("otherDatabase");
374 }
375
376 uint ret = NativeMethods.MsiDatabaseMerge((int) this.Handle, (int) otherDatabase.Handle, errorTable);
377 if (ret != 0)
378 {
379 if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED)
380 {
381 throw new MergeException(this, errorTable);
382 }
383 else if (ret == (uint) NativeMethods.Error.DATATYPE_MISMATCH)
384 {
385 throw new MergeException("Schema difference between the two databases.");
386 }
387 else
388 {
389 throw InstallerException.ExceptionFromReturnCode(ret);
390 }
391 }
392 }
393
394 /// <summary>
395 /// Merges another database with this database.
396 /// </summary>
397 /// <param name="otherDatabase">The database to be merged into this database</param>
398 /// <exception cref="MergeException">merge failed due to a schema difference or data conflict</exception>
399 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
400 /// <remarks><p>
401 /// MsiDatabaseMerge does not copy over embedded cabinet files or embedded transforms from
402 /// the reference database into the target database. Embedded data streams that are listed in
403 /// the Binary table or Icon table are copied from the reference database to the target database.
404 /// Storage embedded in the reference database are not copied to the target database.
405 /// </p><p>
406 /// The Merge method merges the data of two databases. These databases must have the same
407 /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists
408 /// if the data in any row in the first database differs from the data in the corresponding row
409 /// of the second database. Corresponding rows are in the same table of both databases and have
410 /// the same primary key in both databases. The tables of non-conflicting databases must have
411 /// the same number of primary keys, same number of columns, same column types, same column names,
412 /// and the same data in rows with identical primary keys. Temporary columns however don't matter
413 /// in the column count and corresponding tables can have a different number of temporary columns
414 /// without creating conflict as long as the persistent columns match.
415 /// </p><p>
416 /// If the number, type, or name of columns in corresponding tables are different, the
417 /// schema of the two databases are incompatible and the installer will stop processing tables
418 /// and the merge fails. The installer checks that the two databases have the same schema before
419 /// checking for row merge conflicts. If the schemas are incompatible, the databases have be
420 /// modified.
421 /// </p><p>
422 /// Win32 MSI API:
423 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasemerge.asp">MsiDatabaseMerge</a>
424 /// </p></remarks>
425 public void Merge(Database otherDatabase) { this.Merge(otherDatabase, null); }
426
427 /// <summary>
428 /// Checks whether a table exists and is persistent in the database.
429 /// </summary>
430 /// <param name="table">The table to the checked</param>
431 /// <returns>true if the table exists and is persistent in the database; false otherwise</returns>
432 /// <exception cref="ArgumentException">the table is unknown</exception>
433 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
434 /// <remarks><p>
435 /// To check whether a table exists regardless of persistence,
436 /// use <see cref="TableCollection.Contains"/>.
437 /// </p><p>
438 /// Win32 MSI API:
439 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseistablepersistent.asp">MsiDatabaseIsTablePersistent</a>
440 /// </p></remarks>
441 public bool IsTablePersistent(string table)
442 {
443 if (String.IsNullOrEmpty(table))
444 {
445 throw new ArgumentNullException("table");
446 }
447 uint ret = RemotableNativeMethods.MsiDatabaseIsTablePersistent((int) this.Handle, table);
448 if (ret == 3) // MSICONDITION_ERROR
449 {
450 throw new InstallerException();
451 }
452 return ret == 1;
453 }
454
455 /// <summary>
456 /// Checks whether a table contains a persistent column with a given name.
457 /// </summary>
458 /// <param name="table">The table to the checked</param>
459 /// <param name="column">The name of the column to be checked</param>
460 /// <returns>true if the column exists in the table; false if the column is temporary or does not exist.</returns>
461 /// <exception cref="InstallerException">the View could not be executed</exception>
462 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
463 /// <remarks><p>
464 /// To check whether a column exists regardless of persistence,
465 /// use <see cref="ColumnCollection.Contains"/>.
466 /// </p></remarks>
467 public bool IsColumnPersistent(string table, string column)
468 {
469 if (String.IsNullOrEmpty(table))
470 {
471 throw new ArgumentNullException("table");
472 }
473 if (String.IsNullOrEmpty(column))
474 {
475 throw new ArgumentNullException("column");
476 }
477 using (View view = this.OpenView(
478 "SELECT `Number` FROM `_Columns` WHERE `Table` = '{0}' AND `Name` = '{1}'", table, column))
479 {
480 view.Execute();
481 using (Record rec = view.Fetch())
482 {
483 return (rec != null);
484 }
485 }
486 }
487
488 /// <summary>
489 /// Gets the count of all rows in the table.
490 /// </summary>
491 /// <param name="table">Name of the table whose rows are to be counted</param>
492 /// <returns>The count of all rows in the table</returns>
493 /// <exception cref="InstallerException">the View could not be executed</exception>
494 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
495 public int CountRows(string table)
496 {
497 return this.CountRows(table, null);
498 }
499
500 /// <summary>
501 /// Gets the count of all rows in the table that satisfy a given condition.
502 /// </summary>
503 /// <param name="table">Name of the table whose rows are to be counted</param>
504 /// <param name="where">Conditional expression, such as could be placed on the end of a SQL WHERE clause</param>
505 /// <returns>The count of all rows in the table satisfying the condition</returns>
506 /// <exception cref="BadQuerySyntaxException">the SQL WHERE syntax is invalid</exception>
507 /// <exception cref="InstallerException">the View could not be executed</exception>
508 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
509 public int CountRows(string table, string where)
510 {
511 if (String.IsNullOrEmpty(table))
512 {
513 throw new ArgumentNullException("table");
514 }
515
516 // to support temporary tables like _Streams, run the query even if the table isn't persistent
517 TableInfo tableInfo = this.Tables[table];
518 string primaryKeys = tableInfo == null ? "*" : String.Concat("`", tableInfo.PrimaryKeys[0], "`");
519 int count;
520
521 try
522 {
523 using (View view = this.OpenView(
524 "SELECT {0} FROM `{1}`{2}",
525 primaryKeys,
526 table,
527 (where != null && where.Length != 0 ? " WHERE " + where : "")))
528 {
529 view.Execute();
530 for (count = 0; ; count++)
531 {
532 // Avoid creating unnecessary Record objects by not calling View.Fetch().
533 int recordHandle;
534 uint ret = RemotableNativeMethods.MsiViewFetch((int)view.Handle, out recordHandle);
535 if (ret == (uint)NativeMethods.Error.NO_MORE_ITEMS)
536 {
537 break;
538 }
539
540 if (ret != 0)
541 {
542 throw InstallerException.ExceptionFromReturnCode(ret);
543 }
544
545 RemotableNativeMethods.MsiCloseHandle(recordHandle);
546 }
547 }
548 }
549 catch (BadQuerySyntaxException)
550 {
551 // table was missing
552 count = 0;
553 }
554
555 return count;
556 }
557
558 /// <summary>
559 /// Finalizes the persistent form of the database. All persistent data is written
560 /// to the writeable database, and no temporary columns or rows are written.
561 /// </summary>
562 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
563 /// <remarks><p>
564 /// For a database open in <see cref="DatabaseOpenMode.ReadOnly"/> mode, this method has no effect.
565 /// </p><p>
566 /// For a database open in <see cref="DatabaseOpenMode.CreateDirect" /> or <see cref="DatabaseOpenMode.Direct" />
567 /// mode, it is not necessary to call this method because the database will be automatically committed
568 /// when it is closed. However this method may be called at any time to persist the current state of tables
569 /// loaded into memory.
570 /// </p><p>
571 /// For a database open in <see cref="DatabaseOpenMode.Create" /> or <see cref="DatabaseOpenMode.Transact" />
572 /// mode, no changes will be persisted until this method is called. If the database object is closed without
573 /// calling this method, the database file remains unmodified.
574 /// </p><p>
575 /// Win32 MSI API:
576 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasecommit.asp">MsiDatabaseCommit</a>
577 /// </p></remarks>
578 public void Commit()
579 {
580 if (this.summaryInfo != null && !this.summaryInfo.IsClosed)
581 {
582 this.summaryInfo.Persist();
583 this.summaryInfo.Close();
584 this.summaryInfo = null;
585 }
586 uint ret = NativeMethods.MsiDatabaseCommit((int) this.Handle);
587 if (ret != 0)
588 {
589 throw InstallerException.ExceptionFromReturnCode(ret);
590 }
591 }
592
593 /// <summary>
594 /// Copies the structure and data from a specified table to a text archive file.
595 /// </summary>
596 /// <param name="table">Name of the table to be exported</param>
597 /// <param name="exportFilePath">Path to the file to be created</param>
598 /// <exception cref="FileNotFoundException">the file path is invalid</exception>
599 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
600 /// <remarks><p>
601 /// Win32 MSI API:
602 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseexport.asp">MsiDatabaseExport</a>
603 /// </p></remarks>
604 public void Export(string table, string exportFilePath)
605 {
606 if (table == null)
607 {
608 throw new ArgumentNullException("table");
609 }
610
611 FileInfo file = new FileInfo(exportFilePath);
612 uint ret = NativeMethods.MsiDatabaseExport((int) this.Handle, table, file.DirectoryName, file.Name);
613 if (ret != 0)
614 {
615 if (ret == (uint) NativeMethods.Error.BAD_PATHNAME)
616 {
617 throw new FileNotFoundException(null, exportFilePath);
618 }
619 else
620 {
621 throw InstallerException.ExceptionFromReturnCode(ret);
622 }
623 }
624 }
625
626 /// <summary>
627 /// Imports a database table from a text archive file, dropping any existing table.
628 /// </summary>
629 /// <param name="importFilePath">Path to the file to be imported.
630 /// The table name is specified within the file.</param>
631 /// <exception cref="FileNotFoundException">the file path is invalid</exception>
632 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
633 /// <remarks><p>
634 /// Win32 MSI API:
635 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseimport.asp">MsiDatabaseImport</a>
636 /// </p></remarks>
637 public void Import(string importFilePath)
638 {
639 if (String.IsNullOrEmpty(importFilePath))
640 {
641 throw new ArgumentNullException("importFilePath");
642 }
643
644 FileInfo file = new FileInfo(importFilePath);
645 uint ret = NativeMethods.MsiDatabaseImport((int) this.Handle, file.DirectoryName, file.Name);
646 if (ret != 0)
647 {
648 if (ret == (uint) NativeMethods.Error.BAD_PATHNAME)
649 {
650 throw new FileNotFoundException(null, importFilePath);
651 }
652 else
653 {
654 throw InstallerException.ExceptionFromReturnCode(ret);
655 }
656 }
657 }
658
659 /// <summary>
660 /// Exports all database tables, streams, and summary information to archive files.
661 /// </summary>
662 /// <param name="directoryPath">Path to the directory where archive files will be created</param>
663 /// <exception cref="FileNotFoundException">the directory path is invalid</exception>
664 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
665 /// <remarks><p>
666 /// The directory will be created if it does not already exist.
667 /// </p><p>
668 /// Win32 MSI API:
669 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseexport.asp">MsiDatabaseExport</a>
670 /// </p></remarks>
671 public void ExportAll(string directoryPath)
672 {
673 if (String.IsNullOrEmpty(directoryPath))
674 {
675 throw new ArgumentNullException("directoryPath");
676 }
677
678 if (!Directory.Exists(directoryPath))
679 {
680 Directory.CreateDirectory(directoryPath);
681 }
682
683 this.Export("_SummaryInformation", Path.Combine(directoryPath, "_SummaryInformation.idt"));
684
685 using (View view = this.OpenView("SELECT `Name` FROM `_Tables`"))
686 {
687 view.Execute();
688
689 foreach (Record rec in view) using (rec)
690 {
691 string table = (string) rec[1];
692
693 this.Export(table, Path.Combine(directoryPath, table + ".idt"));
694 }
695 }
696
697 if (!Directory.Exists(Path.Combine(directoryPath, "_Streams")))
698 {
699 Directory.CreateDirectory(Path.Combine(directoryPath, "_Streams"));
700 }
701
702 using (View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`"))
703 {
704 view.Execute();
705
706 foreach (Record rec in view) using (rec)
707 {
708 string stream = (string) rec[1];
709 if (stream.EndsWith("SummaryInformation", StringComparison.Ordinal)) continue;
710
711 int i = stream.IndexOf('.');
712 if (i >= 0)
713 {
714 if (File.Exists(Path.Combine(
715 directoryPath,
716 Path.Combine(stream.Substring(0, i), stream.Substring(i + 1) + ".ibd"))))
717 {
718 continue;
719 }
720 }
721 rec.GetStream(2, Path.Combine(directoryPath, Path.Combine("_Streams", stream)));
722 }
723 }
724 }
725
726 /// <summary>
727 /// Imports all database tables, streams, and summary information from archive files.
728 /// </summary>
729 /// <param name="directoryPath">Path to the directory from which archive files will be imported</param>
730 /// <exception cref="FileNotFoundException">the directory path is invalid</exception>
731 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
732 /// <remarks><p>
733 /// Win32 MSI API:
734 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseimport.asp">MsiDatabaseImport</a>
735 /// </p></remarks>
736 public void ImportAll(string directoryPath)
737 {
738 if (String.IsNullOrEmpty(directoryPath))
739 {
740 throw new ArgumentNullException("directoryPath");
741 }
742
743 if (File.Exists(Path.Combine(directoryPath, "_SummaryInformation.idt")))
744 {
745 this.Import(Path.Combine(directoryPath, "_SummaryInformation.idt"));
746 }
747
748 string[] idtFiles = Directory.GetFiles(directoryPath, "*.idt");
749 foreach (string file in idtFiles)
750 {
751 if (Path.GetFileName(file) != "_SummaryInformation.idt")
752 {
753 this.Import(file);
754 }
755 }
756
757 if (Directory.Exists(Path.Combine(directoryPath, "_Streams")))
758 {
759 View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`");
760 Record rec = null;
761 try
762 {
763 view.Execute();
764 string[] streamFiles = Directory.GetFiles(Path.Combine(directoryPath, "_Streams"));
765 foreach (string file in streamFiles)
766 {
767 rec = this.CreateRecord(2);
768 rec[1] = Path.GetFileName(file);
769 rec.SetStream(2, file);
770 view.Insert(rec);
771 rec.Close();
772 rec = null;
773 }
774 }
775 finally
776 {
777 if (rec != null) rec.Close();
778 view.Close();
779 }
780 }
781 }
782
783 /// <summary>
784 /// Creates a new record object with the requested number of fields.
785 /// </summary>
786 /// <param name="fieldCount">Required number of fields, which may be 0.
787 /// The maximum number of fields in a record is limited to 65535.</param>
788 /// <returns>A new record object that can be used with the database.</returns>
789 /// <remarks><p>
790 /// This method is equivalent to directly calling the <see cref="Record" />
791 /// constructor in all cases outside of a custom action context. When in a
792 /// custom action session, this method allows creation of a record that can
793 /// work with a database other than the session database.
794 /// </p><p>
795 /// The Record object should be <see cref="InstallerHandle.Close"/>d after use.
796 /// It is best that the handle be closed manually as soon as it is no longer
797 /// needed, as leaving lots of unused handles open can degrade performance.
798 /// </p><p>
799 /// Win32 MSI API:
800 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicreaterecord.asp">MsiCreateRecord</a>
801 /// </p></remarks>
802 public Record CreateRecord(int fieldCount)
803 {
804 int hRecord = RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, (int) this.Handle);
805 return new Record((IntPtr) hRecord, true, (View) null);
806 }
807
808 /// <summary>
809 /// Returns the file path of this database, or the handle value if a file path was not specified.
810 /// </summary>
811 public override string ToString()
812 {
813 if (this.FilePath != null)
814 {
815 return this.FilePath;
816 }
817 else
818 {
819 return "#" + ((int) this.Handle).ToString(CultureInfo.InvariantCulture);
820 }
821 }
822
823 /// <summary>
824 /// Closes the database handle. After closing a handle, further method calls may throw <see cref="InvalidHandleException"/>.
825 /// </summary>
826 /// <param name="disposing">If true, the method has been called directly or
827 /// indirectly by a user's code, so managed and unmanaged resources will be
828 /// disposed. If false, only unmanaged resources will be disposed.</param>
829 protected override void Dispose(bool disposing)
830 {
831 if (!this.IsClosed &&
832 (this.OpenMode == DatabaseOpenMode.CreateDirect ||
833 this.OpenMode == DatabaseOpenMode.Direct))
834 {
835 // Always commit a direct-opened database before closing.
836 // This avoids unexpected corruption of the database.
837 this.Commit();
838 }
839
840 base.Dispose(disposing);
841
842 if (disposing)
843 {
844 if (this.summaryInfo != null)
845 {
846 this.summaryInfo.Close();
847 this.summaryInfo = null;
848 }
849
850 if (this.deleteOnClose != null)
851 {
852 foreach (string path in this.deleteOnClose)
853 {
854 try
855 {
856 if (Directory.Exists(path))
857 {
858 Directory.Delete(path, true);
859 }
860 else
861 {
862 if (File.Exists(path)) File.Delete(path);
863 }
864 }
865 catch (IOException)
866 {
867 }
868 catch (UnauthorizedAccessException)
869 {
870 }
871 }
872 this.deleteOnClose = null;
873 }
874 }
875 }
876
877 private static int Open(string filePath, string outputPath)
878 {
879 if (String.IsNullOrEmpty(filePath))
880 {
881 throw new ArgumentNullException("filePath");
882 }
883
884 if (String.IsNullOrEmpty(outputPath))
885 {
886 throw new ArgumentNullException("outputPath");
887 }
888
889 int hDb;
890 uint ret = NativeMethods.MsiOpenDatabase(filePath, outputPath, out hDb);
891 if (ret != 0)
892 {
893 throw InstallerException.ExceptionFromReturnCode(ret);
894 }
895 return hDb;
896 }
897
898 private static int Open(string filePath, DatabaseOpenMode mode)
899 {
900 if (String.IsNullOrEmpty(filePath))
901 {
902 throw new ArgumentNullException("filePath");
903 }
904
905 if (Path.GetExtension(filePath).Equals(".msp", StringComparison.Ordinal))
906 {
907 const int DATABASEOPENMODE_PATCH = 32;
908 int patchMode = (int) mode | DATABASEOPENMODE_PATCH;
909 mode = (DatabaseOpenMode) patchMode;
910 }
911
912 int hDb;
913 uint ret = NativeMethods.MsiOpenDatabase(filePath, (IntPtr) mode, out hDb);
914 if (ret != 0)
915 {
916 throw InstallerException.ExceptionFromReturnCode(
917 ret,
918 String.Format(CultureInfo.InvariantCulture, "Database=\"{0}\"", filePath));
919 }
920 return hDb;
921 }
922
923 /// <summary>
924 /// Returns the value of the specified property.
925 /// </summary>
926 /// <param name="property">Name of the property to retrieve.</param>
927 public string ExecutePropertyQuery(string property)
928 {
929 IList<string> values = this.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", property);
930 return (values.Count > 0 ? values[0] : null);
931 }
932 }
933}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/DatabaseQuery.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/DatabaseQuery.cs
new file mode 100644
index 00000000..7c9e011e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/DatabaseQuery.cs
@@ -0,0 +1,412 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 public partial class Database
12 {
13 /// <summary>
14 /// Gets a View object representing the query specified by a SQL string.
15 /// </summary>
16 /// <param name="sqlFormat">SQL query string, which may contain format items</param>
17 /// <param name="args">Zero or more objects to format</param>
18 /// <returns>A View object representing the query specified by a SQL string</returns>
19 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
20 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
21 /// <remarks><p>
22 /// The <paramref name="sqlFormat"/> parameter is formatted using <see cref="String.Format(string,object[])"/>.
23 /// </p><p>
24 /// The View object should be <see cref="InstallerHandle.Close"/>d after use.
25 /// It is best that the handle be closed manually as soon as it is no longer
26 /// needed, as leaving lots of unused handles open can degrade performance.
27 /// </p><p>
28 /// Win32 MSI API:
29 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>
30 /// </p></remarks>
31 public View OpenView(string sqlFormat, params object[] args)
32 {
33 if (String.IsNullOrEmpty(sqlFormat))
34 {
35 throw new ArgumentNullException("sqlFormat");
36 }
37
38 string sql = (args == null || args.Length == 0 ? sqlFormat :
39 String.Format(CultureInfo.InvariantCulture, sqlFormat, args));
40 int viewHandle;
41 uint ret = RemotableNativeMethods.MsiDatabaseOpenView((int) this.Handle, sql, out viewHandle);
42 if (ret != 0)
43 {
44 throw InstallerException.ExceptionFromReturnCode(ret);
45 }
46
47 return new View((IntPtr) viewHandle, sql, this);
48 }
49
50 /// <summary>
51 /// Executes the query specified by a SQL string. The query may not be a SELECT statement.
52 /// </summary>
53 /// <param name="sqlFormat">SQL query string, which may contain format items</param>
54 /// <param name="args">Zero or more objects to format</param>
55 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
56 /// <exception cref="InstallerException">the View could not be executed</exception>
57 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
58 /// <remarks><p>
59 /// The <paramref name="sqlFormat"/> parameter is formatted using
60 /// <see cref="String.Format(string,object[])"/>.
61 /// </p><p>
62 /// Win32 MSI APIs:
63 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
64 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>
65 /// </p></remarks>
66 public void Execute(string sqlFormat, params object[] args)
67 {
68 if (String.IsNullOrEmpty(sqlFormat))
69 {
70 throw new ArgumentNullException("sqlFormat");
71 }
72
73 this.Execute(
74 args == null || args.Length == 0 ?
75 sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args),
76 (Record) null);
77 }
78
79 /// <summary>
80 /// Executes the query specified by a SQL string. The query may not be a SELECT statement.
81 /// </summary>
82 /// <param name="sql">SQL query string</param>
83 /// <param name="record">Optional Record object containing the values that replace
84 /// the parameter tokens (?) in the SQL query.</param>
85 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
86 /// <exception cref="InstallerException">the View could not be executed</exception>
87 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
88 /// <remarks><p>
89 /// Win32 MSI APIs:
90 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
91 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>
92 /// </p></remarks>
93 public void Execute(string sql, Record record)
94 {
95 if (String.IsNullOrEmpty(sql))
96 {
97 throw new ArgumentNullException("sql");
98 }
99
100 using (View view = this.OpenView(sql))
101 {
102 view.Execute(record);
103 }
104 }
105
106 /// <summary>
107 /// Executes the specified SQL SELECT query and returns all results.
108 /// </summary>
109 /// <param name="sqlFormat">SQL query string, which may contain format items</param>
110 /// <param name="args">Zero or more objects to format</param>
111 /// <returns>All results combined into an array</returns>
112 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
113 /// <exception cref="InstallerException">the View could not be executed</exception>
114 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
115 /// <remarks><p>
116 /// The <paramref name="sqlFormat"/> parameter is formatted using
117 /// <see cref="String.Format(string,object[])"/>.
118 /// </p><p>
119 /// Multiple rows columns will be collapsed into a single one-dimensional list.
120 /// </p><p>
121 /// Win32 MSI APIs:
122 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
123 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>,
124 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
125 /// </p></remarks>
126 public IList ExecuteQuery(string sqlFormat, params object[] args)
127 {
128 if (String.IsNullOrEmpty(sqlFormat))
129 {
130 throw new ArgumentNullException("sqlFormat");
131 }
132
133 return this.ExecuteQuery(
134 args == null || args.Length == 0 ?
135 sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args),
136 (Record) null);
137 }
138
139 /// <summary>
140 /// Executes the specified SQL SELECT query and returns all results.
141 /// </summary>
142 /// <param name="sql">SQL SELECT query string</param>
143 /// <param name="record">Optional Record object containing the values that replace
144 /// the parameter tokens (?) in the SQL query.</param>
145 /// <returns>All results combined into an array</returns>
146 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
147 /// <exception cref="InstallerException">the View could not be executed</exception>
148 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
149 /// <remarks><p>
150 /// Multiple rows columns will be collapsed into a single one-dimensional list.
151 /// </p><p>
152 /// Win32 MSI APIs:
153 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
154 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>,
155 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
156 /// </p></remarks>
157 public IList ExecuteQuery(string sql, Record record)
158 {
159 if (String.IsNullOrEmpty(sql))
160 {
161 throw new ArgumentNullException("sql");
162 }
163
164 using (View view = this.OpenView(sql))
165 {
166 view.Execute(record);
167 IList results = new ArrayList();
168 int fieldCount = 0;
169
170 foreach (Record rec in view) using (rec)
171 {
172 if (fieldCount == 0) fieldCount = rec.FieldCount;
173 for (int i = 1; i <= fieldCount; i++)
174 {
175 results.Add(rec[i]);
176 }
177 }
178
179 return results;
180 }
181 }
182
183 /// <summary>
184 /// Executes the specified SQL SELECT query and returns all results as integers.
185 /// </summary>
186 /// <param name="sqlFormat">SQL query string, which may contain format items</param>
187 /// <param name="args">Zero or more objects to format</param>
188 /// <returns>All results combined into an array</returns>
189 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
190 /// <exception cref="InstallerException">the View could not be executed</exception>
191 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
192 /// <remarks><p>
193 /// The <paramref name="sqlFormat"/> parameter is formatted using
194 /// <see cref="String.Format(string,object[])"/>.
195 /// </p><p>
196 /// Multiple rows columns will be collapsed into a single one-dimensional list.
197 /// </p><p>
198 /// Win32 MSI APIs:
199 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
200 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>,
201 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
202 /// </p></remarks>
203 public IList<int> ExecuteIntegerQuery(string sqlFormat, params object[] args)
204 {
205 if (String.IsNullOrEmpty(sqlFormat))
206 {
207 throw new ArgumentNullException("sqlFormat");
208 }
209
210 return this.ExecuteIntegerQuery(
211 args == null || args.Length == 0 ?
212 sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args),
213 (Record) null);
214 }
215
216 /// <summary>
217 /// Executes the specified SQL SELECT query and returns all results as integers.
218 /// </summary>
219 /// <param name="sql">SQL SELECT query string</param>
220 /// <param name="record">Optional Record object containing the values that replace
221 /// the parameter tokens (?) in the SQL query.</param>
222 /// <returns>All results combined into an array</returns>
223 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
224 /// <exception cref="InstallerException">the View could not be executed</exception>
225 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
226 /// <remarks><p>
227 /// Multiple rows columns will be collapsed into a single one-dimensional list.
228 /// </p><p>
229 /// Win32 MSI APIs:
230 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
231 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>,
232 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
233 /// </p></remarks>
234 public IList<int> ExecuteIntegerQuery(string sql, Record record)
235 {
236 if (String.IsNullOrEmpty(sql))
237 {
238 throw new ArgumentNullException("sql");
239 }
240
241 using (View view = this.OpenView(sql))
242 {
243 view.Execute(record);
244 IList<int> results = new List<int>();
245 int fieldCount = 0;
246
247 foreach (Record rec in view) using (rec)
248 {
249 if (fieldCount == 0) fieldCount = rec.FieldCount;
250 for (int i = 1; i <= fieldCount; i++)
251 {
252 results.Add(rec.GetInteger(i));
253 }
254 }
255
256 return results;
257 }
258 }
259
260 /// <summary>
261 /// Executes the specified SQL SELECT query and returns all results as strings.
262 /// </summary>
263 /// <param name="sqlFormat">SQL query string, which may contain format items</param>
264 /// <param name="args">Zero or more objects to format</param>
265 /// <returns>All results combined into an array</returns>
266 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
267 /// <exception cref="InstallerException">the View could not be executed</exception>
268 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
269 /// <remarks><p>
270 /// The <paramref name="sqlFormat"/> parameter is formatted using
271 /// <see cref="String.Format(string,object[])"/>.
272 /// </p><p>
273 /// Multiple rows columns will be collapsed into a single on-dimensional list.
274 /// </p><p>
275 /// Win32 MSI APIs:
276 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
277 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>,
278 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
279 /// </p></remarks>
280 public IList<string> ExecuteStringQuery(string sqlFormat, params object[] args)
281 {
282 if (String.IsNullOrEmpty(sqlFormat))
283 {
284 throw new ArgumentNullException("sqlFormat");
285 }
286
287 return this.ExecuteStringQuery(
288 args == null || args.Length == 0 ?
289 sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args),
290 (Record) null);
291 }
292
293 /// <summary>
294 /// Executes the specified SQL SELECT query and returns all results as strings.
295 /// </summary>
296 /// <param name="sql">SQL SELECT query string</param>
297 /// <param name="record">Optional Record object containing the values that replace
298 /// the parameter tokens (?) in the SQL query.</param>
299 /// <returns>All results combined into an array</returns>
300 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
301 /// <exception cref="InstallerException">the View could not be executed</exception>
302 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
303 /// <remarks><p>
304 /// Multiple rows columns will be collapsed into a single on-dimensional list.
305 /// </p><p>
306 /// Win32 MSI APIs:
307 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
308 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>,
309 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
310 /// </p></remarks>
311 public IList<string> ExecuteStringQuery(string sql, Record record)
312 {
313 if (String.IsNullOrEmpty(sql))
314 {
315 throw new ArgumentNullException("sql");
316 }
317
318 using (View view = this.OpenView(sql))
319 {
320 view.Execute(record);
321 IList<string> results = new List<string>();
322 int fieldCount = 0;
323
324 foreach (Record rec in view) using (rec)
325 {
326 if (fieldCount == 0) fieldCount = rec.FieldCount;
327 for (int i = 1; i <= fieldCount; i++)
328 {
329 results.Add(rec.GetString(i));
330 }
331 }
332
333 return results;
334 }
335 }
336
337 /// <summary>
338 /// Executes the specified SQL SELECT query and returns a single result.
339 /// </summary>
340 /// <param name="sqlFormat">SQL query string, which may contain format items</param>
341 /// <param name="args">Zero or more objects to format</param>
342 /// <returns>First field of the first result</returns>
343 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
344 /// <exception cref="InstallerException">the View could not be executed
345 /// or the query returned 0 results</exception>
346 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
347 /// <remarks><p>
348 /// The <paramref name="sqlFormat"/> parameter is formatted using
349 /// <see cref="String.Format(string,object[])"/>.
350 /// </p><p>
351 /// Win32 MSI APIs:
352 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
353 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>,
354 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
355 /// </p></remarks>
356 public object ExecuteScalar(string sqlFormat, params object[] args)
357 {
358 if (String.IsNullOrEmpty(sqlFormat))
359 {
360 throw new ArgumentNullException("sqlFormat");
361 }
362
363 return this.ExecuteScalar(
364 args == null || args.Length == 0 ?
365 sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args),
366 (Record) null);
367 }
368
369 /// <summary>
370 /// Executes the specified SQL SELECT query and returns a single result.
371 /// </summary>
372 /// <param name="sql">SQL SELECT query string</param>
373 /// <param name="record">Optional Record object containing the values that replace
374 /// the parameter tokens (?) in the SQL query.</param>
375 /// <returns>First field of the first result</returns>
376 /// <exception cref="BadQuerySyntaxException">the SQL syntax is invalid</exception>
377 /// <exception cref="InstallerException">the View could not be executed
378 /// or the query returned 0 results</exception>
379 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
380 /// <remarks><p>
381 /// Win32 MSI APIs:
382 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseopenview.asp">MsiDatabaseOpenView</a>,
383 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>,
384 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
385 /// </p></remarks>
386 public object ExecuteScalar(string sql, Record record)
387 {
388 if (String.IsNullOrEmpty(sql))
389 {
390 throw new ArgumentNullException("sql");
391 }
392
393 View view = this.OpenView(sql);
394 Record rec = null;
395 try
396 {
397 view.Execute(record);
398 rec = view.Fetch();
399 if (rec == null)
400 {
401 throw InstallerException.ExceptionFromReturnCode((uint) NativeMethods.Error.NO_MORE_ITEMS);
402 }
403 return rec[1];
404 }
405 finally
406 {
407 if (rec != null) rec.Close();
408 view.Close();
409 }
410 }
411 }
412}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/DatabaseTransform.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/DatabaseTransform.cs
new file mode 100644
index 00000000..fa843012
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/DatabaseTransform.cs
@@ -0,0 +1,278 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Globalization;
8 using System.Diagnostics.CodeAnalysis;
9
10 public partial class Database
11 {
12 /// <summary>
13 /// Creates a transform that, when applied to the object database, results in the reference database.
14 /// </summary>
15 /// <param name="referenceDatabase">Database that does not include the changes</param>
16 /// <param name="transformFile">Name of the generated transform file, or null to only
17 /// check whether or not the two database are identical</param>
18 /// <returns>true if a transform is generated, or false if a transform is not generated
19 /// because there are no differences between the two databases.</returns>
20 /// <exception cref="InstallerException">the transform could not be generated</exception>
21 /// <exception cref="InvalidHandleException">a Database handle is invalid</exception>
22 /// <remarks><p>
23 /// A transform can add non-primary key columns to the end of a table. A transform cannot
24 /// be created that adds primary key columns to a table. A transform cannot be created that
25 /// changes the order, names, or definitions of columns.
26 /// </p><p>
27 /// If the transform is to be applied during an installation you must use the
28 /// <see cref="Database.CreateTransformSummaryInfo"/> method to populate the
29 /// summary information stream.
30 /// </p><p>
31 /// Win32 MSI API:
32 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabasegeneratetransform.asp">MsiDatabaseGenerateTransform</a>
33 /// </p></remarks>
34 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
35 public bool GenerateTransform(Database referenceDatabase, string transformFile)
36 {
37 if (referenceDatabase == null)
38 {
39 throw new ArgumentNullException("referenceDatabase");
40 }
41
42 if (String.IsNullOrEmpty(transformFile))
43 {
44 throw new ArgumentNullException("transformFile");
45 }
46
47 uint ret = NativeMethods.MsiDatabaseGenerateTransform((int) this.Handle, (int) referenceDatabase.Handle, transformFile, 0, 0);
48 if (ret == (uint) NativeMethods.Error.NO_DATA)
49 {
50 return false;
51 }
52 else if (ret != 0)
53 {
54 throw InstallerException.ExceptionFromReturnCode(ret);
55 }
56 return true;
57 }
58
59 /// <summary>
60 /// Creates and populates the summary information stream of an existing transform file, and
61 /// fills in the properties with the base and reference ProductCode and ProductVersion.
62 /// </summary>
63 /// <param name="referenceDatabase">Database that does not include the changes</param>
64 /// <param name="transformFile">Name of the generated transform file</param>
65 /// <param name="errors">Error conditions that should be suppressed
66 /// when the transform is applied</param>
67 /// <param name="validations">Defines which properties should be validated
68 /// to verify that this transform can be applied to a database.</param>
69 /// <exception cref="InstallerException">the transform summary info could not be
70 /// generated</exception>
71 /// <exception cref="InvalidHandleException">a Database handle is invalid</exception>
72 /// <remarks><p>
73 /// Win32 MSI API:
74 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicreatetransformsummaryinfo.asp">MsiCreateTransformSummaryInfo</a>
75 /// </p></remarks>
76 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
77 public void CreateTransformSummaryInfo(
78 Database referenceDatabase,
79 string transformFile,
80 TransformErrors errors,
81 TransformValidations validations)
82 {
83 if (referenceDatabase == null)
84 {
85 throw new ArgumentNullException("referenceDatabase");
86 }
87
88 if (String.IsNullOrEmpty(transformFile))
89 {
90 throw new ArgumentNullException("transformFile");
91 }
92
93 uint ret = NativeMethods.MsiCreateTransformSummaryInfo(
94 (int) this.Handle,
95 (int) referenceDatabase.Handle,
96 transformFile,
97 (int) errors,
98 (int) validations);
99 if (ret != 0)
100 {
101 throw InstallerException.ExceptionFromReturnCode(ret);
102 }
103 }
104
105 /// <summary>
106 /// Apply a transform to the database, recording the changes in the "_TransformView" table.
107 /// </summary>
108 /// <param name="transformFile">Path to the transform file</param>
109 /// <exception cref="InstallerException">the transform could not be applied</exception>
110 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
111 /// <remarks><p>
112 /// Win32 MSI API:
113 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseapplytransform.asp">MsiDatabaseApplyTransform</a>
114 /// </p></remarks>
115 public void ViewTransform(string transformFile)
116 {
117 TransformErrors transformErrors =
118 TransformErrors.AddExistingRow |
119 TransformErrors.DelMissingRow |
120 TransformErrors.AddExistingTable |
121 TransformErrors.DelMissingTable |
122 TransformErrors.UpdateMissingRow |
123 TransformErrors.ChangeCodePage |
124 TransformErrors.ViewTransform;
125 this.ApplyTransform(transformFile, transformErrors);
126 }
127
128 /// <summary>
129 /// Apply a transform to the database, suppressing any error conditions
130 /// specified by the transform's summary information.
131 /// </summary>
132 /// <param name="transformFile">Path to the transform file</param>
133 /// <exception cref="InstallerException">the transform could not be applied</exception>
134 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
135 /// <remarks><p>
136 /// Win32 MSI API:
137 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseapplytransform.asp">MsiDatabaseApplyTransform</a>
138 /// </p></remarks>
139 public void ApplyTransform(string transformFile)
140 {
141 if (String.IsNullOrEmpty(transformFile))
142 {
143 throw new ArgumentNullException("transformFile");
144 }
145
146 TransformErrors errorConditionsToSuppress;
147 using (SummaryInfo transformSummInfo = new SummaryInfo(transformFile, false))
148 {
149 int errorConditions = transformSummInfo.CharacterCount & 0xFFFF;
150 errorConditionsToSuppress = (TransformErrors) errorConditions;
151 }
152 this.ApplyTransform(transformFile, errorConditionsToSuppress);
153 }
154
155 /// <summary>
156 /// Apply a transform to the database, specifying error conditions to suppress.
157 /// </summary>
158 /// <param name="transformFile">Path to the transform file</param>
159 /// <param name="errorConditionsToSuppress">Error conditions that are to be suppressed</param>
160 /// <exception cref="InstallerException">the transform could not be applied</exception>
161 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
162 /// <remarks><p>
163 /// Win32 MSI API:
164 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidatabaseapplytransform.asp">MsiDatabaseApplyTransform</a>
165 /// </p></remarks>
166 public void ApplyTransform(string transformFile, TransformErrors errorConditionsToSuppress)
167 {
168 if (String.IsNullOrEmpty(transformFile))
169 {
170 throw new ArgumentNullException("transformFile");
171 }
172
173 uint ret = NativeMethods.MsiDatabaseApplyTransform((int) this.Handle, transformFile, (int) errorConditionsToSuppress);
174 if (ret != 0)
175 {
176 throw InstallerException.ExceptionFromReturnCode(ret);
177 }
178 }
179
180 /// <summary>
181 /// Checks whether a transform is valid for this Database, according to its validation data and flags.
182 /// </summary>
183 /// <param name="transformFile">Path to the transform file</param>
184 /// <returns>true if the transform can be validly applied to this Database; false otherwise</returns>
185 /// <exception cref="InstallerException">the transform could not be applied</exception>
186 /// <exception cref="InvalidHandleException">the Database handle is invalid</exception>
187 public bool IsTransformValid(string transformFile)
188 {
189 if (String.IsNullOrEmpty(transformFile))
190 {
191 throw new ArgumentNullException("transformFile");
192 }
193
194 using (SummaryInfo transformSummInfo = new SummaryInfo(transformFile, false))
195 {
196 return this.IsTransformValid(transformSummInfo);
197 }
198 }
199
200 /// <summary>
201 /// Checks whether a transform is valid for this Database, according to its SummaryInfo data.
202 /// </summary>
203 /// <param name="transformSummaryInfo">SummaryInfo data of a transform file</param>
204 /// <returns>true if the transform can be validly applied to this Database; false otherwise</returns>
205 /// <exception cref="InstallerException">error processing summary info</exception>
206 /// <exception cref="InvalidHandleException">the Database or SummaryInfo handle is invalid</exception>
207 public bool IsTransformValid(SummaryInfo transformSummaryInfo)
208 {
209 if (transformSummaryInfo == null)
210 {
211 throw new ArgumentNullException("transformSummaryInfo");
212 }
213
214 string[] rev = transformSummaryInfo.RevisionNumber.Split(new char[] { ';' }, 3);
215 string targetProductCode = rev[0].Substring(0, 38);
216 string targetProductVersion = rev[0].Substring(38);
217 string upgradeCode = rev[2];
218
219 string[] templ = transformSummaryInfo.Template.Split(new char[] { ';' }, 2);
220 int targetProductLanguage = 0;
221 if (templ.Length >= 2 && templ[1].Length > 0)
222 {
223 targetProductLanguage = Int32.Parse(templ[1], CultureInfo.InvariantCulture.NumberFormat);
224 }
225
226 int flags = transformSummaryInfo.CharacterCount;
227 int validateFlags = flags >> 16;
228
229 string thisProductCode = this.ExecutePropertyQuery("ProductCode");
230 string thisProductVersion = this.ExecutePropertyQuery("ProductVersion");
231 string thisUpgradeCode = this.ExecutePropertyQuery("UpgradeCode");
232 string thisProductLang = this.ExecutePropertyQuery("ProductLanguage");
233 int thisProductLanguage = 0;
234 if (!String.IsNullOrEmpty(thisProductLang))
235 {
236 thisProductLanguage = Int32.Parse(thisProductLang, CultureInfo.InvariantCulture.NumberFormat);
237 }
238
239 if ((validateFlags & (int) TransformValidations.Product) != 0 &&
240 thisProductCode != targetProductCode)
241 {
242 return false;
243 }
244
245 if ((validateFlags & (int) TransformValidations.UpgradeCode) != 0 &&
246 thisUpgradeCode != upgradeCode)
247 {
248 return false;
249 }
250
251 if ((validateFlags & (int) TransformValidations.Language) != 0 &&
252 targetProductLanguage != 0 && thisProductLanguage != targetProductLanguage)
253 {
254 return false;
255 }
256
257 Version thisProductVer = new Version(thisProductVersion);
258 Version targetProductVer = new Version(targetProductVersion);
259 if ((validateFlags & (int) TransformValidations.UpdateVersion) != 0)
260 {
261 if (thisProductVer.Major != targetProductVer.Major) return false;
262 if (thisProductVer.Minor != targetProductVer.Minor) return false;
263 if (thisProductVer.Build != targetProductVer.Build) return false;
264 }
265 else if ((validateFlags & (int) TransformValidations.MinorVersion) != 0)
266 {
267 if (thisProductVer.Major != targetProductVer.Major) return false;
268 if (thisProductVer.Minor != targetProductVer.Minor) return false;
269 }
270 else if ((validateFlags & (int) TransformValidations.MajorVersion) != 0)
271 {
272 if (thisProductVer.Major != targetProductVer.Major) return false;
273 }
274
275 return true;
276 }
277 }
278}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/EmbeddedUIProxy.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/EmbeddedUIProxy.cs
new file mode 100644
index 00000000..05e910d4
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/EmbeddedUIProxy.cs
@@ -0,0 +1,231 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Collections;
7 using System.Configuration;
8 using System.Diagnostics.CodeAnalysis;
9 using System.IO;
10 using System.Reflection;
11 using System.Runtime.InteropServices;
12 using System.Security;
13 using System.Text;
14
15 /// <summary>
16 /// Managed-code portion of the embedded UI proxy.
17 /// </summary>
18 internal static class EmbeddedUIProxy
19 {
20 private static IEmbeddedUI uiInstance;
21 private static string uiClass;
22
23 private static bool DebugBreakEnabled(string method)
24 {
25 return CustomActionProxy.DebugBreakEnabled(new string[] { method, EmbeddedUIProxy.uiClass + "." + method } );
26 }
27
28 /// <summary>
29 /// Initializes managed embedded UI by loading the UI class and invoking its Initialize method.
30 /// </summary>
31 /// <param name="sessionHandle">Integer handle to the installer session.</param>
32 /// <param name="uiClass">Name of the class that implements the embedded UI. This must
33 /// be of the form: &quot;AssemblyName!Namespace.Class&quot;</param>
34 /// <param name="internalUILevel">On entry, contains the current UI level for the installation. After this
35 /// method returns, the installer resets the UI level to the returned value of this parameter.</param>
36 /// <returns>0 if the embedded UI was successfully loaded and initialized,
37 /// ERROR_INSTALL_USEREXIT if the user canceled the installation during initialization,
38 /// or ERROR_INSTALL_FAILURE if the embedded UI could not be initialized.</returns>
39 /// <remarks>
40 /// Due to interop limitations, the successful resulting UILevel is actually returned
41 /// as the high-word of the return value instead of via a ref parameter.
42 /// </remarks>
43 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
44 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
45 public static int Initialize(int sessionHandle, string uiClass, int internalUILevel)
46 {
47 Session session = null;
48
49 try
50 {
51 session = new Session((IntPtr) sessionHandle, false);
52
53 if (String.IsNullOrEmpty(uiClass))
54 {
55 throw new ArgumentNullException("uiClass");
56 }
57
58 EmbeddedUIProxy.uiInstance = EmbeddedUIProxy.InstantiateUI(session, uiClass);
59 }
60 catch (Exception ex)
61 {
62 if (session != null)
63 {
64 try
65 {
66 session.Log("Exception while loading embedded UI:");
67 session.Log(ex.ToString());
68 }
69 catch (Exception)
70 {
71 }
72 }
73 }
74
75 if (EmbeddedUIProxy.uiInstance == null)
76 {
77 return (int) ActionResult.Failure;
78 }
79
80 try
81 {
82 string resourcePath = Path.GetDirectoryName(EmbeddedUIProxy.uiInstance.GetType().Assembly.Location);
83 InstallUIOptions uiOptions = (InstallUIOptions) internalUILevel;
84 if (EmbeddedUIProxy.DebugBreakEnabled("Initialize"))
85 {
86 System.Diagnostics.Debugger.Launch();
87 }
88
89 if (EmbeddedUIProxy.uiInstance.Initialize(session, resourcePath, ref uiOptions))
90 {
91 // The embedded UI initialized and the installation should continue
92 // with internal UI reset according to options.
93 return ((int) uiOptions) << 16;
94 }
95 else
96 {
97 // The embedded UI did not initialize but the installation should still continue
98 // with internal UI reset according to options.
99 return (int) uiOptions;
100 }
101 }
102 catch (InstallCanceledException)
103 {
104 // The installation was canceled by the user.
105 return (int) ActionResult.UserExit;
106 }
107 catch (Exception ex)
108 {
109 // An unhandled exception causes the installation to fail immediately.
110 session.Log("Exception thrown by embedded UI initialization:");
111 session.Log(ex.ToString());
112 return (int) ActionResult.Failure;
113 }
114 }
115
116 /// <summary>
117 /// Passes a progress message to the UI class.
118 /// </summary>
119 /// <param name="messageType">Installer message type and message box options.</param>
120 /// <param name="recordHandle">Handle to a record containing message data.</param>
121 /// <returns>Return value returned by the UI class.</returns>
122 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
123 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
124 public static int ProcessMessage(int messageType, int recordHandle)
125 {
126 if (EmbeddedUIProxy.uiInstance != null)
127 {
128 try
129 {
130 int msgType = messageType & 0x7F000000;
131 int buttons = messageType & 0x0000000F;
132 int icon = messageType & 0x000000F0;
133 int defButton = messageType & 0x00000F00;
134
135 Record msgRec = (recordHandle != 0 ? Record.FromHandle((IntPtr) recordHandle, false) : null);
136 using (msgRec)
137 {
138 if (EmbeddedUIProxy.DebugBreakEnabled("ProcessMessage"))
139 {
140 System.Diagnostics.Debugger.Launch();
141 }
142
143 return (int) EmbeddedUIProxy.uiInstance.ProcessMessage(
144 (InstallMessage) msgType,
145 msgRec,
146 (MessageButtons) buttons,
147 (MessageIcon) icon,
148 (MessageDefaultButton) defButton);
149 }
150 }
151 catch (Exception)
152 {
153 // Ignore it... just hope future messages will not throw exceptions.
154 }
155 }
156
157 return 0;
158 }
159
160 /// <summary>
161 /// Passes a shutdown message to the UI class.
162 /// </summary>
163 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
164 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
165 public static void Shutdown()
166 {
167 if (EmbeddedUIProxy.uiInstance != null)
168 {
169 try
170 {
171 if (EmbeddedUIProxy.DebugBreakEnabled("Shutdown"))
172 {
173 System.Diagnostics.Debugger.Launch();
174 }
175
176 EmbeddedUIProxy.uiInstance.Shutdown();
177 }
178 catch (Exception)
179 {
180 // Nothing to do at this point... the installation is done anyway.
181 }
182
183 EmbeddedUIProxy.uiInstance = null;
184 }
185 }
186
187 /// <summary>
188 /// Instantiates a UI class from a given assembly and class name.
189 /// </summary>
190 /// <param name="session">Installer session, for logging.</param>
191 /// <param name="uiClass">Name of the class that implements the embedded UI. This must
192 /// be of the form: &quot;AssemblyName!Namespace.Class&quot;</param>
193 /// <returns>Interface on the UI class for handling UI messages.</returns>
194 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
195 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
196 private static IEmbeddedUI InstantiateUI(Session session, string uiClass)
197 {
198 int assemblySplit = uiClass.IndexOf('!');
199 if (assemblySplit < 0)
200 {
201 session.Log("Error: invalid embedded UI assembly and class:" + uiClass);
202 return null;
203 }
204
205 string assemblyName = uiClass.Substring(0, assemblySplit);
206 EmbeddedUIProxy.uiClass = uiClass.Substring(assemblySplit + 1);
207
208 Assembly uiAssembly;
209 try
210 {
211 uiAssembly = AppDomain.CurrentDomain.Load(assemblyName);
212
213 // This calls out to CustomActionProxy.DebugBreakEnabled() directly instead
214 // of calling EmbeddedUIProxy.DebugBreakEnabled() because we don't compose a
215 // class.method name for this breakpoint.
216 if (CustomActionProxy.DebugBreakEnabled(new string[] { "EmbeddedUI" }))
217 {
218 System.Diagnostics.Debugger.Launch();
219 }
220
221 return (IEmbeddedUI) uiAssembly.CreateInstance(EmbeddedUIProxy.uiClass);
222 }
223 catch (Exception ex)
224 {
225 session.Log("Error: could not load embedded UI class " + EmbeddedUIProxy.uiClass + " from assembly: " + assemblyName);
226 session.Log(ex.ToString());
227 return null;
228 }
229 }
230 }
231}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Enums.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Enums.cs
new file mode 100644
index 00000000..64ed0e7f
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Enums.cs
@@ -0,0 +1,909 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Diagnostics.CodeAnalysis;
7
8 // Enumerations are in alphabetical order.
9
10 /// <summary>
11 /// Specifies a return status value for custom actions.
12 /// </summary>
13 public enum ActionResult : int
14 {
15 /// <summary>Action completed successfully.</summary>
16 Success = 0,
17
18 /// <summary>Skip remaining actions, not an error.</summary>
19 SkipRemainingActions = 259,
20
21 /// <summary>User terminated prematurely.</summary>
22 UserExit = 1602,
23
24 /// <summary>Unrecoverable error or unhandled exception occurred.</summary>
25 Failure = 1603,
26
27 /// <summary>Action not executed.</summary>
28 NotExecuted = 1626,
29 }
30
31 /// <summary>
32 /// Specifies the open mode for a <see cref="Database"/>.
33 /// </summary>
34 public enum DatabaseOpenMode : int
35 {
36 /// <summary>Open a database read-only, no persistent changes.</summary>
37 ReadOnly = 0,
38
39 /// <summary>Open a database read/write in transaction mode.</summary>
40 Transact = 1,
41
42 /// <summary>Open a database direct read/write without transaction.</summary>
43 Direct = 2,
44
45 /// <summary>Create a new database, transact mode read/write.</summary>
46 Create = 3,
47
48 /// <summary>Create a new database, direct mode read/write.</summary>
49 CreateDirect = 4,
50 }
51
52 /// <summary>
53 /// Log modes available for <see cref="Installer.EnableLog(InstallLogModes,string)"/>
54 /// and <see cref="Installer.SetExternalUI(ExternalUIHandler,InstallLogModes)"/>.
55 /// </summary>
56 [Flags]
57 public enum InstallLogModes : int
58 {
59 /// <summary>Disable logging.</summary>
60 None = 0,
61
62 /// <summary>Log out of memory or fatal exit information.</summary>
63 FatalExit = (1 << ((int) InstallMessage.FatalExit >> 24)),
64
65 /// <summary>Log error messages.</summary>
66 Error = (1 << ((int) InstallMessage.Error >> 24)),
67
68 /// <summary>Log warning messages.</summary>
69 Warning = (1 << ((int) InstallMessage.Warning >> 24)),
70
71 /// <summary>Log user requests.</summary>
72 User = (1 << ((int) InstallMessage.User >> 24)),
73
74 /// <summary>Log status messages that are not displayed.</summary>
75 Info = (1 << ((int) InstallMessage.Info >> 24)),
76
77 /// <summary>Log request to determine a valid source location.</summary>
78 ResolveSource = (1 << ((int) InstallMessage.ResolveSource >> 24)),
79
80 /// <summary>Log insufficient disk space error.</summary>
81 OutOfDiskSpace = (1 << ((int) InstallMessage.OutOfDiskSpace >> 24)),
82
83 /// <summary>Log the start of installation actions.</summary>
84 ActionStart = (1 << ((int) InstallMessage.ActionStart >> 24)),
85
86 /// <summary>Log the data record for installation actions.</summary>
87 ActionData = (1 << ((int) InstallMessage.ActionData >> 24)),
88
89 /// <summary>Log parameters for user-interface initialization.</summary>
90 CommonData = (1 << ((int) InstallMessage.CommonData >> 24)),
91
92 /// <summary>Log the property values at termination.</summary>
93 PropertyDump = (1 << ((int) InstallMessage.Progress >> 24)), // log only
94
95 /// <summary>
96 /// Sends large amounts of information to log file not generally useful to users.
97 /// May be used for support.
98 /// </summary>
99 Verbose = (1 << ((int) InstallMessage.Initialize >> 24)), // log only
100
101 /// <summary>
102 /// Log extra debugging information.
103 /// </summary>
104 ExtraDebug = (1 << ((int) InstallMessage.Terminate >> 24)), // log only
105
106 /// <summary>
107 /// Log only on error.
108 /// </summary>
109 LogOnlyOnError = (1 << ((int) InstallMessage.ShowDialog >> 24)), // log only
110
111 /// <summary>
112 /// Log progress bar information. This message includes information on units so far and total number
113 /// of units. See <see cref="Session.Message"/> for an explanation of the message format. This message
114 /// is only sent to an external user interface and is not logged.
115 /// </summary>
116 Progress = (1 << ((int) InstallMessage.Progress >> 24)), // external handler only
117
118 /// <summary>
119 /// If this is not a quiet installation, then the basic UI has been initialized. If this is a full
120 /// UI installation, the Full UI is not yet initialized. This message is only sent to an external
121 /// user interface and is not logged.
122 /// </summary>
123 Initialize = (1 << ((int) InstallMessage.Initialize >> 24)), // external handler only
124
125 /// <summary>
126 /// If a full UI is being used, the full UI has ended. If this is not a quiet installation, the basic
127 /// UI has not yet ended. This message is only sent to an external user interface and is not logged.
128 /// </summary>
129 Terminate = (1 << ((int) InstallMessage.Terminate >> 24)), // external handler only
130
131 /// <summary>
132 /// Sent prior to display of the Full UI dialog. This message is only sent to an external user
133 /// interface and is not logged.
134 /// </summary>
135 ShowDialog = (1 << ((int) InstallMessage.ShowDialog >> 24)), // external handler only
136
137 /// <summary>
138 /// List of files in use that need to be replaced.
139 /// </summary>
140 FilesInUse = (1 << ((int) InstallMessage.FilesInUse >> 24)), // external handler only
141
142 /// <summary>
143 /// [MSI 4.0] List of apps that the user can request Restart Manager to shut down and restart.
144 /// </summary>
145 RMFilesInUse = (1 << ((int) InstallMessage.RMFilesInUse >> 24)), // external handler only
146 }
147
148 /// <summary>
149 /// Type of message to be processed by <see cref="Session.Message"/>,
150 /// <see cref="ExternalUIHandler"/>, or <see cref="ExternalUIRecordHandler"/>.
151 /// </summary>
152 [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")]
153 public enum InstallMessage : int
154 {
155 /// <summary>Premature termination, possibly fatal OOM.</summary>
156 FatalExit = 0x00000000,
157
158 /// <summary>Formatted error message.</summary>
159 Error = 0x01000000,
160
161 /// <summary>Formatted warning message.</summary>
162 Warning = 0x02000000,
163
164 /// <summary>User request message.</summary>
165 User = 0x03000000,
166
167 /// <summary>Informative message for log.</summary>
168 Info = 0x04000000,
169
170 /// <summary>List of files in use that need to be replaced.</summary>
171 FilesInUse = 0x05000000,
172
173 /// <summary>Request to determine a valid source location.</summary>
174 ResolveSource = 0x06000000,
175
176 /// <summary>Insufficient disk space message.</summary>
177 OutOfDiskSpace = 0x07000000,
178
179 /// <summary>Start of action: action name &amp; description.</summary>
180 ActionStart = 0x08000000,
181
182 /// <summary>Formatted data associated with individual action item.</summary>
183 ActionData = 0x09000000,
184
185 /// <summary>Progress gauge info: units so far, total.</summary>
186 Progress = 0x0A000000,
187
188 /// <summary>Product info for dialog: language Id, dialog caption.</summary>
189 CommonData = 0x0B000000,
190
191 /// <summary>Sent prior to UI initialization, no string data.</summary>
192 Initialize = 0x0C000000,
193
194 /// <summary>Sent after UI termination, no string data.</summary>
195 Terminate = 0x0D000000,
196
197 /// <summary>Sent prior to display or authored dialog or wizard.</summary>
198 ShowDialog = 0x0E000000,
199
200 /// <summary>[MSI 4.0] List of apps that the user can request Restart Manager to shut down and restart.</summary>
201 RMFilesInUse = 0x19000000,
202
203 /// <summary>[MSI 4.5] Sent prior to install of a product.</summary>
204 InstallStart = 0x1A000000,
205
206 /// <summary>[MSI 4.5] Sent after install of a product.</summary>
207 InstallEnd = 0x1B000000,
208 }
209
210 /// <summary>
211 /// Specifies the install mode for <see cref="Installer.ProvideComponent"/> or <see cref="Installer.ProvideQualifiedComponent"/>.
212 /// </summary>
213 public enum InstallMode : int
214 {
215 /// <summary>Provide the component only if the feature's installation state is <see cref="InstallState.Local"/>.</summary>
216 NoSourceResolution = -3,
217
218 /// <summary>Only check that the component is registered, without verifying that the key file of the component exists.</summary>
219 NoDetection = -2,
220
221 /// <summary>Provide the component only if the feature exists.</summary>
222 Existing = -1,
223
224 /// <summary>Provide the component and perform any installation necessary to provide the component.</summary>
225 Default = 0,
226 }
227
228 /// <summary>
229 /// Specifies the run mode for <see cref="Session.GetMode"/>.
230 /// </summary>
231 [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")]
232 public enum InstallRunMode : int
233 {
234 /// <summary>The administrative mode is installing, or the product is installing.</summary>
235 Admin = 0,
236
237 /// <summary>The advertisements are installing or the product is installing or updating.</summary>
238 Advertise = 1,
239
240 /// <summary>An existing installation is being modified or there is a new installation.</summary>
241 Maintenance = 2,
242
243 /// <summary>Rollback is enabled.</summary>
244 RollbackEnabled = 3,
245
246 /// <summary>The log file is active. It was enabled prior to the installation session.</summary>
247 LogEnabled = 4,
248
249 /// <summary>Execute operations are spooling or they are in the determination phase.</summary>
250 Operations = 5,
251
252 /// <summary>A reboot is necessary after a successful installation (settable).</summary>
253 RebootAtEnd = 6,
254
255 /// <summary>A reboot is necessary to continue the installation (settable).</summary>
256 RebootNow = 7,
257
258 /// <summary>Files from cabinets and Media table files are installing.</summary>
259 Cabinet = 8,
260
261 /// <summary>The source LongFileNames is suppressed through the PID_MSISOURCE summary property.</summary>
262 SourceShortNames = 9,
263
264 /// <summary>The target LongFileNames is suppressed through the SHORTFILENAMES property.</summary>
265 TargetShortNames = 10,
266
267 // <summary>Reserved for future use.</summary>
268 //Reserved11 = 11,
269
270 /// <summary>The operating system is Windows 95, Windows 98, or Windows ME.</summary>
271 [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "x")]
272 Windows9x = 12,
273
274 /// <summary>The operating system supports demand installation.</summary>
275 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Zaw")]
276 ZawEnabled = 13,
277
278 // <summary>Reserved for future use.</summary>
279 //Reserved14 = 14,
280
281 // <summary>Reserved for future use.</summary>
282 //Reserved15 = 15,
283
284 /// <summary>A custom action called from install script execution.</summary>
285 Scheduled = 16,
286
287 /// <summary>A custom action called from rollback execution script.</summary>
288 Rollback = 17,
289
290 /// <summary>A custom action called from commit execution script.</summary>
291 Commit = 18,
292 }
293
294 /// <summary>
295 /// Installed state of a Component or Feature.
296 /// </summary>
297 public enum InstallState : int
298 {
299 /// <summary>The component is disabled.</summary>
300 NotUsed = -7,
301
302 /// <summary>The installation configuration data is corrupt.</summary>
303 BadConfig = -6,
304
305 /// <summary>The installation is suspended or in progress.</summary>
306 Incomplete = -5,
307
308 /// <summary>Component is set to run from source, but source is unavailable.</summary>
309 SourceAbsent = -4,
310
311 /// <summary>The buffer overflow is returned.</summary>
312 MoreData = -3,
313
314 /// <summary>An invalid parameter was passed to the function.</summary>
315 InvalidArgument = -2,
316
317 /// <summary>An unrecognized product or feature name was passed to the function.</summary>
318 Unknown = -1,
319
320 /// <summary>The component is broken.</summary>
321 Broken = 0,
322
323 /// <summary>The feature is advertised.</summary>
324 Advertised = 1,
325
326 /// <summary>The component is being removed. In action state and not settable.</summary>
327 Removed = 1,
328
329 /// <summary>The component is not installed, or action state is absent but clients remain.</summary>
330 Absent = 2,
331
332 /// <summary>The component is installed on the local drive.</summary>
333 Local = 3,
334
335 /// <summary>The component will run from the source, CD, or network.</summary>
336 Source = 4,
337
338 /// <summary>The component will be installed in the default location: local or source.</summary>
339 Default = 5,
340 }
341
342 /// <summary>
343 /// Specifies the type of installation for <see cref="Installer.ApplyPatch(string,string,InstallType,string)"/>.
344 /// </summary>
345 public enum InstallType : int
346 {
347 /// <summary>Searches system for products to patch.</summary>
348 Default = 0,
349
350 /// <summary>Indicates a administrative installation.</summary>
351 NetworkImage = 1,
352
353 /// <summary>Indicates a particular instance.</summary>
354 SingleInstance = 2,
355 }
356
357 /// <summary>
358 /// Level of the installation user interface, specified with
359 /// <see cref="Installer.SetInternalUI(InstallUIOptions)"/>.
360 /// </summary>
361 [SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue")]
362 [Flags]
363 public enum InstallUIOptions : int
364 {
365 /// <summary>Does not change UI level.</summary>
366 NoChange = 0,
367
368 /// <summary>Uses Default UI level.</summary>
369 Default = 1,
370
371 /// <summary>Silent installation.</summary>
372 Silent = 2,
373
374 /// <summary>Simple progress and error handling.</summary>
375 Basic = 3,
376
377 /// <summary>Authored UI, wizard dialogs suppressed.</summary>
378 Reduced = 4,
379
380 /// <summary>Authored UI with wizards, progress, and errors.</summary>
381 Full = 5,
382
383 /// <summary>
384 /// When combined with the <see cref="Basic"/> value, the installer does not display
385 /// the cancel button in the progress dialog.
386 /// </summary>
387 HideCancel = 0x20,
388
389 /// <summary>
390 /// When combined with the <see cref="Basic"/> value, the installer displays progress
391 /// dialog boxes but does not display any modal dialog boxes or error dialog boxes.
392 /// </summary>
393 ProgressOnly = 0x40,
394
395 /// <summary>
396 /// When combined with another value, the installer displays a modal dialog
397 /// box at the end of a successful installation or if there has been an error.
398 /// No dialog box is displayed if the user cancels.
399 /// </summary>
400 EndDialog = 0x80,
401
402 /// <summary>
403 /// Forces display of the source resolution dialog even if the UI is otherwise silent.
404 /// </summary>
405 SourceResolutionOnly = 0x100,
406
407 /// <summary>
408 /// [MSI 5.0] Forces display of the UAC dialog even if the UI is otherwise silent.
409 /// </summary>
410 UacOnly = 0x200,
411 }
412
413 /// <summary>
414 /// Specifies a return status value for message handlers. These values are returned by
415 /// <see cref="Session.Message"/>, <see cref="ExternalUIHandler"/>, and <see cref="IEmbeddedUI.ProcessMessage"/>.
416 /// </summary>
417 public enum MessageResult : int
418 {
419 /// <summary>An error was found in the message handler.</summary>
420 Error = -1,
421
422 /// <summary>No action was taken.</summary>
423 None = 0,
424
425 /// <summary>IDOK</summary>
426 [SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase")]
427 OK = 1,
428
429 /// <summary>IDCANCEL</summary>
430 Cancel = 2,
431
432 /// <summary>IDABORT</summary>
433 Abort = 3,
434
435 /// <summary>IDRETRY</summary>
436 Retry = 4,
437
438 /// <summary>IDIGNORE</summary>
439 Ignore = 5,
440
441 /// <summary>IDYES</summary>
442 Yes = 6,
443
444 /// <summary>IDNO</summary>
445 No = 7,
446 }
447
448 /// <summary>
449 /// Specifies constants defining which buttons to display for a message. This can be cast to
450 /// the MessageBoxButtons enum in System.Windows.Forms and System.Windows.
451 /// </summary>
452 public enum MessageButtons
453 {
454 /// <summary>
455 /// The message contains an OK button.
456 /// </summary>
457 OK = 0,
458
459 /// <summary>
460 /// The message contains OK and Cancel buttons.
461 /// </summary>
462 OKCancel = 1,
463
464 /// <summary>
465 /// The message contains Abort, Retry, and Ignore buttons.
466 /// </summary>
467 AbortRetryIgnore = 2,
468
469 /// <summary>
470 /// The message contains Yes, No, and Cancel buttons.
471 /// </summary>
472 YesNoCancel = 3,
473
474 /// <summary>
475 /// The message contains Yes and No buttons.
476 /// </summary>
477 YesNo = 4,
478
479 /// <summary>
480 /// The message contains Retry and Cancel buttons.
481 /// </summary>
482 RetryCancel = 5,
483 }
484
485 /// <summary>
486 /// Specifies constants defining which information to display. This can be cast to
487 /// the MessageBoxIcon enum in System.Windows.Forms and System.Windows.
488 /// </summary>
489 public enum MessageIcon
490 {
491 /// <summary>
492 /// The message contain no symbols.
493 /// </summary>
494 None = 0,
495
496 /// <summary>
497 /// The message contains a symbol consisting of white X in a circle with a red background.
498 /// </summary>
499 Error = 16,
500
501 /// <summary>
502 /// The message contains a symbol consisting of a white X in a circle with a red background.
503 /// </summary>
504 Hand = 16,
505
506 /// <summary>
507 /// The message contains a symbol consisting of white X in a circle with a red background.
508 /// </summary>
509 Stop = 16,
510
511 /// <summary>
512 /// The message contains a symbol consisting of a question mark in a circle.
513 /// </summary>
514 Question = 32,
515
516 /// <summary>
517 /// The message contains a symbol consisting of an exclamation point in a triangle with a yellow background.
518 /// </summary>
519 Exclamation = 48,
520
521 /// <summary>
522 /// The message contains a symbol consisting of an exclamation point in a triangle with a yellow background.
523 /// </summary>
524 Warning = 48,
525
526 /// <summary>
527 /// The message contains a symbol consisting of a lowercase letter i in a circle.
528 /// </summary>
529 Information = 64,
530
531 /// <summary>
532 /// The message contains a symbol consisting of a lowercase letter i in a circle.
533 /// </summary>
534 Asterisk = 64,
535 }
536
537 /// <summary>
538 /// Specifies constants defining the default button on a message. This can be cast to
539 /// the MessageBoxDefaultButton enum in System.Windows.Forms and System.Windows.
540 /// </summary>
541 public enum MessageDefaultButton
542 {
543 /// <summary>
544 /// The first button on the message is the default button.
545 /// </summary>
546 Button1 = 0,
547
548 /// <summary>
549 /// The second button on the message is the default button.
550 /// </summary>
551 Button2 = 256,
552
553 /// <summary>
554 /// The third button on the message is the default button.
555 /// </summary>
556 Button3 = 512,
557 }
558
559 /// <summary>
560 /// Additional styles for use with message boxes.
561 /// </summary>
562 [Flags]
563 internal enum MessageBoxStyles
564 {
565 /// <summary>
566 /// The message box is created with the WS_EX_TOPMOST window style.
567 /// </summary>
568 TopMost = 0x00040000,
569
570 /// <summary>
571 /// The caller is a service notifying the user of an event.
572 /// The function displays a message box on the current active desktop, even if there is no user logged on to the computer.
573 /// </summary>
574 ServiceNotification = 0x00200000,
575 }
576
577 /// <summary>
578 /// Specifies the different patch states for <see cref="PatchInstallation.GetPatches(string, string, string, UserContexts, PatchStates)"/>.
579 /// </summary>
580 [Flags]
581 public enum PatchStates : int
582 {
583 /// <summary>Invalid value.</summary>
584 None = 0,
585
586 /// <summary>Patches applied to a product.</summary>
587 Applied = 1,
588
589 /// <summary>Patches that are superseded by other patches.</summary>
590 Superseded = 2,
591
592 /// <summary>Patches that are obsolesced by other patches.</summary>
593 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Obsoleted")]
594 Obsoleted = 4,
595
596 /// <summary>Patches that are registered to a product but not applied.</summary>
597 Registered = 8,
598
599 /// <summary>All valid patch states.</summary>
600 All = (Applied | Superseded | Obsoleted | Registered)
601 }
602
603 /// <summary>
604 /// Specifies the reinstall mode for <see cref="Installer.ReinstallFeature"/> or <see cref="Installer.ReinstallProduct"/>.
605 /// </summary>
606 [Flags]
607 public enum ReinstallModes : int
608 {
609 /// <summary>Reinstall only if file is missing.</summary>
610 FileMissing = 0x00000002,
611
612 /// <summary>Reinstall if file is missing, or older version.</summary>
613 FileOlderVersion = 0x00000004,
614
615 /// <summary>Reinstall if file is missing, or equal or older version.</summary>
616 FileEqualVersion = 0x00000008,
617
618 /// <summary>Reinstall if file is missing, or not exact version.</summary>
619 FileExact = 0x00000010,
620
621 /// <summary>Checksum executables, reinstall if missing or corrupt.</summary>
622 FileVerify = 0x00000020,
623
624 /// <summary>Reinstall all files, regardless of version.</summary>
625 FileReplace = 0x00000040,
626
627 /// <summary>Insure required machine reg entries.</summary>
628 MachineData = 0x00000080,
629
630 /// <summary>Insure required user reg entries.</summary>
631 UserData = 0x00000100,
632
633 /// <summary>Validate shortcuts items.</summary>
634 Shortcut = 0x00000200,
635
636 /// <summary>Use re-cache source install package.</summary>
637 Package = 0x00000400,
638 }
639
640 /// <summary>
641 /// Attributes for <see cref="Transaction"/> methods.
642 /// </summary>
643 [Flags]
644 public enum TransactionAttributes : int
645 {
646 /// <summary>No attributes.</summary>
647 None = 0x00000000,
648
649 /// <summary>Request that the Windows Installer not shutdown the embedded UI until the transaction is complete.</summary>
650 ChainEmbeddedUI = 0x00000001,
651
652 /// <summary>Request that the Windows Installer transfer the embedded UI from the original installation.</summary>
653 JoinExistingEmbeddedUI = 0x00000002,
654 }
655
656 /// <summary>
657 /// Transform error conditions available for <see cref="Database.CreateTransformSummaryInfo"/> or
658 /// <see cref="Database.ApplyTransform(string,TransformErrors)"/>.
659 /// </summary>
660 [Flags]
661 public enum TransformErrors : int
662 {
663 /// <summary>No error conditions.</summary>
664 None = 0x0000,
665
666 /// <summary>Adding a row that already exists.</summary>
667 AddExistingRow = 0x0001,
668
669 /// <summary>Deleting a row that doesn't exist.</summary>
670 DelMissingRow = 0x0002,
671
672 /// <summary>Adding a table that already exists.</summary>
673 AddExistingTable = 0x0004,
674
675 /// <summary>Deleting a table that doesn't exist.</summary>
676 DelMissingTable = 0x0008,
677
678 /// <summary>Updating a row that doesn't exist.</summary>
679 UpdateMissingRow = 0x0010,
680
681 /// <summary>Transform and database code pages do not match and neither code page is neutral.</summary>
682 ChangeCodePage = 0x0020,
683
684 /// <summary>Create the temporary _TransformView table when applying the transform.</summary>
685 ViewTransform = 0x0100,
686 }
687
688 /// <summary>
689 /// Transform validation flags available for <see cref="Database.CreateTransformSummaryInfo"/>.
690 /// </summary>
691 [Flags]
692 public enum TransformValidations : int
693 {
694 /// <summary>Validate no properties.</summary>
695 None = 0x0000,
696
697 /// <summary>Default language must match base database.</summary>
698 Language = 0x0001,
699
700 /// <summary>Product must match base database.</summary>
701 Product = 0x0002,
702
703 /// <summary>Check major version only.</summary>
704 MajorVersion = 0x0008,
705
706 /// <summary>Check major and minor versions only.</summary>
707 MinorVersion = 0x0010,
708
709 /// <summary>Check major, minor, and update versions.</summary>
710 UpdateVersion = 0x0020,
711
712 /// <summary>Installed version &lt; base version.</summary>
713 NewLessBaseVersion = 0x0040,
714
715 /// <summary>Installed version &lt;= base version.</summary>
716 NewLessEqualBaseVersion = 0x0080,
717
718 /// <summary>Installed version = base version.</summary>
719 NewEqualBaseVersion = 0x0100,
720
721 /// <summary>Installed version &gt;= base version.</summary>
722 NewGreaterEqualBaseVersion = 0x0200,
723
724 /// <summary>Installed version &gt; base version.</summary>
725 NewGreaterBaseVersion = 0x0400,
726
727 /// <summary>UpgradeCode must match base database.</summary>
728 UpgradeCode = 0x0800,
729 }
730
731 /// <summary>
732 /// Specifies the installation context for <see cref="ProductInstallation"/>s,
733 /// <see cref="PatchInstallation"/>es, and
734 /// <see cref="Installer.DetermineApplicablePatches(string,string[],InapplicablePatchHandler,string,UserContexts)"/>
735 /// </summary>
736 [Flags]
737 public enum UserContexts : int
738 {
739 /// <summary>Not installed.</summary>
740 None = 0,
741
742 /// <summary>User managed install context.</summary>
743 UserManaged = 1,
744
745 /// <summary>User non-managed context.</summary>
746 UserUnmanaged = 2,
747
748 /// <summary>Per-machine context.</summary>
749 Machine = 4,
750
751 /// <summary>All contexts, or all valid values.</summary>
752 All = (UserManaged | UserUnmanaged | Machine),
753
754 /// <summary>All user-managed contexts.</summary>
755 AllUserManaged = 8,
756 }
757
758 /// <summary>
759 /// Defines the type of error encountered by the <see cref="View.Validate"/>, <see cref="View.ValidateNew"/>,
760 /// or <see cref="View.ValidateFields"/> methods of the <see cref="View"/> class.
761 /// </summary>
762 public enum ValidationError : int
763 {
764 /*
765 InvalidArg = -3,
766 MoreData = -2,
767 FunctionError = -1,
768 */
769
770 /// <summary>No error.</summary>
771 None = 0,
772
773 /// <summary>The new record duplicates primary keys of the existing record in a table.</summary>
774 DuplicateKey = 1,
775
776 /// <summary>There are no null values allowed, or the column is about to be deleted but is referenced by another row.</summary>
777 Required = 2,
778
779 /// <summary>The corresponding record in a foreign table was not found.</summary>
780 BadLink = 3,
781
782 /// <summary>The data is greater than the maximum value allowed.</summary>
783 Overflow = 4,
784
785 /// <summary>The data is less than the minimum value allowed.</summary>
786 Underflow = 5,
787
788 /// <summary>The data is not a member of the values permitted in the set.</summary>
789 NotInSet = 6,
790
791 /// <summary>An invalid version string was supplied.</summary>
792 BadVersion = 7,
793
794 /// <summary>The case was invalid. The case must be all uppercase or all lowercase.</summary>
795 BadCase = 8,
796
797 /// <summary>An invalid GUID was supplied.</summary>
798 BadGuid = 9,
799
800 /// <summary>An invalid wildcard file name was supplied, or the use of wildcards was invalid.</summary>
801 BadWildcard = 10,
802
803 /// <summary>An invalid identifier was supplied.</summary>
804 BadIdentifier = 11,
805
806 /// <summary>Invalid language IDs were supplied.</summary>
807 BadLanguage = 12,
808
809 /// <summary>An invalid file name was supplied.</summary>
810 BadFileName = 13,
811
812 /// <summary>An invalid path was supplied.</summary>
813 BadPath = 14,
814
815 /// <summary>An invalid conditional statement was supplied.</summary>
816 BadCondition = 15,
817
818 /// <summary>An invalid format string was supplied.</summary>
819 BadFormatted = 16,
820
821 /// <summary>An invalid template string was supplied.</summary>
822 BadTemplate = 17,
823
824 /// <summary>An invalid string was supplied in the DefaultDir column of the Directory table.</summary>
825 BadDefaultDir = 18,
826
827 /// <summary>An invalid registry path string was supplied.</summary>
828 BadRegPath = 19,
829
830 /// <summary>An invalid string was supplied in the CustomSource column of the CustomAction table.</summary>
831 BadCustomSource = 20,
832
833 /// <summary>An invalid property string was supplied.</summary>
834 BadProperty = 21,
835
836 /// <summary>The _Validation table is missing a reference to a column.</summary>
837 MissingData = 22,
838
839 /// <summary>The category column of the _Validation table for the column is invalid.</summary>
840 BadCategory = 23,
841
842 /// <summary>The table in the Keytable column of the _Validation table was not found or loaded.</summary>
843 BadKeyTable = 24,
844
845 /// <summary>The value in the MaxValue column of the _Validation table is less than the value in the MinValue column.</summary>
846 BadMaxMinValues = 25,
847
848 /// <summary>An invalid cabinet name was supplied.</summary>
849 BadCabinet = 26,
850
851 /// <summary>An invalid shortcut target name was supplied.</summary>
852 BadShortcut = 27,
853
854 /// <summary>The string is too long for the length specified by the column definition.</summary>
855 StringOverflow = 28,
856
857 /// <summary>An invalid localization attribute was supplied. (Primary keys cannot be localized.)</summary>
858 BadLocalizeAttrib = 29
859 }
860
861 /// <summary>
862 /// Specifies the modify mode for <see cref="View.Modify"/>.
863 /// </summary>
864 public enum ViewModifyMode : int
865 {
866 /// <summary>
867 /// Refreshes the information in the supplied record without changing the position
868 /// in the result set and without affecting subsequent fetch operations.
869 /// </summary>
870 Seek = -1,
871
872 /// <summary>Refreshes the data in a Record.</summary>
873 Refresh = 0,
874
875 /// <summary>Inserts a Record into the view.</summary>
876 Insert = 1,
877
878 /// <summary>Updates the View with new data from the Record.</summary>
879 Update = 2,
880
881 /// <summary>Updates or inserts a Record into the View.</summary>
882 Assign = 3,
883
884 /// <summary>Updates or deletes and inserts a Record into the View.</summary>
885 Replace = 4,
886
887 /// <summary>Inserts or validates a record.</summary>
888 Merge = 5,
889
890 /// <summary>Deletes a Record from the View.</summary>
891 Delete = 6,
892
893 /// <summary>Inserts a Record into the View. The inserted data is not persistent.</summary>
894 InsertTemporary = 7,
895
896 /// <summary>Validates a record.</summary>
897 Validate = 8,
898
899 /// <summary>Validates a new record.</summary>
900 [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
901 ValidateNew = 9,
902
903 /// <summary>Validates fields of a fetched or new record. Can validate one or more fields of an incomplete record.</summary>
904 ValidateField = 10,
905
906 /// <summary>Validates a record that will be deleted later.</summary>
907 ValidateDelete = 11,
908 }
909}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Errors.resources b/src/dtf/WixToolset.Dtf.WindowsInstaller/Errors.resources
new file mode 100644
index 00000000..5564e88a
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Errors.resources
Binary files differ
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Errors.txt b/src/dtf/WixToolset.Dtf.WindowsInstaller/Errors.txt
new file mode 100644
index 00000000..ec7c97b1
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Errors.txt
@@ -0,0 +1,404 @@
1; Windows Installer Internal Error Messages
2;
3; Error numbers greater than 2000 are "internal errors" and do not have
4; localized strings in msimsg.dll. The messages are provided here
5; (in English only) because they can be informative and time-saving
6; during developement.
7;
8
92101=Shortcuts not supported by the OS.
102102=Invalid .INI action: [2]
112103=Could not resolve path for shell folder [2].
122104=Writing .INI file: [3]: System error: [2]
132105=Shortcut Creation [3] Failed. System error: [2]
142106=Shortcut Deletion [3] Failed. System error: [2]
152107=Error [3] registering type library [2].
162108=Error [3] unregistering type library [2].
172109=Section missing for INI action.
182110=Key missing for INI action.
192111=Detection of running apps failed, could not get performance data. Reg operation returned : [2].
202112=Detection of running apps failed, could not get performance index. Reg operation returned : [2].
212113=Detection of running apps failed.
222200=Database: [2]. Database object creation failed, mode = [3].
232201=Database: [2]. Initialization failed, out of memory.
242202=Database: [2]. Data access failed, out of memory.
252203=Database: [2]. Cannot open database file. System error [3].
262204=Database: [2]. Table already exists: [3]
272205=Database: [2]. Table does not exist: [3]
282206=Database: [2]. Table could not be dropped: [3]
292207=Database: [2]. Intent violation.
302208=Database: [2]. Insufficient parameters for Execute.
312209=Database: [2]. Cursor in invalid state.
322210=Database: [2]. Invalid update data type in column [3]
332211=Database: [2]. Could not create database table [3]
342212=Database: [2]. Database not in writable state.
352213=Database: [2]. Error saving database tables.
362214=Database: [2]. Error writing export file: [3]
372215=Database: [2]. Cannot open import file: [3]
382216=Database: [2]. Import file format error: [3], Line [4]
392217=Database: [2]. Wrong state to CreateOutputDatabase [3].
402218=Database: [2]. Table name not supplied.
412219=Database: [2]. Invalid Installer database format.
422220=Database: [2]. Invalid row/field data.
432221=Database: [2]. Code page conflict in import file: [3].
442222=Database: [2]. Transform or merge code page [3] differs from database code page [4].
452223=Database: [2]. Databases are the same. No transform generated.
462224=Database: [2]. GenerateTransform: Database corrupt. Table: [3]
472225=Database: [2]. Transform: Cannot transform a temporary table. Table: [3]
482226=Database: [2]. Transform failed.
492227=Database: [2]. Invalid identifier '[3]' in SQL query: [4]
502228=Database: [2]. Unknown table '[3]' in SQL query: [4]
512229=Database: [2]. Could not load table '[3]' in SQL query: [4]
522230=Database: [2]. Repeated table '[3]' in SQL query: [4]
532231=Database: [2]. Missing ')' in SQL query: [3]
542232=Database: [2]. Unexpected token '[3]' in SQL query: [4]
552233=Database: [2]. No columns in SELECT clause in SQL query: [3]
562234=Database: [2]. No columns in ORDER BY clause in SQL query: [3]
572235=Database: [2]. Column '[3]' not present or ambiguous in SQL query: [4]
582236=Database: [2]. Invalid operator '[3]' in SQL query: [4]
592237=Database: [2]. Invalid or missing query string: [3]
602238=Database: [2]. Missing FROM clause in SQL query: [3]
612239=Database: [2]. Insufficient values in INSERT SQL stmt.
622240=Database: [2]. Missing update columns in UPDATE SQL stmt.
632241=Database: [2]. Missing insert columns in INSERT SQL stmt.
642242=Database: [2]. Column '[3]' repeated
652243=Database: [2]. No primary columns defined for table creation.
662244=Database: [2]. Invalid type specifier '[3]' in SQL query [4].
672245=IStorage::Stat failed with error [3]
682246=Database: [2]. Invalid Installer transform format.
692247=Database: [2] Transform stream read/write failure.
702248=Database: [2] GenerateTransform/Merge: Column type in base table doesn't match reference table. Table: [3] Col #: [4]
712249=Database: [2] GenerateTransform: More columns in base table than in reference table. Table: [3]
722250=Database: [2] Transform: Cannot add existing row. Table: [3]
732251=Database: [2] Transform: Cannot delete row that doesn't exist. Table: [3]
742252=Database: [2] Transform: Cannot add existing table. Table: [3]
752253=Database: [2] Transform: Cannot delete table that doesn't exist. Table: [3]
762254=Database: [2] Transform: Cannot update row that doesn't exist. Table: [3]
772255=Database: [2] Transform: Column with this name already exists. Table: [3] Col: [4]
782256=Database: [2] GenerateTransform/Merge: Number of primary keys in base table doesn't match reference table. Table: [3]
792257=Database: [2]. Intent to modify read only table: [3]
802258=Database: [2]. Type mismatch in parameter: [3]
812259=Database: [2] Table(s) Update failed.
822260=Storage CopyTo failed. System error: [3]
832261=Could not remove stream [2]. System error: [3]
842262=Stream does not exist: [2]. System error: [3]
852263=Could not open stream [2]. System error: [3]
862264=Could not remove stream [2]. System error: [3]
872265=Could not commit storage. System error: [3]
882266=Could not rollback storage. System error: [3]
892267=Could not delete storage [2]. System error: [3]
902268=Database: [2]. Merge: There were merge conflicts reported in [3] tables.
912269=Database: [2]. Merge: The column count differed in the '[3]' table of the two databases.
922270=Database: [2]. GenerateTransform/Merge: Column name in base table doesn't match reference table. Table: [3] Col #: [4]
932271=SummaryInformation write for transform failed.
942272=Database: [2]. MergeDatabase will not write any changes because the database is open read-only.
952273=Database: [2]. MergeDatabase: A reference to the base database was passed as the reference database.
962274=Database: [2]. MergeDatabase: Unable to write errors to Error table. Could be due to a non-nullable column in a predefined Error table.
972275=Database: [2]. Specified Modify [3] operation invalid for table joins.
982276=Database: [2]. Code page [3] not supported by the system.
992277=Database: [2]. Failed to save table [3].
1002278=Database: [2]. Exceeded number of expressions limit of 32 in WHERE clause of SQL query: [3].
1012279=Database: [2] Transform: Too many columns in base table [3]
1022280=Database: [2]. Could not create column [3] for table [4]
1032281=Could not rename stream [2]. System error: [3]
1042282=Stream name invalid [2].
1052302=Patch notify: [2] bytes patched to far.
1062303=Error getting volume info. GetLastError: [2]
1072304=Error getting disk free space. GetLastError: [2]. Volume: [3]
1082305=Error waiting for patch thread. GetLastError: [2].
1092306=Could not create thread for patch application. GetLastError: [2].
1102307=Source file key name is null.
1112308=Destination File Name is Null
1122309=Attempting to patch file [2] when patch already in progress.
1132310=Attempting to continue patch when no patch is in progress.
1142315=Missing Path Separator: [2]
1152318=File does not exist: [2]
1162319=Error setting file attribute: [3] GetLastError: [2]
1172320=File not writable: [2]
1182321=Error creating file: [2]
1192322=User canceled
1202323=Invalid File Attribute
1212324=Could not open file: [3] GetLastError: [2]
1222325=Could not get file time for file: [3] GetLastError: [2]
1232326=Error in FileToDosDateTime.
1242327=Could not remove directory: [3] GetLastError: [2]
1252328=Error getting file version info for file: [2]
1262329=Error deleting file: [3]. GetLastError: [2]
1272330=Error getting file attributes: [3]. GetLastError: [2]
1282331=Error loading library [2] or finding entry point [3]
1292332=Error getting file attributes. GetLastError: [2]
1302333=Error setting file attributes. GetLastError: [2]
1312334=Error converting file time to local time for file: [3]. GetLastError: [2]
1322335=Path: [2] is not a parent of [3]
1332336=Error creating temp file on path: [3]. GetLastError: [2]
1342337=Could not close file: [3] GetLastError: [2]
1352338=Could not update resource for file: [3] GetLastError: [2]
1362339=Could not set file time for file: [3] GetLastError: [2]
1372340=Could not update resource for file: [3], Missing Resource
1382341=Could not update resource for file: [3], Resource too large
1392342=Could not update resource for file: [3] GetLastError: [2]
1402343=Specified path is empty.
1412344=Could not find required file IMAGEHLP.DLL to validate file:[2]
1422345=[2]: File does not contain a valid checksum value.
1432347=User ignore
1442348=Error attempting to read from cabinet stream.
1452349=Copy Resumed With Different Info
1462350=FDI Server Error
1472351=File key '[2]' not found in cabinet '[3]'. The installation cannot continue.
1482352=Couldn't initialize cabinet file server. The required file 'Cabinet.dll' may be missing.
1492353=Not a cabinet
1502354=Cannot handle cabinet
1512355=Corrupt cabinet
1522356=Couldn't locate cabinet in stream: [2].
1532357=Cannot set attributes
1542358=Error determining whether file is in-use: [3]. GetLastError: [2]
1552359=Unable to create the target file - file may be in use.
1562360=progress tick.
1572361=Need next cabinet.
1582362=Folder not found: [2]
1592363=Could not enumerate subfolders for folder: [2]
1602364=Bad enumeration constant in CreateCopier call.
1612365=Could not BindImage exe file [2]
1622366=User Failure
1632367=User Abort.
1642368=Failed to get network resource information. Error [2], network path [3]. Extended error: network provider [5], error code [4], error description [6].
1652370=Invalid CRC checksum value for [2] file.{ Its header says [3] for checksum, its computed value is [4].}
1662371=Could not apply patch to file [2]. GetLastError: [3]
1672372=Patch file [2] is corrupt or of an invalid format. Attempting to patch file [3]. GetLastError: [4]
1682373=File [2] is not a valid patch file.
1692374=File [2] is not a valid destination file for patch file [3].
1702375=Unknown patching error: [2].
1712376=Cabinet not found.
1722379=Error opening file for read: [3] GetLastError: [2]
1732380=Error opening file for write: [3] GetLastError: [2]
1742381=Directory does not exist: [2]
1752382=Drive not ready: [2]
1762401=64-bit registry operation attempted on 32-bit operating system for key [2].
1772402=Out of memory.
1782501=Could not create rollback script enumerator
1792502=Called InstallFinalize when no install in progress.
1802503=Called RunScript when not marked in progress.
1812601=Invalid value for property [2]: '[3]'
1822602=The [2] table entry '[3]' has no associated entry in the Media table.
1832603=Duplicate Table Name [2]
1842604=[2] property undefined.
1852605=Could not find server [2] in [3] or [4].
1862606=Value of property [2] is not a valid full path: '[3]'.
1872607=Media table not found or empty (required for installation of files).
1882608=Could not create security descriptor for object. Error: '[2]'.
1892609=Attempt to migrate product settings before initialization.
1902611=The file [2] is marked as compressed, but the associated media entry does not specify a cabinet.
1912612=Stream not found in '[2]' column. Primary key: '[3]'.
1922613=RemoveExistingProducts action sequenced incorrectly.
1932614=Could not access IStorage object from installation package.
1942615=Skipped unregistration of Module [2] due to source resolution failure.
1952616=Companion file [2] parent missing.
1962617=Shared component [2] not found in Component table.
1972618=Isolated application component [2] not found in Component table.
1982619=Isolated components [2], [3] not part of same feature.
1992620=Key file of isolated application component [2] not in File table.
2002621=Resource DLL or Resource ID information for shortcut [2] set incorrectly.
2012701=The Component Table exceeds the acceptable tree depth of [2] levels.
2022702=A Feature Table record ([2]) references a non-existent parent in the Attributes field.
2032703=Property name for root source path not defined: [2]
2042704=Root directory property undefined: [2]
2052705=Invalid table: [2]; Could not be linked as tree.
2062706=Source paths not created. No path exists for entry [2] in Directory Table
2072707=Target paths not created. No path exists for entry [2] in Directory Table
2082708=No entries found in the file table.
2092709=The specified Component name ('[2]') not found in Component Table.
2102710=The requested 'Select' state is illegal for this Component.
2112711=The specified Feature name ('[2]') not found in Feature Table.
2122712=Invalid return from modeless dialog: [3], in action [2].
2132713=Null value in a non-nullable column ('[2]' in '[3]' column of the '[4]' table.
2142714=Invalid value for default folder name: [2].
2152715=The specified File key ('[2]') not found in the File Table.
2162716=Couldn't create a random subcomponent name for component '[2]'.
2172717=Bad action condition or error calling custom action '[2]'.
2182718=Missing package name for product code '[2]'.
2192719=Neither UNC nor drive letter path found in source '[2]'.
2202720=Error opening source list key. Error: '[2]'
2212721=Custom action [2] not found in Binary table stream
2222722=Custom action [2] not found in File table
2232723=Custom action [2] specifies unsupported type
2242724=The volume label '[2]' on the media you're running from doesn't match the label '[3]' given in the Media table. This is allowed only if you have only 1 entry in your Media table.
2252725=Invalid database tables
2262726=Action not found: [2]
2272727=The directory entry '[2]' does not exist in the Directory table
2282728=Table definition error: [2]
2292729=Install engine not initialized.
2302730=Bad value in database. Table: '[2]'; Primary key: '[3]'; Column: '[4]'
2312731=Selection Manager not initialized.
2322732=Directory Manager not initialized.
2332733=Bad foreign key ('[2]') in '[3]' column of the '[4]' table.
2342734=Invalid Reinstall mode character.
2352735=Custom action '[2]' has caused an unhandled exception and has been stopped. This may be the result of an internal error in the custom action, such as an access violation.
2362736=Generation of custom action temp file failed: [2]
2372737=Could not access custom action [2], entry [3], library [4]
2382738=Could not access VBScript runtime for custom action [2]
2392739=Could not access JavaScript runtime for custom action [2]
2402740=Custom action [2] script error [3], [4]: [5] Line [6], Column [7], [8]
2412741=Configuration information for product [2] is corrupt. Invalid info: [2]
2422742=Marshaling to Server failed: [2]
2432743=Could not execute custom action [2], location: [3], command: [4]
2442744=EXE failed called by custom action [2], location: [3], command: [4]
2452745=Transform [2] invalid for package [3]. Expected language [4], found language [5].
2462746=Transform [2] invalid for package [3]. Expected product [4], found product [5].
2472747=Transform [2] invalid for package [3]. Expected product version < [4], found product version [5].
2482748=Transform [2] invalid for package [3]. Expected product version <= [4], found product version [5].
2492749=Transform [2] invalid for package [3]. Expected product version == [4], found product version [5].
2502750=Transform [2] invalid for package [3]. Expected product version >= [4], found product version [5].
2512751=Transform [2] invalid for package [3]. Expected product version > [4], found product version [5].
2522752=Could not open transform [2] stored as child storage of package [4].
2532753=The File '[2]' is not marked for installation.
2542754=The File '[2]' is not a valid patch file.
2552755=Server returned unexpected error [2] attempting to install package [3].
2562756=The property '[2]' was used as a directory property in one or more tables, but no value was ever assigned.
2572757=Could not create summary info for transform [2].
2582758=Transform [2] doesn't contain a MSI version.
2592759=Transform [2] version [3] incompatible with engine; Min: [4], Max: [5].
2602760=Transform [2] invalid for package [3]. Expected upgrade code [4], found [5].
2612761=Cannot begin transaction. Global mutex not properly initialized.
2622762=Cannot write script record. Transaction not started.
2632763=Cannot run script. Transaction not started.
2642765=Assembly name missing from AssemblyName table : Component: [4].
2652766=The file [2] is an invalid MSI storage file.
2662767=No more data{ while enumerating [2]}.
2672768=Transform in patch package is invalid.
2682769=Custom Action [2] did not close [3] handles.
2692770=Cached folder [2] not defined in internal cache folder table.
2702771=Upgrade of feature [2] has a missing component.
2712772=New upgrade feature [2] must be a leaf feature.
2722801=Unknown Message -- Type [2]. No action is taken.
2732802=No publisher is found for the event [2].
2742803=Dialog View did not find a record for the dialog [2].
2752804=On activation of the control [3] on dialog [2], failed to evaluate the condition [3].
2762805=
2772806=The dialog [2] failed to evaluate the condition [3].
2782807=The action [2] is not recognized.
2792808=Default button is ill-defined on dialog [2].
2802809=On the dialog [2] the next control pointers do not form a cycle. There is a pointer from [3] to [4], but there is no further pointer.
2812810=On the dialog [2] the next control pointers do not form a cycle. There is a pointer from both [3] and [5] to [4].
2822811=On dialog [2] control [3] has to take focus, but it is unable to do so.
2832812=The event [2] is not recognized.
2842813=The EndDialog event was called with the argument [2], but the dialog has a parent.
2852814=On the dialog [2] the control [3] names a non-existent control [4] as the next control.
2862815=ControlCondition table has a row without condition for the dialog [2].
2872816=The EventMapping table refers to an invalid control [4] on dialog [2] for the event [3].
2882817=The event [2] failed to set the attribute for the control [4] on dialog [3].
2892818=In the ControlEvent table EndDialog has an unrecognized argument [2].
2902819=Control [3] on dialog [2] needs a property linked to it.
2912820=Attempted to initialize an already initialized handler.
2922821=Attempted to initialize an already initialized dialog: [2].
2932822=No other method can be called on dialog [2] until all the controls are added.
2942823=Attempted to initialize an already initialized control: [3] on dialog [2].
2952824=The dialog attribute [3] needs a record of at least [2] field(s).
2962825=The control attribute [3] needs a record of at least [2] field(s).
2972826=Control [3] on dialog [2] extends beyond the boundaries of the dialog [4] by [5] pixels.
2982827=The button [4] on the radio button group [3] on dialog [2] extends beyond the boundaries of the group [5] by [6] pixels.
2992828=Tried to remove control [3] from dialog [2], but the control is not part of the dialog.
3002829=Attempt to use an uninitialized dialog.
3012830=Attempt to use an uninitialized control on dialog [2].
3022831=The control [3] on dialog [2] does not support [5] the attribute [4].
3032832=The dialog [2] does not support the attribute [3].
3042833=Control [4] on dialog [3] ignored the message [2].
3052834=The next pointers on the dialog [2] do not form a single loop.
3062835=The control [2] was not found on dialog [3].
3072836=The control [3] on the dialog [2] cannot take focus.
3082837=The control [3] on dialog [2] wants the win proc to return [4].
3092838=The item [2] in the selection table has itself as a parent.
3102839=Setting the property [2] failed.
3112840=Error dialog name mismatch.
3122841=No OK button was found on the error dialog
3132842=No text field was found on the error dialog.
3142843=The ErrorString attribute is not supported for standard dialogs.
3152844=Cannot execute an error dialog if the error string is not set.
3162845=The total width of the buttons exceeds the size of the error dialog.
3172846=SetFocus did not find the required control on the error dialog.
3182847=The control [3] on dialog [2] has both the icon and the bitmap style set.
3192848=Tried to set control [3] as the default button on dialog [2], but the control does not exist.
3202849=The control [3] on dialog [2] is of a type, that cannot be integer valued.
3212850=Unrecognized volume type.
3222851=The data for the icon [2] is not valid.
3232852=At least one control has to be added to dialog [2] before it is used.
3242853=Dialog [2] is a modeless dialog. The execute method should not be called on it.
3252854=On the dialog [2] the control [3] is designated as first active control, but there is no such control.
3262855=The radio button group [3] on dialog [2] has fewer than 2 buttons.
3272856=Creating a second copy of the dialog [2].
3282857=The directory [2] is mentioned in the selection table but not found.
3292858=The data for the bitmap [2] is not valid.
3302859=Test error message.
3312860=Cancel button is ill-defined on dialog [2].
3322861=The next pointers for the radio buttons on dialog [2] control [3] do not form a cycle.
3332862=The attributes for the control [3] on dialog [2] do not define a valid icon size. Setting the size to 16.
3342863=The control [3] on dialog [2] needs the icon [4] in size [5]x[5], but that size is not available. Loading the first available size.
3352864=The control [3] on dialog [2] received a browse event, but there is no configurable directory for the present selection. Likely cause: browse button is not authored correctly.
3362865=Control [3] on billboard [2] extends beyond the boundaries of the billboard [4] by [5] pixels.
3372866=The dialog [2] is not allowed to return the argument [3].
3382867=The error dialog property is not set.
3392868=The error dialog [2] does not have the error style bit set.
3402869=The dialog [2] has the error style bit set, but is not an error dialog.
3412870=The help string [4] for control [3] on dialog [2] does not contain the separator character.
3422871=The [2] table is out of date: [3]
3432872=The argument of the CheckPath control event on dialog [2] is invalid.
3442873=On the dialog [2] the control [3] has an invalid string length limit: [4]
3452874=Changing the text font to [2] failed.
3462875=Changing the text color to [2] failed.
3472876=The control [3] on dialog [2] had to truncate the string: [4]
3482877=The binary data [2] was not found.
3492878=On the dialog [2] the control [3] has a possible value: [4]. This is an invalid or duplicate value.
3502879=The control [3] on dialog [2] cannot parse the mask string: [4]
3512880=Do not perform the remaining control events.
3522881=Initialization failed.
3532882=Dialog window class registration failed.
3542883=CreateNewDialog failed for the dialog [2].
3552884=Failed to create a window for the dialog [2]!
3562885=Failed to create the control [3] on the dialog [2].
3572886=Creating the [2] table failed.
3582887=Creating a cursor to the [2] table failed.
3592888=Executing the [2] view failed.
3602889=Creating the window for the control [3] on dialog [2] failed.
3612890=The handler failed in creating an initialized dialog.
3622891=Failed to destroy window for dialog [2].
3632892=[2] is an integer only control, [3] is not a valid integer value.
3642893=The control [3] on dialog [2] can accept property values that are at most [5] characters long. The value [4] exceeds this limit, and has been truncated.
3652894=Loading RichEd20.dll failed. GetLastError() returned: [2]
3662895=Freeing RichEd20.dll failed. GetLastError() returned: [2]
3672896=Executing action [2] failed.
3682897=Failed to create any [2] font on this system.
3692898=For [2] text style, the system created a '[3]' font, in [4] character set.
3702899=Failed to create [2] text style. GetLastError() returned: [3].
3712901=Invalid parameter to operation [2]: Parameter [3]
3722902=Operation [2] called out of sequence.
3732903=The file [2] is missing.
3742904=Could not BindImage file [2].
3752905=Could not read record from script file [2].
3762906=Missing header in script file [2].
3772907=Could not create secure security descriptor. Error: [2]
3782908=Could not register component [2].
3792909=Could not unregister component [2].
3802910=Could not determine user's security id.
3812911=Could not remove the folder [2].
3822912=Could not schedule file [2] for removal on reboot.
3832919=No cabinet specified for compressed file: [2]
3842920=Source directory not specified for file [2].
3852924=Script [2] version unsupported. Script version: [3], minimum version: [4], maximum version: [5].
3862927=ShellFolder id [2] is invalid.
3872928=Exceeded maximum number of sources. Skipping source '[2]'.
3882929=Could not determine publishing root. Error: [2]
3892932=Could not create file [2] from script data. Error: [3]
3902933=Could not initialize rollback script [2].
3912934=Could not secure transform [2]. Error [3]
3922935=Could not un-secure transform [2]. Error [3]
3932936=Could not find transform [2].
3942937=The Windows Installer cannot install a system file protection catalog. Catalog: [2], Error: [3]
3952938=The Windows Installer cannot retrieve a system file protection catalog from the cache. Catalog: [2], Error: [3]
3962939=The Windows Installer cannot delete a system file protection catalog from the cache. Catalog: [2], Error: [3]
3972940=Directory Manager not supplied for source resolution.
3982941=Unable to compute the CRC for file [2].
3992942=BindImage action has not been executed on [2] file.
4002943=This version of Windows does not support deploying 64-bit packages. The script [2] is for a 64-bit package.
4012944=GetProductAssignmentType failed.
4022945=Installation of ComPlus App [2] failed with error [3].
4033001=The patches in this list contain incorrect sequencing information: [2][3][4][5][6][7][8][9][10][11][12][13][14][15][16]. 3.0
4043002=Patch [2] contains invalid sequencing information.
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Exceptions.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Exceptions.cs
new file mode 100644
index 00000000..4954cda0
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Exceptions.cs
@@ -0,0 +1,573 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections.Generic;
9 using System.Globalization;
10 using System.Runtime.Serialization;
11 using System.Diagnostics.CodeAnalysis;
12
13 /// <summary>
14 /// Base class for Windows Installer exceptions.
15 /// </summary>
16 [Serializable]
17 public class InstallerException : SystemException
18 {
19 private int errorCode;
20 private object[] errorData;
21
22 /// <summary>
23 /// Creates a new InstallerException with a specified error message and a reference to the
24 /// inner exception that is the cause of this exception.
25 /// </summary>
26 /// <param name="msg">The message that describes the error.</param>
27 /// <param name="innerException">The exception that is the cause of the current exception. If the
28 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
29 /// is raised in a catch block that handles the inner exception.</param>
30 public InstallerException(string msg, Exception innerException)
31 : this(0, msg, innerException)
32 {
33 }
34
35 /// <summary>
36 /// Creates a new InstallerException with a specified error message.
37 /// </summary>
38 /// <param name="msg">The message that describes the error.</param>
39 public InstallerException(string msg)
40 : this(0, msg)
41 {
42 }
43
44 /// <summary>
45 /// Creates a new InstallerException.
46 /// </summary>
47 public InstallerException()
48 : this(0, null)
49 {
50 }
51
52 internal InstallerException(int errorCode, string msg, Exception innerException)
53 : base(msg, innerException)
54 {
55 this.errorCode = errorCode;
56 this.SaveErrorRecord();
57 }
58
59 internal InstallerException(int errorCode, string msg)
60 : this(errorCode, msg, null)
61 {
62 }
63
64 /// <summary>
65 /// Initializes a new instance of the InstallerException class with serialized data.
66 /// </summary>
67 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
68 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
69 protected InstallerException(SerializationInfo info, StreamingContext context) : base(info, context)
70 {
71 if (info == null)
72 {
73 throw new ArgumentNullException("info");
74 }
75
76 this.errorCode = info.GetInt32("msiErrorCode");
77 }
78
79 /// <summary>
80 /// Gets the system error code that resulted in this exception, or 0 if not applicable.
81 /// </summary>
82 public int ErrorCode
83 {
84 get
85 {
86 return this.errorCode;
87 }
88 }
89
90 /// <summary>
91 /// Gets a message that describes the exception. This message may contain detailed
92 /// formatted error data if it was available.
93 /// </summary>
94 public override String Message
95 {
96 get
97 {
98 string msg = base.Message;
99 using (Record errorRec = this.GetErrorRecord())
100 {
101 if (errorRec != null)
102 {
103 string errorMsg = Installer.GetErrorMessage(errorRec, CultureInfo.InvariantCulture);
104 msg = Combine(msg, errorMsg);
105 }
106 }
107 return msg;
108 }
109 }
110
111 /// <summary>
112 /// Sets the SerializationInfo with information about the exception.
113 /// </summary>
114 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
115 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
116 public override void GetObjectData(SerializationInfo info, StreamingContext context)
117 {
118 if (info == null)
119 {
120 throw new ArgumentNullException("info");
121 }
122
123 info.AddValue("msiErrorCode", this.errorCode);
124 base.GetObjectData(info, context);
125 }
126
127 /// <summary>
128 /// Gets extended information about the error, or null if no further information
129 /// is available.
130 /// </summary>
131 /// <returns>A Record object. Field 1 of the Record contains the installer
132 /// message code. Other fields contain data specific to the particular error.</returns>
133 /// <remarks><p>
134 /// If the record is passed to <see cref="Session.Message"/>, it is formatted
135 /// by looking up the string in the current database. If there is no installation
136 /// session, the formatted error message may be obtained by a query on the Error table using
137 /// the error code, followed by a call to <see cref="Record.ToString()"/>.
138 /// Alternatively, the standard MSI message can by retrieved by calling the
139 /// <see cref="Installer.GetErrorMessage(Record,CultureInfo)"/> method.
140 /// </p><p>
141 /// The following methods and properties may report extended error data:
142 /// <list type="bullet">
143 /// <item><see cref="Database"/> (constructor)</item>
144 /// <item><see cref="Database"/>.<see cref="Database.ApplyTransform(string,TransformErrors)"/></item>
145 /// <item><see cref="Database"/>.<see cref="Database.Commit"/></item>
146 /// <item><see cref="Database"/>.<see cref="Database.Execute(string,object[])"/></item>
147 /// <item><see cref="Database"/>.<see cref="Database.ExecuteQuery(string,object[])"/></item>
148 /// <item><see cref="Database"/>.<see cref="Database.ExecuteIntegerQuery(string,object[])"/></item>
149 /// <item><see cref="Database"/>.<see cref="Database.ExecuteStringQuery(string,object[])"/></item>
150 /// <item><see cref="Database"/>.<see cref="Database.Export"/></item>
151 /// <item><see cref="Database"/>.<see cref="Database.ExportAll"/></item>
152 /// <item><see cref="Database"/>.<see cref="Database.GenerateTransform"/></item>
153 /// <item><see cref="Database"/>.<see cref="Database.Import"/></item>
154 /// <item><see cref="Database"/>.<see cref="Database.ImportAll"/></item>
155 /// <item><see cref="Database"/>.<see cref="Database.Merge(Database,string)"/></item>
156 /// <item><see cref="Database"/>.<see cref="Database.OpenView"/></item>
157 /// <item><see cref="Database"/>.<see cref="Database.SummaryInfo"/></item>
158 /// <item><see cref="Database"/>.<see cref="Database.ViewTransform"/></item>
159 /// <item><see cref="View"/>.<see cref="View.Assign"/></item>
160 /// <item><see cref="View"/>.<see cref="View.Delete"/></item>
161 /// <item><see cref="View"/>.<see cref="View.Execute(Record)"/></item>
162 /// <item><see cref="View"/>.<see cref="View.Insert"/></item>
163 /// <item><see cref="View"/>.<see cref="View.InsertTemporary"/></item>
164 /// <item><see cref="View"/>.<see cref="View.Merge"/></item>
165 /// <item><see cref="View"/>.<see cref="View.Modify"/></item>
166 /// <item><see cref="View"/>.<see cref="View.Refresh"/></item>
167 /// <item><see cref="View"/>.<see cref="View.Replace"/></item>
168 /// <item><see cref="View"/>.<see cref="View.Seek"/></item>
169 /// <item><see cref="View"/>.<see cref="View.Update"/></item>
170 /// <item><see cref="View"/>.<see cref="View.Validate"/></item>
171 /// <item><see cref="View"/>.<see cref="View.ValidateFields"/></item>
172 /// <item><see cref="View"/>.<see cref="View.ValidateDelete"/></item>
173 /// <item><see cref="View"/>.<see cref="View.ValidateNew"/></item>
174 /// <item><see cref="SummaryInfo"/> (constructor)</item>
175 /// <item><see cref="Record"/>.<see cref="Record.SetStream(int,string)"/></item>
176 /// <item><see cref="Session"/>.<see cref="Session.SetInstallLevel"/></item>
177 /// <item><see cref="Session"/>.<see cref="Session.GetSourcePath"/></item>
178 /// <item><see cref="Session"/>.<see cref="Session.GetTargetPath"/></item>
179 /// <item><see cref="Session"/>.<see cref="Session.SetTargetPath"/></item>
180 /// <item><see cref="ComponentInfo"/>.<see cref="ComponentInfo.CurrentState"/></item>
181 /// <item><see cref="FeatureInfo"/>.<see cref="FeatureInfo.CurrentState"/></item>
182 /// <item><see cref="FeatureInfo"/>.<see cref="FeatureInfo.ValidStates"/></item>
183 /// <item><see cref="FeatureInfo"/>.<see cref="FeatureInfo.GetCost"/></item>
184 /// </list>
185 /// </p><p>
186 /// The Record object should be <see cref="InstallerHandle.Close"/>d after use.
187 /// It is best that the handle be closed manually as soon as it is no longer
188 /// needed, as leaving lots of unused handles open can degrade performance.
189 /// </p><p>
190 /// Win32 MSI API:
191 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetlasterrorrecord.asp">MsiGetLastErrorRecord</a>
192 /// </p></remarks>
193 [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
194 public Record GetErrorRecord()
195 {
196 return this.errorData != null ? new Record(this.errorData) : null;
197 }
198
199 internal static Exception ExceptionFromReturnCode(uint errorCode)
200 {
201 return ExceptionFromReturnCode(errorCode, null);
202 }
203
204 internal static Exception ExceptionFromReturnCode(uint errorCode, string msg)
205 {
206 msg = Combine(GetSystemMessage(errorCode), msg);
207 switch (errorCode)
208 {
209 case (uint) NativeMethods.Error.FILE_NOT_FOUND:
210 case (uint) NativeMethods.Error.PATH_NOT_FOUND: return new FileNotFoundException(msg);
211
212 case (uint) NativeMethods.Error.INVALID_PARAMETER:
213 case (uint) NativeMethods.Error.DIRECTORY:
214 case (uint) NativeMethods.Error.UNKNOWN_PROPERTY:
215 case (uint) NativeMethods.Error.UNKNOWN_PRODUCT:
216 case (uint) NativeMethods.Error.UNKNOWN_FEATURE:
217 case (uint) NativeMethods.Error.UNKNOWN_COMPONENT: return new ArgumentException(msg);
218
219 case (uint) NativeMethods.Error.BAD_QUERY_SYNTAX: return new BadQuerySyntaxException(msg);
220
221 case (uint) NativeMethods.Error.INVALID_HANDLE_STATE:
222 case (uint) NativeMethods.Error.INVALID_HANDLE:
223 InvalidHandleException ihex = new InvalidHandleException(msg);
224 ihex.errorCode = (int) errorCode;
225 return ihex;
226
227 case (uint) NativeMethods.Error.INSTALL_USEREXIT: return new InstallCanceledException(msg);
228
229 case (uint) NativeMethods.Error.CALL_NOT_IMPLEMENTED: return new NotImplementedException(msg);
230
231 default: return new InstallerException((int) errorCode, msg);
232 }
233 }
234
235 internal static string GetSystemMessage(uint errorCode)
236 {
237 const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
238 const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000;
239
240 StringBuilder buf = new StringBuilder(1024);
241 uint formatCount = NativeMethods.FormatMessage(
242 FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
243 IntPtr.Zero,
244 (uint) errorCode,
245 0,
246 buf,
247 (uint) buf.Capacity,
248 IntPtr.Zero);
249
250 if (formatCount != 0)
251 {
252 return buf.ToString().Trim();
253 }
254 else
255 {
256 return null;
257 }
258 }
259
260 internal void SaveErrorRecord()
261 {
262 // TODO: pass an affinity handle here?
263 int recordHandle = RemotableNativeMethods.MsiGetLastErrorRecord(0);
264 if (recordHandle != 0)
265 {
266 using (Record errorRec = new Record((IntPtr) recordHandle, true, null))
267 {
268 this.errorData = new object[errorRec.FieldCount];
269 for (int i = 0; i < this.errorData.Length; i++)
270 {
271 this.errorData[i] = errorRec[i + 1];
272 }
273 }
274 }
275 else
276 {
277 this.errorData = null;
278 }
279 }
280
281 private static string Combine(string msg1, string msg2)
282 {
283 if (msg1 == null) return msg2;
284 if (msg2 == null) return msg1;
285 return msg1 + " " + msg2;
286 }
287 }
288
289 /// <summary>
290 /// User Canceled the installation.
291 /// </summary>
292 [Serializable]
293 public class InstallCanceledException : InstallerException
294 {
295 /// <summary>
296 /// Creates a new InstallCanceledException with a specified error message and a reference to the
297 /// inner exception that is the cause of this exception.
298 /// </summary>
299 /// <param name="msg">The message that describes the error.</param>
300 /// <param name="innerException">The exception that is the cause of the current exception. If the
301 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
302 /// is raised in a catch block that handles the inner exception.</param>
303 public InstallCanceledException(string msg, Exception innerException)
304 : base((int) NativeMethods.Error.INSTALL_USEREXIT, msg, innerException)
305 {
306 }
307
308 /// <summary>
309 /// Creates a new InstallCanceledException with a specified error message.
310 /// </summary>
311 /// <param name="msg">The message that describes the error.</param>
312 public InstallCanceledException(string msg)
313 : this(msg, null)
314 {
315 }
316
317 /// <summary>
318 /// Creates a new InstallCanceledException.
319 /// </summary>
320 public InstallCanceledException()
321 : this(null, null)
322 {
323 }
324
325 /// <summary>
326 /// Initializes a new instance of the InstallCanceledException class with serialized data.
327 /// </summary>
328 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
329 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
330 protected InstallCanceledException(SerializationInfo info, StreamingContext context)
331 : base(info, context)
332 {
333 }
334 }
335
336 /// <summary>
337 /// A bad SQL query string was passed to <see cref="Database.OpenView"/> or <see cref="Database.Execute(string,object[])"/>.
338 /// </summary>
339 [Serializable]
340 public class BadQuerySyntaxException : InstallerException
341 {
342 /// <summary>
343 /// Creates a new BadQuerySyntaxException with a specified error message and a reference to the
344 /// inner exception that is the cause of this exception.
345 /// </summary>
346 /// <param name="msg">The message that describes the error.</param>
347 /// <param name="innerException">The exception that is the cause of the current exception. If the
348 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
349 /// is raised in a catch block that handles the inner exception.</param>
350 public BadQuerySyntaxException(string msg, Exception innerException)
351 : base((int) NativeMethods.Error.BAD_QUERY_SYNTAX, msg, innerException)
352 {
353 }
354
355 /// <summary>
356 /// Creates a new BadQuerySyntaxException with a specified error message.
357 /// </summary>
358 /// <param name="msg">The message that describes the error.</param>
359 public BadQuerySyntaxException(string msg)
360 : this(msg, null)
361 {
362 }
363
364 /// <summary>
365 /// Creates a new BadQuerySyntaxException.
366 /// </summary>
367 public BadQuerySyntaxException()
368 : this(null, null)
369 {
370 }
371
372 /// <summary>
373 /// Initializes a new instance of the BadQuerySyntaxException class with serialized data.
374 /// </summary>
375 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
376 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
377 protected BadQuerySyntaxException(SerializationInfo info, StreamingContext context)
378 : base(info, context)
379 {
380 }
381 }
382
383 /// <summary>
384 /// A method was called on an invalid installer handle. The handle may have been already closed.
385 /// </summary>
386 [Serializable]
387 public class InvalidHandleException : InstallerException
388 {
389 /// <summary>
390 /// Creates a new InvalidHandleException with a specified error message and a reference to the
391 /// inner exception that is the cause of this exception.
392 /// </summary>
393 /// <param name="msg">The message that describes the error.</param>
394 /// <param name="innerException">The exception that is the cause of the current exception. If the
395 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
396 /// is raised in a catch block that handles the inner exception.</param>
397 public InvalidHandleException(string msg, Exception innerException)
398 : base((int) NativeMethods.Error.INVALID_HANDLE, msg, innerException)
399 {
400 }
401
402 /// <summary>
403 /// Creates a new InvalidHandleException with a specified error message.
404 /// </summary>
405 /// <param name="msg">The message that describes the error.</param>
406 public InvalidHandleException(string msg)
407 : this(msg, null)
408 {
409 }
410
411 /// <summary>
412 /// Creates a new InvalidHandleException.
413 /// </summary>
414 public InvalidHandleException()
415 : this(null, null)
416 {
417 }
418
419 /// <summary>
420 /// Initializes a new instance of the InvalidHandleException class with serialized data.
421 /// </summary>
422 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
423 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
424 protected InvalidHandleException(SerializationInfo info, StreamingContext context)
425 : base(info, context)
426 {
427 }
428 }
429
430 /// <summary>
431 /// A failure occurred when executing <see cref="Database.Merge(Database,string)"/>. The exception may contain
432 /// details about the merge conflict.
433 /// </summary>
434 [Serializable]
435 public class MergeException : InstallerException
436 {
437 private IList<string> conflictTables;
438 private IList<int> conflictCounts;
439
440 /// <summary>
441 /// Creates a new MergeException with a specified error message and a reference to the
442 /// inner exception that is the cause of this exception.
443 /// </summary>
444 /// <param name="msg">The message that describes the error.</param>
445 /// <param name="innerException">The exception that is the cause of the current exception. If the
446 /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception
447 /// is raised in a catch block that handles the inner exception.</param>
448 public MergeException(string msg, Exception innerException)
449 : base(msg, innerException)
450 {
451 }
452
453 /// <summary>
454 /// Creates a new MergeException with a specified error message.
455 /// </summary>
456 /// <param name="msg">The message that describes the error.</param>
457 public MergeException(string msg)
458 : base(msg)
459 {
460 }
461
462 /// <summary>
463 /// Creates a new MergeException.
464 /// </summary>
465 public MergeException()
466 : base()
467 {
468 }
469
470 internal MergeException(Database db, string conflictsTableName)
471 : base("Merge failed.")
472 {
473 if (conflictsTableName != null)
474 {
475 IList<string> conflictTableList = new List<string>();
476 IList<int> conflictCountList = new List<int>();
477
478 using (View view = db.OpenView("SELECT `Table`, `NumRowMergeConflicts` FROM `" + conflictsTableName + "`"))
479 {
480 view.Execute();
481
482 foreach (Record rec in view) using (rec)
483 {
484 conflictTableList.Add(rec.GetString(1));
485 conflictCountList.Add((int) rec.GetInteger(2));
486 }
487 }
488
489 this.conflictTables = conflictTableList;
490 this.conflictCounts = conflictCountList;
491 }
492 }
493
494 /// <summary>
495 /// Initializes a new instance of the MergeException class with serialized data.
496 /// </summary>
497 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
498 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
499 protected MergeException(SerializationInfo info, StreamingContext context) : base(info, context)
500 {
501 if (info == null)
502 {
503 throw new ArgumentNullException("info");
504 }
505
506 this.conflictTables = (string[]) info.GetValue("mergeConflictTables", typeof(string[]));
507 this.conflictCounts = (int[]) info.GetValue("mergeConflictCounts", typeof(int[]));
508 }
509
510 /// <summary>
511 /// Gets the number of merge conflicts in each table, corresponding to the tables returned by
512 /// <see cref="ConflictTables"/>.
513 /// </summary>
514 public IList<int> ConflictCounts
515 {
516 get
517 {
518 return new List<int>(this.conflictCounts);
519 }
520 }
521
522 /// <summary>
523 /// Gets the list of tables containing merge conflicts.
524 /// </summary>
525 public IList<string> ConflictTables
526 {
527 get
528 {
529 return new List<string>(this.conflictTables);
530 }
531 }
532
533 /// <summary>
534 /// Gets a message that describes the merge conflits.
535 /// </summary>
536 public override String Message
537 {
538 get
539 {
540 StringBuilder msg = new StringBuilder(base.Message);
541 if (this.conflictTables != null)
542 {
543 for (int i = 0; i < this.conflictTables.Count; i++)
544 {
545 msg.Append(i == 0 ? " Conflicts: " : ", ");
546 msg.Append(this.conflictTables[i]);
547 msg.Append('(');
548 msg.Append(this.conflictCounts[i]);
549 msg.Append(')');
550 }
551 }
552 return msg.ToString();
553 }
554 }
555
556 /// <summary>
557 /// Sets the SerializationInfo with information about the exception.
558 /// </summary>
559 /// <param name="info">The SerializationInfo that holds the serialized object data about the exception being thrown.</param>
560 /// <param name="context">The StreamingContext that contains contextual information about the source or destination.</param>
561 public override void GetObjectData(SerializationInfo info, StreamingContext context)
562 {
563 if (info == null)
564 {
565 throw new ArgumentNullException("info");
566 }
567
568 info.AddValue("mergeConflictTables", this.conflictTables);
569 info.AddValue("mergeConflictCounts", this.conflictCounts);
570 base.GetObjectData(info, context);
571 }
572 }
573}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ExternalUIHandler.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ExternalUIHandler.cs
new file mode 100644
index 00000000..08f00867
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ExternalUIHandler.cs
@@ -0,0 +1,223 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Collections;
7 using System.Runtime.InteropServices;
8 using System.Diagnostics.CodeAnalysis;
9
10 /// <summary>
11 /// Defines a callback function that the installer calls for progress notification and error messages.
12 /// </summary>
13 public delegate MessageResult ExternalUIHandler(
14 InstallMessage messageType,
15 string message,
16 MessageButtons buttons,
17 MessageIcon icon,
18 MessageDefaultButton defaultButton);
19
20 /// <summary>
21 /// [MSI 3.1] Defines a callback function that the installer calls for record-based progress notification and error messages.
22 /// </summary>
23 public delegate MessageResult ExternalUIRecordHandler(
24 InstallMessage messageType,
25 Record messageRecord,
26 MessageButtons buttons,
27 MessageIcon icon,
28 MessageDefaultButton defaultButton);
29
30 internal delegate int NativeExternalUIHandler(IntPtr context, int messageType, [MarshalAs(UnmanagedType.LPWStr)] string message);
31
32 internal delegate int NativeExternalUIRecordHandler(IntPtr context, int messageType, int recordHandle);
33
34 internal class ExternalUIProxy
35 {
36 private ExternalUIHandler handler;
37
38 internal ExternalUIProxy(ExternalUIHandler handler)
39 {
40 this.handler = handler;
41 }
42
43 public ExternalUIHandler Handler
44 {
45 get { return this.handler; }
46 }
47
48 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
49 public int ProxyHandler(IntPtr contextPtr, int messageType, [MarshalAs(UnmanagedType.LPWStr)] string message)
50 {
51 try
52 {
53 int msgType = messageType & 0x7F000000;
54 int buttons = messageType & 0x0000000F;
55 int icon = messageType & 0x000000F0;
56 int defButton = messageType & 0x00000F00;
57
58 return (int) this.handler(
59 (InstallMessage) msgType,
60 message,
61 (MessageButtons) buttons,
62 (MessageIcon) icon,
63 (MessageDefaultButton) defButton);
64 }
65 catch
66 {
67 return (int) MessageResult.Error;
68 }
69 }
70 }
71
72 internal class ExternalUIRecordProxy
73 {
74 private ExternalUIRecordHandler handler;
75
76 internal ExternalUIRecordProxy(ExternalUIRecordHandler handler)
77 {
78 this.handler = handler;
79 }
80
81 public ExternalUIRecordHandler Handler
82 {
83 get { return this.handler; }
84 }
85
86 [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
87 public int ProxyHandler(IntPtr contextPtr, int messageType, int recordHandle)
88 {
89 try
90 {
91 int msgType = messageType & 0x7F000000;
92 int buttons = messageType & 0x0000000F;
93 int icon = messageType & 0x000000F0;
94 int defButton = messageType & 0x00000F00;
95
96 Record msgRec = (recordHandle != 0 ? Record.FromHandle((IntPtr) recordHandle, false) : null);
97 using (msgRec)
98 {
99 return (int) this.handler(
100 (InstallMessage) msgType,
101 msgRec,
102 (MessageButtons) buttons,
103 (MessageIcon) icon,
104 (MessageDefaultButton) defButton);
105 }
106 }
107 catch
108 {
109 return (int) MessageResult.Error;
110 }
111 }
112 }
113
114 public static partial class Installer
115 {
116 private static IList externalUIHandlers = ArrayList.Synchronized(new ArrayList());
117
118 /// <summary>
119 /// Enables an external user-interface handler. This external UI handler is called before the
120 /// normal internal user-interface handler. The external UI handler has the option to suppress
121 /// the internal UI by returning a non-zero value to indicate that it has handled the messages.
122 /// </summary>
123 /// <param name="uiHandler">A callback delegate that handles the UI messages</param>
124 /// <param name="messageFilter">Specifies which messages to handle using the external message handler.
125 /// If the external handler returns a non-zero result, then that message will not be sent to the UI,
126 /// instead the message will be logged if logging has been enabled.</param>
127 /// <returns>The previously set external handler, or null if there was no previously set handler</returns>
128 /// <remarks><p>
129 /// To restore the previous UI handler, a second call is made to SetExternalUI using the
130 /// ExternalUIHandler returned by the first call to SetExternalUI and specifying
131 /// <see cref="InstallLogModes.None"/> as the message filter.
132 /// </p><p>
133 /// The external user interface handler does not have full control over the external user
134 /// interface unless <see cref="SetInternalUI(InstallUIOptions)"/> is called with the uiLevel parameter set to
135 /// <see cref="InstallUIOptions.Silent"/>. If SetInternalUI is not called, the internal user
136 /// interface level defaults to <see cref="InstallUIOptions.Basic"/>. As a result, any message not
137 /// handled by the external user interface handler is handled by Windows Installer. The initial
138 /// "Preparing to install..." dialog always appears even if the external user interface
139 /// handler handles all messages.
140 /// </p><p>
141 /// SetExternalUI should only be called from a bootstrapping application. You cannot call
142 /// it from a custom action
143 /// </p><p>
144 /// Win32 MSI API:
145 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetexternalui.asp">MsiSetExternalUI</a>
146 /// </p></remarks>
147 public static ExternalUIHandler SetExternalUI(ExternalUIHandler uiHandler, InstallLogModes messageFilter)
148 {
149 NativeExternalUIHandler nativeHandler = null;
150 if (uiHandler != null)
151 {
152 nativeHandler = new ExternalUIProxy(uiHandler).ProxyHandler;
153 Installer.externalUIHandlers.Add(nativeHandler);
154 }
155 NativeExternalUIHandler oldNativeHandler = NativeMethods.MsiSetExternalUI(nativeHandler, (uint) messageFilter, IntPtr.Zero);
156 if (oldNativeHandler != null && oldNativeHandler.Target is ExternalUIProxy)
157 {
158 Installer.externalUIHandlers.Remove(oldNativeHandler);
159 return ((ExternalUIProxy) oldNativeHandler.Target).Handler;
160 }
161 else
162 {
163 return null;
164 }
165 }
166
167 /// <summary>
168 /// [MSI 3.1] Enables a record-based external user-interface handler. This external UI handler is called
169 /// before the normal internal user-interface handler. The external UI handler has the option to suppress
170 /// the internal UI by returning a non-zero value to indicate that it has handled the messages.
171 /// </summary>
172 /// <param name="uiHandler">A callback delegate that handles the UI messages</param>
173 /// <param name="messageFilter">Specifies which messages to handle using the external message handler.
174 /// If the external handler returns a non-zero result, then that message will not be sent to the UI,
175 /// instead the message will be logged if logging has been enabled.</param>
176 /// <returns>The previously set external handler, or null if there was no previously set handler</returns>
177 /// <remarks><p>
178 /// To restore the previous UI handler, a second call is made to SetExternalUI using the
179 /// ExternalUIHandler returned by the first call to SetExternalUI and specifying
180 /// <see cref="InstallLogModes.None"/> as the message filter.
181 /// </p><p>
182 /// The external user interface handler does not have full control over the external user
183 /// interface unless <see cref="SetInternalUI(InstallUIOptions)"/> is called with the uiLevel parameter set to
184 /// <see cref="InstallUIOptions.Silent"/>. If SetInternalUI is not called, the internal user
185 /// interface level defaults to <see cref="InstallUIOptions.Basic"/>. As a result, any message not
186 /// handled by the external user interface handler is handled by Windows Installer. The initial
187 /// "Preparing to install..." dialog always appears even if the external user interface
188 /// handler handles all messages.
189 /// </p><p>
190 /// SetExternalUI should only be called from a bootstrapping application. You cannot call
191 /// it from a custom action
192 /// </p><p>
193 /// Win32 MSI API:
194 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetexternaluirecord.asp">MsiSetExternalUIRecord</a>
195 /// </p></remarks>
196 public static ExternalUIRecordHandler SetExternalUI(ExternalUIRecordHandler uiHandler, InstallLogModes messageFilter)
197 {
198 NativeExternalUIRecordHandler nativeHandler = null;
199 if (uiHandler != null)
200 {
201 nativeHandler = new ExternalUIRecordProxy(uiHandler).ProxyHandler;
202 Installer.externalUIHandlers.Add(nativeHandler);
203 }
204 NativeExternalUIRecordHandler oldNativeHandler;
205 uint ret = NativeMethods.MsiSetExternalUIRecord(nativeHandler, (uint) messageFilter, IntPtr.Zero, out oldNativeHandler);
206 if (ret != 0)
207 {
208 Installer.externalUIHandlers.Remove(nativeHandler);
209 throw InstallerException.ExceptionFromReturnCode(ret);
210 }
211
212 if (oldNativeHandler != null && oldNativeHandler.Target is ExternalUIRecordProxy)
213 {
214 Installer.externalUIHandlers.Remove(oldNativeHandler);
215 return ((ExternalUIRecordProxy) oldNativeHandler.Target).Handler;
216 }
217 else
218 {
219 return null;
220 }
221 }
222 }
223}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/FeatureInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/FeatureInfo.cs
new file mode 100644
index 00000000..9a1a859a
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/FeatureInfo.cs
@@ -0,0 +1,497 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections;
8 using System.Collections.Generic;
9
10 /// <summary>
11 /// Accessor for information about features within the context of an installation session.
12 /// </summary>
13 public sealed class FeatureInfoCollection : ICollection<FeatureInfo>
14 {
15 private Session session;
16
17 internal FeatureInfoCollection(Session session)
18 {
19 this.session = session;
20 }
21
22 /// <summary>
23 /// Gets information about a feature within the context of an installation session.
24 /// </summary>
25 /// <param name="feature">name of the feature</param>
26 /// <returns>feature object</returns>
27 public FeatureInfo this[string feature]
28 {
29 get
30 {
31 return new FeatureInfo(this.session, feature);
32 }
33 }
34
35 void ICollection<FeatureInfo>.Add(FeatureInfo item)
36 {
37 throw new InvalidOperationException();
38 }
39
40 void ICollection<FeatureInfo>.Clear()
41 {
42 throw new InvalidOperationException();
43 }
44
45 /// <summary>
46 /// Checks if the collection contains a feature.
47 /// </summary>
48 /// <param name="feature">name of the feature</param>
49 /// <returns>true if the feature is in the collection, else false</returns>
50 public bool Contains(string feature)
51 {
52 return this.session.Database.CountRows(
53 "Feature", "`Feature` = '" + feature + "'") == 1;
54 }
55
56 bool ICollection<FeatureInfo>.Contains(FeatureInfo item)
57 {
58 return item != null && this.Contains(item.Name);
59 }
60
61 /// <summary>
62 /// Copies the features into an array.
63 /// </summary>
64 /// <param name="array">array that receives the features</param>
65 /// <param name="arrayIndex">offset into the array</param>
66 public void CopyTo(FeatureInfo[] array, int arrayIndex)
67 {
68 foreach (FeatureInfo feature in this)
69 {
70 array[arrayIndex++] = feature;
71 }
72 }
73
74 /// <summary>
75 /// Gets the number of features defined for the product.
76 /// </summary>
77 public int Count
78 {
79 get
80 {
81 return this.session.Database.CountRows("Feature");
82 }
83 }
84
85 bool ICollection<FeatureInfo>.IsReadOnly
86 {
87 get
88 {
89 return true;
90 }
91 }
92
93 bool ICollection<FeatureInfo>.Remove(FeatureInfo item)
94 {
95 throw new InvalidOperationException();
96 }
97
98 /// <summary>
99 /// Enumerates the features in the collection.
100 /// </summary>
101 /// <returns>an enumerator over all features in the collection</returns>
102 public IEnumerator<FeatureInfo> GetEnumerator()
103 {
104 using (View featureView = this.session.Database.OpenView(
105 "SELECT `Feature` FROM `Feature`"))
106 {
107 featureView.Execute();
108
109 foreach (Record featureRec in featureView) using (featureRec)
110 {
111 string feature = featureRec.GetString(1);
112 yield return new FeatureInfo(this.session, feature);
113 }
114 }
115 }
116
117 IEnumerator IEnumerable.GetEnumerator()
118 {
119 return this.GetEnumerator();
120 }
121 }
122
123 /// <summary>
124 /// Provides access to information about a feature within the context of an installation session.
125 /// </summary>
126 public class FeatureInfo
127 {
128 private Session session;
129 private string name;
130
131 internal FeatureInfo(Session session, string name)
132 {
133 this.session = session;
134 this.name = name;
135 }
136
137 /// <summary>
138 /// Gets the name of the feature (primary key in the Feature table).
139 /// </summary>
140 public string Name
141 {
142 get
143 {
144 return this.name;
145 }
146 }
147
148 /// <summary>
149 /// Gets the current install state of the feature.
150 /// </summary>
151 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
152 /// <exception cref="ArgumentException">an unknown feature was requested</exception>
153 /// <remarks><p>
154 /// Win32 MSI API:
155 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeaturestate.asp">MsiGetFeatureState</a>
156 /// </p></remarks>
157 public InstallState CurrentState
158 {
159 get
160 {
161 int installState, actionState;
162 uint ret = RemotableNativeMethods.MsiGetFeatureState((int) this.session.Handle, this.name, out installState, out actionState);
163 if (ret != 0)
164 {
165 if (ret == (uint) NativeMethods.Error.UNKNOWN_FEATURE)
166 {
167 throw InstallerException.ExceptionFromReturnCode(ret, this.name);
168 }
169 else
170 {
171 throw InstallerException.ExceptionFromReturnCode(ret);
172 }
173 }
174
175 if (installState == (int) InstallState.Advertised)
176 {
177 return InstallState.Advertised;
178 }
179 return (InstallState) installState;
180 }
181 }
182
183 /// <summary>
184 /// Gets or sets the action state of the feature.
185 /// </summary>
186 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
187 /// <exception cref="ArgumentException">an unknown feature was requested</exception>
188 /// <remarks><p>
189 /// When changing the feature action, the action state of all the Components linked to the changed
190 /// Feature records are also updated appropriately, based on the new feature Select state.
191 /// All Features can be configured at once by specifying the keyword ALL instead of a specific feature name.
192 /// </p><p>
193 /// Win32 MSI APIs:
194 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeaturestate.asp">MsiGetFeatureState</a>,
195 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetfeaturestate.asp">MsiSetFeatureState</a>
196 /// </p></remarks>
197 public InstallState RequestState
198 {
199 get
200 {
201 int installState, actionState;
202 uint ret = RemotableNativeMethods.MsiGetFeatureState((int) this.session.Handle, this.name, out installState, out actionState);
203 if (ret != 0)
204 {
205 if (ret == (uint) NativeMethods.Error.UNKNOWN_FEATURE)
206 {
207 throw InstallerException.ExceptionFromReturnCode(ret, this.name);
208 }
209 else
210 {
211 throw InstallerException.ExceptionFromReturnCode(ret);
212 }
213 }
214 return (InstallState) actionState;
215 }
216
217 set
218 {
219 uint ret = RemotableNativeMethods.MsiSetFeatureState((int) this.session.Handle, this.name, (int) value);
220 if (ret != 0)
221 {
222 if (ret == (uint) NativeMethods.Error.UNKNOWN_FEATURE)
223 {
224 throw InstallerException.ExceptionFromReturnCode(ret, this.name);
225 }
226 else
227 {
228 throw InstallerException.ExceptionFromReturnCode(ret);
229 }
230 }
231 }
232 }
233
234 /// <summary>
235 /// Gets a list of valid installation states for the feature.
236 /// </summary>
237 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
238 /// <exception cref="ArgumentException">an unknown feature was requested</exception>
239 /// <remarks><p>
240 /// Win32 MSI API:
241 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeaturevalidstates.asp">MsiGetFeatureValidStates</a>
242 /// </p></remarks>
243 public ICollection<InstallState> ValidStates
244 {
245 get
246 {
247 List<InstallState> states = new List<InstallState>();
248 uint installState;
249 uint ret = RemotableNativeMethods.MsiGetFeatureValidStates((int) this.session.Handle, this.name, out installState);
250 if (ret != 0)
251 {
252 if (ret == (uint) NativeMethods.Error.UNKNOWN_FEATURE)
253 {
254 throw InstallerException.ExceptionFromReturnCode(ret, this.name);
255 }
256 else
257 {
258 throw InstallerException.ExceptionFromReturnCode(ret);
259 }
260 }
261
262 for (int i = 1; i <= (int) InstallState.Default; i++)
263 {
264 if (((int) installState & (1 << i)) != 0)
265 {
266 states.Add((InstallState) i);
267 }
268 }
269 return states.AsReadOnly();
270 }
271 }
272
273 /// <summary>
274 /// Gets or sets the attributes of the feature.
275 /// </summary>
276 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
277 /// <exception cref="ArgumentException">an unknown feature was requested</exception>
278 /// <remarks><p>
279 /// Win32 MSI APIs:
280 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeatureinfo.asp">MsiGetFeatureInfo</a>,
281 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetfeatureattributes.asp">MsiSetFeatureAttributes</a>
282 /// </p><p>
283 /// Since the lpAttributes paramter of
284 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeatureinfo.asp">MsiGetFeatureInfo</a>
285 /// does not contain an equivalent flag for <see cref="FeatureAttributes.UIDisallowAbsent"/>, this flag will
286 /// not be retrieved.
287 /// </p><p>
288 /// Since the dwAttributes parameter of
289 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetfeatureattributes.asp">MsiSetFeatureAttributes</a>
290 /// does not contain an equivalent flag for <see cref="FeatureAttributes.UIDisallowAbsent"/>, the presence
291 /// of this flag will be ignored.
292 /// </p></remarks>
293 public FeatureAttributes Attributes
294 {
295 get
296 {
297 FeatureAttributes attributes;
298 uint titleBufSize = 0;
299 uint descBufSize = 0;
300 uint attr;
301 uint ret = NativeMethods.MsiGetFeatureInfo(
302 (int) this.session.Handle,
303 this.name,
304 out attr,
305 null,
306 ref titleBufSize,
307 null,
308 ref descBufSize);
309
310 if (ret != 0)
311 {
312 throw InstallerException.ExceptionFromReturnCode(ret);
313 }
314
315 // Values for attributes that MsiGetFeatureInfo returns are
316 // double the values in the Attributes column of the Feature Table.
317 attributes = (FeatureAttributes) (attr >> 1);
318
319 // MsiGetFeatureInfo MSDN documentation indicates
320 // NOUNSUPPORTEDADVERTISE is 32. Conversion above changes this to 16
321 // which is UIDisallowAbsent. MsiGetFeatureInfo isn't documented to
322 // return an attribute for 'UIDisallowAbsent', so if UIDisallowAbsent
323 // is set, change it to NoUnsupportedAdvertise which then maps correctly
324 // to NOUNSUPPORTEDADVERTISE.
325 if ((attributes & FeatureAttributes.UIDisallowAbsent) == FeatureAttributes.UIDisallowAbsent)
326 {
327 attributes &= ~FeatureAttributes.UIDisallowAbsent;
328 attributes |= FeatureAttributes.NoUnsupportedAdvertise;
329 }
330
331 return attributes;
332 }
333
334 set
335 {
336 // MsiSetFeatureAttributes doesn't indicate UIDisallowAbsent is valid
337 // so remove it.
338 FeatureAttributes attributes = value;
339 attributes &= ~FeatureAttributes.UIDisallowAbsent;
340
341 // Values for attributes that MsiSetFeatureAttributes uses are
342 // double the values in the Attributes column of the Feature Table.
343 uint attr = ((uint) attributes) << 1;
344
345 // MsiSetFeatureAttributes MSDN documentation indicates
346 // NOUNSUPPORTEDADVERTISE is 32. Conversion above changes this to 64
347 // which is undefined. Change this back to 32.
348 uint noUnsupportedAdvertiseDbl = ((uint)FeatureAttributes.NoUnsupportedAdvertise) << 1;
349 if ((attr & noUnsupportedAdvertiseDbl) == noUnsupportedAdvertiseDbl)
350 {
351 attr &= ~noUnsupportedAdvertiseDbl;
352 attr |= (uint) FeatureAttributes.NoUnsupportedAdvertise;
353 }
354
355 uint ret = RemotableNativeMethods.MsiSetFeatureAttributes((int) this.session.Handle, this.name, attr);
356
357 if (ret != (uint)NativeMethods.Error.SUCCESS)
358 {
359 if (ret == (uint)NativeMethods.Error.UNKNOWN_FEATURE)
360 {
361 throw InstallerException.ExceptionFromReturnCode(ret, this.name);
362 }
363 else
364 {
365 throw InstallerException.ExceptionFromReturnCode(ret);
366 }
367 }
368 }
369 }
370
371 /// <summary>
372 /// Gets the title of the feature.
373 /// </summary>
374 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
375 /// <exception cref="ArgumentException">an unknown feature was requested</exception>
376 /// <remarks><p>
377 /// Win32 MSI API:
378 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeatureinfo.asp">MsiGetFeatureInfo</a>
379 /// </p></remarks>
380 public string Title
381 {
382 get
383 {
384 StringBuilder titleBuf = new StringBuilder(80);
385 uint titleBufSize = (uint) titleBuf.Capacity;
386 uint descBufSize = 0;
387 uint attr;
388 uint ret = NativeMethods.MsiGetFeatureInfo(
389 (int) this.session.Handle,
390 this.name,
391 out attr,
392 titleBuf,
393 ref titleBufSize,
394 null,
395 ref descBufSize);
396
397 if (ret == (uint) NativeMethods.Error.MORE_DATA)
398 {
399 titleBuf.Capacity = (int) ++titleBufSize;
400 ret = NativeMethods.MsiGetFeatureInfo(
401 (int) this.session.Handle,
402 this.name,
403 out attr,
404 titleBuf,
405 ref titleBufSize,
406 null,
407 ref descBufSize);
408 }
409
410 if (ret != 0)
411 {
412 throw InstallerException.ExceptionFromReturnCode(ret);
413 }
414
415 return titleBuf.ToString();
416 }
417 }
418
419 /// <summary>
420 /// Gets the description of the feature.
421 /// </summary>
422 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
423 /// <exception cref="ArgumentException">an unknown feature was requested</exception>
424 /// <remarks><p>
425 /// Win32 MSI API:
426 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeatureinfo.asp">MsiGetFeatureInfo</a>
427 /// </p></remarks>
428 public string Description
429 {
430 get
431 {
432 StringBuilder descBuf = new StringBuilder(256);
433 uint titleBufSize = 0;
434 uint descBufSize = (uint) descBuf.Capacity;
435 uint attr;
436 uint ret = NativeMethods.MsiGetFeatureInfo(
437 (int) this.session.Handle,
438 this.name,
439 out attr,
440 null,
441 ref titleBufSize,
442 descBuf,
443 ref descBufSize);
444
445 if (ret == (uint) NativeMethods.Error.MORE_DATA)
446 {
447 descBuf.Capacity = (int) ++descBufSize;
448 ret = NativeMethods.MsiGetFeatureInfo(
449 (int) this.session.Handle,
450 this.name,
451 out attr,
452 null,
453 ref titleBufSize,
454 descBuf,
455 ref descBufSize);
456 }
457
458 if (ret != 0)
459 {
460 throw InstallerException.ExceptionFromReturnCode(ret);
461 }
462
463 return descBuf.ToString();
464 }
465 }
466
467 /// <summary>
468 /// Calculates the disk space required by the feature and its selected children and parent features.
469 /// </summary>
470 /// <param name="includeParents">If true, the parent features are included in the cost.</param>
471 /// <param name="includeChildren">If true, the child features are included in the cost.</param>
472 /// <param name="installState">Specifies the installation state.</param>
473 /// <returns>The disk space requirement in bytes.</returns>
474 /// <remarks><p>
475 /// Win32 MSI API:
476 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeaturecost.asp">MsiGetFeatureCost</a>
477 /// </p></remarks>
478 public long GetCost(bool includeParents, bool includeChildren, InstallState installState)
479 {
480 const int MSICOSTTREE_CHILDREN = 1;
481 const int MSICOSTTREE_PARENTS = 2;
482
483 int cost;
484 uint ret = RemotableNativeMethods.MsiGetFeatureCost(
485 (int) this.session.Handle,
486 this.name,
487 (includeParents ? MSICOSTTREE_PARENTS : 0) | (includeChildren ? MSICOSTTREE_CHILDREN : 0),
488 (int) installState,
489 out cost);
490 if (ret != 0)
491 {
492 throw InstallerException.ExceptionFromReturnCode(ret);
493 }
494 return cost * 512L;
495 }
496 }
497}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/FeatureInstallation.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/FeatureInstallation.cs
new file mode 100644
index 00000000..aa8ffe34
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/FeatureInstallation.cs
@@ -0,0 +1,174 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections.Generic;
8 using System.Diagnostics.CodeAnalysis;
9
10 /// <summary>
11 /// Represents an instance of a feature of an installed product.
12 /// </summary>
13 public class FeatureInstallation : InstallationPart
14 {
15 /// <summary>
16 /// Creates a new FeatureInstallation instance for a feature of a product.
17 /// </summary>
18 /// <param name="featureName">feature name</param>
19 /// <param name="productCode">ProductCode GUID</param>
20 public FeatureInstallation(string featureName, string productCode)
21 : base(featureName, productCode)
22 {
23 if (String.IsNullOrEmpty(featureName))
24 {
25 throw new ArgumentNullException("featureName");
26 }
27 }
28
29 /// <summary>
30 /// Gets the name of the feature.
31 /// </summary>
32 public string FeatureName
33 {
34 get
35 {
36 return this.Id;
37 }
38 }
39
40 /// <summary>
41 /// Gets the installed state of the feature.
42 /// </summary>
43 /// <remarks><p>
44 /// Win32 MSI API:
45 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiqueryfeaturestate.asp">MsiQueryFeatureState</a>
46 /// </p></remarks>
47 public override InstallState State
48 {
49 get
50 {
51 int installState = NativeMethods.MsiQueryFeatureState(
52 this.ProductCode, this.FeatureName);
53 return (InstallState) installState;
54 }
55 }
56
57 /// <summary>
58 /// Gets the parent of the feature, or null if the feature has no parent (it is a root feature).
59 /// </summary>
60 /// <remarks>
61 /// Invocation of this property may be slightly costly for products with many features,
62 /// because it involves an enumeration of all the features in the product.
63 /// </remarks>
64 public FeatureInstallation Parent
65 {
66 get
67 {
68 StringBuilder featureBuf = new StringBuilder(256);
69 StringBuilder parentBuf = new StringBuilder(256);
70 for (uint i = 0; ; i++)
71 {
72 uint ret = NativeMethods.MsiEnumFeatures(this.ProductCode, i, featureBuf, parentBuf);
73
74 if (ret != 0)
75 {
76 break;
77 }
78
79 if (featureBuf.ToString() == this.FeatureName)
80 {
81 if (parentBuf.Length > 0)
82 {
83 return new FeatureInstallation(parentBuf.ToString(), this.ProductCode);
84 }
85 else
86 {
87 return null;
88 }
89 }
90 }
91
92 return null;
93 }
94 }
95
96 /// <summary>
97 /// Gets the usage metrics for the feature.
98 /// </summary>
99 /// <remarks><p>
100 /// If no usage metrics are recorded, the <see cref="UsageData.UseCount" /> value is 0.
101 /// </p><p>
102 /// Win32 MSI API:
103 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfeatureusage.asp">MsiGetFeatureUsage</a>
104 /// </p></remarks>
105 public FeatureInstallation.UsageData Usage
106 {
107 get
108 {
109 uint useCount;
110 ushort useDate;
111 uint ret = NativeMethods.MsiGetFeatureUsage(
112 this.ProductCode, this.FeatureName, out useCount, out useDate);
113 if (ret != 0)
114 {
115 throw InstallerException.ExceptionFromReturnCode(ret);
116 }
117
118 DateTime lastUsedDate;
119 if (useCount == 0)
120 {
121 lastUsedDate = DateTime.MinValue;
122 }
123 else
124 {
125 lastUsedDate = new DateTime(
126 1980 + (useDate >> 9),
127 (useDate & 0x01FF) >> 5,
128 (useDate & 0x001F));
129 }
130
131 return new UsageData((int) useCount, lastUsedDate);
132 }
133 }
134
135 /// <summary>
136 /// Holds data about the usage of a feature.
137 /// </summary>
138 [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")]
139 [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
140 public struct UsageData
141 {
142 private int useCount;
143 private DateTime lastUsedDate;
144
145 internal UsageData(int useCount, DateTime lastUsedDate)
146 {
147 this.useCount = useCount;
148 this.lastUsedDate = lastUsedDate;
149 }
150
151 /// <summary>
152 /// Gets count of the number of times the feature has been used.
153 /// </summary>
154 public int UseCount
155 {
156 get
157 {
158 return this.useCount;
159 }
160 }
161
162 /// <summary>
163 /// Gets the date the feature was last used.
164 /// </summary>
165 public DateTime LastUsedDate
166 {
167 get
168 {
169 return this.lastUsedDate;
170 }
171 }
172 }
173 }
174}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Handle.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Handle.cs
new file mode 100644
index 00000000..c3d3625f
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Handle.cs
@@ -0,0 +1,154 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.ComponentModel;
7 using System.Diagnostics.CodeAnalysis;
8
9 /// <summary>
10 /// Base class for Windows Installer handle types (Database, View, Record, SummaryInfo).
11 /// </summary>
12 /// <remarks><p>
13 /// These classes implement the <see cref="IDisposable"/> interface, because they
14 /// hold unmanaged resources (MSI handles) that should be properly disposed
15 /// when no longer needed.
16 /// </p></remarks>
17 public abstract class InstallerHandle : MarshalByRefObject, IDisposable
18 {
19 private NativeMethods.MsiHandle handle;
20
21 /// <summary>
22 /// Constructs a handle object from a native integer handle.
23 /// </summary>
24 /// <param name="handle">Native integer handle.</param>
25 /// <param name="ownsHandle">true to close the handle when this object is disposed or finalized</param>
26 protected InstallerHandle(IntPtr handle, bool ownsHandle)
27 {
28 if (handle == IntPtr.Zero)
29 {
30 throw new InvalidHandleException();
31 }
32
33 this.handle = new NativeMethods.MsiHandle(handle, ownsHandle);
34 }
35
36 /// <summary>
37 /// Gets the native integer handle.
38 /// </summary>
39 [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")]
40 public IntPtr Handle
41 {
42 get
43 {
44 if (this.IsClosed)
45 {
46 throw new InvalidHandleException();
47 }
48 return this.handle;
49 }
50 }
51
52 /// <summary>
53 /// Checks if the handle is closed. When closed, method calls on the handle object may throw an <see cref="InvalidHandleException"/>.
54 /// </summary>
55 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
56 public bool IsClosed
57 {
58 get
59 {
60 return this.handle.IsClosed;
61 }
62 }
63
64 /// <summary>
65 /// Closes the handle. After closing a handle, further method calls may throw an <see cref="InvalidHandleException"/>.
66 /// </summary>
67 /// <remarks><p>
68 /// The finalizer of this class will NOT close the handle if it is still open,
69 /// because finalization can run on a separate thread from the application,
70 /// resulting in potential problems if handles are closed from that thread.
71 /// It is best that the handle be closed manually as soon as it is no longer needed,
72 /// as leaving lots of unused handles open can degrade performance.
73 /// </p><p>
74 /// Win32 MSI API:
75 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiclosehandle.asp">MsiCloseHandle</a>
76 /// </p></remarks>
77 /// <seealso cref="Close"/>
78 public void Dispose()
79 {
80 this.Dispose(true);
81 GC.SuppressFinalize(this);
82 }
83
84 /// <summary>
85 /// Closes the handle. After closing a handle, further method calls may throw an <see cref="InvalidHandleException"/>.
86 /// </summary>
87 /// <remarks><p>
88 /// The finalizer of this class will NOT close the handle if it is still open,
89 /// because finalization can run on a separate thread from the application,
90 /// resulting in potential problems if handles are closed from that thread.
91 /// It is best that the handle be closed manually as soon as it is no longer needed,
92 /// as leaving lots of unused handles open can degrade performance.
93 /// </p><p>
94 /// This method is merely an alias for the <see cref="Dispose()"/> method.
95 /// </p><p>
96 /// Win32 MSI API:
97 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiclosehandle.asp">MsiCloseHandle</a>
98 /// </p></remarks>
99 public void Close()
100 {
101 this.Dispose();
102 }
103
104 /// <summary>
105 /// Tests whether this handle object is equal to another handle object. Two handle objects are equal
106 /// if their types are the same and their native integer handles are the same.
107 /// </summary>
108 /// <param name="obj">The handle object to compare with the current handle object.</param>
109 /// <returns>true if the specified handle object is equal to the current handle object; otherwise false</returns>
110 public override bool Equals(object obj)
111 {
112 return (obj != null && this.GetType() == obj.GetType() &&
113 this.Handle == ((InstallerHandle) obj).Handle);
114 }
115
116 /// <summary>
117 /// Gets a hash value for the handle object.
118 /// </summary>
119 /// <returns>A hash code for the handle object.</returns>
120 /// <remarks><p>
121 /// The hash code is derived from the native integer handle.
122 /// </p></remarks>
123 public override int GetHashCode()
124 {
125 return this.Handle.GetHashCode();
126 }
127
128 /// <summary>
129 /// Gets an object that can be used internally for safe syncronization.
130 /// </summary>
131 internal object Sync
132 {
133 get
134 {
135 return this.handle;
136 }
137 }
138
139 /// <summary>
140 /// Closes the handle. After closing a handle, further method calls may throw an <see cref="InvalidHandleException"/>.
141 /// </summary>
142 /// <param name="disposing">If true, the method has been called directly or indirectly by a user's code,
143 /// so managed and unmanaged resources will be disposed. If false, the method has been called by the
144 /// runtime from inside the finalizer, and only unmanaged resources will be disposed.</param>
145 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
146 protected virtual void Dispose(bool disposing)
147 {
148 if (disposing)
149 {
150 this.handle.Dispose();
151 }
152 }
153 }
154}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/IEmbeddedUI.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/IEmbeddedUI.cs
new file mode 100644
index 00000000..d77c82a9
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/IEmbeddedUI.cs
@@ -0,0 +1,67 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System.Diagnostics.CodeAnalysis;
6
7 /// <summary>
8 /// [MSI 4.5] Interface for an embedded external user interface for an installation.
9 /// </summary>
10 /// <remarks>
11 /// Classes which implement this interface must have a public constructor that takes no parameters.
12 /// </remarks>
13 public interface IEmbeddedUI
14 {
15 /// <summary>
16 /// Initializes the embedded UI.
17 /// </summary>
18 /// <param name="session">Handle to the installer which can be used to get and set properties.
19 /// The handle is only valid for the duration of this method call.</param>
20 /// <param name="resourcePath">Path to the directory that contains all the files from the MsiEmbeddedUI table.</param>
21 /// <param name="internalUILevel">On entry, contains the current UI level for the installation. After this
22 /// method returns, the installer resets the UI level to the returned value of this parameter.</param>
23 /// <returns>True if the embedded UI was successfully initialized; false if the installation
24 /// should continue without the embedded UI.</returns>
25 /// <exception cref="InstallCanceledException">The installation was canceled by the user.</exception>
26 /// <exception cref="InstallerException">The embedded UI failed to initialize and
27 /// causes the installation to fail.</exception>
28 /// <remarks><p>
29 /// Win32 MSI API:
30 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/initializeembeddedui.asp">InitializeEmbeddedUI</a>
31 /// </p></remarks>
32 [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference")]
33 bool Initialize(Session session, string resourcePath, ref InstallUIOptions internalUILevel);
34
35 /// <summary>
36 /// Processes information and progress messages sent to the user interface.
37 /// </summary>
38 /// <param name="messageType">Message type.</param>
39 /// <param name="messageRecord">Record that contains message data.</param>
40 /// <param name="buttons">Message buttons.</param>
41 /// <param name="icon">Message box icon.</param>
42 /// <param name="defaultButton">Message box default button.</param>
43 /// <returns>Result of processing the message.</returns>
44 /// <remarks><p>
45 /// Win32 MSI API:
46 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/embeddeduihandler.asp">EmbeddedUIHandler</a>
47 /// </p></remarks>
48 MessageResult ProcessMessage(
49 InstallMessage messageType,
50 Record messageRecord,
51 MessageButtons buttons,
52 MessageIcon icon,
53 MessageDefaultButton defaultButton);
54
55 /// <summary>
56 /// Shuts down the embedded UI at the end of the installation.
57 /// </summary>
58 /// <remarks>
59 /// If the installation was canceled during initialization, this method will not be called.
60 /// If the installation was canceled or failed at any later point, this method will be called at the end.
61 /// <p>
62 /// Win32 MSI API:
63 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/shutdownembeddedui.asp">ShutdownEmbeddedUI</a>
64 /// </p></remarks>
65 void Shutdown();
66 }
67}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallCost.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallCost.cs
new file mode 100644
index 00000000..f29612d6
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallCost.cs
@@ -0,0 +1,67 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System.Diagnostics.CodeAnalysis;
6
7 /// <summary>
8 /// Represents a per-drive disk space cost for an installation.
9 /// </summary>
10 [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
11 public struct InstallCost
12 {
13 private string driveName;
14 private long cost;
15 private long tempCost;
16
17 /// <summary>
18 /// Creates a new InstallCost object.
19 /// </summary>
20 /// <param name="driveName">name of the drive this cost data applies to</param>
21 /// <param name="cost">installation cost on this drive, as a number of bytes</param>
22 /// <param name="tempCost">temporary disk space required on this drive, as a number of bytes</param>
23 internal InstallCost(string driveName, long cost, long tempCost)
24 {
25 this.driveName = driveName;
26 this.cost = cost;
27 this.tempCost = tempCost;
28 }
29
30 /// <summary>
31 /// The name of the drive this cost data applies to.
32 /// </summary>
33 public string DriveName
34 {
35 get
36 {
37 return this.driveName;
38 }
39 }
40
41 /// <summary>
42 /// The installation cost on this drive, as a number of bytes.
43 /// </summary>
44 public long Cost
45 {
46 get
47 {
48 return this.cost;
49 }
50 }
51
52 /// <summary>
53 /// The temporary disk space required on this drive, as a number of bytes.
54 /// </summary>
55 /// <remarks><p>
56 /// This temporary space requirement is space needed only for the duration
57 /// of the installation, over the final footprint on disk.
58 /// </p></remarks>
59 public long TempCost
60 {
61 get
62 {
63 return this.tempCost;
64 }
65 }
66 }
67}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Installation.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Installation.cs
new file mode 100644
index 00000000..47ca00a1
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Installation.cs
@@ -0,0 +1,100 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Globalization;
8
9 /// <summary>
10 /// Subclasses of this abstract class represent a unique instance of a
11 /// registered product or patch installation.
12 /// </summary>
13 public abstract class Installation
14 {
15 private string installationCode;
16 private string userSid;
17 private UserContexts context;
18 private SourceList sourceList;
19
20 internal Installation(string installationCode, string userSid, UserContexts context)
21 {
22 if (context == UserContexts.Machine)
23 {
24 userSid = null;
25 }
26 this.installationCode = installationCode;
27 this.userSid = userSid;
28 this.context = context;
29 }
30
31 /// <summary>
32 /// Gets the user security identifier (SID) under which this product or patch
33 /// installation is available.
34 /// </summary>
35 public string UserSid
36 {
37 get
38 {
39 return this.userSid;
40 }
41 }
42
43 /// <summary>
44 /// Gets the user context of this product or patch installation.
45 /// </summary>
46 public UserContexts Context
47 {
48 get
49 {
50 return this.context;
51 }
52 }
53
54 /// <summary>
55 /// Gets the source list of this product or patch installation.
56 /// </summary>
57 public virtual SourceList SourceList
58 {
59 get
60 {
61 if (this.sourceList == null)
62 {
63 this.sourceList = new SourceList(this);
64 }
65 return this.sourceList;
66 }
67 }
68
69 /// <summary>
70 /// Gets a value indicating whether this product or patch is installed on the current system.
71 /// </summary>
72 public abstract bool IsInstalled
73 {
74 get;
75 }
76
77 internal string InstallationCode
78 {
79 get
80 {
81 return this.installationCode;
82 }
83 }
84
85 internal abstract int InstallationType
86 {
87 get;
88 }
89
90 /// <summary>
91 /// Gets a property about the product or patch installation.
92 /// </summary>
93 /// <param name="propertyName">Name of the property being retrieved.</param>
94 /// <returns></returns>
95 public abstract string this[string propertyName]
96 {
97 get;
98 }
99 }
100}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallationPart.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallationPart.cs
new file mode 100644
index 00000000..ce5a6a94
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallationPart.cs
@@ -0,0 +1,82 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 /// <summary>
6 /// Subclasses of this abstract class represent an instance
7 /// of a registered feature or component.
8 /// </summary>
9 public abstract class InstallationPart
10 {
11 private string id;
12 private string productCode;
13 private string userSid;
14 private UserContexts context;
15
16 internal InstallationPart(string id, string productCode)
17 : this(id, productCode, null, UserContexts.None)
18 {
19 }
20
21 internal InstallationPart(string id, string productCode, string userSid, UserContexts context)
22 {
23 this.id = id;
24 this.productCode = productCode;
25 this.userSid = userSid;
26 this.context = context;
27 }
28
29 internal string Id
30 {
31 get
32 {
33 return this.id;
34 }
35 }
36
37 internal string ProductCode
38 {
39 get
40 {
41 return this.productCode;
42 }
43 }
44
45 internal string UserSid
46 {
47 get
48 {
49 return this.userSid;
50 }
51 }
52
53 internal UserContexts Context
54 {
55 get
56 {
57 return this.context;
58 }
59 }
60
61 /// <summary>
62 /// Gets the product that this item is a part of.
63 /// </summary>
64 public ProductInstallation Product
65 {
66 get
67 {
68 return this.productCode != null ?
69 new ProductInstallation(this.productCode, userSid, context) : null;
70 }
71 }
72
73 /// <summary>
74 /// Gets the current installation state of the item.
75 /// </summary>
76 public abstract InstallState State
77 {
78 get;
79 }
80 }
81
82}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Installer.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Installer.cs
new file mode 100644
index 00000000..8df0aed9
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Installer.cs
@@ -0,0 +1,890 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Resources;
9 using System.Reflection;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Runtime.InteropServices;
13 using System.Diagnostics.CodeAnalysis;
14
15/// <summary>
16/// Receives an exception from
17/// <see cref="Installer.DetermineApplicablePatches(string,string[],InapplicablePatchHandler,string,UserContexts)"/>
18/// indicating the reason a particular patch is not applicable to a product.
19/// </summary>
20/// <param name="patch">MSP file path, XML file path, or XML blob that was passed to
21/// <see cref="Installer.DetermineApplicablePatches(string,string[],InapplicablePatchHandler,string,UserContexts)"/></param>
22/// <param name="exception">exception indicating the reason the patch is not applicable</param>
23/// <remarks><p>
24/// If <paramref name="exception"/> is an <see cref="InstallerException"/> or subclass, then
25/// its <see cref="InstallerException.ErrorCode"/> and <see cref="InstallerException.Message"/>
26/// properties will indicate a more specific reason the patch was not applicable.
27/// </p><p>
28/// The <paramref name="exception"/> could also be a FileNotFoundException if the
29/// patch string was a file path.
30/// </p></remarks>
31public delegate void InapplicablePatchHandler(string patch, Exception exception);
32
33/// <summary>
34/// Provides static methods for installing and configuring products and patches.
35/// </summary>
36public static partial class Installer
37{
38 private static bool rebootRequired;
39 private static bool rebootInitiated;
40 private static ResourceManager errorResources;
41
42 /// <summary>
43 /// Indicates whether a system reboot is required after running an installation or configuration operation.
44 /// </summary>
45 public static bool RebootRequired
46 {
47 get
48 {
49 return Installer.rebootRequired;
50 }
51 }
52
53 /// <summary>
54 /// Indicates whether a system reboot has been initiated after running an installation or configuration operation.
55 /// </summary>
56 public static bool RebootInitiated
57 {
58 get
59 {
60 return Installer.rebootInitiated;
61 }
62 }
63
64 /// <summary>
65 /// Enables the installer's internal user interface. Then this user interface is used
66 /// for all subsequent calls to user-interface-generating installer functions in this process.
67 /// </summary>
68 /// <param name="uiOptions">Specifies the level of complexity of the user interface</param>
69 /// <param name="windowHandle">Handle to a window, which becomes the owner of any user interface created.
70 /// A pointer to the previous owner of the user interface is returned.</param>
71 /// <returns>The previous user interface level</returns>
72 /// <remarks><p>
73 /// Win32 MSI API:
74 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetinternalui.asp">MsiSetInternalUI</a>
75 /// </p></remarks>
76 [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference")]
77 public static InstallUIOptions SetInternalUI(InstallUIOptions uiOptions, ref IntPtr windowHandle)
78 {
79 return (InstallUIOptions) NativeMethods.MsiSetInternalUI((uint) uiOptions, ref windowHandle);
80 }
81
82 /// <summary>
83 /// Enables the installer's internal user interface. Then this user interface is used
84 /// for all subsequent calls to user-interface-generating installer functions in this process.
85 /// The owner of the user interface does not change.
86 /// </summary>
87 /// <param name="uiOptions">Specifies the level of complexity of the user interface</param>
88 /// <returns>The previous user interface level</returns>
89 /// <remarks><p>
90 /// Win32 MSI API:
91 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetinternalui.asp">MsiSetInternalUI</a>
92 /// </p></remarks>
93 public static InstallUIOptions SetInternalUI(InstallUIOptions uiOptions)
94 {
95 return (InstallUIOptions) NativeMethods.MsiSetInternalUI((uint) uiOptions, IntPtr.Zero);
96 }
97
98 /// <summary>
99 /// Enables logging of the selected message type for all subsequent install sessions in
100 /// the current process space.
101 /// </summary>
102 /// <param name="logModes">One or more mode flags specifying the type of messages to log</param>
103 /// <param name="logFile">Full path to the log file. A null path disables logging,
104 /// in which case the logModes paraneter is ignored.</param>
105 /// <exception cref="ArgumentException">an invalid log mode was specified</exception>
106 /// <remarks>This method takes effect on any new installation processes. Calling this
107 /// method from within a custom action will not start logging for that installation.</remarks>
108 public static void EnableLog(InstallLogModes logModes, string logFile)
109 {
110 Installer.EnableLog(logModes, logFile, false, true);
111 }
112
113 /// <summary>
114 /// Enables logging of the selected message type for all subsequent install sessions in
115 /// the current process space.
116 /// </summary>
117 /// <param name="logModes">One or more mode flags specifying the type of messages to log</param>
118 /// <param name="logFile">Full path to the log file. A null path disables logging,
119 /// in which case the logModes paraneter is ignored.</param>
120 /// <param name="append">If true, the log lines will be appended to any existing file content.
121 /// If false, the log file will be truncated if it exists. The default is false.</param>
122 /// <param name="flushEveryLine">If true, the log will be flushed after every line.
123 /// If false, the log will be flushed every 20 lines. The default is true.</param>
124 /// <exception cref="ArgumentException">an invalid log mode was specified</exception>
125 /// <remarks><p>
126 /// This method takes effect on any new installation processes. Calling this
127 /// method from within a custom action will not start logging for that installation.
128 /// </p><p>
129 /// Win32 MSI API:
130 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienablelog.asp">MsiEnableLog</a>
131 /// </p></remarks>
132 public static void EnableLog(InstallLogModes logModes, string logFile, bool append, bool flushEveryLine)
133 {
134 uint ret = NativeMethods.MsiEnableLog((uint) logModes, logFile, (append ? (uint) 1 : 0) + (flushEveryLine ? (uint) 2 : 0));
135 if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID)
136 {
137 throw InstallerException.ExceptionFromReturnCode(ret);
138 }
139 }
140
141 /// <summary>
142 /// increments the usage count for a particular feature and returns the installation state for
143 /// that feature. This method should be used to indicate an application's intent to use a feature.
144 /// </summary>
145 /// <param name="productCode">The product code of the product.</param>
146 /// <param name="feature">The feature to be used.</param>
147 /// <param name="installMode">Must have the value <see cref="InstallMode.NoDetection"/>.</param>
148 /// <returns>The installed state of the feature.</returns>
149 /// <remarks><p>
150 /// The UseFeature method should only be used on features known to be published. The application
151 /// should determine the status of the feature by calling either the FeatureState method or
152 /// Features method.
153 /// </p><p>
154 /// Win32 MSI APIs:
155 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiusefeature.asp">MsiUseFeature</a>,
156 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiusefeatureex.asp">MsiUseFeatureEx</a>
157 /// </p></remarks>
158 public static InstallState UseFeature(string productCode, string feature, InstallMode installMode)
159 {
160 int installState = NativeMethods.MsiUseFeatureEx(productCode, feature, unchecked ((uint) installMode), 0);
161 return (InstallState) installState;
162 }
163
164 /// <summary>
165 /// Opens an installer package for use with functions that access the product database and install engine,
166 /// returning an Session object.
167 /// </summary>
168 /// <param name="packagePath">Path to the package</param>
169 /// <param name="ignoreMachineState">Specifies whether or not the create a Session object that ignores the
170 /// computer state and that is incapable of changing the current computer state. A value of false yields
171 /// the normal behavior. A value of true creates a "safe" Session object that cannot change of the current
172 /// machine state.</param>
173 /// <returns>A Session object allowing access to the product database and install engine</returns>
174 /// <exception cref="InstallerException">The product could not be opened</exception>
175 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
176 /// <remarks><p>
177 /// Note that only one Session object can be opened by a single process. OpenPackage cannot be used in a
178 /// custom action because the active installation is the only session allowed.
179 /// </p><p>
180 /// A "safe" Session object ignores the current computer state when opening the package and prevents
181 /// changes to the current computer state.
182 /// </p><p>
183 /// The Session object should be <see cref="InstallerHandle.Close"/>d after use.
184 /// It is best that the handle be closed manually as soon as it is no longer
185 /// needed, as leaving lots of unused handles open can degrade performance.
186 /// </p><p>
187 /// Win32 MSI APIs:
188 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopenpackage.asp">MsiOpenPackage</a>,
189 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopenpackageex.asp">MsiOpenPackageEx</a>
190 /// </p></remarks>
191 public static Session OpenPackage(string packagePath, bool ignoreMachineState)
192 {
193 int sessionHandle;
194 uint ret = NativeMethods.MsiOpenPackageEx(packagePath, ignoreMachineState ? (uint) 1 : 0, out sessionHandle);
195 if (ret != 0)
196 {
197 throw InstallerException.ExceptionFromReturnCode(ret);
198 }
199 return new Session((IntPtr) sessionHandle, true);
200 }
201
202 /// <summary>
203 /// Opens an installer package for use with functions that access the product database and install engine,
204 /// returning an Session object.
205 /// </summary>
206 /// <param name="database">Database used to create the session</param>
207 /// <param name="ignoreMachineState">Specifies whether or not the create a Session object that ignores the
208 /// computer state and that is incapable of changing the current computer state. A value of false yields
209 /// the normal behavior. A value of true creates a "safe" Session object that cannot change of the current
210 /// machine state.</param>
211 /// <returns>A Session object allowing access to the product database and install engine</returns>
212 /// <exception cref="InstallerException">The product could not be opened</exception>
213 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
214 /// <remarks><p>
215 /// Note that only one Session object can be opened by a single process. OpenPackage cannot be used in a
216 /// custom action because the active installation is the only session allowed.
217 /// </p><p>
218 /// A "safe" Session object ignores the current computer state when opening the package and prevents
219 /// changes to the current computer state.
220 /// </p><p>
221 /// The Session object should be <see cref="InstallerHandle.Close"/>d after use.
222 /// It is best that the handle be closed manually as soon as it is no longer
223 /// needed, as leaving lots of unused handles open can degrade performance.
224 /// </p><p>
225 /// Win32 MSI APIs:
226 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopenpackage.asp">MsiOpenPackage</a>,
227 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopenpackageex.asp">MsiOpenPackageEx</a>
228 /// </p></remarks>
229 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
230 public static Session OpenPackage(Database database, bool ignoreMachineState)
231 {
232 if (database == null)
233 {
234 throw new ArgumentNullException("database");
235 }
236
237 return Installer.OpenPackage(
238 String.Format(CultureInfo.InvariantCulture, "#{0}", database.Handle),
239 ignoreMachineState);
240 }
241
242 /// <summary>
243 /// Opens an installer package for an installed product using the product code.
244 /// </summary>
245 /// <param name="productCode">Product code of the installed product</param>
246 /// <returns>A Session object allowing access to the product database and install engine,
247 /// or null if the specified product is not installed.</returns>
248 /// <exception cref="ArgumentException">An unknown product was requested</exception>
249 /// <exception cref="InstallerException">The product could not be opened</exception>
250 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
251 /// <remarks><p>
252 /// Note that only one Session object can be opened by a single process. OpenProduct cannot be
253 /// used in a custom action because the active installation is the only session allowed.
254 /// </p><p>
255 /// The Session object should be <see cref="InstallerHandle.Close"/>d after use.
256 /// It is best that the handle be closed manually as soon as it is no longer
257 /// needed, as leaving lots of unused handles open can degrade performance.
258 /// </p><p>
259 /// Win32 MSI API:
260 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiopenproduct.asp">MsiOpenProduct</a>
261 /// </p></remarks>
262 public static Session OpenProduct(string productCode)
263 {
264 int sessionHandle;
265 uint ret = NativeMethods.MsiOpenProduct(productCode, out sessionHandle);
266 if (ret != 0)
267 {
268 if (ret == (uint) NativeMethods.Error.UNKNOWN_PRODUCT)
269 {
270 return null;
271 }
272 else
273 {
274 throw InstallerException.ExceptionFromReturnCode(ret);
275 }
276 }
277 return new Session((IntPtr) sessionHandle, true);
278 }
279
280 /// <summary>
281 /// Gets the full component path, performing any necessary installation. This method prompts for source if
282 /// necessary and increments the usage count for the feature.
283 /// </summary>
284 /// <param name="product">Product code for the product that contains the feature with the necessary component</param>
285 /// <param name="feature">Feature ID of the feature with the necessary component</param>
286 /// <param name="component">Component code of the necessary component</param>
287 /// <param name="installMode">Installation mode; this can also include bits from <see cref="ReinstallModes"/></param>
288 /// <returns>Path to the component</returns>
289 /// <remarks><p>
290 /// Win32 MSI API:
291 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiprovidecomponent.asp">MsiProvideComponent</a>
292 /// </p></remarks>
293 public static string ProvideComponent(string product, string feature, string component, InstallMode installMode)
294 {
295 StringBuilder pathBuf = new StringBuilder(512);
296 uint pathBufSize = (uint) pathBuf.Capacity;
297 uint ret = NativeMethods.MsiProvideComponent(product, feature, component, unchecked((uint)installMode), pathBuf, ref pathBufSize);
298 if (ret == (uint) NativeMethods.Error.MORE_DATA)
299 {
300 pathBuf.Capacity = (int) ++pathBufSize;
301 ret = NativeMethods.MsiProvideComponent(product, feature, component, unchecked((uint)installMode), pathBuf, ref pathBufSize);
302 }
303
304 if (ret != 0)
305 {
306 throw InstallerException.ExceptionFromReturnCode(ret);
307 }
308 return pathBuf.ToString();
309 }
310
311 /// <summary>
312 /// Gets the full component path for a qualified component that is published by a product and
313 /// performs any necessary installation. This method prompts for source if necessary and increments
314 /// the usage count for the feature.
315 /// </summary>
316 /// <param name="component">Specifies the component ID for the requested component. This may not be the
317 /// GUID for the component itself but rather a server that provides the correct functionality, as in the
318 /// ComponentId column of the PublishComponent table.</param>
319 /// <param name="qualifier">Specifies a qualifier into a list of advertising components (from PublishComponent Table).</param>
320 /// <param name="installMode">Installation mode; this can also include bits from <see cref="ReinstallModes"/></param>
321 /// <param name="product">Optional; specifies the product to match that has published the qualified component.</param>
322 /// <returns>Path to the component</returns>
323 /// <remarks><p>
324 /// Win32 MSI APIs:
325 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiprovidequalifiedcomponent.asp">MsiProvideQualifiedComponent</a>
326 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiprovidequalifiedcomponentex.asp">MsiProvideQualifiedComponentEx</a>
327 /// </p></remarks>
328 public static string ProvideQualifiedComponent(string component, string qualifier, InstallMode installMode, string product)
329 {
330 StringBuilder pathBuf = new StringBuilder(512);
331 uint pathBufSize = (uint) pathBuf.Capacity;
332 uint ret = NativeMethods.MsiProvideQualifiedComponentEx(component, qualifier, unchecked((uint)installMode), product, 0, 0, pathBuf, ref pathBufSize);
333 if (ret == (uint) NativeMethods.Error.MORE_DATA)
334 {
335 pathBuf.Capacity = (int) ++pathBufSize;
336 ret = NativeMethods.MsiProvideQualifiedComponentEx(component, qualifier, unchecked((uint)installMode), product, 0, 0, pathBuf, ref pathBufSize);
337 }
338
339 if (ret != 0)
340 {
341 throw InstallerException.ExceptionFromReturnCode(ret);
342 }
343 return pathBuf.ToString();
344 }
345
346 /// <summary>
347 /// Gets the full path to a Windows Installer component containing an assembly. This method prompts for a source and
348 /// increments the usage count for the feature.
349 /// </summary>
350 /// <param name="assemblyName">Assembly name</param>
351 /// <param name="appContext">Set to null for global assemblies. For private assemblies, set to the full path of the
352 /// application configuration file (.cfg file) or executable file (.exe) of the application to which the assembly
353 /// has been made private.</param>
354 /// <param name="installMode">Installation mode; this can also include bits from <see cref="ReinstallModes"/></param>
355 /// <param name="isWin32Assembly">True if this is a Win32 assembly, false if it is a .NET assembly</param>
356 /// <returns>Path to the assembly</returns>
357 /// <remarks><p>
358 /// Win32 MSI API:
359 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiprovideassembly.asp">MsiProvideAssembly</a>
360 /// </p></remarks>
361 public static string ProvideAssembly(string assemblyName, string appContext, InstallMode installMode, bool isWin32Assembly)
362 {
363 StringBuilder pathBuf = new StringBuilder(512);
364 uint pathBufSize = (uint) pathBuf.Capacity;
365 uint ret = NativeMethods.MsiProvideAssembly(assemblyName, appContext, unchecked ((uint) installMode), (isWin32Assembly ? (uint) 1 : 0), pathBuf, ref pathBufSize);
366 if (ret == (uint) NativeMethods.Error.MORE_DATA)
367 {
368 pathBuf.Capacity = (int) ++pathBufSize;
369 ret = NativeMethods.MsiProvideAssembly(assemblyName, appContext, unchecked ((uint) installMode), (isWin32Assembly ? (uint) 1 : 0), pathBuf, ref pathBufSize);
370 }
371
372 if (ret != 0)
373 {
374 throw InstallerException.ExceptionFromReturnCode(ret);
375 }
376 return pathBuf.ToString();
377 }
378
379 /// <summary>
380 /// Installs files that are unexpectedly missing.
381 /// </summary>
382 /// <param name="product">Product code for the product that owns the component to be installed</param>
383 /// <param name="component">Component to be installed</param>
384 /// <param name="installState">Specifies the way the component should be installed.</param>
385 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
386 /// <remarks><p>
387 /// Win32 MSI API:
388 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiinstallmissingcomponent.asp">MsiInstallMissingComponent</a>
389 /// </p></remarks>
390 public static void InstallMissingComponent(string product, string component, InstallState installState)
391 {
392 uint ret = NativeMethods.MsiInstallMissingComponent(product, component, (int) installState);
393 if (ret != 0)
394 {
395 throw InstallerException.ExceptionFromReturnCode(ret);
396 }
397 }
398
399 /// <summary>
400 /// Installs files that are unexpectedly missing.
401 /// </summary>
402 /// <param name="product">Product code for the product that owns the file to be installed</param>
403 /// <param name="file">File to be installed</param>
404 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
405 /// <remarks><p>
406 /// Win32 MSI API:
407 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiinstallmissingfile.asp">MsiInstallMissingFile</a>
408 /// </p></remarks>
409 public static void InstallMissingFile(string product, string file)
410 {
411 uint ret = NativeMethods.MsiInstallMissingFile(product, file);
412 if (ret != 0)
413 {
414 throw InstallerException.ExceptionFromReturnCode(ret);
415 }
416 }
417
418 /// <summary>
419 /// Reinstalls a feature.
420 /// </summary>
421 /// <param name="product">Product code for the product containing the feature to be reinstalled</param>
422 /// <param name="feature">Feature to be reinstalled</param>
423 /// <param name="reinstallModes">Reinstall modes</param>
424 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
425 /// <remarks><p>
426 /// Win32 MSI API:
427 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msireinstallfeature.asp">MsiReinstallFeature</a>
428 /// </p></remarks>
429 public static void ReinstallFeature(string product, string feature, ReinstallModes reinstallModes)
430 {
431 uint ret = NativeMethods.MsiReinstallFeature(product, feature, (uint) reinstallModes);
432 if (ret != 0)
433 {
434 throw InstallerException.ExceptionFromReturnCode(ret);
435 }
436 }
437
438 /// <summary>
439 /// Reinstalls a product.
440 /// </summary>
441 /// <param name="product">Product code for the product to be reinstalled</param>
442 /// <param name="reinstallModes">Reinstall modes</param>
443 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
444 /// <remarks><p>
445 /// Win32 MSI API:
446 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msireinstallproduct.asp">MsiReinstallProduct</a>
447 /// </p></remarks>
448 public static void ReinstallProduct(string product, ReinstallModes reinstallModes)
449 {
450 uint ret = NativeMethods.MsiReinstallProduct(product, (uint) reinstallModes);
451 if (ret != 0)
452 {
453 throw InstallerException.ExceptionFromReturnCode(ret);
454 }
455 }
456
457 /// <summary>
458 /// Opens an installer package and initializes an install session.
459 /// </summary>
460 /// <param name="packagePath">path to the patch package</param>
461 /// <param name="commandLine">command line property settings</param>
462 /// <exception cref="InstallerException">There was an error installing the product</exception>
463 /// <remarks><p>
464 /// To completely remove a product, set REMOVE=ALL in <paramRef name="commandLine"/>.
465 /// </p><p>
466 /// This method displays the user interface with the current settings and
467 /// log mode. You can change user interface settings with the <see cref="SetInternalUI(InstallUIOptions)"/>
468 /// and <see cref="SetExternalUI(ExternalUIHandler,InstallLogModes)"/> functions. You can set the log mode with the
469 /// <see cref="EnableLog(InstallLogModes,string)"/> function.
470 /// </p><p>
471 /// The <see cref="RebootRequired"/> and <see cref="RebootInitiated"/> properties should be
472 /// tested after calling this method.
473 /// </p><p>
474 /// Win32 MSI API:
475 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiinstallproduct.asp">MsiInstallProduct</a>
476 /// </p></remarks>
477 public static void InstallProduct(string packagePath, string commandLine)
478 {
479 uint ret = NativeMethods.MsiInstallProduct(packagePath, commandLine);
480 Installer.CheckInstallResult(ret);
481 }
482
483 /// <summary>
484 /// Installs or uninstalls a product.
485 /// </summary>
486 /// <param name="productCode">Product code of the product to be configured.</param>
487 /// <param name="installLevel">Specifies the default installation configuration of the
488 /// product. The <paramref name="installLevel"/> parameter is ignored and all features
489 /// are installed if the <paramref name="installState"/> parameter is set to any other
490 /// value than <see cref="InstallState.Default"/>. This parameter must be either 0
491 /// (install using authored feature levels), 65535 (install all features), or a value
492 /// between 0 and 65535 to install a subset of available features. </param>
493 /// <param name="installState">Specifies the installation state for the product.</param>
494 /// <param name="commandLine">Specifies the command line property settings. This should
495 /// be a list of the format Property=Setting Property=Setting.</param>
496 /// <exception cref="InstallerException">There was an error configuring the product</exception>
497 /// <remarks><p>
498 /// This method displays the user interface with the current settings and
499 /// log mode. You can change user interface settings with the <see cref="SetInternalUI(InstallUIOptions)"/>
500 /// and <see cref="SetExternalUI(ExternalUIHandler,InstallLogModes)"/> functions. You can set the log mode with the
501 /// <see cref="EnableLog(InstallLogModes,string)"/> function.
502 /// </p><p>
503 /// The <see cref="RebootRequired"/> and <see cref="RebootInitiated"/> properties should be
504 /// tested after calling this method.
505 /// </p><p>
506 /// Win32 MSI APIs:
507 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiconfigureproduct.asp">MsiConfigureProduct</a>,
508 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiconfigureproductex.asp">MsiConfigureProductEx</a>
509 /// </p></remarks>
510 public static void ConfigureProduct(string productCode, int installLevel, InstallState installState, string commandLine)
511 {
512 uint ret = NativeMethods.MsiConfigureProductEx(productCode, installLevel, (int) installState, commandLine);
513 Installer.CheckInstallResult(ret);
514 }
515
516 /// <summary>
517 /// Configures the installed state for a product feature.
518 /// </summary>
519 /// <param name="productCode">Product code of the product to be configured.</param>
520 /// <param name="feature">Specifies the feature ID for the feature to be configured.</param>
521 /// <param name="installState">Specifies the installation state for the feature.</param>
522 /// <exception cref="InstallerException">There was an error configuring the feature</exception>
523 /// <remarks><p>
524 /// The <see cref="RebootRequired"/> and <see cref="RebootInitiated"/> properties should be
525 /// tested after calling this method.
526 /// </p><p>
527 /// Win32 MSI API:
528 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiconfigurefeature.asp">MsiConfigureFeature</a>
529 /// </p></remarks>
530 public static void ConfigureFeature(string productCode, string feature, InstallState installState)
531 {
532 uint ret = NativeMethods.MsiConfigureFeature(productCode, feature, (int) installState);
533 Installer.CheckInstallResult(ret);
534 }
535
536 /// <summary>
537 /// For each product listed by the patch package as eligible to receive the patch, ApplyPatch invokes
538 /// an installation and sets the PATCH property to the path of the patch package.
539 /// </summary>
540 /// <param name="patchPackage">path to the patch package</param>
541 /// <param name="commandLine">optional command line property settings</param>
542 /// <exception cref="InstallerException">There was an error applying the patch</exception>
543 /// <remarks><p>
544 /// The <see cref="RebootRequired"/> and <see cref="RebootInitiated"/> properties should be
545 /// tested after calling this method.
546 /// </p><p>
547 /// Win32 MSI API:
548 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiapplypatch.asp">MsiApplyPatch</a>
549 /// </p></remarks>
550 public static void ApplyPatch(string patchPackage, string commandLine)
551 {
552 Installer.ApplyPatch(patchPackage, null, InstallType.Default, commandLine);
553 }
554
555 /// <summary>
556 /// For each product listed by the patch package as eligible to receive the patch, ApplyPatch invokes
557 /// an installation and sets the PATCH property to the path of the patch package.
558 /// </summary>
559 /// <param name="patchPackage">path to the patch package</param>
560 /// <param name="installPackage">path to the product to be patched, if installType
561 /// is set to <see cref="InstallType.NetworkImage"/></param>
562 /// <param name="installType">type of installation to patch</param>
563 /// <param name="commandLine">optional command line property settings</param>
564 /// <exception cref="InstallerException">There was an error applying the patch</exception>
565 /// <remarks><p>
566 /// The <see cref="RebootRequired"/> and <see cref="RebootInitiated"/> properties should be
567 /// tested after calling this method.
568 /// </p><p>
569 /// Win32 MSI API:
570 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiapplypatch.asp">MsiApplyPatch</a>
571 /// </p></remarks>
572 public static void ApplyPatch(string patchPackage, string installPackage, InstallType installType, string commandLine)
573 {
574 uint ret = NativeMethods.MsiApplyPatch(patchPackage, installPackage, (int) installType, commandLine);
575 Installer.CheckInstallResult(ret);
576 }
577
578 /// <summary>
579 /// Removes one or more patches from a single product. To remove a patch from
580 /// multiple products, RemovePatches must be called for each product.
581 /// </summary>
582 /// <param name="patches">List of patches to remove. Each patch can be specified by the GUID
583 /// of the patch or the full path to the patch package.</param>
584 /// <param name="productCode">The ProductCode (GUID) of the product from which the patches
585 /// are removed. This parameter cannot be null.</param>
586 /// <param name="commandLine">optional command line property settings</param>
587 /// <exception cref="InstallerException">There was an error removing the patches</exception>
588 /// <remarks><p>
589 /// The <see cref="RebootRequired"/> and <see cref="RebootInitiated"/> properties should be
590 /// tested after calling this method.
591 /// </p><p>
592 /// Win32 MSI API:
593 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiremovepatches.asp">MsiRemovePatches</a>
594 /// </p></remarks>
595 public static void RemovePatches(IList<string> patches, string productCode, string commandLine)
596 {
597 if (patches == null || patches.Count == 0)
598 {
599 throw new ArgumentNullException("patches");
600 }
601
602 if (productCode == null)
603 {
604 throw new ArgumentNullException("productCode");
605 }
606
607 StringBuilder patchList = new StringBuilder();
608 foreach (string patch in patches)
609 {
610 if (patch != null)
611 {
612 if (patchList.Length != 0)
613 {
614 patchList.Append(';');
615 }
616
617 patchList.Append(patch);
618 }
619 }
620
621 if (patchList.Length == 0)
622 {
623 throw new ArgumentNullException("patches");
624 }
625
626 uint ret = NativeMethods.MsiRemovePatches(patchList.ToString(), productCode, (int) InstallType.SingleInstance, commandLine);
627 Installer.CheckInstallResult(ret);
628 }
629
630 /// <summary>
631 /// Determines which patches apply to a specified product MSI and in what sequence.
632 /// </summary>
633 /// <param name="productPackage">Full path to an MSI file that is the target product
634 /// for the set of patches.</param>
635 /// <param name="patches">An array of strings specifying the patches to be checked. Each item
636 /// may be the path to an MSP file, the path an XML file, or just an XML blob.</param>
637 /// <param name="errorHandler">Callback to be invoked for each inapplicable patch, reporting the
638 /// reason the patch is not applicable. This value may be left null if that information is not
639 /// desired.</param>
640 /// <returns>An array of selected patch strings from <paramref name="patches"/>, indicating
641 /// the set of applicable patches. The items are re-ordered to be in the best sequence.</returns>
642 /// <remarks><p>
643 /// If an item in <paramref name="patches"/> is a file path but does not end in .MSP or .XML,
644 /// it is assumed to be an MSP file.
645 /// </p><p>
646 /// As this overload uses InstallContext.None, it does not consider the current state of
647 /// the system.
648 /// </p><p>
649 /// Win32 MSI API:
650 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidetermineapplicablepatches.asp">MsiDetermineApplicablePatches</a>
651 /// </p></remarks>
652 public static IList<string> DetermineApplicablePatches(
653 string productPackage,
654 string[] patches,
655 InapplicablePatchHandler errorHandler)
656 {
657 return DetermineApplicablePatches(productPackage, patches, errorHandler, null, UserContexts.None);
658 }
659
660 /// <summary>
661 /// Determines which patches apply to a specified product and in what sequence. If
662 /// the product is installed, this method accounts for patches that have already been applied to
663 /// the product and accounts for obsolete and superceded patches.
664 /// </summary>
665 /// <param name="product">The product that is the target for the set of patches. This may be
666 /// either a ProductCode (GUID) of a product that is currently installed, or the path to a an
667 /// MSI file.</param>
668 /// <param name="patches">An array of strings specifying the patches to be checked. Each item
669 /// may be the path to an MSP file, the path an XML file, or just an XML blob.</param>
670 /// <param name="errorHandler">Callback to be invoked for each inapplicable patch, reporting the
671 /// reason the patch is not applicable. This value may be left null if that information is not
672 /// desired.</param>
673 /// <param name="userSid">Specifies a security identifier (SID) of a user. This parameter restricts
674 /// the context of enumeration for this user account. This parameter cannot be the special SID
675 /// strings s-1-1-0 (everyone) or s-1-5-18 (local system). If <paramref name="context"/> is set to
676 /// <see cref="UserContexts.None"/> or <see cref="UserContexts.Machine"/>, then
677 /// <paramref name="userSid"/> must be null. For the current user context, <paramref name="userSid"/>
678 /// can be null and <paramref name="context"/> can be set to <see cref="UserContexts.UserManaged"/>
679 /// or <see cref="UserContexts.UserUnmanaged"/>.</param>
680 /// <param name="context">Restricts the enumeration to per-user-unmanaged, per-user-managed,
681 /// or per-machine context, or (if referring to an MSI) to no system context at all. This
682 /// parameter can be <see cref="UserContexts.Machine"/>, <see cref="UserContexts.UserManaged"/>,
683 /// <see cref="UserContexts.UserUnmanaged"/>, or <see cref="UserContexts.None"/>.</param>
684 /// <returns>An array of selected patch strings from <paramref name="patches"/>, indicating
685 /// the set of applicable patches. The items are re-ordered to be in the best sequence.</returns>
686 /// <remarks><p>
687 /// If an item in <paramref name="patches"/> is a file path but does not end in .MSP or .XML,
688 /// it is assumed to be an MSP file.
689 /// </p><p>
690 /// Passing an InstallContext of None only analyzes the MSI file; it does not consider the
691 /// current state of the system. You cannot use InstallContext.None with a ProductCode GUID.
692 /// </p><p>
693 /// Win32 MSI APIs:
694 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidetermineapplicablepatches.asp">MsiDetermineApplicablePatches</a>
695 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msideterminepatchsequence.asp">MsiDeterminePatchSequence</a>
696 /// </p></remarks>
697 public static IList<string> DetermineApplicablePatches(
698 string product,
699 string[] patches,
700 InapplicablePatchHandler errorHandler,
701 string userSid,
702 UserContexts context)
703 {
704 if (String.IsNullOrEmpty(product))
705 {
706 throw new ArgumentNullException("product");
707 }
708
709 if (patches == null)
710 {
711 throw new ArgumentNullException("patches");
712 }
713
714 NativeMethods.MsiPatchSequenceData[] sequenceData = new NativeMethods.MsiPatchSequenceData[patches.Length];
715 for (int i = 0; i < patches.Length; i++)
716 {
717 if (String.IsNullOrEmpty(patches[i]))
718 {
719 throw new ArgumentNullException("patches[" + i + "]");
720 }
721
722 sequenceData[i].szPatchData = patches[i];
723 sequenceData[i].ePatchDataType = GetPatchStringDataType(patches[i]);
724 sequenceData[i].dwOrder = -1;
725 sequenceData[i].dwStatus = 0;
726 }
727
728 uint ret;
729 if (context == UserContexts.None)
730 {
731 ret = NativeMethods.MsiDetermineApplicablePatches(product, (uint) sequenceData.Length, sequenceData);
732 }
733 else
734 {
735 ret = NativeMethods.MsiDeterminePatchSequence(product, userSid, context, (uint) sequenceData.Length, sequenceData);
736 }
737
738 if (errorHandler != null)
739 {
740 for (int i = 0; i < sequenceData.Length; i++)
741 {
742 if (sequenceData[i].dwOrder < 0 && sequenceData[i].dwStatus != 0)
743 {
744 errorHandler(sequenceData[i].szPatchData, InstallerException.ExceptionFromReturnCode(sequenceData[i].dwStatus));
745 }
746 }
747 }
748
749 if (ret != 0)
750 {
751 throw InstallerException.ExceptionFromReturnCode(ret);
752 }
753
754 IList<string> patchSeq = new List<string>(patches.Length);
755 for (int i = 0; i < sequenceData.Length; i++)
756 {
757 for (int j = 0; j < sequenceData.Length; j++)
758 {
759 if (sequenceData[j].dwOrder == i)
760 {
761 patchSeq.Add(sequenceData[j].szPatchData);
762 }
763 }
764 }
765 return patchSeq;
766 }
767
768 /// <summary>
769 /// Applies one or more patches to products that are eligible to receive the patch.
770 /// For each product listed by the patch package as eligible to receive the patch, ApplyPatch invokes
771 /// an installation and sets the PATCH property to the path of the patch package.
772 /// </summary>
773 /// <param name="patchPackages">The set of patch packages to be applied.
774 /// Each item is the full path to an MSP file.</param>
775 /// <param name="productCode">Provides the ProductCode of the product being patched. If this parameter
776 /// is null, the patches are applied to all products that are eligible to receive these patches.</param>
777 /// <param name="commandLine">optional command line property settings</param>
778 /// <remarks><p>
779 /// Win32 MSI API:
780 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiapplymultiplepatches.asp">MsiApplyMultiplePatches</a>
781 /// </p></remarks>
782 public static void ApplyMultiplePatches(
783 IList<string> patchPackages, string productCode, string commandLine)
784 {
785 if (patchPackages == null || patchPackages.Count == 0)
786 {
787 throw new ArgumentNullException("patchPackages");
788 }
789
790 StringBuilder patchList = new StringBuilder();
791 foreach (string patch in patchPackages)
792 {
793 if (patch != null)
794 {
795 if (patchList.Length != 0)
796 {
797 patchList.Append(';');
798 }
799
800 patchList.Append(patch);
801 }
802 }
803
804 if (patchList.Length == 0)
805 {
806 throw new ArgumentNullException("patchPackages");
807 }
808
809 uint ret = NativeMethods.MsiApplyMultiplePatches(patchList.ToString(), productCode, commandLine);
810 Installer.CheckInstallResult(ret);
811 }
812
813 /// <summary>
814 /// Extracts information from a patch that can be used to determine whether the patch
815 /// applies on a target system. The method returns an XML string that can be provided to
816 /// <see cref="DetermineApplicablePatches(string,string[],InapplicablePatchHandler,string,UserContexts)"/>
817 /// instead of the full patch file.
818 /// </summary>
819 /// <param name="patchPath">Full path to the patch being queried.</param>
820 /// <returns>XML string containing patch data.</returns>
821 /// <remarks><p>
822 /// Win32 MSI API:
823 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiextractpatchxmldata.asp">MsiExtractPatchXMLData</a>
824 /// </p></remarks>
825 public static string ExtractPatchXmlData(string patchPath)
826 {
827 StringBuilder buf = new StringBuilder("");
828 uint bufSize = 0;
829 uint ret = NativeMethods.MsiExtractPatchXMLData(patchPath, 0, buf, ref bufSize);
830 if (ret == (uint) NativeMethods.Error.MORE_DATA)
831 {
832 buf.Capacity = (int) ++bufSize;
833 ret = NativeMethods.MsiExtractPatchXMLData(patchPath, 0, buf, ref bufSize);
834 }
835
836 if (ret != 0)
837 {
838 throw InstallerException.ExceptionFromReturnCode(ret);
839 }
840 return buf.ToString();
841 }
842
843 /// <summary>
844 /// [MSI 3.1] Migrates a user's application configuration data to a new SID.
845 /// </summary>
846 /// <param name="oldSid">Previous user SID that data is to be migrated from</param>
847 /// <param name="newSid">New user SID that data is to be migrated to</param>
848 /// <remarks><p>
849 /// Win32 MSI API:
850 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msinotifysidchange.asp">MsiNotifySidChange</a>
851 /// </p></remarks>
852 public static void NotifySidChange(string oldSid, string newSid)
853 {
854 uint ret = NativeMethods.MsiNotifySidChange(oldSid, newSid);
855 if (ret != 0)
856 {
857 throw InstallerException.ExceptionFromReturnCode(ret);
858 }
859 }
860
861 private static void CheckInstallResult(uint ret)
862 {
863 switch (ret)
864 {
865 case (uint) NativeMethods.Error.SUCCESS: break;
866 case (uint) NativeMethods.Error.SUCCESS_REBOOT_REQUIRED: Installer.rebootRequired = true; break;
867 case (uint) NativeMethods.Error.SUCCESS_REBOOT_INITIATED: Installer.rebootInitiated = true; break;
868 default: throw InstallerException.ExceptionFromReturnCode(ret);
869 }
870 }
871
872 private static int GetPatchStringDataType(string patchData)
873 {
874 if (patchData.IndexOf("<", StringComparison.Ordinal) >= 0 &&
875 patchData.IndexOf(">", StringComparison.Ordinal) >= 0)
876 {
877 return 2; // XML blob
878 }
879 else if (String.Compare(Path.GetExtension(patchData), ".xml",
880 StringComparison.OrdinalIgnoreCase) == 0)
881 {
882 return 1; // XML file path
883 }
884 else
885 {
886 return 0; // MSP file path
887 }
888 }
889}
890}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerAdvertise.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerAdvertise.cs
new file mode 100644
index 00000000..9da593d9
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerAdvertise.cs
@@ -0,0 +1,270 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Reflection;
9 using System.Globalization;
10 using System.Collections.Generic;
11 using System.Diagnostics.CodeAnalysis;
12
13 public static partial class Installer
14 {
15 /// <summary>
16 /// Advertises a product to the local computer.
17 /// </summary>
18 /// <param name="packagePath">Path to the package of the product being advertised</param>
19 /// <param name="perUser">True if the product is user-assigned; false if it is machine-assigned.</param>
20 /// <param name="transforms">Semi-colon delimited list of transforms to be applied. This parameter may be null.</param>
21 /// <param name="locale">The language to use if the source supports multiple languages</param>
22 /// <exception cref="FileNotFoundException">the specified package file does not exist</exception>
23 /// <seealso cref="GenerateAdvertiseScript(string,string,string,int,ProcessorArchitecture,bool)"/>
24 /// <remarks><p>
25 /// Win32 MSI APIs:
26 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiadvertiseproduct.asp">MsiAdvertiseProduct</a>,
27 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiadvertiseproductex.asp">MsiAdvertiseProductEx</a>
28 /// </p></remarks>
29 public static void AdvertiseProduct(string packagePath, bool perUser, string transforms, int locale)
30 {
31 if (String.IsNullOrEmpty(packagePath))
32 {
33 throw new ArgumentNullException("packagePath");
34 }
35
36 if (!File.Exists(packagePath))
37 {
38 throw new FileNotFoundException(null, packagePath);
39 }
40
41 uint ret = NativeMethods.MsiAdvertiseProduct(packagePath, new IntPtr(perUser ? 1 : 0), transforms, (ushort) locale);
42 if (ret != 0)
43 {
44 throw InstallerException.ExceptionFromReturnCode(ret);
45 }
46 }
47
48 /// <summary>
49 /// Generates an advertise script. The method enables the installer to write to a
50 /// script the registry and shortcut information used to assign or publish a product.
51 /// </summary>
52 /// <param name="packagePath">Path to the package of the product being advertised</param>
53 /// <param name="scriptFilePath">path to script file to be created with the advertise information</param>
54 /// <param name="transforms">Semi-colon delimited list of transforms to be applied. This parameter may be null.</param>
55 /// <param name="locale">The language to use if the source supports multiple languages</param>
56 /// <exception cref="FileNotFoundException">the specified package file does not exist</exception>
57 /// <seealso cref="AdvertiseProduct"/>
58 /// <remarks><p>
59 /// Win32 MSI APIs:
60 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiadvertiseproduct.asp">MsiAdvertiseProduct</a>,
61 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiadvertiseproductex.asp">MsiAdvertiseProductEx</a>
62 /// </p></remarks>
63 public static void GenerateAdvertiseScript(string packagePath, string scriptFilePath, string transforms, int locale)
64 {
65 if (String.IsNullOrEmpty(packagePath))
66 {
67 throw new ArgumentNullException("packagePath");
68 }
69
70 if (!File.Exists(packagePath))
71 {
72 throw new FileNotFoundException(null, packagePath);
73 }
74
75 uint ret = NativeMethods.MsiAdvertiseProduct(packagePath, scriptFilePath, transforms, (ushort) locale);
76 if (ret != 0)
77 {
78 throw InstallerException.ExceptionFromReturnCode(ret);
79 }
80 }
81
82 /// <summary>
83 /// Generates an advertise script. The method enables the installer to write to a
84 /// script the registry and shortcut information used to assign or publish a product.
85 /// </summary>
86 /// <param name="packagePath">Path to the package of the product being advertised</param>
87 /// <param name="scriptFilePath">path to script file to be created with the advertise information</param>
88 /// <param name="transforms">Semi-colon delimited list of transforms to be applied. This parameter may be null.</param>
89 /// <param name="locale">The language to use if the source supports multiple languages</param>
90 /// <param name="processor">Targeted processor architecture.</param>
91 /// <param name="instance">True to install multiple instances through product code changing transform.
92 /// Advertises a new instance of the product. Requires that the <paramref name="transforms"/> parameter
93 /// includes the instance transform that changes the product code.</param>
94 /// <seealso cref="AdvertiseProduct"/>
95 /// <remarks><p>
96 /// Win32 MSI APIs:
97 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiadvertiseproduct.asp">MsiAdvertiseProduct</a>,
98 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiadvertiseproductex.asp">MsiAdvertiseProductEx</a>
99 /// </p></remarks>
100 public static void GenerateAdvertiseScript(
101 string packagePath,
102 string scriptFilePath,
103 string transforms,
104 int locale,
105 ProcessorArchitecture processor,
106 bool instance)
107 {
108 if (String.IsNullOrEmpty(packagePath))
109 {
110 throw new ArgumentNullException("packagePath");
111 }
112
113 if (String.IsNullOrEmpty(scriptFilePath))
114 {
115 throw new ArgumentNullException("scriptFilePath");
116 }
117
118 if (!File.Exists(packagePath))
119 {
120 throw new FileNotFoundException(null, packagePath);
121 }
122
123 uint platform = 0;
124 switch (processor)
125 {
126 case ProcessorArchitecture.X86: platform = (uint) 1; break;
127 case ProcessorArchitecture.IA64: platform = (uint) 2; break;
128 case ProcessorArchitecture.Amd64: platform = (uint) 4; break;
129 }
130
131 uint ret = NativeMethods.MsiAdvertiseProductEx(
132 packagePath,
133 scriptFilePath,
134 transforms,
135 (ushort) locale,
136 platform,
137 instance ? (uint) 1 : 0);
138 if (ret != 0)
139 {
140 throw InstallerException.ExceptionFromReturnCode(ret);
141 }
142 }
143
144 /// <summary>
145 /// Copies an advertise script file to the local computer.
146 /// </summary>
147 /// <param name="scriptFile">Path to a script file generated by
148 /// <see cref="GenerateAdvertiseScript(string,string,string,int,ProcessorArchitecture,bool)"/></param>
149 /// <param name="flags">Flags controlling advertisement</param>
150 /// <param name="removeItems">True if specified items are to be removed instead of being created</param>
151 /// <remarks><p>
152 /// The process calling this function must be running under the LocalSystem account. To advertise an
153 /// application for per-user installation to a targeted user, the thread that calls this function must
154 /// impersonate the targeted user. If the thread calling this function is not impersonating a targeted
155 /// user, the application is advertised to all users for installation with elevated privileges.
156 /// </p></remarks>
157 [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "flags")]
158 public static void AdvertiseScript(string scriptFile, int flags, bool removeItems)
159 {
160 uint ret = NativeMethods.MsiAdvertiseScript(scriptFile, (uint) flags, IntPtr.Zero, removeItems);
161 if (ret != 0)
162 {
163 throw InstallerException.ExceptionFromReturnCode(ret);
164 }
165 }
166
167 /// <summary>
168 /// Processes an advertise script file into the specified locations.
169 /// </summary>
170 /// <param name="scriptFile">Path to a script file generated by
171 /// <see cref="GenerateAdvertiseScript(string,string,string,int,ProcessorArchitecture,bool)"/></param>
172 /// <param name="iconFolder">An optional path to a folder in which advertised icon files and transform
173 /// files are located. If this parameter is null, no icon or transform files are written.</param>
174 /// <param name="shortcuts">True if shortcuts should be created</param>
175 /// <param name="removeItems">True if specified items are to be removed instead of created</param>
176 /// <remarks><p>
177 /// The process calling this function must be running under the LocalSystem account. To advertise an
178 /// application for per-user installation to a targeted user, the thread that calls this function must
179 /// impersonate the targeted user. If the thread calling this function is not impersonating a targeted
180 /// user, the application is advertised to all users for installation with elevated privileges.
181 /// </p><p>
182 /// Win32 MSI API:
183 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiprocessadvertisescript.asp">MsiProcessAdvertiseScript</a>
184 /// </p></remarks>
185 public static void ProcessAdvertiseScript(string scriptFile, string iconFolder, bool shortcuts, bool removeItems)
186 {
187 uint ret = NativeMethods.MsiProcessAdvertiseScript(scriptFile, iconFolder, IntPtr.Zero, shortcuts, removeItems);
188 if (ret != 0)
189 {
190 throw InstallerException.ExceptionFromReturnCode(ret);
191 }
192 }
193
194 /// <summary>
195 /// Gets product information for an installer script file.
196 /// </summary>
197 /// <param name="scriptFile">Path to a script file generated by
198 /// <see cref="GenerateAdvertiseScript(string,string,string,int,ProcessorArchitecture,bool)"/></param>
199 /// <returns>ProductInstallation stub with advertise-related properties filled in.</returns>
200 /// <exception cref="ArgumentOutOfRangeException">An invalid product property was requested</exception>
201 /// <remarks><p>
202 /// Only the following properties will be filled in in the returned object:<ul>
203 /// <li><see cref="ProductInstallation.ProductCode"/></li>
204 /// <li><see cref="ProductInstallation.AdvertisedLanguage"/></li>
205 /// <li><see cref="ProductInstallation.AdvertisedVersion"/></li>
206 /// <li><see cref="ProductInstallation.AdvertisedProductName"/></li>
207 /// <li><see cref="ProductInstallation.AdvertisedPackageName"/></li>
208 /// </ul>Other properties will be null.
209 /// </p><p>
210 /// Win32 MSI API:
211 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetproductinfofromscript.asp">MsiGetProductInfoFromScript</a>
212 /// </p></remarks>
213 public static ProductInstallation GetProductInfoFromScript(string scriptFile)
214 {
215 if (String.IsNullOrEmpty(scriptFile))
216 {
217 throw new ArgumentNullException("scriptFile");
218 }
219 StringBuilder productCodeBuf = new StringBuilder(40);
220 ushort lang;
221 uint ver;
222 StringBuilder productNameBuf = new StringBuilder(100);
223 StringBuilder packageNameBuf = new StringBuilder(40);
224 uint productCodeBufSize = (uint) productCodeBuf.Capacity;
225 uint productNameBufSize = (uint) productNameBuf.Capacity;
226 uint packageNameBufSize = (uint) packageNameBuf.Capacity;
227 uint ret = NativeMethods.MsiGetProductInfoFromScript(
228 scriptFile,
229 productCodeBuf,
230 out lang,
231 out ver,
232 productNameBuf,
233 ref productNameBufSize,
234 packageNameBuf,
235 ref packageNameBufSize);
236 if (ret == (uint) NativeMethods.Error.MORE_DATA)
237 {
238 productCodeBuf.Capacity = (int) ++productCodeBufSize;
239 productNameBuf.Capacity = (int) ++productNameBufSize;
240 packageNameBuf.Capacity = (int) ++packageNameBufSize;
241 ret = NativeMethods.MsiGetProductInfoFromScript(
242 scriptFile,
243 productCodeBuf,
244 out lang,
245 out ver,
246 productNameBuf,
247 ref productNameBufSize,
248 packageNameBuf,
249 ref packageNameBufSize);
250 }
251
252 if (ret != 0)
253 {
254 throw InstallerException.ExceptionFromReturnCode(ret);
255 }
256 uint verPart1 = ver >> 24;
257 uint verPart2 = (ver & 0x00FFFFFF) >> 16;
258 uint verPart3 = ver & 0x0000FFFF;
259 Version version = new Version((int) verPart1, (int) verPart2, (int) verPart3);
260
261 IDictionary<string, string> props = new Dictionary<string, string>();
262 props["ProductCode"] = productCodeBuf.ToString();
263 props["Language"] = lang.ToString(CultureInfo.InvariantCulture);
264 props["Version"] = version.ToString();
265 props["ProductName"] = productNameBuf.ToString();
266 props["PackageName"] = packageNameBuf.ToString();
267 return new ProductInstallation(props);
268 }
269 }
270}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs
new file mode 100644
index 00000000..8d9cf0a1
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/InstallerUtils.cs
@@ -0,0 +1,472 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Resources;
9 using System.Reflection;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Runtime.InteropServices;
13 using System.Diagnostics.CodeAnalysis;
14
15 public static partial class Installer
16 {
17 /// <summary>
18 /// Gets the current version of the installer.
19 /// </summary>
20 public static Version Version
21 {
22 get
23 {
24 // TODO: Use the extended form of version info to get the 4th component of the verison.
25 uint[] dllVersionInfo = new uint[5];
26 dllVersionInfo[0] = 20;
27 int hr = NativeMethods.DllGetVersion(dllVersionInfo);
28 if (hr != 0)
29 {
30 Marshal.ThrowExceptionForHR(hr);
31 }
32
33 return new Version((int) dllVersionInfo[1], (int) dllVersionInfo[2], (int) dllVersionInfo[3]);
34 }
35 }
36
37 internal static ResourceManager ErrorResources
38 {
39 get
40 {
41 if (errorResources == null)
42 {
43 errorResources = new ResourceManager(typeof(Installer).Namespace + ".Errors", typeof(Installer).Assembly);
44 }
45 return errorResources;
46 }
47 }
48
49 /// <summary>
50 /// Gets a Windows Installer error message in the system default language.
51 /// </summary>
52 /// <param name="errorNumber">The error number.</param>
53 /// <returns>The message string, or null if the error message is not found.</returns>
54 /// <remarks><p>
55 /// The returned string may have tokens such as [2] and [3] that are meant to be substituted
56 /// with context-specific values.
57 /// </p><p>
58 /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always
59 /// returned in English.
60 /// </p></remarks>
61 public static string GetErrorMessage(int errorNumber)
62 {
63 return Installer.GetErrorMessage(errorNumber, null);
64 }
65
66 /// <summary>
67 /// Gets a Windows Installer error message in a specified language.
68 /// </summary>
69 /// <param name="errorNumber">The error number.</param>
70 /// <param name="culture">The locale for the message.</param>
71 /// <returns>The message string, or null if the error message or locale is not found.</returns>
72 /// <remarks><p>
73 /// The returned string may have tokens such as [2] and [3] that are meant to be substituted
74 /// with context-specific values.
75 /// </p><p>
76 /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always
77 /// returned in English.
78 /// </p></remarks>
79 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
80 public static string GetErrorMessage(int errorNumber, CultureInfo culture)
81 {
82 if (culture == null)
83 {
84 culture = CultureInfo.CurrentCulture;
85 }
86
87 string msg = Installer.ErrorResources.GetString(
88 errorNumber.ToString(CultureInfo.InvariantCulture.NumberFormat),
89 culture);
90 if (msg == null)
91 {
92 string msiMsgModule = Path.Combine(
93 Environment.SystemDirectory, "msimsg.dll");
94 msg = Installer.GetMessageFromModule(
95 msiMsgModule, errorNumber, culture);
96 }
97 return msg;
98 }
99
100 private static string GetMessageFromModule(
101 string modulePath, int errorNumber, CultureInfo culture)
102 {
103 const uint LOAD_LIBRARY_AS_DATAFILE = 2;
104 const int RT_RCDATA = 10;
105
106 IntPtr msgModule = NativeMethods.LoadLibraryEx(
107 modulePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE);
108 if (msgModule == IntPtr.Zero)
109 {
110 return null;
111 }
112
113 try
114 {
115 // On pre-Vista systems, the messages are stored as RCDATA resources.
116
117 int lcid = (culture == CultureInfo.InvariantCulture) ?
118 0 : culture.LCID;
119 IntPtr resourceInfo = NativeMethods.FindResourceEx(
120 msgModule,
121 new IntPtr(RT_RCDATA),
122 new IntPtr(errorNumber),
123 (ushort) lcid);
124 if (resourceInfo != IntPtr.Zero)
125 {
126 IntPtr resourceData = NativeMethods.LoadResource(
127 msgModule, resourceInfo);
128 IntPtr resourcePtr = NativeMethods.LockResource(resourceData);
129
130 if (lcid == 0)
131 {
132 string msg = Marshal.PtrToStringAnsi(resourcePtr);
133 return msg;
134 }
135 else
136 {
137 int len = 0;
138 while (Marshal.ReadByte(resourcePtr, len) != 0)
139 {
140 len++;
141 }
142 byte[] msgBytes = new byte[len + 1];
143 Marshal.Copy(resourcePtr, msgBytes, 0, msgBytes.Length);
144 Encoding encoding = Encoding.GetEncoding(
145 culture.TextInfo.ANSICodePage);
146 string msg = encoding.GetString(msgBytes);
147 return msg;
148 }
149 }
150 else
151 {
152 // On Vista (and above?), the messages are stored in the module message table.
153 // They're actually in MUI files, and the redirection happens automatically here.
154
155 const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200;
156 const uint FORMAT_MESSAGE_FROM_HMODULE = 0x00000800;
157 const uint MESSAGE_OFFSET = 20000; // Not documented, but observed on Vista
158
159 StringBuilder buf = new StringBuilder(1024);
160 uint formatCount = NativeMethods.FormatMessage(
161 FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
162 msgModule,
163 (uint) errorNumber + MESSAGE_OFFSET,
164 (ushort) lcid,
165 buf,
166 (uint) buf.Capacity,
167 IntPtr.Zero);
168
169 return formatCount != 0 ? buf.ToString().Trim() : null;
170 }
171 }
172 finally
173 {
174 NativeMethods.FreeLibrary(msgModule);
175 }
176 }
177
178 /// <summary>
179 /// Gets a formatted Windows Installer error message in the system default language.
180 /// </summary>
181 /// <param name="errorRecord">Error record containing the error number in the first field, and
182 /// error-specific parameters in the other fields.</param>
183 /// <returns>The message string, or null if the error message is not found.</returns>
184 /// <remarks><p>
185 /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always
186 /// returned in English.
187 /// </p></remarks>
188 public static string GetErrorMessage(Record errorRecord) { return Installer.GetErrorMessage(errorRecord, null); }
189
190 /// <summary>
191 /// Gets a formatted Windows Installer error message in a specified language.
192 /// </summary>
193 /// <param name="errorRecord">Error record containing the error number in the first field, and
194 /// error-specific parameters in the other fields.</param>
195 /// <param name="culture">The locale for the message.</param>
196 /// <returns>The message string, or null if the error message or locale is not found.</returns>
197 /// <remarks><p>
198 /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always
199 /// returned in English.
200 /// </p></remarks>
201 public static string GetErrorMessage(Record errorRecord, CultureInfo culture)
202 {
203 if (errorRecord == null)
204 {
205 throw new ArgumentNullException("errorRecord");
206 }
207 int errorNumber;
208 if (errorRecord.FieldCount < 1 || (errorNumber = (int) errorRecord.GetInteger(1)) == 0)
209 {
210 throw new ArgumentOutOfRangeException("errorRecord");
211 }
212
213 string msg = Installer.GetErrorMessage(errorNumber, culture);
214 if (msg != null)
215 {
216 errorRecord.FormatString = msg;
217 msg = errorRecord.ToString((IFormatProvider)null);
218 }
219 return msg;
220 }
221
222 /// <summary>
223 /// Gets the version string of the path specified using the format that the installer
224 /// expects to find it in in the database.
225 /// </summary>
226 /// <param name="path">Path to the file</param>
227 /// <returns>Version string in the "#.#.#.#" format, or an empty string if the file
228 /// does not contain version information</returns>
229 /// <exception cref="FileNotFoundException">the file does not exist or could not be read</exception>
230 /// <remarks><p>
231 /// Win32 MSI API:
232 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfileversion.asp">MsiGetFileVersion</a>
233 /// </p></remarks>
234 public static string GetFileVersion(string path)
235 {
236 StringBuilder version = new StringBuilder(20);
237 uint verBufSize = 0, langBufSize = 0;
238 uint ret = NativeMethods.MsiGetFileVersion(path, version, ref verBufSize, null, ref langBufSize);
239 if (ret == (uint) NativeMethods.Error.MORE_DATA)
240 {
241 version.Capacity = (int) ++verBufSize;
242 ret = NativeMethods.MsiGetFileVersion(path, version, ref verBufSize, null, ref langBufSize);
243 }
244
245 if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID)
246 {
247 if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND ||
248 ret == (uint) NativeMethods.Error.ACCESS_DENIED)
249 {
250 throw new FileNotFoundException(null, path);
251 }
252 else
253 {
254 throw InstallerException.ExceptionFromReturnCode(ret);
255 }
256 }
257 return version.ToString();
258 }
259
260 /// <summary>
261 /// Gets the language string of the path specified using the format that the installer
262 /// expects to find them in in the database.
263 /// </summary>
264 /// <param name="path">Path to the file</param>
265 /// <returns>Language string in the form of a decimal language ID, or an empty string if the file
266 /// does not contain a language ID</returns>
267 /// <exception cref="FileNotFoundException">the file does not exist or could not be read</exception>
268 /// <remarks><p>
269 /// Win32 MSI API:
270 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfileversion.asp">MsiGetFileVersion</a>
271 /// </p></remarks>
272 public static string GetFileLanguage(string path)
273 {
274 StringBuilder language = new StringBuilder("", 10);
275 uint verBufSize = 0, langBufSize = 0;
276 uint ret = NativeMethods.MsiGetFileVersion(path, null, ref verBufSize, language, ref langBufSize);
277 if (ret == (uint) NativeMethods.Error.MORE_DATA)
278 {
279 language.Capacity = (int) ++langBufSize;
280 ret = NativeMethods.MsiGetFileVersion(path, null, ref verBufSize, language, ref langBufSize);
281 }
282
283 if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID)
284 {
285 if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND ||
286 ret == (uint) NativeMethods.Error.ACCESS_DENIED)
287 {
288 throw new FileNotFoundException(null, path);
289 }
290 else
291 {
292 throw InstallerException.ExceptionFromReturnCode(ret);
293 }
294 }
295 return language.ToString();
296 }
297
298 /// <summary>
299 /// Gets a 128-bit hash of the specified file.
300 /// </summary>
301 /// <param name="path">Path to the file</param>
302 /// <param name="hash">Integer array of length 4 which receives the
303 /// four 32-bit parts of the hash value.</param>
304 /// <exception cref="FileNotFoundException">the file does not exist or
305 /// could not be read</exception>
306 /// <remarks><p>
307 /// Win32 MSI API:
308 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetfilehash.asp">MsiGetFileHash</a>
309 /// </p></remarks>
310 public static void GetFileHash(string path, int[] hash)
311 {
312 if (hash == null)
313 {
314 throw new ArgumentNullException("hash");
315 }
316
317 uint[] tempHash = new uint[5];
318 tempHash[0] = 20;
319 uint ret = NativeMethods.MsiGetFileHash(path, 0, tempHash);
320 if (ret != 0)
321 {
322 if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND ||
323 ret == (uint) NativeMethods.Error.ACCESS_DENIED)
324 {
325 throw new FileNotFoundException(null, path);
326 }
327 else
328 {
329 throw InstallerException.ExceptionFromReturnCode(ret);
330 }
331 }
332
333 for (int i = 0; i < 4; i++)
334 {
335 hash[i] = unchecked ((int) tempHash[i + 1]);
336 }
337 }
338
339 /// <summary>
340 /// Examines a shortcut and returns its product, feature name, and component if available.
341 /// </summary>
342 /// <param name="shortcut">Full path to a shortcut</param>
343 /// <returns>ShortcutTarget structure containing target product code, feature, and component code</returns>
344 /// <remarks><p>
345 /// Win32 MSI API:
346 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetshortcuttarget.asp">MsiGetShortcutTarget</a>
347 /// </p></remarks>
348 public static ShortcutTarget GetShortcutTarget(string shortcut)
349 {
350 StringBuilder productBuf = new StringBuilder(40);
351 StringBuilder featureBuf = new StringBuilder(40);
352 StringBuilder componentBuf = new StringBuilder(40);
353
354 uint ret = NativeMethods.MsiGetShortcutTarget(shortcut, productBuf, featureBuf, componentBuf);
355 if (ret != 0)
356 {
357 throw InstallerException.ExceptionFromReturnCode(ret);
358 }
359 return new ShortcutTarget(
360 productBuf.Length > 0 ? productBuf.ToString() : null,
361 featureBuf.Length > 0 ? featureBuf.ToString() : null,
362 componentBuf.Length > 0 ? componentBuf.ToString() : null);
363 }
364
365 /// <summary>
366 /// Verifies that the given file is an installation package.
367 /// </summary>
368 /// <param name="packagePath">Path to the package</param>
369 /// <returns>True if the file is an installation package; false otherwise.</returns>
370 /// <exception cref="FileNotFoundException">the specified package file does not exist</exception>
371 /// <exception cref="InstallerException">the package file could not be opened</exception>
372 /// <remarks><p>
373 /// Win32 MSI API:
374 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiverifypackage.asp">MsiVerifyPackage</a>
375 /// </p></remarks>
376 public static bool VerifyPackage(string packagePath)
377 {
378 if (String.IsNullOrEmpty(packagePath))
379 {
380 throw new ArgumentNullException("packagePath");
381 }
382
383 if (!File.Exists(packagePath))
384 {
385 throw new FileNotFoundException(null, packagePath);
386 }
387
388 uint ret = NativeMethods.MsiVerifyPackage(packagePath);
389 if (ret == (uint) NativeMethods.Error.INSTALL_PACKAGE_INVALID)
390 {
391 return false;
392 }
393 else if (ret != 0)
394 {
395 throw InstallerException.ExceptionFromReturnCode(ret);
396 }
397 return true;
398 }
399
400 /// <summary>
401 /// [MSI 4.0] Gets the list of files that can be updated by one or more patches.
402 /// </summary>
403 /// <param name="productCode">ProductCode (GUID) of the product which is
404 /// the target of the patches</param>
405 /// <param name="patches">list of file paths of one or more patches to be
406 /// analyzed</param>
407 /// <returns>List of absolute paths of files that can be updated when the
408 /// patches are applied on this system.</returns>
409 /// <remarks><p>
410 /// Win32 MSI API:
411 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetpatchfilelist.asp">MsiGetPatchFileList</a>
412 /// </p></remarks>
413 public static IList<string> GetPatchFileList(string productCode, IList<string> patches)
414 {
415 if (String.IsNullOrEmpty(productCode))
416 {
417 throw new ArgumentNullException("productCode");
418 }
419
420 if (patches == null || patches.Count == 0)
421 {
422 throw new ArgumentNullException("patches");
423 }
424
425 StringBuilder patchList = new StringBuilder();
426 foreach (string patch in patches)
427 {
428 if (patch != null)
429 {
430 if (patchList.Length != 0)
431 {
432 patchList.Append(';');
433 }
434
435 patchList.Append(patch);
436 }
437 }
438
439 if (patchList.Length == 0)
440 {
441 throw new ArgumentNullException("patches");
442 }
443
444 IntPtr phFileRecords;
445 uint cFiles;
446
447 uint ret = NativeMethods.MsiGetPatchFileList(
448 productCode,
449 patchList.ToString(),
450 out cFiles,
451 out phFileRecords);
452 if (ret != 0)
453 {
454 throw InstallerException.ExceptionFromReturnCode(ret);
455 }
456
457 List<string> files = new List<string>();
458
459 for (uint i = 0; i < cFiles; i++)
460 {
461 int hFileRec = Marshal.ReadInt32(phFileRecords, (int) i);
462
463 using (Record fileRec = new Record(hFileRec, true, null))
464 {
465 files.Add(fileRec.GetString(1));
466 }
467 }
468
469 return files;
470 }
471 }
472}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/MediaDisk.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/MediaDisk.cs
new file mode 100644
index 00000000..7b196b3e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/MediaDisk.cs
@@ -0,0 +1,58 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Diagnostics.CodeAnalysis;
7
8 /// <summary>
9 /// Represents a media disk source of a product or a patch.
10 /// </summary>
11 [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
12 public struct MediaDisk
13 {
14 private int diskId;
15 private string volumeLabel;
16 private string diskPrompt;
17
18 /// <summary>
19 /// Creates a new media disk.
20 /// </summary>
21 /// <param name="diskId"></param>
22 /// <param name="volumeLabel"></param>
23 /// <param name="diskPrompt"></param>
24 public MediaDisk(int diskId, string volumeLabel, string diskPrompt)
25 {
26 this.diskId = diskId;
27 this.volumeLabel = volumeLabel;
28 this.diskPrompt = diskPrompt;
29 }
30
31 /// <summary>
32 /// Gets or sets the disk id of the media disk.
33 /// </summary>
34 public int DiskId
35 {
36 get { return this.diskId; }
37 set { this.diskId = value; }
38 }
39
40 /// <summary>
41 /// Gets or sets the volume label of the media disk.
42 /// </summary>
43 public string VolumeLabel
44 {
45 get { return this.volumeLabel; }
46 set { this.volumeLabel = value; }
47 }
48
49 /// <summary>
50 /// Gets or sets the disk prompt of the media disk.
51 /// </summary>
52 public string DiskPrompt
53 {
54 get { return this.diskPrompt; }
55 set { this.diskPrompt = value; }
56 }
57 }
58}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/NativeMethods.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/NativeMethods.cs
new file mode 100644
index 00000000..a438b640
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/NativeMethods.cs
@@ -0,0 +1,309 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5using System;
6using System.Text;
7using System.Runtime.InteropServices;
8using System.Diagnostics.CodeAnalysis;
9
10using IStream = System.Runtime.InteropServices.ComTypes.IStream;
11using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
12using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG;
13
14[Guid("0000000b-0000-0000-C000-000000000046")]
15[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
16internal interface IStorage
17{
18 [return: MarshalAs(UnmanagedType.Interface)]
19 IStream CreateStream([MarshalAs(UnmanagedType.LPWStr)] string wcsName, uint grfMode, uint reserved1, uint reserved2);
20 [return: MarshalAs(UnmanagedType.Interface)]
21 IStream OpenStream([MarshalAs(UnmanagedType.LPWStr)] string wcsName, IntPtr reserved1, uint grfMode, uint reserved2);
22 [return: MarshalAs(UnmanagedType.Interface)]
23 IStorage CreateStorage([MarshalAs(UnmanagedType.LPWStr)] string wcsName, uint grfMode, uint reserved1, uint reserved2);
24 [return: MarshalAs(UnmanagedType.Interface)]
25 IStorage OpenStorage([MarshalAs(UnmanagedType.LPWStr)] string wcsName, IntPtr stgPriority, uint grfMode, IntPtr snbExclude, uint reserved);
26 void CopyTo(uint ciidExclude, IntPtr rgiidExclude, IntPtr snbExclude, [MarshalAs(UnmanagedType.Interface)] IStorage stgDest);
27 void MoveElementTo([MarshalAs(UnmanagedType.LPWStr)] string wcsName, [MarshalAs(UnmanagedType.Interface)] IStorage stgDest, [MarshalAs(UnmanagedType.LPWStr)] string wcsNewName, uint grfFlags);
28 void Commit(uint grfCommitFlags);
29 void Revert();
30 IntPtr EnumElements(uint reserved1, IntPtr reserved2, uint reserved3);
31 void DestroyElement([MarshalAs(UnmanagedType.LPWStr)] string wcsName);
32 void RenameElement([MarshalAs(UnmanagedType.LPWStr)] string wcsOldName, [MarshalAs(UnmanagedType.LPWStr)] string wcsNewName);
33 void SetElementTimes([MarshalAs(UnmanagedType.LPWStr)] string wcsName, ref FILETIME ctime, ref FILETIME atime, ref FILETIME mtime);
34 void SetClass(ref Guid clsid);
35 void SetStateBits(uint grfStateBits, uint grfMask);
36 void Stat(ref STATSTG statstg, uint grfStatFlag);
37}
38
39internal static class NativeMethods
40{
41 internal enum Error : uint
42 {
43 SUCCESS = 0,
44 FILE_NOT_FOUND = 2,
45 PATH_NOT_FOUND = 3,
46 ACCESS_DENIED = 5,
47 INVALID_HANDLE = 6,
48 INVALID_DATA = 13,
49 INVALID_PARAMETER = 87,
50 OPEN_FAILED = 110,
51 DISK_FULL = 112,
52 CALL_NOT_IMPLEMENTED = 120,
53 BAD_PATHNAME = 161,
54 NO_DATA = 232,
55 MORE_DATA = 234,
56 NO_MORE_ITEMS = 259,
57 DIRECTORY = 267,
58 INSTALL_USEREXIT = 1602,
59 INSTALL_FAILURE = 1603,
60 FILE_INVALID = 1006,
61 UNKNOWN_PRODUCT = 1605,
62 UNKNOWN_FEATURE = 1606,
63 UNKNOWN_COMPONENT = 1607,
64 UNKNOWN_PROPERTY = 1608,
65 INVALID_HANDLE_STATE = 1609,
66 INSTALL_SOURCE_ABSENT = 1612,
67 BAD_QUERY_SYNTAX = 1615,
68 INSTALL_PACKAGE_INVALID = 1620,
69 FUNCTION_FAILED = 1627,
70 INVALID_TABLE = 1628,
71 DATATYPE_MISMATCH = 1629,
72 CREATE_FAILED = 1631,
73 SUCCESS_REBOOT_INITIATED = 1641,
74 SUCCESS_REBOOT_REQUIRED = 3010,
75 }
76
77 internal enum SourceType : int
78 {
79 Unknown = 0,
80 Network = 1,
81 Url = 2,
82 Media = 3,
83 }
84
85 [Flags]
86 internal enum STGM : uint
87 {
88 DIRECT = 0x00000000,
89 TRANSACTED = 0x00010000,
90 SIMPLE = 0x08000000,
91
92 READ = 0x00000000,
93 WRITE = 0x00000001,
94 READWRITE = 0x00000002,
95
96 SHARE_DENY_NONE = 0x00000040,
97 SHARE_DENY_READ = 0x00000030,
98 SHARE_DENY_WRITE = 0x00000020,
99 SHARE_EXCLUSIVE = 0x00000010,
100
101 PRIORITY = 0x00040000,
102 DELETEONRELEASE = 0x04000000,
103 NOSCRATCH = 0x00100000,
104
105 CREATE = 0x00001000,
106 CONVERT = 0x00020000,
107 FAILIFTHERE = 0x00000000,
108
109 NOSNAPSHOT = 0x00200000,
110 DIRECT_SWMR = 0x00400000,
111 }
112
113[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int DllGetVersion(uint[] dvi);
114
115[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetInternalUI(uint dwUILevel, ref IntPtr phWnd);
116[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetInternalUI(uint dwUILevel, IntPtr phWnd);
117[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern NativeExternalUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] NativeExternalUIHandler puiHandler, uint dwMessageFilter, IntPtr pvContext);
118[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnableLog(uint dwLogMode, string szLogFile, uint dwLogAttributes);
119//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumProducts(uint iProductIndex, StringBuilder lpProductBuf);
120[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductInfo(string szProduct, string szProperty, StringBuilder lpValueBuf, ref uint pcchValueBuf);
121//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumPatches(string szProduct, uint iPatchIndex, StringBuilder lpPatchBuf, StringBuilder lpTransformsBuf, ref uint pcchTransformsBuf);
122[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetPatchInfo(string szPatch, string szAttribute, StringBuilder lpValueBuf, ref uint pcchValueBuf);
123[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumFeatures(string szProduct, uint iFeatureIndex, StringBuilder lpFeatureBuf, StringBuilder lpParentBuf);
124[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiQueryFeatureState(string szProduct, string szFeature);
125[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiUseFeatureEx(string szProduct, string szFeature, uint dwInstallMode, uint dwReserved);
126[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiQueryProductState(string szProduct);
127[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetShortcutTarget(string szShortcut, StringBuilder szProductCode, StringBuilder szFeatureId, StringBuilder szComponentCode);
128[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiProvideComponent(string szProduct, string szFeature, string szComponent, uint dwInstallMode, StringBuilder lpPathBuf, ref uint cchPathBuf);
129[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiProvideQualifiedComponentEx(string szComponent, string szQualifier, uint dwInstallMode, string szProduct, uint dwUnused1, uint dwUnused2, StringBuilder lpPathBuf, ref uint cchPathBuf);
130[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiReinstallFeature(string szFeature, string szProduct, uint dwReinstallMode);
131[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiReinstallProduct(string szProduct, uint dwReinstallMode);
132//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListAddSource(string szProduct, string szUserName, uint dwReserved, string szSource);
133//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListClearAll(string szProduct, string szUserName, uint dwReserved);
134//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListForceResolution(string szProduct, string szUserName, uint dwReserved);
135[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiCollectUserInfo(string szProduct);
136//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetUserInfo(string szProduct, StringBuilder lpUserNameBuf, ref uint cchUserNameBuf, StringBuilder lpOrgNameBuf, ref uint cchOrgNameBuf, StringBuilder lpSerialBuf, ref uint cchSerialBuf);
137[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiOpenPackageEx(string szPackagePath, uint dwOptions, out int hProduct);
138[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiOpenProduct(string szProduct, out int hProduct);
139[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiInstallProduct(string szPackagePath, string szCommandLine);
140[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiConfigureProductEx(string szProduct, int iInstallLevel, int eInstallState, string szCommandLine);
141[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiConfigureFeature(string szProduct, string szFeature, int eInstallState);
142[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiApplyPatch(string szPatchPackage, string szInstallPackage, int eInstallType, string szCommandLine);
143[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiOpenDatabase(string szDatabasePath, IntPtr uiOpenMode, out int hDatabase);
144[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiOpenDatabase(string szDatabasePath, string szPersist, out int hDatabase);
145[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetDatabaseState(int hDatabase);
146[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseOpenView(int hDatabase, string szQuery, out int hView);
147[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseMerge(int hDatabase, int hDatabaseMerge, string szTableName);
148[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseCommit(int hDatabase);
149[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseGetPrimaryKeys(int hDatabase, string szTableName, out int hRecord);
150[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseIsTablePersistent(int hDatabase, string szTableName);
151[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseExport(int hDatabase, string szTableName, string szFolderPath, string szFileName);
152[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseImport(int hDatabase, string szFolderPath, string szFileName);
153[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseGenerateTransform(int hDatabase, int hDatabaseReference, string szTransformFile, int iReserved1, int iReserved2);
154[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiCreateTransformSummaryInfo(int hDatabase, int hDatabaseReference, string szTransformFile, int iErrorConditions, int iValidation);
155[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseApplyTransform(int hDatabase, string szTransformFile, int iErrorConditions);
156[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiViewExecute(int hView, int hRecord);
157[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiViewFetch(int hView, out int hRecord);
158[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiViewModify(int hView, int iModifyMode, int hRecord);
159[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiViewGetError(int hView, StringBuilder szColumnNameBuffer, ref uint cchBuf);
160[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiViewGetColumnInfo(int hView, uint eColumnInfo, out int hRecord);
161[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiCreateRecord(uint cParams);
162[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiFormatRecord(int hInstall, int hRecord, StringBuilder szResultBuf, ref uint cchResultBuf);
163[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordClearData(int hRecord);
164[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordGetFieldCount(int hRecord);
165[DllImport("msi.dll", CharSet=CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool MsiRecordIsNull(int hRecord, uint iField);
166[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiRecordGetInteger(int hRecord, uint iField);
167[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordGetString(int hRecord, uint iField, StringBuilder szValueBuf, ref uint cchValueBuf);
168[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordSetInteger(int hRecord, uint iField, int iValue);
169[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordSetString(int hRecord, uint iField, string szValue);
170[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordDataSize(int hRecord, uint iField);
171[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordReadStream(int hRecord, uint iField, byte[] szDataBuf, ref uint cbDataBuf);
172[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordSetStream(int hRecord, uint iField, string szFilePath);
173[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetSummaryInformation(int hDatabase, string szDatabasePath, uint uiUpdateCount, out int hSummaryInfo);
174//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSummaryInfoGetPropertyCount(int hSummaryInfo, out uint uiPropertyCount);
175[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSummaryInfoGetProperty(int hSummaryInfo, uint uiProperty, out uint uiDataType, out int iValue, ref long ftValue, StringBuilder szValueBuf, ref uint cchValueBuf);
176[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSummaryInfoSetProperty(int hSummaryInfo, uint uiProperty, uint uiDataType, int iValue, ref long ftValue, string szValue);
177[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSummaryInfoPersist(int hSummaryInfo);
178[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiCloseHandle(int hAny);
179[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFileVersion(string szFilePath, StringBuilder szVersionBuf, ref uint cchVersionBuf, StringBuilder szLangBuf, ref uint cchLangBuf);
180[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFileHash(string szFilePath, uint dwOptions, uint[] hash);
181[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetActiveDatabase(int hInstall);
182[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProperty(int hInstall, string szName, StringBuilder szValueBuf, ref uint cchValueBuf);
183[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetProperty(int hInstall, string szName, string szValue);
184[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiProcessMessage(int hInstall, uint eMessageType, int hRecord);
185[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEvaluateCondition(int hInstall, string szCondition);
186[DllImport("msi.dll", CharSet=CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool MsiGetMode(int hInstall, uint iRunMode);
187[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetMode(int hInstall, uint iRunMode, [MarshalAs(UnmanagedType.Bool)] bool fState);
188[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDoAction(int hInstall, string szAction);
189[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSequence(int hInstall, string szTable, int iSequenceMode);
190[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetSourcePath(int hInstall, string szFolder, StringBuilder szPathBuf, ref uint cchPathBuf);
191[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetTargetPath(int hInstall, string szFolder, StringBuilder szPathBuf, ref uint cchPathBuf);
192[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetTargetPath(int hInstall, string szFolder, string szFolderPath);
193[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetComponentState(int hInstall, string szComponent, out int iInstalled, out int iAction);
194[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetComponentState(int hInstall, string szComponent, int iState);
195[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureState(int hInstall, string szFeature, out int iInstalled, out int iAction);
196[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetFeatureState(int hInstall, string szFeature, int iState);
197[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureValidStates(int hInstall, string szFeature, out uint dwInstallState);
198[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetInstallLevel(int hInstall, int iInstallLevel);
199[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern ushort MsiGetLanguage(int hInstall);
200[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumComponents(uint iComponentIndex, StringBuilder lpComponentBuf);
201[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumComponentsEx(string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwIndex, StringBuilder szInstalledProductCode, [MarshalAs(UnmanagedType.I4)] out UserContexts pdwInstalledContext, StringBuilder szSid, ref uint pcchSid);
202[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumClients(string szComponent, uint iProductIndex, StringBuilder lpProductBuf);
203[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumClientsEx(string szComponent, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint iProductIndex, StringBuilder lpProductBuf, [MarshalAs(UnmanagedType.I4)] out UserContexts pdwInstalledContext, StringBuilder szSid, ref uint pcchSid);
204[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetComponentPath(string szProduct, string szComponent, StringBuilder lpPathBuf, ref uint pcchBuf);
205[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetComponentPathEx(string szProduct, string szComponent, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, StringBuilder lpPathBuf, ref uint pcchBuf);
206[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumComponentQualifiers(string szComponent, uint iIndex, StringBuilder lpQualifierBuf, ref uint pcchQualifierBuf, StringBuilder lpApplicationDataBuf, ref uint pcchApplicationDataBuf);
207[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetLastErrorRecord();
208[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumRelatedProducts(string upgradeCode, uint dwReserved, uint iProductIndex, StringBuilder lpProductBuf);
209[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductCode(string szComponent, StringBuilder lpProductBuf);
210[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureUsage(string szProduct, string szFeature, out uint dwUseCount, out ushort dwDateUsed);
211[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureCost(int hInstall, string szFeature, int iCostTree, int iState, out int iCost);
212[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiVerifyPackage(string szPackagePath);
213[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiIsProductElevated(string szProductCode, [MarshalAs(UnmanagedType.Bool)] out bool fElevated);
214[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiAdvertiseProduct(string szPackagePath, IntPtr szScriptFilePath, string szTransforms, ushort lgidLanguage);
215[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiAdvertiseProduct(string szPackagePath, string szScriptFilePath, string szTransforms, ushort lgidLanguage);
216[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiAdvertiseProductEx(string szPackagePath, string szScriptFilePath, string szTransforms, ushort lgidLanguage, uint dwPlatform, uint dwReserved);
217[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiAdvertiseScript(string szScriptFile, uint dwFlags, IntPtr phRegData, [MarshalAs(UnmanagedType.Bool)] bool fRemoveItems);
218[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiProcessAdvertiseScript(string szScriptFile, string szIconFolder, IntPtr hRegData, [MarshalAs(UnmanagedType.Bool)] bool fShortcuts, [MarshalAs(UnmanagedType.Bool)] bool fRemoveItems);
219[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductInfoFromScript(string szScriptFile, StringBuilder lpProductBuf39, out ushort plgidLanguage, out uint pdwVersion, StringBuilder lpNameBuf, ref uint cchNameBuf, StringBuilder lpPackageBuf, ref uint cchPackageBuf);
220[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiProvideAssembly(string szAssemblyName, string szAppContext, uint dwInstallMode, uint dwAssemblyInfo, StringBuilder lpPathBuf, ref uint cchPathBuf);
221[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiInstallMissingComponent(string szProduct, string szComponent, int eInstallState);
222[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiInstallMissingFile(string szProduct, string szFile);
223[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiLocateComponent(string szComponent, StringBuilder lpPathBuf, ref uint cchBuf);
224[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductProperty(int hProduct, string szProperty, StringBuilder lpValueBuf, ref uint cchValueBuf);
225[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureInfo(int hProduct, string szFeature, out uint lpAttributes, StringBuilder lpTitleBuf, ref uint cchTitleBuf, StringBuilder lpHelpBuf, ref uint cchHelpBuf);
226[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiVerifyDiskSpace(int hInstall);
227[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumComponentCosts(int hInstall, string szComponent, uint dwIndex, int iState, StringBuilder lpDriveBuf, ref uint cchDriveBuf, out int iCost, out int iTempCost);
228[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetFeatureAttributes(int hInstall, string szFeature, uint dwAttributes);
229
230[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRemovePatches(string szPatchList, string szProductCode, int eUninstallType, string szPropertyList);
231[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDetermineApplicablePatches(string szProductPackagePath, uint cPatchInfo, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1), In, Out] MsiPatchSequenceData[] pPatchInfo);
232[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDeterminePatchSequence(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint cPatchInfo, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=3), In, Out] MsiPatchSequenceData[] pPatchInfo);
233[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiApplyMultiplePatches(string szPatchPackages, string szProductCode, string szPropertiesList);
234[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumPatchesEx(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwFilter, uint dwIndex, StringBuilder szPatchCode, StringBuilder szTargetProductCode, [MarshalAs(UnmanagedType.I4)] out UserContexts pdwTargetProductContext, StringBuilder szTargetUserSid, ref uint pcchTargetUserSid);
235[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetPatchInfoEx(string szPatchCode, string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, string szProperty, StringBuilder lpValue, ref uint pcchValue);
236[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumProductsEx(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwIndex, StringBuilder szInstalledProductCode, [MarshalAs(UnmanagedType.I4)] out UserContexts pdwInstalledContext, StringBuilder szSid, ref uint pcchSid);
237[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductInfoEx(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, string szProperty, StringBuilder lpValue, ref uint pcchValue);
238[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiQueryFeatureStateEx(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, string szFeature, out int pdwState);
239[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiQueryComponentState(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, string szComponent, out int pdwState);
240[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiExtractPatchXMLData(string szPatchPath, uint dwReserved, StringBuilder szXMLData, ref uint pcchXMLData);
241[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListEnumSources(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, uint dwIndex, StringBuilder szSource, ref uint pcchSource);
242[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListAddSourceEx(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, string szSource, uint dwIndex);
243[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListClearSource(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, string szSource);
244[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListClearAllEx(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions);
245[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListForceResolutionEx(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions);
246[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListGetInfo(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, string szProperty, StringBuilder szValue, ref uint pcchValue);
247[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListSetInfo(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, string szProperty, string szValue);
248[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListEnumMediaDisks(string szProductCodeOrPatchCode, string szUserSID, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, uint dwIndex, out uint pdwDiskId, StringBuilder szVolumeLabel, ref uint pcchVolumeLabel, StringBuilder szDiskPrompt, ref uint pcchDiskPrompt);
249[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListAddMediaDisk(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, uint dwDiskId, string szVolumeLabel, string szDiskPrompt);
250[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListClearMediaDisk(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, uint dwDiskID);
251[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiNotifySidChange(string szOldSid, string szNewSid);
252[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetExternalUIRecord([MarshalAs(UnmanagedType.FunctionPtr)] NativeExternalUIRecordHandler puiHandler, uint dwMessageFilter, IntPtr pvContext, out NativeExternalUIRecordHandler ppuiPrevHandler);
253
254[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetPatchFileList(string szProductCode, string szPatchList, out uint cFiles, out IntPtr phFileRecords);
255
256[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiBeginTransaction(string szTransactionName, int dwTransactionAttributes, out int hTransaction, out IntPtr phChangeOfOwnerEvent);
257[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEndTransaction(int dwTransactionState);
258[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiJoinTransaction(int hTransaction, int dwTransactionAttributes, out IntPtr phChangeOfOwnerEvent);
259
260[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode, EntryPoint="LoadLibraryExW")] internal static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, uint flags);
261[DllImport("kernel32.dll", SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool FreeLibrary(IntPtr hModule);
262[DllImport("kernel32.dll", SetLastError=true)] internal static extern IntPtr FindResourceEx(IntPtr hModule, IntPtr type, IntPtr name, ushort langId);
263[DllImport("kernel32.dll", SetLastError=true)] internal static extern IntPtr LoadResource(IntPtr hModule, IntPtr lpResourceInfo);
264[DllImport("kernel32.dll", SetLastError=true)] internal static extern IntPtr LockResource(IntPtr resourceData);
265[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode, EntryPoint="FormatMessageW")] internal static extern uint FormatMessage(uint dwFlags, IntPtr lpSource, uint dwMessageId, uint dwLanguageId, StringBuilder lpBuffer, uint nSize, IntPtr Arguments);
266[DllImport("kernel32.dll", SetLastError=true)] internal static extern int WaitForSingleObject(IntPtr handle, int milliseconds);
267
268[DllImport("ole32.dll")] internal static extern int StgOpenStorage([MarshalAs(UnmanagedType.LPWStr)] string wcsName, IntPtr stgPriority, uint grfMode, IntPtr snbExclude, uint reserved, [MarshalAs(UnmanagedType.Interface)] out IStorage stgOpen);
269[DllImport("ole32.dll")] internal static extern int StgCreateDocfile([MarshalAs(UnmanagedType.LPWStr)] string wcsName, uint grfMode, uint reserved, [MarshalAs(UnmanagedType.Interface)] out IStorage stgOpen);
270
271[DllImport("user32.dll", CharSet=CharSet.Unicode, EntryPoint="MessageBoxW")] internal static extern MessageResult MessageBox(IntPtr hWnd, string lpText, string lpCaption, [MarshalAs(UnmanagedType.U4)] int uType);
272
273 [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
274 internal struct MsiPatchSequenceData
275 {
276 public string szPatchData;
277 public int ePatchDataType;
278 public int dwOrder;
279 public uint dwStatus;
280 }
281
282 internal class MsiHandle : SafeHandle
283 {
284 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
285 public MsiHandle(IntPtr handle, bool ownsHandle)
286 : base(handle, ownsHandle)
287 {
288 }
289
290 public override bool IsInvalid
291 {
292 get
293 {
294 return this.handle == IntPtr.Zero;
295 }
296 }
297
298 public static implicit operator IntPtr(MsiHandle msiHandle)
299 {
300 return msiHandle.handle;
301 }
302
303 protected override bool ReleaseHandle()
304 {
305 return RemotableNativeMethods.MsiCloseHandle((int) this.handle) == 0;
306 }
307 }
308}
309}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/PatchInstallation.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/PatchInstallation.cs
new file mode 100644
index 00000000..defbf64a
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/PatchInstallation.cs
@@ -0,0 +1,413 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// The Patch object represents a unique instance of a patch that has been
13 /// registered or applied.
14 /// </summary>
15 public class PatchInstallation : Installation
16 {
17 /// <summary>
18 /// Enumerates all patch installations on the system.
19 /// </summary>
20 /// <returns>Enumeration of patch objects.</returns>
21 /// <remarks><p>
22 /// Win32 MSI API:
23 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumpatches.asp">MsiEnumPatches</a>
24 /// </p></remarks>
25 public static IEnumerable<PatchInstallation> AllPatches
26 {
27 get
28 {
29 return PatchInstallation.GetPatches(null, null, null, UserContexts.All, PatchStates.All);
30 }
31 }
32
33 /// <summary>
34 /// Enumerates patch installations based on certain criteria.
35 /// </summary>
36 /// <param name="patchCode">PatchCode (GUID) of the patch to be enumerated. Only
37 /// instances of patches within the scope of the context specified by the
38 /// <paramref name="userSid"/> and <paramref name="context"/> parameters will be
39 /// enumerated. This parameter may be set to null to enumerate all patches in the specified
40 /// context.</param>
41 /// <param name="targetProductCode">ProductCode (GUID) product whose patches are to be
42 /// enumerated. If non-null, patch enumeration is restricted to instances of this product
43 /// within the specified context. If null, the patches for all products under the specified
44 /// context are enumerated.</param>
45 /// <param name="userSid">Specifies a security identifier (SID) that restricts the context
46 /// of enumeration. A SID value other than s-1-1-0 is considered a user SID and restricts
47 /// enumeration to the current user or any user in the system. The special SID string
48 /// s-1-1-0 (Everyone) specifies enumeration across all users in the system. This parameter
49 /// can be set to null to restrict the enumeration scope to the current user. When
50 /// <paramref name="userSid"/> must be null.</param>
51 /// <param name="context">Specifies the user context.</param>
52 /// <param name="states">The <see cref="PatchStates"/> of patches to return.</param>
53 /// <remarks><p>
54 /// Win32 MSI APIs:
55 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumpatchesex.asp">MsiEnumPatchesEx</a>
56 /// </p></remarks>
57 public static IEnumerable<PatchInstallation> GetPatches(
58 string patchCode,
59 string targetProductCode,
60 string userSid,
61 UserContexts context,
62 PatchStates states)
63 {
64 StringBuilder buf = new StringBuilder(40);
65 StringBuilder targetProductBuf = new StringBuilder(40);
66 UserContexts targetContext;
67 StringBuilder targetSidBuf = new StringBuilder(40);
68 for (uint i = 0; ; i++)
69 {
70 uint targetSidBufSize = (uint) targetSidBuf.Capacity;
71 uint ret = NativeMethods.MsiEnumPatchesEx(
72 targetProductCode,
73 userSid,
74 context,
75 (uint) states,
76 i,
77 buf,
78 targetProductBuf,
79 out targetContext,
80 targetSidBuf,
81 ref targetSidBufSize);
82 if (ret == (uint) NativeMethods.Error.MORE_DATA)
83 {
84 targetSidBuf.Capacity = (int) ++targetSidBufSize;
85 ret = NativeMethods.MsiEnumPatchesEx(
86 targetProductCode,
87 userSid,
88 context,
89 (uint) states,
90 i,
91 buf,
92 targetProductBuf,
93 out targetContext,
94 targetSidBuf,
95 ref targetSidBufSize);
96 }
97
98 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS)
99 {
100 break;
101 }
102
103 if (ret != 0)
104 {
105 throw InstallerException.ExceptionFromReturnCode(ret);
106 }
107
108 string thisPatchCode = buf.ToString();
109 if (patchCode == null || patchCode == thisPatchCode)
110 {
111 yield return new PatchInstallation(
112 buf.ToString(),
113 targetProductBuf.ToString(),
114 targetSidBuf.ToString(),
115 targetContext);
116 }
117 }
118 }
119
120 private string productCode;
121
122 /// <summary>
123 /// Creates a new object for accessing information about a patch installation on the current system.
124 /// </summary>
125 /// <param name="patchCode">Patch code (GUID) of the patch.</param>
126 /// <param name="productCode">ProductCode (GUID) the patch has been applied to.
127 /// This parameter may be null for patches that are registered only and not yet
128 /// applied to any product.</param>
129 /// <remarks><p>
130 /// All available user contexts will be queried.
131 /// </p></remarks>
132 public PatchInstallation(string patchCode, string productCode)
133 : this(patchCode, productCode, null, UserContexts.All)
134 {
135 }
136
137 /// <summary>
138 /// Creates a new object for accessing information about a patch installation on the current system.
139 /// </summary>
140 /// <param name="patchCode">Registered patch code (GUID) of the patch.</param>
141 /// <param name="productCode">ProductCode (GUID) the patch has been applied to.
142 /// This parameter may be null for patches that are registered only and not yet
143 /// applied to any product.</param>
144 /// <param name="userSid">The specific user, when working in a user context. This
145 /// parameter may be null to indicate the current user. The parameter must be null
146 /// when working in a machine context.</param>
147 /// <param name="context">The user context. The calling process must have administrative
148 /// privileges to get information for a product installed for a user other than the
149 /// current user.</param>
150 /// <remarks><p>
151 /// If the <paramref name="productCode"/> is null, the Patch object may
152 /// only be used to read and update the patch's SourceList information.
153 /// </p></remarks>
154 public PatchInstallation(string patchCode, string productCode, string userSid, UserContexts context)
155 : base(patchCode, userSid, context)
156 {
157 if (String.IsNullOrEmpty(patchCode))
158 {
159 throw new ArgumentNullException("patchCode");
160 }
161
162 this.productCode = productCode;
163 }
164
165 /// <summary>
166 /// Gets the patch code (GUID) of the patch.
167 /// </summary>
168 public string PatchCode
169 {
170 get
171 {
172 return this.InstallationCode;
173 }
174 }
175
176 /// <summary>
177 /// Gets the ProductCode (GUID) of the product.
178 /// </summary>
179 public string ProductCode
180 {
181 get
182 {
183 return this.productCode;
184 }
185 }
186
187 /// <summary>
188 /// Gets a value indicating whether this patch is currently installed.
189 /// </summary>
190 public override bool IsInstalled
191 {
192 get
193 {
194 return (this.State & PatchStates.Applied) != 0;
195 }
196 }
197
198 /// <summary>
199 /// Gets a value indicating whether this patch is marked as obsolte.
200 /// </summary>
201 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Obsoleted")]
202 public bool IsObsoleted
203 {
204 get
205 {
206 return (this.State & PatchStates.Obsoleted) != 0;
207 }
208 }
209
210 /// <summary>
211 /// Gets a value indicating whether this patch is present but has been
212 /// superseded by a more recent installed patch.
213 /// </summary>
214 public bool IsSuperseded
215 {
216 get
217 {
218 return (this.State & PatchStates.Superseded) != 0;
219 }
220 }
221
222 internal override int InstallationType
223 {
224 get
225 {
226 const int MSICODE_PATCH = 0x40000000;
227 return MSICODE_PATCH;
228 }
229 }
230
231 /// <summary>
232 /// Gets the installation state of this instance of the patch.
233 /// </summary>
234 /// <exception cref="ArgumentException">An unknown patch was requested</exception>
235 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
236 public PatchStates State
237 {
238 get
239 {
240 string stateString = this["State"];
241 return (PatchStates) Int32.Parse(stateString, CultureInfo.InvariantCulture.NumberFormat);
242 }
243 }
244
245 /// <summary>
246 /// Gets the cached patch file that the product uses.
247 /// </summary>
248 public string LocalPackage
249 {
250 get
251 {
252 return this["LocalPackage"];
253 }
254 }
255
256 /// <summary>
257 /// Gets the set of patch transforms that the last patch
258 /// installation applied to the product.
259 /// </summary>
260 /// <remarks><p>
261 /// This value may not be available for per-user, non-managed applications
262 /// if the user is not logged on.
263 /// </p></remarks>
264 public string Transforms
265 {
266 get
267 {
268 // TODO: convert to IList<string>?
269 return this["Transforms"];
270 }
271 }
272
273 /// <summary>
274 /// Gets the date and time when the patch is applied to the product.
275 /// </summary>
276 public DateTime InstallDate
277 {
278 get
279 {
280 try
281 {
282 return DateTime.ParseExact(
283 this["InstallDate"], "yyyyMMdd", CultureInfo.InvariantCulture);
284 }
285 catch (FormatException)
286 {
287 return DateTime.MinValue;
288 }
289 }
290 }
291
292 /// <summary>
293 /// True patch is marked as possible to uninstall from the product.
294 /// </summary>
295 /// <remarks><p>
296 /// Even if this property is true, the installer can still block the
297 /// uninstallation if this patch is required by another patch that
298 /// cannot be uninstalled.
299 /// </p></remarks>
300 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Uninstallable")]
301 public bool Uninstallable
302 {
303 get
304 {
305 return this["Uninstallable"] == "1";
306 }
307 }
308
309 /// <summary>
310 /// Get the registered display name for the patch.
311 /// </summary>
312 public string DisplayName
313 {
314 get
315 {
316 return this["DisplayName"];
317 }
318 }
319
320 /// <summary>
321 /// Gets the registered support information URL for the patch.
322 /// </summary>
323 public Uri MoreInfoUrl
324 {
325 get
326 {
327 string value = this["MoreInfoURL"];
328 if (!String.IsNullOrEmpty(value))
329 {
330 try
331 {
332 return new Uri(value);
333 }
334 catch (UriFormatException) { }
335 }
336
337 return null;
338 }
339 }
340
341 /// <summary>
342 /// Gets information about a specific patch installation.
343 /// </summary>
344 /// <param name="propertyName">The property being retrieved; see remarks for valid properties.</param>
345 /// <returns>The property value, or an empty string if the property is not set for the patch.</returns>
346 /// <exception cref="ArgumentOutOfRangeException">An unknown patch or property was requested</exception>
347 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
348 /// <remarks><p>
349 /// Win32 MSI APIs:
350 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetpatchinfo.asp">MsiGetPatchInfo</a>,
351 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetpatchinfoex.asp">MsiGetPatchInfoEx</a>
352 /// </p></remarks>
353 public override string this[string propertyName]
354 {
355 get
356 {
357 StringBuilder buf = new StringBuilder("");
358 uint bufSize = 0;
359 uint ret;
360
361 if (this.Context == UserContexts.UserManaged ||
362 this.Context == UserContexts.UserUnmanaged ||
363 this.Context == UserContexts.Machine)
364 {
365 ret = NativeMethods.MsiGetPatchInfoEx(
366 this.PatchCode,
367 this.ProductCode,
368 this.UserSid,
369 this.Context,
370 propertyName,
371 buf,
372 ref bufSize);
373 if (ret == (uint) NativeMethods.Error.MORE_DATA)
374 {
375 buf.Capacity = (int) ++bufSize;
376 ret = NativeMethods.MsiGetPatchInfoEx(
377 this.PatchCode,
378 this.ProductCode,
379 this.UserSid,
380 this.Context,
381 propertyName,
382 buf,
383 ref bufSize);
384 }
385 }
386 else
387 {
388 ret = NativeMethods.MsiGetPatchInfo(
389 this.PatchCode,
390 propertyName,
391 buf,
392 ref bufSize);
393 if (ret == (uint) NativeMethods.Error.MORE_DATA)
394 {
395 buf.Capacity = (int) ++bufSize;
396 ret = NativeMethods.MsiGetPatchInfo(
397 this.PatchCode,
398 propertyName,
399 buf,
400 ref bufSize);
401 }
402 }
403
404 if (ret != 0)
405 {
406 return null;
407 }
408
409 return buf.ToString();
410 }
411 }
412 }
413}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ProductInstallation.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ProductInstallation.cs
new file mode 100644
index 00000000..27739e17
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ProductInstallation.cs
@@ -0,0 +1,801 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// Represents a unique instance of a product that
13 /// is either advertised, installed or unknown.
14 /// </summary>
15 public class ProductInstallation : Installation
16 {
17 /// <summary>
18 /// Gets the set of all products with a specified upgrade code. This method lists the
19 /// currently installed and advertised products that have the specified UpgradeCode
20 /// property in their Property table.
21 /// </summary>
22 /// <param name="upgradeCode">Upgrade code of related products</param>
23 /// <returns>Enumeration of product codes</returns>
24 /// <remarks><p>
25 /// Win32 MSI API:
26 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumrelatedproducts.asp">MsiEnumRelatedProducts</a>
27 /// </p></remarks>
28 public static IEnumerable<ProductInstallation> GetRelatedProducts(string upgradeCode)
29 {
30 StringBuilder buf = new StringBuilder(40);
31 for (uint i = 0; true; i++)
32 {
33 uint ret = NativeMethods.MsiEnumRelatedProducts(upgradeCode, 0, i, buf);
34 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break;
35 if (ret != 0)
36 {
37 throw InstallerException.ExceptionFromReturnCode(ret);
38 }
39 yield return new ProductInstallation(buf.ToString());
40 }
41 }
42
43 /// <summary>
44 /// Enumerates all product installations on the system.
45 /// </summary>
46 /// <returns>An enumeration of product objects.</returns>
47 /// <remarks><p>
48 /// Win32 MSI API:
49 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumproducts.asp">MsiEnumProducts</a>,
50 /// </p></remarks>
51 public static IEnumerable<ProductInstallation> AllProducts
52 {
53 get
54 {
55 return GetProducts(null, null, UserContexts.All);
56 }
57 }
58
59 /// <summary>
60 /// Enumerates product installations based on certain criteria.
61 /// </summary>
62 /// <param name="productCode">ProductCode (GUID) of the product instances to be enumerated. Only
63 /// instances of products within the scope of the context specified by the
64 /// <paramref name="userSid"/> and <paramref name="context"/> parameters will be
65 /// enumerated. This parameter may be set to null to enumerate all products in the specified
66 /// context.</param>
67 /// <param name="userSid">Specifies a security identifier (SID) that restricts the context
68 /// of enumeration. A SID value other than s-1-1-0 is considered a user SID and restricts
69 /// enumeration to the current user or any user in the system. The special SID string
70 /// s-1-1-0 (Everyone) specifies enumeration across all users in the system. This parameter
71 /// can be set to null to restrict the enumeration scope to the current user. When
72 /// <paramref name="context"/> is set to the machine context only,
73 /// <paramref name="userSid"/> must be null.</param>
74 /// <param name="context">Specifies the user context.</param>
75 /// <returns>An enumeration of product objects for enumerated product instances.</returns>
76 /// <remarks><p>
77 /// Win32 MSI API:
78 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumproductsex.asp">MsiEnumProductsEx</a>
79 /// </p></remarks>
80 public static IEnumerable<ProductInstallation> GetProducts(
81 string productCode, string userSid, UserContexts context)
82 {
83 StringBuilder buf = new StringBuilder(40);
84 UserContexts targetContext;
85 StringBuilder targetSidBuf = new StringBuilder(40);
86 for (uint i = 0; ; i++)
87 {
88 uint targetSidBufSize = (uint) targetSidBuf.Capacity;
89 uint ret = NativeMethods.MsiEnumProductsEx(
90 productCode,
91 userSid,
92 context,
93 i,
94 buf,
95 out targetContext,
96 targetSidBuf,
97 ref targetSidBufSize);
98 if (ret == (uint) NativeMethods.Error.MORE_DATA)
99 {
100 targetSidBuf.Capacity = (int) ++targetSidBufSize;
101 ret = NativeMethods.MsiEnumProductsEx(
102 productCode,
103 userSid,
104 context,
105 i,
106 buf,
107 out targetContext,
108 targetSidBuf,
109 ref targetSidBufSize);
110 }
111
112 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS)
113 {
114 break;
115 }
116
117 if (ret != 0)
118 {
119 throw InstallerException.ExceptionFromReturnCode(ret);
120 }
121
122 yield return new ProductInstallation(
123 buf.ToString(),
124 targetSidBuf.ToString(),
125 targetContext);
126 }
127 }
128
129 private IDictionary<string, string> properties;
130
131 /// <summary>
132 /// Creates a new object for accessing information about a product installation on the current system.
133 /// </summary>
134 /// <param name="productCode">ProductCode (GUID) of the product.</param>
135 /// <remarks><p>
136 /// All available user contexts will be queried.
137 /// </p></remarks>
138 public ProductInstallation(string productCode)
139 : this(productCode, null, UserContexts.All)
140 {
141 }
142
143 /// <summary>
144 /// Creates a new object for accessing information about a product installation on the current system.
145 /// </summary>
146 /// <param name="productCode">ProductCode (GUID) of the product.</param>
147 /// <param name="userSid">The specific user, when working in a user context. This
148 /// parameter may be null to indicate the current user. The parameter must be null
149 /// when working in a machine context.</param>
150 /// <param name="context">The user context. The calling process must have administrative
151 /// privileges to get information for a product installed for a user other than the
152 /// current user.</param>
153 public ProductInstallation(string productCode, string userSid, UserContexts context)
154 : base(productCode, userSid, context)
155 {
156 if (String.IsNullOrEmpty(productCode))
157 {
158 throw new ArgumentNullException("productCode");
159 }
160 }
161
162 internal ProductInstallation(IDictionary<string, string> properties)
163 : base(properties["ProductCode"], null, UserContexts.None)
164 {
165 this.properties = properties;
166 }
167
168 /// <summary>
169 /// Gets the set of published features for the product.
170 /// </summary>
171 /// <returns>Enumeration of published features for the product.</returns>
172 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
173 /// <remarks><p>
174 /// Because features are not ordered, any new feature has an arbitrary index, meaning
175 /// this property can return features in any order.
176 /// </p><p>
177 /// Win32 MSI API:
178 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumfeatures.asp">MsiEnumFeatures</a>
179 /// </p></remarks>
180 public IEnumerable<FeatureInstallation> Features
181 {
182 get
183 {
184 StringBuilder buf = new StringBuilder(256);
185 for (uint i = 0; ; i++)
186 {
187 uint ret = NativeMethods.MsiEnumFeatures(this.ProductCode, i, buf, null);
188
189 if (ret != 0)
190 {
191 break;
192 }
193
194 yield return new FeatureInstallation(buf.ToString(), this.ProductCode);
195 }
196 }
197 }
198
199 /// <summary>
200 /// Gets the ProductCode (GUID) of the product.
201 /// </summary>
202 public string ProductCode
203 {
204 get { return this.InstallationCode; }
205 }
206
207 /// <summary>
208 /// Gets a value indicating whether this product is installed on the current system.
209 /// </summary>
210 public override bool IsInstalled
211 {
212 get
213 {
214 return (this.State == InstallState.Default);
215 }
216 }
217
218 /// <summary>
219 /// Gets a value indicating whether this product is advertised on the current system.
220 /// </summary>
221 public bool IsAdvertised
222 {
223 get
224 {
225 return (this.State == InstallState.Advertised);
226 }
227 }
228
229 /// <summary>
230 /// Checks whether the product is installed with elevated privileges. An application is called
231 /// a "managed application" if elevated (system) privileges are used to install the application.
232 /// </summary>
233 /// <returns>True if the product is elevated; false otherwise</returns>
234 /// <remarks><p>
235 /// Note that this property does not take into account policies such as AlwaysInstallElevated,
236 /// but verifies that the local system owns the product's registry data.
237 /// </p></remarks>
238 public bool IsElevated
239 {
240 get
241 {
242 bool isElevated;
243 uint ret = NativeMethods.MsiIsProductElevated(this.ProductCode, out isElevated);
244 if (ret != 0)
245 {
246 throw InstallerException.ExceptionFromReturnCode(ret);
247 }
248 return isElevated;
249 }
250 }
251
252 /// <summary>
253 /// Gets the source list of this product installation.
254 /// </summary>
255 public override SourceList SourceList
256 {
257 get
258 {
259 return this.properties == null ? base.SourceList : null;
260 }
261 }
262
263 internal InstallState State
264 {
265 get
266 {
267 if (this.properties != null)
268 {
269 return InstallState.Unknown;
270 }
271 else
272 {
273 int installState = NativeMethods.MsiQueryProductState(this.ProductCode);
274 return (InstallState) installState;
275 }
276 }
277 }
278
279 internal override int InstallationType
280 {
281 get
282 {
283 const int MSICODE_PRODUCT = 0x00000000;
284 return MSICODE_PRODUCT;
285 }
286 }
287
288 /// <summary>
289 /// The support link.
290 /// </summary>
291 public string HelpLink
292 {
293 get
294 {
295 return this["HelpLink"];
296 }
297 }
298
299 /// <summary>
300 /// The support telephone.
301 /// </summary>
302 public string HelpTelephone
303 {
304 get
305 {
306 return this["HelpTelephone"];
307 }
308 }
309
310 /// <summary>
311 /// Date and time the product was installed.
312 /// </summary>
313 public DateTime InstallDate
314 {
315 get
316 {
317 try
318 {
319 return DateTime.ParseExact(
320 this["InstallDate"], "yyyyMMdd", CultureInfo.InvariantCulture);
321 }
322 catch (FormatException)
323 {
324 return DateTime.MinValue;
325 }
326 }
327 }
328
329 /// <summary>
330 /// The installed product name.
331 /// </summary>
332 public string ProductName
333 {
334 get
335 {
336 return this["InstalledProductName"];
337 }
338 }
339
340 /// <summary>
341 /// The installation location.
342 /// </summary>
343 public string InstallLocation
344 {
345 get
346 {
347 return this["InstallLocation"];
348 }
349 }
350
351 /// <summary>
352 /// The installation source.
353 /// </summary>
354 public string InstallSource
355 {
356 get
357 {
358 return this["InstallSource"];
359 }
360 }
361
362 /// <summary>
363 /// The local cached package.
364 /// </summary>
365 public string LocalPackage
366 {
367 get
368 {
369 return this["LocalPackage"];
370 }
371 }
372
373 /// <summary>
374 /// The publisher.
375 /// </summary>
376 public string Publisher
377 {
378 get
379 {
380 return this["Publisher"];
381 }
382 }
383
384 /// <summary>
385 /// URL about information.
386 /// </summary>
387 public Uri UrlInfoAbout
388 {
389 get
390 {
391 string value = this["URLInfoAbout"];
392 if (!String.IsNullOrEmpty(value))
393 {
394 try
395 {
396 return new Uri(value);
397 }
398 catch (UriFormatException) { }
399 }
400
401 return null;
402 }
403 }
404
405 /// <summary>
406 /// The URL update information.
407 /// </summary>
408 public Uri UrlUpdateInfo
409 {
410 get
411 {
412 string value = this["URLUpdateInfo"];
413 if (!String.IsNullOrEmpty(value))
414 {
415 try
416 {
417 return new Uri(value);
418 }
419 catch (UriFormatException) { }
420 }
421
422 return null;
423 }
424 }
425
426 /// <summary>
427 /// The product version.
428 /// </summary>
429 public Version ProductVersion
430 {
431 get
432 {
433 string ver = this["VersionString"];
434 return ProductInstallation.ParseVersion(ver);
435 }
436 }
437
438 /// <summary>
439 /// The product identifier.
440 /// </summary>
441 /// <remarks><p>
442 /// For more information, see
443 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/productid.asp">ProductID</a>
444 /// </p></remarks>
445 public string ProductId
446 {
447 get
448 {
449 return this["ProductID"];
450 }
451 }
452
453 /// <summary>
454 /// The company that is registered to use the product.
455 /// </summary>
456 public string RegCompany
457 {
458 get
459 {
460 return this["RegCompany"];
461 }
462 }
463
464 /// <summary>
465 /// The owner who is registered to use the product.
466 /// </summary>
467 public string RegOwner
468 {
469 get
470 {
471 return this["RegOwner"];
472 }
473 }
474
475 /// <summary>
476 /// Transforms.
477 /// </summary>
478 public string AdvertisedTransforms
479 {
480 get
481 {
482 return this["Transforms"];
483 }
484 }
485
486 /// <summary>
487 /// Product language.
488 /// </summary>
489 public string AdvertisedLanguage
490 {
491 get
492 {
493 return this["Language"];
494 }
495 }
496
497 /// <summary>
498 /// Human readable product name.
499 /// </summary>
500 public string AdvertisedProductName
501 {
502 get
503 {
504 return this["ProductName"];
505 }
506 }
507
508 /// <summary>
509 /// True if the product is advertised per-machine;
510 /// false if it is per-user or not advertised.
511 /// </summary>
512 public bool AdvertisedPerMachine
513 {
514 get
515 {
516 return this["AssignmentType"] == "1";
517 }
518 }
519
520 /// <summary>
521 /// Identifier of the package that a product is installed from.
522 /// </summary>
523 public string AdvertisedPackageCode
524 {
525 get
526 {
527 return this["PackageCode"];
528 }
529 }
530
531 /// <summary>
532 /// Version of the advertised product.
533 /// </summary>
534 public Version AdvertisedVersion
535 {
536 get
537 {
538 string ver = this["Version"];
539 return ProductInstallation.ParseVersion(ver);
540 }
541 }
542
543 /// <summary>
544 /// Primary icon for the package.
545 /// </summary>
546 public string AdvertisedProductIcon
547 {
548 get
549 {
550 return this["ProductIcon"];
551 }
552 }
553
554 /// <summary>
555 /// Name of the installation package for the advertised product.
556 /// </summary>
557 public string AdvertisedPackageName
558 {
559 get
560 {
561 return this["PackageName"];
562 }
563 }
564
565 /// <summary>
566 /// True if the advertised product can be serviced by
567 /// non-administrators without elevation.
568 /// </summary>
569 public bool PrivilegedPatchingAuthorized
570 {
571 get
572 {
573 return this["AuthorizedLUAApp"] == "1";
574 }
575 }
576
577 /// <summary>
578 /// Gets information about an installation of a product.
579 /// </summary>
580 /// <param name="propertyName">Name of the property being retrieved.</param>
581 /// <exception cref="ArgumentOutOfRangeException">An unknown product or property was requested</exception>
582 /// <exception cref="InstallerException">The installer configuration data is corrupt</exception>
583 /// <remarks><p>
584 /// Win32 MSI APIs:
585 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetproductinfo.asp">MsiGetProductInfo</a>,
586 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetproductinfoex.asp">MsiGetProductInfoEx</a>
587 /// </p></remarks>
588 public override string this[string propertyName]
589 {
590 get
591 {
592 if (this.properties != null)
593 {
594 string value = null;
595 this.properties.TryGetValue(propertyName, out value);
596 return value;
597 }
598 else
599 {
600 StringBuilder buf = new StringBuilder(40);
601 uint bufSize = (uint) buf.Capacity;
602 uint ret;
603
604 if (this.Context == UserContexts.UserManaged ||
605 this.Context == UserContexts.UserUnmanaged ||
606 this.Context == UserContexts.Machine)
607 {
608 ret = NativeMethods.MsiGetProductInfoEx(
609 this.ProductCode,
610 this.UserSid,
611 this.Context,
612 propertyName,
613 buf,
614 ref bufSize);
615 if (ret == (uint) NativeMethods.Error.MORE_DATA)
616 {
617 buf.Capacity = (int) ++bufSize;
618 ret = NativeMethods.MsiGetProductInfoEx(
619 this.ProductCode,
620 this.UserSid,
621 this.Context,
622 propertyName,
623 buf,
624 ref bufSize);
625 }
626 }
627 else
628 {
629 ret = NativeMethods.MsiGetProductInfo(
630 this.ProductCode,
631 propertyName,
632 buf,
633 ref bufSize);
634 if (ret == (uint) NativeMethods.Error.MORE_DATA)
635 {
636 buf.Capacity = (int) ++bufSize;
637 ret = NativeMethods.MsiGetProductInfo(
638 this.ProductCode,
639 propertyName,
640 buf,
641 ref bufSize);
642 }
643 }
644
645 if (ret != 0)
646 {
647 return null;
648 }
649
650 return buf.ToString();
651 }
652 }
653 }
654
655 /// <summary>
656 /// Gets the installed state for a product feature.
657 /// </summary>
658 /// <param name="feature">The feature being queried; identifier from the
659 /// Feature table</param>
660 /// <returns>Installation state of the feature for the product instance: either
661 /// <see cref="InstallState.Local"/>, <see cref="InstallState.Source"/>,
662 /// or <see cref="InstallState.Advertised"/>.</returns>
663 /// <remarks><p>
664 /// Win32 MSI APIs:
665 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiqueryfeaturestate.asp">MsiQueryFeatureState</a>,
666 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiqueryfeaturestateex.asp">MsiQueryFeatureStateEx</a>
667 /// </p></remarks>
668 public InstallState GetFeatureState(string feature)
669 {
670 if (this.properties != null)
671 {
672 return InstallState.Unknown;
673 }
674 else
675 {
676 int installState;
677 uint ret = NativeMethods.MsiQueryFeatureStateEx(
678 this.ProductCode,
679 this.UserSid,
680 this.Context,
681 feature,
682 out installState);
683 if (ret != 0)
684 {
685 throw InstallerException.ExceptionFromReturnCode(ret);
686 }
687 return (InstallState) installState;
688 }
689 }
690
691 /// <summary>
692 /// Gets the installed state for a product component.
693 /// </summary>
694 /// <param name="component">The component being queried; GUID of the component
695 /// as found in the ComponentId column of the Component table.</param>
696 /// <returns>Installation state of the component for the product instance: either
697 /// <see cref="InstallState.Local"/> or <see cref="InstallState.Source"/>.</returns>
698 /// <remarks><p>
699 /// Win32 MSI API:
700 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiquerycomponnetstate.asp">MsiQueryComponentState</a>
701 /// </p></remarks>
702 public InstallState GetComponentState(string component)
703 {
704 if (this.properties != null)
705 {
706 return InstallState.Unknown;
707 }
708 else
709 {
710 int installState;
711 uint ret = NativeMethods.MsiQueryComponentState(
712 this.ProductCode,
713 this.UserSid,
714 this.Context,
715 component,
716 out installState);
717 if (ret != 0)
718 {
719 throw InstallerException.ExceptionFromReturnCode(ret);
720 }
721 return (InstallState) installState;
722 }
723 }
724
725 /// <summary>
726 /// Obtains and stores the user information and product ID from an installation wizard.
727 /// </summary>
728 /// <remarks><p>
729 /// This method is typically called by an application during the first run of the application. The application
730 /// first gets the <see cref="ProductInstallation.ProductId"/> or <see cref="ProductInstallation.RegOwner"/>.
731 /// If those properties are missing, the application calls CollectUserInfo.
732 /// CollectUserInfo opens the product's installation package and invokes a wizard sequence that collects
733 /// user information. Upon completion of the sequence, user information is registered. Since this API requires
734 /// an authored user interface, the user interface level should be set to full by calling
735 /// <see cref="Installer.SetInternalUI(InstallUIOptions)"/> as <see cref="InstallUIOptions.Full"/>.
736 /// </p><p>
737 /// The CollectUserInfo method invokes a FirstRun dialog from the product installation database.
738 /// </p><p>
739 /// Win32 MSI API:
740 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicollectuserinfo.asp">MsiCollectUserInfo</a>
741 /// </p></remarks>
742 public void CollectUserInfo()
743 {
744 if (this.properties == null)
745 {
746 uint ret = NativeMethods.MsiCollectUserInfo(this.InstallationCode);
747 if (ret != 0)
748 {
749 throw InstallerException.ExceptionFromReturnCode(ret);
750 }
751 }
752 }
753
754 /// <summary>
755 /// Some products might write some invalid/nonstandard version strings to the registry.
756 /// This method tries to get the best data it can.
757 /// </summary>
758 /// <param name="ver">Version string retrieved from the registry.</param>
759 /// <returns>Version object, or null if the version string is completely invalid.</returns>
760 private static Version ParseVersion(string ver)
761 {
762 if (ver != null)
763 {
764 int dotCount = 0;
765 for (int i = 0; i < ver.Length; i++)
766 {
767 char c = ver[i];
768 if (c == '.') dotCount++;
769 else if (!Char.IsDigit(c))
770 {
771 ver = ver.Substring(0, i);
772 break;
773 }
774 }
775
776 if (ver.Length > 0)
777 {
778 if (dotCount == 0)
779 {
780 ver = ver + ".0";
781 }
782 else if (dotCount > 3)
783 {
784 string[] verSplit = ver.Split('.');
785 ver = String.Join(".", verSplit, 0, 4);
786 }
787
788 try
789 {
790 return new Version(ver);
791 }
792 catch (ArgumentException)
793 {
794 }
795 }
796 }
797
798 return null;
799 }
800 }
801}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs
new file mode 100644
index 00000000..2d31fa75
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs
@@ -0,0 +1,1048 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections.Generic;
9 using System.Runtime.InteropServices;
10 using System.Diagnostics.CodeAnalysis;
11
12 /// <summary>
13 /// The Record object is a container for holding and transferring a variable number of values.
14 /// Fields within the record are numerically indexed and can contain strings, integers, streams,
15 /// and null values. Record fields are indexed starting with 1. Field 0 is a special format field.
16 /// </summary>
17 /// <remarks><p>
18 /// Most methods on the Record class have overloads that allow using either a number
19 /// or a name to designate a field. However note that field names only exist when the
20 /// Record is directly returned from a query on a database. For other records, attempting
21 /// to access a field by name will result in an InvalidOperationException.
22 /// </p></remarks>
23 public class Record : InstallerHandle
24 {
25 private View view;
26 private bool isFormatStringInvalid;
27
28 /// <summary>
29 /// IsFormatStringInvalid is set from several View methods that invalidate the FormatString
30 /// and used to determine behavior during Record.ToString().
31 /// </summary>
32 internal protected bool IsFormatStringInvalid
33 {
34 set { this.isFormatStringInvalid = value; }
35
36 get { return this.isFormatStringInvalid; }
37 }
38
39 /// <summary>
40 /// Creates a new record object with the requested number of fields.
41 /// </summary>
42 /// <param name="fieldCount">Required number of fields, which may be 0.
43 /// The maximum number of fields in a record is limited to 65535.</param>
44 /// <remarks><p>
45 /// The Record object should be <see cref="InstallerHandle.Close"/>d after use.
46 /// It is best that the handle be closed manually as soon as it is no longer
47 /// needed, as leaving lots of unused handles open can degrade performance.
48 /// </p><p>
49 /// Win32 MSI API:
50 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicreaterecord.asp">MsiCreateRecord</a>
51 /// </p></remarks>
52 public Record(int fieldCount)
53 : this((IntPtr) RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, 0), true, (View) null)
54 {
55 }
56
57 /// <summary>
58 /// Creates a new record object, providing values for an arbitrary number of fields.
59 /// </summary>
60 /// <param name="fields">The values of the record fields. The parameters should be of type Int16, Int32 or String</param>
61 /// <remarks><p>
62 /// The Record object should be <see cref="InstallerHandle.Close"/>d after use.
63 /// It is best that the handle be closed manually as soon as it is no longer
64 /// needed, as leaving lots of unused handles open can degrade performance.
65 /// </p><p>
66 /// Win32 MSI API:
67 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicreaterecord.asp">MsiCreateRecord</a>
68 /// </p></remarks>
69 public Record(params object[] fields)
70 : this(fields.Length)
71 {
72 for (int i = 0; i < fields.Length; i++)
73 {
74 this[i + 1] = fields[i];
75 }
76 }
77
78 internal Record(IntPtr handle, bool ownsHandle, View view)
79 : base(handle, ownsHandle)
80 {
81 this.view = view;
82 }
83
84 /// <summary>
85 /// Gets the number of fields in a record.
86 /// </summary>
87 /// <remarks><p>
88 /// Win32 MSI API:
89 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetfieldcount.asp">MsiRecordGetFieldCount</a>
90 /// </p></remarks>
91 public int FieldCount
92 {
93 get
94 {
95 return (int) RemotableNativeMethods.MsiRecordGetFieldCount((int) this.Handle);
96 }
97 }
98
99 /// <summary>
100 /// Gets or sets field 0 of the Record, which is the format string.
101 /// </summary>
102 public string FormatString
103 {
104 get { return this.GetString(0); }
105 set { this.SetString(0, value); }
106 }
107
108 /// <summary>
109 /// Gets or sets a record field value.
110 /// </summary>
111 /// <param name="fieldName">Specifies the name of the field of the Record to get or set.</param>
112 /// <exception cref="ArgumentOutOfRangeException">The name does not match any known field of the Record.</exception>
113 /// <remarks><p>
114 /// When getting a field, the type of the object returned depends on the type of the Record field.
115 /// The object will be one of: Int16, Int32, String, Stream, or null.
116 /// </p><p>
117 /// When setting a field, the type of the object provided will be converted to match the View
118 /// query that returned the record, or if Record was not returned from a view then the type of
119 /// the object provided will determine the type of the Record field. The object should be one of:
120 /// Int16, Int32, String, Stream, or null.
121 /// </p></remarks>
122 public object this[string fieldName]
123 {
124 get
125 {
126 int field = this.FindColumn(fieldName);
127 return this[field];
128 }
129
130 set
131 {
132 int field = this.FindColumn(fieldName);
133 this[field] = value;
134 }
135 }
136
137 /// <summary>
138 /// Gets or sets a record field value.
139 /// </summary>
140 /// <param name="field">Specifies the field of the Record to get or set.</param>
141 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
142 /// number of fields in the Record.</exception>
143 /// <remarks><p>
144 /// Record fields are indexed starting with 1. Field 0 is a special format field.
145 /// </p><p>
146 /// When getting a field, the type of the object returned depends on the type of the Record field.
147 /// The object will be one of: Int16, Int32, String, Stream, or null. If the Record was returned
148 /// from a View, the type will match that of the field from the View query. Otherwise, the type
149 /// will match the type of the last value set for the field.
150 /// </p><p>
151 /// When setting a field, the type of the object provided will be converted to match the View
152 /// query that returned the Record, or if Record was not returned from a View then the type of
153 /// the object provided will determine the type of the Record field. The object should be one of:
154 /// Int16, Int32, String, Stream, or null.
155 /// </p><p>
156 /// The type-specific getters and setters are slightly more efficient than this property, since
157 /// they don't have to do the extra work to infer the value's type every time.
158 /// </p><p>
159 /// Win32 MSI APIs:
160 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetinteger.asp">MsiRecordGetInteger</a>,
161 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetstring.asp">MsiRecordGetString</a>,
162 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetinteger.asp">MsiRecordSetInteger</a>,
163 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetstring.asp">MsiRecordSetString</a>
164 /// </p></remarks>
165 public object this[int field]
166 {
167 get
168 {
169 if (field == 0)
170 {
171 return this.GetString(0);
172 }
173 else
174 {
175 Type valueType = null;
176 if (this.view != null)
177 {
178 this.CheckRange(field);
179
180 valueType = this.view.Columns[field - 1].Type;
181 }
182
183 if (valueType == null || valueType == typeof(String))
184 {
185 return this.GetString(field);
186 }
187 else if (valueType == typeof(Stream))
188 {
189 return this.IsNull(field) ? null : new RecordStream(this, field);
190 }
191 else
192 {
193 int? value = this.GetNullableInteger(field);
194 return value.HasValue ? (object) value.Value : null;
195 }
196 }
197 }
198
199 set
200 {
201 if (field == 0)
202 {
203 if (value == null)
204 {
205 value = String.Empty;
206 }
207
208 this.SetString(0, value.ToString());
209 }
210 else if (value == null)
211 {
212 this.SetNullableInteger(field, null);
213 }
214 else
215 {
216 Type valueType = value.GetType();
217 if (valueType == typeof(Int32) || valueType == typeof(Int16))
218 {
219 this.SetInteger(field, (int) value);
220 }
221 else if (valueType.IsSubclassOf(typeof(Stream)))
222 {
223 this.SetStream(field, (Stream) value);
224 }
225 else
226 {
227 this.SetString(field, value.ToString());
228 }
229 }
230 }
231 }
232
233 /// <summary>
234 /// Creates a new Record object from an integer record handle.
235 /// </summary>
236 /// <remarks><p>
237 /// This method is only provided for interop purposes. A Record object
238 /// should normally be obtained by calling <see cref="WixToolset.Dtf.WindowsInstaller.View.Fetch"/>
239 /// other methods.
240 /// <p>The handle will be closed when this object is disposed or finalized.</p>
241 /// </p></remarks>
242 /// <param name="handle">Integer record handle</param>
243 /// <param name="ownsHandle">true to close the handle when this object is disposed or finalized</param>
244 public static Record FromHandle(IntPtr handle, bool ownsHandle)
245 {
246 return new Record(handle, ownsHandle, (View) null);
247 }
248
249 /// <summary>
250 /// Sets all fields in a record to null.
251 /// </summary>
252 /// <remarks><p>
253 /// Win32 MSI API:
254 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordcleardata.asp">MsiRecordClearData</a>
255 /// </p></remarks>
256 public void Clear()
257 {
258 uint ret = RemotableNativeMethods.MsiRecordClearData((int) this.Handle);
259 if (ret != 0)
260 {
261 throw InstallerException.ExceptionFromReturnCode(ret);
262 }
263 }
264
265 /// <summary>
266 /// Reports whether a record field is null.
267 /// </summary>
268 /// <param name="field">Specifies the field to check.</param>
269 /// <returns>True if the field is null, false otherwise.</returns>
270 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
271 /// number of fields in the Record.</exception>
272 /// <remarks><p>
273 /// Win32 MSI API:
274 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordisnull.asp">MsiRecordIsNull</a>
275 /// </p></remarks>
276 public bool IsNull(int field)
277 {
278 this.CheckRange(field);
279 return RemotableNativeMethods.MsiRecordIsNull((int) this.Handle, (uint) field);
280 }
281
282 /// <summary>
283 /// Reports whether a record field is null.
284 /// </summary>
285 /// <param name="fieldName">Specifies the field to check.</param>
286 /// <returns>True if the field is null, false otherwise.</returns>
287 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
288 /// of the named fields in the Record.</exception>
289 public bool IsNull(string fieldName)
290 {
291 int field = this.FindColumn(fieldName);
292 return this.IsNull(field);
293 }
294
295 /// <summary>
296 /// Gets the length of a record field. The count does not include the terminating null.
297 /// </summary>
298 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
299 /// number of fields in the Record.</exception>
300 /// <remarks><p>
301 /// The returned data size is 0 if the field is null, non-existent,
302 /// or an internal object pointer. The method also returns 0 if the handle is not a valid
303 /// Record handle.
304 /// </p><p>
305 /// If the data is in integer format, the property returns 2 or 4.
306 /// </p><p>
307 /// If the data is in string format, the property returns the character count
308 /// (not including the NULL terminator).
309 /// </p><p>
310 /// If the data is in stream format, the property returns the byte count.
311 /// </p><p>
312 /// Win32 MSI API:
313 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecorddatasize.asp">MsiRecordDataSize</a>
314 /// </p></remarks>
315 public int GetDataSize(int field)
316 {
317 this.CheckRange(field);
318 return (int) RemotableNativeMethods.MsiRecordDataSize((int) this.Handle, (uint) field);
319 }
320
321 /// <summary>
322 /// Gets the length of a record field. The count does not include the terminating null.
323 /// </summary>
324 /// <param name="fieldName">Specifies the field to check.</param>
325 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
326 /// of the named fields in the Record.</exception>
327 /// <remarks><p>The returned data size is 0 if the field is null, non-existent,
328 /// or an internal object pointer. The method also returns 0 if the handle is not a valid
329 /// Record handle.
330 /// </p><p>
331 /// If the data is in integer format, the property returns 2 or 4.
332 /// </p><p>
333 /// If the data is in string format, the property returns the character count
334 /// (not including the NULL terminator).
335 /// </p><p>
336 /// If the data is in stream format, the property returns the byte count.
337 /// </p></remarks>
338 public int GetDataSize(string fieldName)
339 {
340 int field = this.FindColumn(fieldName);
341 return this.GetDataSize(field);
342 }
343
344 /// <summary>
345 /// Gets a field value as an integer.
346 /// </summary>
347 /// <param name="field">Specifies the field to retrieve.</param>
348 /// <returns>Integer value of the field, or 0 if the field is null.</returns>
349 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
350 /// number of fields in the Record.</exception>
351 /// <remarks><p>
352 /// Win32 MSI API:
353 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetinteger.asp">MsiRecordGetInteger</a>
354 /// </p></remarks>
355 /// <seealso cref="GetNullableInteger(int)"/>
356 [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")]
357 public int GetInteger(int field)
358 {
359 this.CheckRange(field);
360
361 int value = RemotableNativeMethods.MsiRecordGetInteger((int) this.Handle, (uint) field);
362 if (value == Int32.MinValue) // MSI_NULL_INTEGER
363 {
364 return 0;
365 }
366 return value;
367 }
368
369 /// <summary>
370 /// Gets a field value as an integer.
371 /// </summary>
372 /// <param name="fieldName">Specifies the field to retrieve.</param>
373 /// <returns>Integer value of the field, or 0 if the field is null.</returns>
374 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
375 /// of the named fields in the Record.</exception>
376 /// <seealso cref="GetNullableInteger(string)"/>
377 [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")]
378 public int GetInteger(string fieldName)
379 {
380 int field = this.FindColumn(fieldName);
381 return this.GetInteger(field);
382 }
383
384 /// <summary>
385 /// Gets a field value as an integer.
386 /// </summary>
387 /// <param name="field">Specifies the field to retrieve.</param>
388 /// <returns>Integer value of the field, or null if the field is null.</returns>
389 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
390 /// number of fields in the Record.</exception>
391 /// <remarks><p>
392 /// Win32 MSI API:
393 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetinteger.asp">MsiRecordGetInteger</a>
394 /// </p></remarks>
395 /// <seealso cref="GetInteger(int)"/>
396 public int? GetNullableInteger(int field)
397 {
398 this.CheckRange(field);
399
400 int value = RemotableNativeMethods.MsiRecordGetInteger((int) this.Handle, (uint) field);
401 if (value == Int32.MinValue) // MSI_NULL_INTEGER
402 {
403 return null;
404 }
405 return value;
406 }
407
408 /// <summary>
409 /// Gets a field value as an integer.
410 /// </summary>
411 /// <param name="fieldName">Specifies the field to retrieve.</param>
412 /// <returns>Integer value of the field, or null if the field is null.</returns>
413 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
414 /// of the named fields in the Record.</exception>
415 /// <seealso cref="GetInteger(string)"/>
416 public int? GetNullableInteger(string fieldName)
417 {
418 int field = this.FindColumn(fieldName);
419 return this.GetInteger(field);
420 }
421
422 /// <summary>
423 /// Sets the value of a field to an integer.
424 /// </summary>
425 /// <param name="field">Specifies the field to set.</param>
426 /// <param name="value">new value of the field</param>
427 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
428 /// number of fields in the Record.</exception>
429 /// <remarks><p>
430 /// Win32 MSI API:
431 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetinteger.asp">MsiRecordSetInteger</a>
432 /// </p></remarks>
433 /// <seealso cref="SetNullableInteger(int,int?)"/>
434 public void SetInteger(int field, int value)
435 {
436 this.CheckRange(field);
437
438 uint ret = RemotableNativeMethods.MsiRecordSetInteger((int) this.Handle, (uint) field, value);
439 if (ret != 0)
440 {
441 throw InstallerException.ExceptionFromReturnCode(ret);
442 }
443 }
444
445 /// <summary>
446 /// Sets the value of a field to an integer.
447 /// </summary>
448 /// <param name="fieldName">Specifies the field to set.</param>
449 /// <param name="value">new value of the field</param>
450 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
451 /// of the named fields in the Record.</exception>
452 /// <seealso cref="SetNullableInteger(string,int?)"/>
453 public void SetInteger(string fieldName, int value)
454 {
455 int field = this.FindColumn(fieldName);
456 this.SetInteger(field, value);
457 }
458
459 /// <summary>
460 /// Sets the value of a field to a nullable integer.
461 /// </summary>
462 /// <param name="field">Specifies the field to set.</param>
463 /// <param name="value">new value of the field</param>
464 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
465 /// number of fields in the Record.</exception>
466 /// <remarks><p>
467 /// Win32 MSI API:
468 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetinteger.asp">MsiRecordSetInteger</a>
469 /// </p></remarks>
470 /// <seealso cref="SetInteger(int,int)"/>
471 public void SetNullableInteger(int field, int? value)
472 {
473 this.CheckRange(field);
474
475 uint ret = RemotableNativeMethods.MsiRecordSetInteger(
476 (int) this.Handle,
477 (uint) field,
478 value.HasValue ? (int) value : Int32.MinValue);
479 if (ret != 0)
480 {
481 throw InstallerException.ExceptionFromReturnCode(ret);
482 }
483 }
484
485 /// <summary>
486 /// Sets the value of a field to a nullable integer.
487 /// </summary>
488 /// <param name="fieldName">Specifies the field to set.</param>
489 /// <param name="value">new value of the field</param>
490 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
491 /// of the named fields in the Record.</exception>
492 /// <seealso cref="SetInteger(string,int)"/>
493 public void SetNullableInteger(string fieldName, int? value)
494 {
495 int field = this.FindColumn(fieldName);
496 this.SetNullableInteger(field, value);
497 }
498
499 /// <summary>
500 /// Gets a field value as a string.
501 /// </summary>
502 /// <param name="field">Specifies the field to retrieve.</param>
503 /// <returns>String value of the field, or an empty string if the field is null.</returns>
504 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
505 /// number of fields in the Record.</exception>
506 /// <remarks><p>
507 /// Win32 MSI API:
508 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetstring.asp">MsiRecordGetString</a>
509 /// </p></remarks>
510 public string GetString(int field)
511 {
512 this.CheckRange(field);
513
514 StringBuilder buf = new StringBuilder(String.Empty);
515 uint bufSize = 0;
516 uint ret = RemotableNativeMethods.MsiRecordGetString((int) this.Handle, (uint) field, buf, ref bufSize);
517 if (ret == (uint) NativeMethods.Error.MORE_DATA)
518 {
519 buf.Capacity = (int) ++bufSize;
520 ret = RemotableNativeMethods.MsiRecordGetString((int) this.Handle, (uint) field, buf, ref bufSize);
521 }
522 if (ret != 0)
523 {
524 throw InstallerException.ExceptionFromReturnCode(ret);
525 }
526 return buf.ToString();
527 }
528
529 /// <summary>
530 /// Gets a field value as a string.
531 /// </summary>
532 /// <param name="fieldName">Specifies the field to retrieve.</param>
533 /// <returns>String value of the field, or an empty string if the field is null.</returns>
534 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
535 /// of the named fields in the Record.</exception>
536 public string GetString(string fieldName)
537 {
538 int field = this.FindColumn(fieldName);
539 return this.GetString(field);
540 }
541
542 /// <summary>
543 /// Sets the value of a field to a string.
544 /// </summary>
545 /// <param name="field">Specifies the field to set.</param>
546 /// <param name="value">new value of the field</param>
547 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
548 /// number of fields in the Record.</exception>
549 /// <remarks><p>
550 /// Win32 MSI API:
551 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetstring.asp">MsiRecordSetString</a>
552 /// </p></remarks>
553 public void SetString(int field, string value)
554 {
555 this.CheckRange(field);
556
557 if (value == null)
558 {
559 value = String.Empty;
560 }
561
562 uint ret = RemotableNativeMethods.MsiRecordSetString((int) this.Handle, (uint) field, value);
563 if (ret != 0)
564 {
565 throw InstallerException.ExceptionFromReturnCode(ret);
566 }
567
568 // If we set the FormatString manually, then it should be valid again
569 if (field == 0)
570 {
571 this.IsFormatStringInvalid = false;
572 }
573 }
574
575 /// <summary>
576 /// Sets the value of a field to a string.
577 /// </summary>
578 /// <param name="fieldName">Specifies the field to set.</param>
579 /// <param name="value">new value of the field</param>
580 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
581 /// of the named fields in the Record.</exception>
582 public void SetString(string fieldName, string value)
583 {
584 int field = this.FindColumn(fieldName);
585 this.SetString(field, value);
586 }
587
588 /// <summary>
589 /// Reads a record stream field into a file.
590 /// </summary>
591 /// <param name="field">Specifies the field of the Record to get.</param>
592 /// <param name="filePath">Specifies the path to the file to contain the stream.</param>
593 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
594 /// number of fields in the Record.</exception>
595 /// <exception cref="NotSupportedException">Attempt to extract a storage from a database open
596 /// in read-write mode, or from a database without an associated file path</exception>
597 /// <remarks><p>
598 /// This method is capable of directly extracting substorages. To do so, first select both the
599 /// `Name` and `Data` column of the `_Storages` table, then get the stream of the `Data` field.
600 /// However, substorages may only be extracted from a database that is open in read-only mode.
601 /// </p><p>
602 /// Win32 MSI API:
603 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordreadstream.asp">MsiRecordReadStream</a>
604 /// </p></remarks>
605 public void GetStream(int field, string filePath)
606 {
607 if (String.IsNullOrEmpty(filePath))
608 {
609 throw new ArgumentNullException("filePath");
610 }
611
612 IList<TableInfo> tables = (this.view != null ? this.view.Tables : null);
613 if (tables != null && tables.Count == 1 && tables[0].Name == "_Storages" && field == this.FindColumn("Data"))
614 {
615 if (!this.view.Database.IsReadOnly)
616 {
617 throw new NotSupportedException("Database must be opened read-only to support substorage extraction.");
618 }
619 else if (this.view.Database.FilePath == null)
620 {
621 throw new NotSupportedException("Database must have an associated file path to support substorage extraction.");
622 }
623 else if (this.FindColumn("Name") <= 0)
624 {
625 throw new NotSupportedException("Name column must be part of the Record in order to extract substorage.");
626 }
627 else
628 {
629 Record.ExtractSubStorage(this.view.Database.FilePath, this.GetString("Name"), filePath);
630 }
631 }
632 else
633 {
634 if (!this.IsNull(field))
635 {
636 Stream readStream = null, writeStream = null;
637 try
638 {
639 readStream = new RecordStream(this, field);
640 writeStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
641 int count = 512;
642 byte[] buf = new byte[count];
643 while (count == buf.Length)
644 {
645 if ((count = readStream.Read(buf, 0, buf.Length)) > 0)
646 {
647 writeStream.Write(buf, 0, count);
648 }
649 }
650 }
651 finally
652 {
653 if (readStream != null) readStream.Close();
654 if (writeStream != null) writeStream.Close();
655 }
656 }
657 }
658 }
659
660 /// <summary>
661 /// Reads a record stream field into a file.
662 /// </summary>
663 /// <param name="fieldName">Specifies the field of the Record to get.</param>
664 /// <param name="filePath">Specifies the path to the file to contain the stream.</param>
665 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
666 /// of the named fields in the Record.</exception>
667 /// <exception cref="NotSupportedException">Attempt to extract a storage from a database open
668 /// in read-write mode, or from a database without an associated file path</exception>
669 /// <remarks><p>
670 /// This method is capable of directly extracting substorages. To do so, first select both the
671 /// `Name` and `Data` column of the `_Storages` table, then get the stream of the `Data` field.
672 /// However, substorages may only be extracted from a database that is open in read-only mode.
673 /// </p></remarks>
674 public void GetStream(string fieldName, string filePath)
675 {
676 int field = this.FindColumn(fieldName);
677 this.GetStream(field, filePath);
678 }
679
680 /// <summary>
681 /// Gets a record stream field.
682 /// </summary>
683 /// <param name="field">Specifies the field of the Record to get.</param>
684 /// <returns>A Stream that reads the field data.</returns>
685 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
686 /// number of fields in the Record.</exception>
687 /// <remarks><p>
688 /// This method is not capable of reading substorages. To extract a substorage,
689 /// use <see cref="GetStream(int,string)"/>.
690 /// </p><p>
691 /// Win32 MSI API:
692 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordreadstream.asp">MsiRecordReadStream</a>
693 /// </p></remarks>
694 public Stream GetStream(int field)
695 {
696 this.CheckRange(field);
697
698 return this.IsNull(field) ? null : new RecordStream(this, field);
699 }
700
701 /// <summary>
702 /// Gets a record stream field.
703 /// </summary>
704 /// <param name="fieldName">Specifies the field of the Record to get.</param>
705 /// <returns>A Stream that reads the field data.</returns>
706 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
707 /// of the named fields in the Record.</exception>
708 /// <remarks><p>
709 /// This method is not capable of reading substorages. To extract a substorage,
710 /// use <see cref="GetStream(string,string)"/>.
711 /// </p></remarks>
712 public Stream GetStream(string fieldName)
713 {
714 int field = this.FindColumn(fieldName);
715 return this.GetStream(field);
716 }
717
718 /// <summary>
719 /// Sets a record stream field from a file. Stream data cannot be inserted into temporary fields.
720 /// </summary>
721 /// <param name="field">Specifies the field of the Record to set.</param>
722 /// <param name="filePath">Specifies the path to the file containing the stream.</param>
723 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
724 /// number of fields in the Record.</exception>
725 /// <remarks><p>
726 /// The contents of the specified file are read into a stream object. The stream persists if
727 /// the Record is inserted into the Database and the Database is committed.
728 /// </p><p>
729 /// To reset the stream to its beginning you must pass in null for filePath.
730 /// Do not pass an empty string, "", to reset the stream.
731 /// </p><p>
732 /// Setting a stream with this method is more efficient than setting a field to a
733 /// FileStream object.
734 /// </p><p>
735 /// Win32 MSI API:
736 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetstream.asp">MsiRecordsetStream</a>
737 /// </p></remarks>
738 public void SetStream(int field, string filePath)
739 {
740 this.CheckRange(field);
741
742 if (String.IsNullOrEmpty(filePath))
743 {
744 throw new ArgumentNullException("filePath");
745 }
746
747 uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, filePath);
748 if (ret != 0)
749 {
750 throw InstallerException.ExceptionFromReturnCode(ret);
751 }
752 }
753
754 /// <summary>
755 /// Sets a record stream field from a file. Stream data cannot be inserted into temporary fields.
756 /// </summary>
757 /// <param name="fieldName">Specifies the field name of the Record to set.</param>
758 /// <param name="filePath">Specifies the path to the file containing the stream.</param>
759 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
760 /// of the named fields in the Record.</exception>
761 /// <remarks><p>
762 /// The contents of the specified file are read into a stream object. The stream persists if
763 /// the Record is inserted into the Database and the Database is committed.
764 /// To reset the stream to its beginning you must pass in null for filePath.
765 /// Do not pass an empty string, "", to reset the stream.
766 /// </p><p>
767 /// Setting a stream with this method is more efficient than setting a field to a
768 /// FileStream object.
769 /// </p></remarks>
770 public void SetStream(string fieldName, string filePath)
771 {
772 int field = this.FindColumn(fieldName);
773 this.SetStream(field, filePath);
774 }
775
776 /// <summary>
777 /// Sets a record stream field from a Stream object. Stream data cannot be inserted into temporary fields.
778 /// </summary>
779 /// <param name="field">Specifies the field of the Record to set.</param>
780 /// <param name="stream">Specifies the stream data.</param>
781 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
782 /// number of fields in the Record.</exception>
783 /// <remarks><p>
784 /// The stream persists if the Record is inserted into the Database and the Database is committed.
785 /// </p><p>
786 /// Win32 MSI API:
787 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetstream.asp">MsiRecordsetStream</a>
788 /// </p></remarks>
789 public void SetStream(int field, Stream stream)
790 {
791 this.CheckRange(field);
792
793 if (stream == null)
794 {
795 uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, null);
796 if (ret != 0)
797 {
798 throw InstallerException.ExceptionFromReturnCode(ret);
799 }
800 }
801 else
802 {
803 Stream writeStream = null;
804 string tempPath = Path.GetTempFileName();
805 try
806 {
807 writeStream = new FileStream(tempPath, FileMode.Truncate, FileAccess.Write);
808 byte[] buf = new byte[512];
809 int count;
810 while ((count = stream.Read(buf, 0, buf.Length)) > 0)
811 {
812 writeStream.Write(buf, 0, count);
813 }
814 writeStream.Close();
815 writeStream = null;
816
817 uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, tempPath);
818 if (ret != 0)
819 {
820 throw InstallerException.ExceptionFromReturnCode(ret);
821 }
822 }
823 finally
824 {
825 if (writeStream != null) writeStream.Close();
826 if (File.Exists(tempPath))
827 {
828 try
829 {
830 File.Delete(tempPath);
831 }
832 catch (IOException)
833 {
834 if (this.view != null)
835 {
836 this.view.Database.DeleteOnClose(tempPath);
837 }
838 }
839 }
840 }
841 }
842 }
843
844 /// <summary>
845 /// Sets a record stream field from a Stream object. Stream data cannot be inserted into temporary fields.
846 /// </summary>
847 /// <param name="fieldName">Specifies the field name of the Record to set.</param>
848 /// <param name="stream">Specifies the stream data.</param>
849 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
850 /// of the named fields in the Record.</exception>
851 /// <remarks><p>
852 /// The stream persists if the Record is inserted into the Database and the Database is committed.
853 /// </p></remarks>
854 public void SetStream(string fieldName, Stream stream)
855 {
856 int field = this.FindColumn(fieldName);
857 this.SetStream(field, stream);
858 }
859
860 /// <summary>
861 /// Gets a formatted string representation of the Record.
862 /// </summary>
863 /// <returns>A formatted string representation of the Record.</returns>
864 /// <remarks><p>
865 /// If field 0 of the Record is set to a nonempty string, it is used to format the data in the Record.
866 /// </p><p>
867 /// Win32 MSI API:
868 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
869 /// </p></remarks>
870 /// <seealso cref="FormatString"/>
871 /// <seealso cref="Session.FormatRecord(Record)"/>
872 public override string ToString()
873 {
874 return this.ToString((IFormatProvider) null);
875 }
876
877 /// <summary>
878 /// Gets a formatted string representation of the Record, optionally using a Session to format properties.
879 /// </summary>
880 /// <param name="provider">an optional Session instance that will be used to lookup any
881 /// properties in the Record's format string</param>
882 /// <returns>A formatted string representation of the Record.</returns>
883 /// <remarks><p>
884 /// If field 0 of the Record is set to a nonempty string, it is used to format the data in the Record.
885 /// </p><p>
886 /// Win32 MSI API:
887 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
888 /// </p></remarks>
889 /// <seealso cref="FormatString"/>
890 /// <seealso cref="Session.FormatRecord(Record)"/>
891 public string ToString(IFormatProvider provider)
892 {
893 if (this.IsFormatStringInvalid) // Format string is invalid
894 {
895 // TODO: return all values by default?
896 return String.Empty;
897 }
898
899 InstallerHandle session = provider as InstallerHandle;
900 int sessionHandle = session != null ? (int) session.Handle : 0;
901 StringBuilder buf = new StringBuilder(String.Empty);
902 uint bufSize = 1;
903 uint ret = RemotableNativeMethods.MsiFormatRecord(sessionHandle, (int) this.Handle, buf, ref bufSize);
904 if (ret == (uint) NativeMethods.Error.MORE_DATA)
905 {
906 bufSize++;
907 buf = new StringBuilder((int) bufSize);
908 ret = RemotableNativeMethods.MsiFormatRecord(sessionHandle, (int) this.Handle, buf, ref bufSize);
909 }
910 if (ret != 0)
911 {
912 throw InstallerException.ExceptionFromReturnCode(ret);
913 }
914 return buf.ToString();
915 }
916
917 /// <summary>
918 /// Gets a formatted string representation of the Record.
919 /// </summary>
920 /// <param name="format">String to be used to format the data in the Record,
921 /// instead of the Record's format string.</param>
922 /// <returns>A formatted string representation of the Record.</returns>
923 /// <remarks><p>
924 /// Win32 MSI API:
925 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
926 /// </p></remarks>
927 [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the FormatString " +
928 "property separately before calling the ToString() override that takes no parameters.")]
929 public string ToString(string format)
930 {
931 return this.ToString(format, null);
932 }
933
934 /// <summary>
935 /// Gets a formatted string representation of the Record, optionally using a Session to format properties.
936 /// </summary>
937 /// <param name="format">String to be used to format the data in the Record,
938 /// instead of the Record's format string.</param>
939 /// <param name="provider">an optional Session instance that will be used to lookup any
940 /// properties in the Record's format string</param>
941 /// <returns>A formatted string representation of the Record.</returns>
942 /// <remarks><p>
943 /// Win32 MSI API:
944 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
945 /// </p></remarks>
946 /// <seealso cref="FormatString"/>
947 /// <seealso cref="Session.FormatRecord(Record)"/>
948 [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the FormatString " +
949 "property separately before calling the ToString() override that takes just a format provider.")]
950 public string ToString(string format, IFormatProvider provider)
951 {
952 if (format == null)
953 {
954 return this.ToString(provider);
955 }
956 else if (format.Length == 0)
957 {
958 return String.Empty;
959 }
960 else
961 {
962 string savedFormatString = (string) this[0];
963 try
964 {
965 this.FormatString = format;
966 return this.ToString(provider);
967 }
968 finally
969 {
970 this.FormatString = savedFormatString;
971 }
972 }
973 }
974
975 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
976 private static void ExtractSubStorage(string databaseFile, string storageName, string extractFile)
977 {
978 IStorage storage;
979 NativeMethods.STGM openMode = NativeMethods.STGM.READ | NativeMethods.STGM.SHARE_DENY_WRITE;
980 int hr = NativeMethods.StgOpenStorage(databaseFile, IntPtr.Zero, (uint) openMode, IntPtr.Zero, 0, out storage);
981 if (hr != 0)
982 {
983 Marshal.ThrowExceptionForHR(hr);
984 }
985
986 try
987 {
988 openMode = NativeMethods.STGM.READ | NativeMethods.STGM.SHARE_EXCLUSIVE;
989 IStorage subStorage = storage.OpenStorage(storageName, IntPtr.Zero, (uint) openMode, IntPtr.Zero, 0);
990
991 try
992 {
993 IStorage newStorage;
994 openMode = NativeMethods.STGM.CREATE | NativeMethods.STGM.READWRITE | NativeMethods.STGM.SHARE_EXCLUSIVE;
995 hr = NativeMethods.StgCreateDocfile(extractFile, (uint) openMode, 0, out newStorage);
996 if (hr != 0)
997 {
998 Marshal.ThrowExceptionForHR(hr);
999 }
1000
1001 try
1002 {
1003 subStorage.CopyTo(0, IntPtr.Zero, IntPtr.Zero, newStorage);
1004
1005 newStorage.Commit(0);
1006 }
1007 finally
1008 {
1009 Marshal.ReleaseComObject(newStorage);
1010 }
1011 }
1012 finally
1013 {
1014 Marshal.ReleaseComObject(subStorage);
1015 }
1016 }
1017 finally
1018 {
1019 Marshal.ReleaseComObject(storage);
1020 }
1021 }
1022
1023 private int FindColumn(string fieldName)
1024 {
1025 if (this.view == null)
1026 {
1027 throw new InvalidOperationException();
1028 }
1029 ColumnCollection columns = this.view.Columns;
1030 for (int i = 0; i < columns.Count; i++)
1031 {
1032 if (columns[i].Name == fieldName)
1033 {
1034 return i + 1;
1035 }
1036 }
1037 throw new ArgumentOutOfRangeException("fieldName");
1038 }
1039
1040 private void CheckRange(int field)
1041 {
1042 if (field < 0 || field > this.FieldCount)
1043 {
1044 throw new ArgumentOutOfRangeException("field");
1045 }
1046 }
1047 }
1048}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/RecordStream.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/RecordStream.cs
new file mode 100644
index 00000000..82e8fb46
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/RecordStream.cs
@@ -0,0 +1,92 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7
8 internal class RecordStream : Stream
9 {
10 private Record record;
11 private int field;
12 private long position;
13
14 internal RecordStream(Record record, int field)
15 : base()
16 {
17 this.record = record;
18 this.field = field;
19 }
20
21 public override bool CanRead { get { return true; } }
22 public override bool CanWrite { get { return false; } }
23 public override bool CanSeek { get { return false; } }
24
25 public override long Length
26 {
27 get
28 {
29 return this.record.GetDataSize(this.field);
30 }
31 }
32
33 public override long Position
34 {
35 get
36 {
37 return this.position;
38 }
39
40 set
41 {
42 throw new NotSupportedException();
43 }
44 }
45
46 public override long Seek(long offset, SeekOrigin origin)
47 {
48 throw new NotSupportedException();
49 }
50
51 public override void SetLength(long value)
52 {
53 throw new NotSupportedException();
54 }
55
56 public override void Flush()
57 {
58 throw new NotSupportedException();
59 }
60
61 public override int Read(byte[] buffer, int offset, int count)
62 {
63 if (count > 0)
64 {
65 byte[] readBuffer = (offset == 0 ? buffer : new byte[count]);
66 uint ucount = (uint) count;
67 uint ret = RemotableNativeMethods.MsiRecordReadStream((int) this.record.Handle, (uint) this.field, buffer, ref ucount);
68 if (ret != 0)
69 {
70 throw InstallerException.ExceptionFromReturnCode(ret);
71 }
72 count = (int) ucount;
73 if (offset > 0)
74 {
75 Array.Copy(readBuffer, 0, buffer, offset, count);
76 }
77 this.position += count;
78 }
79 return count;
80 }
81
82 public override void Write(byte[] array, int offset, int count)
83 {
84 throw new NotSupportedException();
85 }
86
87 public override string ToString()
88 {
89 return "[Binary data]";
90 }
91 }
92}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/RemotableNativeMethods.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/RemotableNativeMethods.cs
new file mode 100644
index 00000000..960fd15f
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/RemotableNativeMethods.cs
@@ -0,0 +1,1171 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Runtime.InteropServices;
8 using System.Diagnostics.CodeAnalysis;
9
10 /// <summary>
11 /// Assigns ID numbers to the MSI APIs that are remotable.
12 /// </summary>
13 /// <remarks><p>
14 /// This enumeration MUST stay in sync with the
15 /// unmanaged equivalent in RemoteMsiSession.h!
16 /// </p></remarks>
17 internal enum RemoteMsiFunctionId
18 {
19 EndSession = 0,
20 MsiCloseHandle,
21 MsiCreateRecord,
22 MsiDatabaseGetPrimaryKeys,
23 MsiDatabaseIsTablePersistent,
24 MsiDatabaseOpenView,
25 MsiDoAction,
26 MsiEnumComponentCosts,
27 MsiEvaluateCondition,
28 MsiFormatRecord,
29 MsiGetActiveDatabase,
30 MsiGetComponentState,
31 MsiGetFeatureCost,
32 MsiGetFeatureState,
33 MsiGetFeatureValidStates,
34 MsiGetLanguage,
35 MsiGetLastErrorRecord,
36 MsiGetMode,
37 MsiGetProperty,
38 MsiGetSourcePath,
39 MsiGetSummaryInformation,
40 MsiGetTargetPath,
41 MsiProcessMessage,
42 MsiRecordClearData,
43 MsiRecordDataSize,
44 MsiRecordGetFieldCount,
45 MsiRecordGetInteger,
46 MsiRecordGetString,
47 MsiRecordIsNull,
48 MsiRecordReadStream,
49 MsiRecordSetInteger,
50 MsiRecordSetStream,
51 MsiRecordSetString,
52 MsiSequence,
53 MsiSetComponentState,
54 MsiSetFeatureAttributes,
55 MsiSetFeatureState,
56 MsiSetInstallLevel,
57 MsiSetMode,
58 MsiSetProperty,
59 MsiSetTargetPath,
60 MsiSummaryInfoGetProperty,
61 MsiVerifyDiskSpace,
62 MsiViewExecute,
63 MsiViewFetch,
64 MsiViewGetError,
65 MsiViewGetColumnInfo,
66 MsiViewModify,
67 }
68
69 /// <summary>
70 /// Defines the signature of the native function
71 /// in SfxCA.dll that implements the remoting call.
72 /// </summary>
73 internal delegate void MsiRemoteInvoke(
74 RemoteMsiFunctionId id,
75 [MarshalAs(UnmanagedType.SysInt)]
76 IntPtr request,
77 [MarshalAs(UnmanagedType.SysInt)]
78 out IntPtr response);
79
80 /// <summary>
81 /// Redirects native API calls to either the normal NativeMethods class
82 /// or to out-of-proc calls via the remoting channel.
83 /// </summary>
84 internal static class RemotableNativeMethods
85 {
86 private const int MAX_REQUEST_FIELDS = 4;
87 private static int requestFieldDataOffset;
88 private static int requestFieldSize;
89
90 [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")]
91 private static IntPtr requestBuf;
92
93 private static MsiRemoteInvoke remotingDelegate;
94
95 /// <summary>
96 /// Checks if the current process is using remoting to access the
97 /// MSI session and database APIs.
98 /// </summary>
99 internal static bool RemotingEnabled
100 {
101 get
102 {
103 return RemotableNativeMethods.remotingDelegate != null;
104 }
105 }
106
107 /// <summary>
108 /// Sets a delegate that is used to make remote API calls.
109 /// </summary>
110 /// <remarks><p>
111 /// The implementation of this delegate is provided by the
112 /// custom action host DLL.
113 /// </p></remarks>
114 [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
115 internal static MsiRemoteInvoke RemotingDelegate
116 {
117 set
118 {
119 RemotableNativeMethods.remotingDelegate = value;
120
121 if (value != null && requestBuf == IntPtr.Zero)
122 {
123 requestFieldDataOffset = Marshal.SizeOf(typeof(IntPtr));
124 requestFieldSize = 2 * Marshal.SizeOf(typeof(IntPtr));
125 RemotableNativeMethods.requestBuf = Marshal.AllocHGlobal(
126 requestFieldSize * MAX_REQUEST_FIELDS);
127 }
128 }
129 }
130
131 internal static bool IsRemoteHandle(int handle)
132 {
133 return (handle & Int32.MinValue) != 0;
134 }
135
136 internal static int MakeRemoteHandle(int handle)
137 {
138 if (handle == 0)
139 {
140 return handle;
141 }
142
143 if (RemotableNativeMethods.IsRemoteHandle(handle))
144 {
145 throw new InvalidOperationException("Handle already has the remote bit set.");
146 }
147
148 return handle ^ Int32.MinValue;
149 }
150
151 internal static int GetRemoteHandle(int handle)
152 {
153 if (handle == 0)
154 {
155 return handle;
156 }
157
158 if (!RemotableNativeMethods.IsRemoteHandle(handle))
159 {
160 throw new InvalidOperationException("Handle does not have the remote bit set.");
161 }
162
163 return handle ^ Int32.MinValue;
164 }
165
166 private static void ClearData(IntPtr buf)
167 {
168 for (int i = 0; i < MAX_REQUEST_FIELDS; i++)
169 {
170 Marshal.WriteInt32(buf, (i * requestFieldSize), (int) VarEnum.VT_NULL);
171 Marshal.WriteIntPtr(buf, (i * requestFieldSize) + requestFieldDataOffset, IntPtr.Zero);
172 }
173 }
174
175 private static void WriteInt(IntPtr buf, int field, int value)
176 {
177 Marshal.WriteInt32(buf, (field * requestFieldSize), (int) VarEnum.VT_I4);
178 Marshal.WriteInt32(buf, (field * requestFieldSize) + requestFieldDataOffset, value);
179 }
180
181 private static void WriteString(IntPtr buf, int field, string value)
182 {
183 if (value == null)
184 {
185 Marshal.WriteInt32(buf, (field * requestFieldSize), (int) VarEnum.VT_NULL);
186 Marshal.WriteIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset, IntPtr.Zero);
187 }
188 else
189 {
190 IntPtr stringPtr = Marshal.StringToHGlobalUni(value);
191 Marshal.WriteInt32(buf, (field * requestFieldSize), (int) VarEnum.VT_LPWSTR);
192 Marshal.WriteIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset, stringPtr);
193 }
194 }
195
196 private static int ReadInt(IntPtr buf, int field)
197 {
198 VarEnum vt = (VarEnum) Marshal.ReadInt32(buf, (field * requestFieldSize));
199 if (vt == VarEnum.VT_EMPTY)
200 {
201 return 0;
202 }
203 else if (vt != VarEnum.VT_I4 && vt != VarEnum.VT_UI4)
204 {
205 throw new InstallerException("Invalid data received from remote MSI function invocation.");
206 }
207 return Marshal.ReadInt32(buf, (field * requestFieldSize) + requestFieldDataOffset);
208 }
209
210 private static void ReadString(IntPtr buf, int field, StringBuilder szBuf, ref uint cchBuf)
211 {
212 VarEnum vt = (VarEnum) Marshal.ReadInt32(buf, (field * requestFieldSize));
213 if (vt == VarEnum.VT_NULL)
214 {
215 szBuf.Remove(0, szBuf.Length);
216 return;
217 }
218 else if (vt != VarEnum.VT_LPWSTR)
219 {
220 throw new InstallerException("Invalid data received from remote MSI function invocation.");
221 }
222
223 szBuf.Remove(0, szBuf.Length);
224 IntPtr strPtr = Marshal.ReadIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset);
225 string str = Marshal.PtrToStringUni(strPtr);
226 if (str != null)
227 {
228 szBuf.Append(str);
229 }
230 cchBuf = (uint) szBuf.Length;
231 }
232
233 private static void FreeString(IntPtr buf, int field)
234 {
235 IntPtr stringPtr = Marshal.ReadIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset);
236 if (stringPtr != IntPtr.Zero)
237 {
238 Marshal.FreeHGlobal(stringPtr);
239 }
240 }
241
242 private static void ReadStream(IntPtr buf, int field, byte[] sBuf, int count)
243 {
244 VarEnum vt = (VarEnum) Marshal.ReadInt32(buf, (field * requestFieldSize));
245 if (vt != VarEnum.VT_STREAM)
246 {
247 throw new InstallerException("Invalid data received from remote MSI function invocation.");
248 }
249
250 IntPtr sPtr = Marshal.ReadIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset);
251 Marshal.Copy(sPtr, sBuf, 0, count);
252 }
253
254 private static uint MsiFunc_III(RemoteMsiFunctionId id, int in1, int in2, int in3)
255 {
256 lock (RemotableNativeMethods.remotingDelegate)
257 {
258 ClearData(requestBuf);
259 WriteInt(requestBuf, 0, in1);
260 WriteInt(requestBuf, 1, in2);
261 WriteInt(requestBuf, 2, in3);
262 IntPtr resp;
263 remotingDelegate(id, requestBuf, out resp);
264 return unchecked ((uint) ReadInt(resp, 0));
265 }
266 }
267
268 private static uint MsiFunc_IIS(RemoteMsiFunctionId id, int in1, int in2, string in3)
269 {
270 lock (RemotableNativeMethods.remotingDelegate)
271 {
272 ClearData(requestBuf);
273 WriteInt(requestBuf, 0, in1);
274 WriteInt(requestBuf, 1, in2);
275 WriteString(requestBuf, 2, in3);
276 IntPtr resp;
277 remotingDelegate(id, requestBuf, out resp);
278 FreeString(requestBuf, 2);
279 return unchecked ((uint) ReadInt(resp, 0));
280 }
281 }
282
283 private static uint MsiFunc_ISI(RemoteMsiFunctionId id, int in1, string in2, int in3)
284 {
285 lock (RemotableNativeMethods.remotingDelegate)
286 {
287 ClearData(requestBuf);
288 WriteInt(requestBuf, 0, in1);
289 WriteString(requestBuf, 1, in2);
290 WriteInt(requestBuf, 2, in3);
291 IntPtr resp;
292 remotingDelegate(id, requestBuf, out resp);
293 FreeString(requestBuf, 2);
294 return unchecked ((uint) ReadInt(resp, 0));
295 }
296 }
297
298 private static uint MsiFunc_ISS(RemoteMsiFunctionId id, int in1, string in2, string in3)
299 {
300 lock (RemotableNativeMethods.remotingDelegate)
301 {
302 ClearData(requestBuf);
303 WriteInt(requestBuf, 0, in1);
304 WriteString(requestBuf, 1, in2);
305 WriteString(requestBuf, 2, in3);
306 IntPtr resp;
307 remotingDelegate(id, requestBuf, out resp);
308 FreeString(requestBuf, 1);
309 FreeString(requestBuf, 2);
310 return unchecked ((uint) ReadInt(resp, 0));
311 }
312 }
313
314 private static uint MsiFunc_II_I(RemoteMsiFunctionId id, int in1, int in2, out int out1)
315 {
316 lock (RemotableNativeMethods.remotingDelegate)
317 {
318 ClearData(requestBuf);
319 WriteInt(requestBuf, 0, in1);
320 WriteInt(requestBuf, 1, in2);
321 IntPtr resp;
322 remotingDelegate(id, requestBuf, out resp);
323 uint ret = unchecked ((uint) ReadInt(resp, 0));
324 out1 = ReadInt(resp, 1);
325 return ret;
326 }
327 }
328
329 private static uint MsiFunc_ISII_I(RemoteMsiFunctionId id, int in1, string in2, int in3, int in4, out int out1)
330 {
331 lock (RemotableNativeMethods.remotingDelegate)
332 {
333 ClearData(requestBuf);
334 WriteInt(requestBuf, 0, in1);
335 WriteString(requestBuf, 1, in2);
336 WriteInt(requestBuf, 2, in3);
337 WriteInt(requestBuf, 3, in4);
338 IntPtr resp;
339 remotingDelegate(id, requestBuf, out resp);
340 FreeString(requestBuf, 1);
341 uint ret = unchecked ((uint) ReadInt(resp, 0));
342 out1 = ReadInt(resp, 1);
343 return ret;
344 }
345 }
346
347 private static uint MsiFunc_IS_II(RemoteMsiFunctionId id, int in1, string in2, out int out1, out int out2)
348 {
349 lock (RemotableNativeMethods.remotingDelegate)
350 {
351 ClearData(requestBuf);
352 WriteInt(requestBuf, 0, in1);
353 WriteString(requestBuf, 1, in2);
354 IntPtr resp;
355 remotingDelegate(id, requestBuf, out resp);
356 FreeString(requestBuf, 1);
357 uint ret = unchecked ((uint) ReadInt(resp, 0));
358 out1 = ReadInt(resp, 1);
359 out2 = ReadInt(resp, 2);
360 return ret;
361 }
362 }
363
364 private static uint MsiFunc_II_S(RemoteMsiFunctionId id, int in1, int in2, StringBuilder out1, ref uint cchOut1)
365 {
366 lock (RemotableNativeMethods.remotingDelegate)
367 {
368 ClearData(requestBuf);
369 WriteInt(requestBuf, 0, in1);
370 WriteInt(requestBuf, 1, in2);
371 IntPtr resp;
372 remotingDelegate(id, requestBuf, out resp);
373 uint ret = unchecked ((uint) ReadInt(resp, 0));
374 if (ret == 0) ReadString(resp, 1, out1, ref cchOut1);
375 return ret;
376 }
377 }
378
379 private static uint MsiFunc_IS_S(RemoteMsiFunctionId id, int in1, string in2, StringBuilder out1, ref uint cchOut1)
380 {
381 lock (RemotableNativeMethods.remotingDelegate)
382 {
383 ClearData(requestBuf);
384 WriteInt(requestBuf, 0, in1);
385 WriteString(requestBuf, 1, in2);
386 IntPtr resp;
387 remotingDelegate(id, requestBuf, out resp);
388 FreeString(requestBuf, 1);
389 uint ret = unchecked ((uint) ReadInt(resp, 0));
390 if (ret == 0) ReadString(resp, 1, out1, ref cchOut1);
391 return ret;
392 }
393 }
394
395 private static uint MsiFunc_ISII_SII(RemoteMsiFunctionId id, int in1, string in2, int in3, int in4, StringBuilder out1, ref uint cchOut1, out int out2, out int out3)
396 {
397 lock (RemotableNativeMethods.remotingDelegate)
398 {
399 ClearData(requestBuf);
400 WriteInt(requestBuf, 0, in1);
401 WriteString(requestBuf, 1, in2);
402 WriteInt(requestBuf, 2, in3);
403 WriteInt(requestBuf, 3, in4);
404 IntPtr resp;
405 remotingDelegate(id, requestBuf, out resp);
406 FreeString(requestBuf, 1);
407 uint ret = unchecked ((uint) ReadInt(resp, 0));
408 if (ret == 0) ReadString(resp, 1, out1, ref cchOut1);
409 out2 = ReadInt(resp, 2);
410 out3 = ReadInt(resp, 3);
411 return ret;
412 }
413 }
414
415 internal static int MsiProcessMessage(int hInstall, uint eMessageType, int hRecord)
416 {
417 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
418 {
419 return NativeMethods.MsiProcessMessage(hInstall, eMessageType, hRecord);
420 }
421 else lock (remotingDelegate)
422 {
423 // I don't understand why, but this particular function doesn't work
424 // when using the static requestBuf -- some data doesn't make it through.
425 // But it works when a fresh buffer is allocated here every call.
426 IntPtr buf = Marshal.AllocHGlobal(
427 requestFieldSize * MAX_REQUEST_FIELDS);
428 ClearData(buf);
429 WriteInt(buf, 0, RemotableNativeMethods.GetRemoteHandle(hInstall));
430 WriteInt(buf, 1, unchecked ((int) eMessageType));
431 WriteInt(buf, 2, RemotableNativeMethods.GetRemoteHandle(hRecord));
432 IntPtr resp;
433 remotingDelegate(RemoteMsiFunctionId.MsiProcessMessage, buf, out resp);
434 Marshal.FreeHGlobal(buf);
435 return ReadInt(resp, 0);
436 }
437 }
438
439 internal static uint MsiCloseHandle(int hAny)
440 {
441 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hAny))
442 return NativeMethods.MsiCloseHandle(hAny);
443 else
444 return RemotableNativeMethods.MsiFunc_III(
445 RemoteMsiFunctionId.MsiCloseHandle, RemotableNativeMethods.GetRemoteHandle(hAny), 0, 0);
446 }
447
448 internal static uint MsiGetProperty(int hInstall, string szName, StringBuilder szValueBuf, ref uint cchValueBuf)
449 {
450 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
451 return NativeMethods.MsiGetProperty(hInstall, szName, szValueBuf, ref cchValueBuf);
452 else
453 {
454 return RemotableNativeMethods.MsiFunc_IS_S(
455 RemoteMsiFunctionId.MsiGetProperty,
456 RemotableNativeMethods.GetRemoteHandle(hInstall),
457 szName,
458 szValueBuf,
459 ref cchValueBuf);
460 }
461 }
462
463 internal static uint MsiSetProperty(int hInstall, string szName, string szValue)
464 {
465 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
466 return NativeMethods.MsiSetProperty(hInstall, szName, szValue);
467 else
468 {
469 return RemotableNativeMethods.MsiFunc_ISS(
470 RemoteMsiFunctionId.MsiSetProperty,
471 RemotableNativeMethods.GetRemoteHandle(hInstall),
472 szName,
473 szValue);
474 }
475 }
476
477 internal static int MsiCreateRecord(uint cParams, int hAny)
478 {
479 // When remoting is enabled, we might need to create either a local or
480 // remote record, depending on the handle it is to have an affinity with.
481 // If no affinity handle is specified, create a remote record (the 99% case).
482 if (!RemotingEnabled ||
483 (hAny != 0 && !RemotableNativeMethods.IsRemoteHandle(hAny)))
484 {
485 return NativeMethods.MsiCreateRecord(cParams);
486 }
487 else
488 {
489 int hRecord = unchecked((int)RemotableNativeMethods.MsiFunc_III(
490 RemoteMsiFunctionId.MsiCreateRecord, (int) cParams, 0, 0));
491 return RemotableNativeMethods.MakeRemoteHandle(hRecord);
492 }
493 }
494
495 internal static uint MsiRecordGetFieldCount(int hRecord)
496 {
497 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
498 return NativeMethods.MsiRecordGetFieldCount(hRecord);
499 else
500 {
501 return RemotableNativeMethods.MsiFunc_III(
502 RemoteMsiFunctionId.MsiRecordGetFieldCount,
503 RemotableNativeMethods.GetRemoteHandle(hRecord),
504 0,
505 0);
506 }
507 }
508
509 internal static int MsiRecordGetInteger(int hRecord, uint iField)
510 {
511 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
512 return NativeMethods.MsiRecordGetInteger(hRecord, iField);
513 else
514 {
515 return unchecked ((int) RemotableNativeMethods.MsiFunc_III(
516 RemoteMsiFunctionId.MsiRecordGetInteger,
517 RemotableNativeMethods.GetRemoteHandle(hRecord),
518 (int) iField,
519 0));
520 }
521 }
522
523 internal static uint MsiRecordGetString(int hRecord, uint iField, StringBuilder szValueBuf, ref uint cchValueBuf)
524 {
525 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
526 {
527 return NativeMethods.MsiRecordGetString(hRecord, iField, szValueBuf, ref cchValueBuf);
528 }
529 else
530 {
531 return RemotableNativeMethods.MsiFunc_II_S(
532 RemoteMsiFunctionId.MsiRecordGetString,
533 RemotableNativeMethods.GetRemoteHandle(hRecord),
534 (int) iField,
535 szValueBuf,
536 ref cchValueBuf);
537 }
538 }
539
540 internal static uint MsiRecordSetInteger(int hRecord, uint iField, int iValue)
541 {
542 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
543 return NativeMethods.MsiRecordSetInteger(hRecord, iField, iValue);
544 else
545 {
546 return RemotableNativeMethods.MsiFunc_III(
547 RemoteMsiFunctionId.MsiRecordSetInteger,
548 RemotableNativeMethods.GetRemoteHandle(hRecord),
549 (int) iField,
550 iValue);
551 }
552 }
553
554 internal static uint MsiRecordSetString(int hRecord, uint iField, string szValue)
555 {
556 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
557 return NativeMethods.MsiRecordSetString(hRecord, iField, szValue);
558 else
559 {
560 return RemotableNativeMethods.MsiFunc_IIS(
561 RemoteMsiFunctionId.MsiRecordSetString,
562 RemotableNativeMethods.GetRemoteHandle(hRecord),
563 (int) iField,
564 szValue);
565 }
566 }
567
568 internal static int MsiGetActiveDatabase(int hInstall)
569 {
570 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
571 return NativeMethods.MsiGetActiveDatabase(hInstall);
572 else
573 {
574 int hDatabase = (int)RemotableNativeMethods.MsiFunc_III(
575 RemoteMsiFunctionId.MsiGetActiveDatabase,
576 RemotableNativeMethods.GetRemoteHandle(hInstall),
577 0,
578 0);
579 return RemotableNativeMethods.MakeRemoteHandle(hDatabase);
580 }
581 }
582
583 internal static uint MsiDatabaseOpenView(int hDatabase, string szQuery, out int hView)
584 {
585 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hDatabase))
586 return NativeMethods.MsiDatabaseOpenView(hDatabase, szQuery, out hView);
587 else
588 {
589 uint err = RemotableNativeMethods.MsiFunc_ISII_I(
590 RemoteMsiFunctionId.MsiDatabaseOpenView,
591 RemotableNativeMethods.GetRemoteHandle(hDatabase),
592 szQuery,
593 0,
594 0,
595 out hView);
596 hView = RemotableNativeMethods.MakeRemoteHandle(hView);
597 return err;
598 }
599 }
600
601 internal static uint MsiViewExecute(int hView, int hRecord)
602 {
603 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView))
604 return NativeMethods.MsiViewExecute(hView, hRecord);
605 else
606 {
607 return RemotableNativeMethods.MsiFunc_III(
608 RemoteMsiFunctionId.MsiViewExecute,
609 RemotableNativeMethods.GetRemoteHandle(hView),
610 RemotableNativeMethods.GetRemoteHandle(hRecord),
611 0);
612 }
613 }
614
615 internal static uint MsiViewFetch(int hView, out int hRecord)
616 {
617 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView))
618 return NativeMethods.MsiViewFetch(hView, out hRecord);
619 else
620 {
621 uint err = RemotableNativeMethods.MsiFunc_II_I(
622 RemoteMsiFunctionId.MsiViewFetch,
623 RemotableNativeMethods.GetRemoteHandle(hView),
624 0,
625 out hRecord);
626 hRecord = RemotableNativeMethods.MakeRemoteHandle(hRecord);
627 return err;
628 }
629 }
630
631 internal static uint MsiViewModify(int hView, int iModifyMode, int hRecord)
632 {
633 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView))
634 return NativeMethods.MsiViewModify(hView, iModifyMode, hRecord);
635 else
636 {
637 return RemotableNativeMethods.MsiFunc_III(
638 RemoteMsiFunctionId.MsiViewModify,
639 RemotableNativeMethods.GetRemoteHandle(hView),
640 iModifyMode,
641 RemotableNativeMethods.GetRemoteHandle(hRecord));
642 }
643 }
644
645 internal static int MsiViewGetError(int hView, StringBuilder szColumnNameBuffer, ref uint cchBuf)
646 {
647 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView))
648 return NativeMethods.MsiViewGetError(hView, szColumnNameBuffer, ref cchBuf);
649 else
650 {
651 return unchecked ((int) RemotableNativeMethods.MsiFunc_II_S(
652 RemoteMsiFunctionId.MsiViewGetError,
653 RemotableNativeMethods.GetRemoteHandle(hView),
654 0,
655 szColumnNameBuffer,
656 ref cchBuf));
657 }
658 }
659
660 internal static uint MsiViewGetColumnInfo(int hView, uint eColumnInfo, out int hRecord)
661 {
662 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView))
663 return NativeMethods.MsiViewGetColumnInfo(hView, eColumnInfo, out hRecord);
664 else
665 {
666 uint err = RemotableNativeMethods.MsiFunc_II_I(
667 RemoteMsiFunctionId.MsiViewGetColumnInfo,
668 RemotableNativeMethods.GetRemoteHandle(hView),
669 (int) eColumnInfo,
670 out hRecord);
671 hRecord = RemotableNativeMethods.MakeRemoteHandle(hRecord);
672 return err;
673 }
674 }
675
676 internal static uint MsiFormatRecord(int hInstall, int hRecord, StringBuilder szResultBuf, ref uint cchResultBuf)
677 {
678 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
679 return NativeMethods.MsiFormatRecord(hInstall, hRecord, szResultBuf, ref cchResultBuf);
680 else
681 {
682 return RemotableNativeMethods.MsiFunc_II_S(
683 RemoteMsiFunctionId.MsiFormatRecord,
684 RemotableNativeMethods.GetRemoteHandle(hInstall),
685 RemotableNativeMethods.GetRemoteHandle(hRecord),
686 szResultBuf,
687 ref cchResultBuf);
688 }
689 }
690
691 internal static uint MsiRecordClearData(int hRecord)
692 {
693 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
694 return NativeMethods.MsiRecordClearData(hRecord);
695 else
696 {
697 return RemotableNativeMethods.MsiFunc_III(
698 RemoteMsiFunctionId.MsiRecordClearData,
699 RemotableNativeMethods.GetRemoteHandle(hRecord),
700 0,
701 0);
702 }
703 }
704
705 internal static bool MsiRecordIsNull(int hRecord, uint iField)
706 {
707 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
708 return NativeMethods.MsiRecordIsNull(hRecord, iField);
709 else
710 {
711 return 0 != RemotableNativeMethods.MsiFunc_III(
712 RemoteMsiFunctionId.MsiRecordIsNull,
713 RemotableNativeMethods.GetRemoteHandle(hRecord),
714 (int) iField,
715 0);
716 }
717 }
718
719 internal static uint MsiDatabaseGetPrimaryKeys(int hDatabase, string szTableName, out int hRecord)
720 {
721 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hDatabase))
722 return NativeMethods.MsiDatabaseGetPrimaryKeys(hDatabase, szTableName, out hRecord);
723 else
724 {
725 uint err = RemotableNativeMethods.MsiFunc_ISII_I(
726 RemoteMsiFunctionId.MsiDatabaseGetPrimaryKeys,
727 RemotableNativeMethods.GetRemoteHandle(hDatabase),
728 szTableName,
729 0,
730 0,
731 out hRecord);
732 hRecord = RemotableNativeMethods.MakeRemoteHandle(hRecord);
733 return err;
734 }
735 }
736
737 internal static uint MsiDatabaseIsTablePersistent(int hDatabase, string szTableName)
738 {
739 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hDatabase))
740 return NativeMethods.MsiDatabaseIsTablePersistent(hDatabase, szTableName);
741 else
742 {
743 return RemotableNativeMethods.MsiFunc_ISI(
744 RemoteMsiFunctionId.MsiDatabaseIsTablePersistent,
745 RemotableNativeMethods.GetRemoteHandle(hDatabase),
746 szTableName,
747 0);
748 }
749 }
750
751 internal static uint MsiDoAction(int hInstall, string szAction)
752 {
753 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
754 return NativeMethods.MsiDoAction(hInstall, szAction);
755 else
756 {
757 return RemotableNativeMethods.MsiFunc_ISI(
758 RemoteMsiFunctionId.MsiDoAction,
759 RemotableNativeMethods.GetRemoteHandle(hInstall),
760 szAction,
761 0);
762 }
763 }
764
765 internal static uint MsiEnumComponentCosts(int hInstall, string szComponent, uint dwIndex, int iState, StringBuilder lpDriveBuf, ref uint cchDriveBuf, out int iCost, out int iTempCost)
766 {
767 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
768 return NativeMethods.MsiEnumComponentCosts(hInstall, szComponent, dwIndex, iState, lpDriveBuf, ref cchDriveBuf, out iCost, out iTempCost);
769 else
770 {
771 return RemotableNativeMethods.MsiFunc_ISII_SII(
772 RemoteMsiFunctionId.MsiEvaluateCondition,
773 RemotableNativeMethods.GetRemoteHandle(hInstall),
774 szComponent, (int) dwIndex, iState, lpDriveBuf, ref cchDriveBuf, out iCost, out iTempCost);
775 }
776 }
777
778 internal static uint MsiEvaluateCondition(int hInstall, string szCondition)
779 {
780 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
781 return NativeMethods.MsiEvaluateCondition(hInstall, szCondition);
782 else
783 {
784 return RemotableNativeMethods.MsiFunc_ISI(
785 RemoteMsiFunctionId.MsiEvaluateCondition,
786 RemotableNativeMethods.GetRemoteHandle(hInstall),
787 szCondition,
788 0);
789 }
790 }
791
792 internal static uint MsiGetComponentState(int hInstall, string szComponent, out int iInstalled, out int iAction)
793 {
794 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
795 return NativeMethods.MsiGetComponentState(hInstall, szComponent, out iInstalled, out iAction);
796 else
797 {
798 return RemotableNativeMethods.MsiFunc_IS_II(
799 RemoteMsiFunctionId.MsiGetComponentState,
800 RemotableNativeMethods.GetRemoteHandle(hInstall),
801 szComponent,
802 out iInstalled,
803 out iAction);
804 }
805 }
806
807 internal static uint MsiGetFeatureCost(int hInstall, string szFeature, int iCostTree, int iState, out int iCost)
808 {
809 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
810 return NativeMethods.MsiGetFeatureCost(hInstall, szFeature, iCostTree, iState, out iCost);
811 else
812 {
813 return RemotableNativeMethods.MsiFunc_ISII_I(
814 RemoteMsiFunctionId.MsiGetFeatureCost,
815 RemotableNativeMethods.GetRemoteHandle(hInstall),
816 szFeature,
817 iCostTree,
818 iState,
819 out iCost);
820 }
821 }
822
823 internal static uint MsiGetFeatureState(int hInstall, string szFeature, out int iInstalled, out int iAction)
824 {
825 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
826 return NativeMethods.MsiGetFeatureState(hInstall, szFeature, out iInstalled, out iAction);
827 else
828 {
829 return RemotableNativeMethods.MsiFunc_IS_II(
830 RemoteMsiFunctionId.MsiGetFeatureState,
831 RemotableNativeMethods.GetRemoteHandle(hInstall),
832 szFeature,
833 out iInstalled,
834 out iAction);
835 }
836 }
837
838 internal static uint MsiGetFeatureValidStates(int hInstall, string szFeature, out uint dwInstalledState)
839 {
840 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
841 return NativeMethods.MsiGetFeatureValidStates(hInstall, szFeature, out dwInstalledState);
842 else
843 {
844 int iTemp;
845 uint ret = RemotableNativeMethods.MsiFunc_ISII_I(
846 RemoteMsiFunctionId.MsiGetFeatureValidStates,
847 RemotableNativeMethods.GetRemoteHandle(hInstall),
848 szFeature,
849 0,
850 0,
851 out iTemp);
852 dwInstalledState = (uint) iTemp;
853 return ret;
854 }
855 }
856
857 internal static int MsiGetLanguage(int hInstall)
858 {
859 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
860 return NativeMethods.MsiGetLanguage(hInstall);
861 else
862 {
863 return unchecked((int)RemotableNativeMethods.MsiFunc_III(
864 RemoteMsiFunctionId.MsiGetLanguage,
865 RemotableNativeMethods.GetRemoteHandle(hInstall),
866 0,
867 0));
868 }
869 }
870
871 internal static int MsiGetLastErrorRecord(int hAny)
872 {
873 // When remoting is enabled, we might need to create either a local or
874 // remote record, depending on the handle it is to have an affinity with.
875 // If no affinity handle is specified, create a remote record (the 99% case).
876 if (!RemotingEnabled ||
877 (hAny != 0 && !RemotableNativeMethods.IsRemoteHandle(hAny)))
878 {
879 return NativeMethods.MsiGetLastErrorRecord();
880 }
881 else
882 {
883 int hRecord = unchecked((int) RemotableNativeMethods.MsiFunc_III(
884 RemoteMsiFunctionId.MsiGetLastErrorRecord, 0, 0, 0));
885 return RemotableNativeMethods.MakeRemoteHandle(hRecord);
886 }
887 }
888
889 internal static bool MsiGetMode(int hInstall, uint iRunMode)
890 {
891 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
892 return NativeMethods.MsiGetMode(hInstall, iRunMode);
893 else
894 {
895 return 0 != RemotableNativeMethods.MsiFunc_III(
896 RemoteMsiFunctionId.MsiGetMode,
897 RemotableNativeMethods.GetRemoteHandle(hInstall),
898 (int) iRunMode,
899 0);
900 }
901 }
902
903 internal static uint MsiGetSourcePath(int hInstall, string szFolder, StringBuilder szPathBuf, ref uint cchPathBuf)
904 {
905 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
906 return NativeMethods.MsiGetSourcePath(hInstall, szFolder, szPathBuf, ref cchPathBuf);
907 else
908 {
909 return RemotableNativeMethods.MsiFunc_IS_S(
910 RemoteMsiFunctionId.MsiGetSourcePath,
911 RemotableNativeMethods.GetRemoteHandle(hInstall),
912 szFolder,
913 szPathBuf,
914 ref cchPathBuf);
915 }
916 }
917
918 internal static uint MsiGetSummaryInformation(int hDatabase, string szDatabasePath, uint uiUpdateCount, out int hSummaryInfo)
919 {
920 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hDatabase))
921 return NativeMethods.MsiGetSummaryInformation(hDatabase, szDatabasePath, uiUpdateCount, out hSummaryInfo);
922 else
923 {
924 uint err = RemotableNativeMethods.MsiFunc_ISII_I(
925 RemoteMsiFunctionId.MsiGetSummaryInformation,
926 RemotableNativeMethods.GetRemoteHandle(hDatabase),
927 szDatabasePath,
928 (int)uiUpdateCount,
929 0,
930 out hSummaryInfo);
931 hSummaryInfo = RemotableNativeMethods.MakeRemoteHandle(hSummaryInfo);
932 return err;
933 }
934 }
935
936 internal static uint MsiGetTargetPath(int hInstall, string szFolder, StringBuilder szPathBuf, ref uint cchPathBuf)
937 {
938 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
939 return NativeMethods.MsiGetTargetPath(hInstall, szFolder, szPathBuf, ref cchPathBuf);
940 else
941 {
942 return RemotableNativeMethods.MsiFunc_IS_S(
943 RemoteMsiFunctionId.MsiGetTargetPath,
944 RemotableNativeMethods.GetRemoteHandle(hInstall),
945 szFolder,
946 szPathBuf,
947 ref cchPathBuf);
948 }
949 }
950
951 internal static uint MsiRecordDataSize(int hRecord, uint iField)
952 {
953 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
954 return NativeMethods.MsiRecordDataSize(hRecord, iField);
955 else
956 {
957 return RemotableNativeMethods.MsiFunc_III(
958 RemoteMsiFunctionId.MsiRecordDataSize,
959 RemotableNativeMethods.GetRemoteHandle(hRecord),
960 (int) iField, 0);
961 }
962 }
963
964 internal static uint MsiRecordReadStream(int hRecord, uint iField, byte[] szDataBuf, ref uint cbDataBuf)
965 {
966 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
967 {
968 return NativeMethods.MsiRecordReadStream(hRecord, iField, szDataBuf, ref cbDataBuf);
969 }
970 else lock (RemotableNativeMethods.remotingDelegate)
971 {
972 ClearData(requestBuf);
973 unchecked
974 {
975 WriteInt(requestBuf, 0, RemotableNativeMethods.GetRemoteHandle(hRecord));
976 WriteInt(requestBuf, 1, (int) iField);
977 WriteInt(requestBuf, 2, (int) cbDataBuf);
978 IntPtr resp;
979 remotingDelegate(RemoteMsiFunctionId.MsiRecordReadStream, requestBuf, out resp);
980 uint ret = (uint) ReadInt(resp, 0);
981 if (ret == 0)
982 {
983 cbDataBuf = (uint) ReadInt(resp, 2);
984 if (cbDataBuf > 0)
985 {
986 RemotableNativeMethods.ReadStream(resp, 1, szDataBuf, (int) cbDataBuf);
987 }
988 }
989 return ret;
990 }
991 }
992 }
993
994 internal static uint MsiRecordSetStream(int hRecord, uint iField, string szFilePath)
995 {
996 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord))
997 return NativeMethods.MsiRecordSetStream(hRecord, iField, szFilePath);
998 else
999 {
1000 return RemotableNativeMethods.MsiFunc_IIS(
1001 RemoteMsiFunctionId.MsiRecordSetStream,
1002 RemotableNativeMethods.GetRemoteHandle(hRecord),
1003 (int) iField,
1004 szFilePath);
1005 }
1006 }
1007
1008 internal static uint MsiSequence(int hInstall, string szTable, int iSequenceMode)
1009 {
1010 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
1011 return NativeMethods.MsiSequence(hInstall, szTable, iSequenceMode);
1012 else
1013 {
1014 return RemotableNativeMethods.MsiFunc_ISI(
1015 RemoteMsiFunctionId.MsiSequence,
1016 RemotableNativeMethods.GetRemoteHandle(hInstall),
1017 szTable,
1018 iSequenceMode);
1019 }
1020 }
1021
1022 internal static uint MsiSetComponentState(int hInstall, string szComponent, int iState)
1023 {
1024 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
1025 return NativeMethods.MsiSetComponentState(hInstall, szComponent, iState);
1026 else
1027 {
1028 return RemotableNativeMethods.MsiFunc_ISI(
1029 RemoteMsiFunctionId.MsiSetComponentState,
1030 RemotableNativeMethods.GetRemoteHandle(hInstall),
1031 szComponent,
1032 iState);
1033 }
1034 }
1035
1036 internal static uint MsiSetFeatureAttributes(int hInstall, string szFeature, uint dwAttributes)
1037 {
1038 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
1039 return NativeMethods.MsiSetFeatureAttributes(hInstall, szFeature, dwAttributes);
1040 else
1041 {
1042 return RemotableNativeMethods.MsiFunc_ISI(
1043 RemoteMsiFunctionId.MsiSetFeatureAttributes,
1044 RemotableNativeMethods.GetRemoteHandle(hInstall),
1045 szFeature,
1046 (int) dwAttributes);
1047 }
1048 }
1049
1050 internal static uint MsiSetFeatureState(int hInstall, string szFeature, int iState)
1051 {
1052 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
1053 return NativeMethods.MsiSetFeatureState(hInstall, szFeature, iState);
1054 else
1055 {
1056 return RemotableNativeMethods.MsiFunc_ISI(
1057 RemoteMsiFunctionId.MsiSetFeatureState,
1058 RemotableNativeMethods.GetRemoteHandle(hInstall), szFeature, iState);
1059 }
1060 }
1061
1062 internal static uint MsiSetInstallLevel(int hInstall, int iInstallLevel)
1063 {
1064 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
1065 return NativeMethods.MsiSetInstallLevel(hInstall, iInstallLevel);
1066 else
1067 {
1068 return RemotableNativeMethods.MsiFunc_III(
1069 RemoteMsiFunctionId.MsiSetInstallLevel,
1070 RemotableNativeMethods.GetRemoteHandle(hInstall),
1071 iInstallLevel,
1072 0);
1073 }
1074 }
1075
1076 internal static uint MsiSetMode(int hInstall, uint iRunMode, bool fState)
1077 {
1078 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
1079 return NativeMethods.MsiSetMode(hInstall, iRunMode, fState);
1080 else
1081 {
1082 return RemotableNativeMethods.MsiFunc_III(
1083 RemoteMsiFunctionId.MsiSetMode,
1084 RemotableNativeMethods.GetRemoteHandle(hInstall),
1085 (int) iRunMode,
1086 fState ? 1 : 0);
1087 }
1088 }
1089
1090 internal static uint MsiSetTargetPath(int hInstall, string szFolder, string szFolderPath)
1091 {
1092 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
1093 return NativeMethods.MsiSetTargetPath(hInstall, szFolder, szFolderPath);
1094 else
1095 {
1096 return RemotableNativeMethods.MsiFunc_ISS(
1097 RemoteMsiFunctionId.MsiSetTargetPath,
1098 RemotableNativeMethods.GetRemoteHandle(hInstall),
1099 szFolder,
1100 szFolderPath);
1101 }
1102 }
1103
1104 internal static uint MsiSummaryInfoGetProperty(int hSummaryInfo, uint uiProperty, out uint uiDataType, out int iValue, ref long ftValue, StringBuilder szValueBuf, ref uint cchValueBuf)
1105 {
1106 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hSummaryInfo))
1107 {
1108 return NativeMethods.MsiSummaryInfoGetProperty(hSummaryInfo, uiProperty, out uiDataType, out iValue, ref ftValue, szValueBuf, ref cchValueBuf);
1109 }
1110 else lock (RemotableNativeMethods.remotingDelegate)
1111 {
1112 ClearData(requestBuf);
1113 WriteInt(requestBuf, 0, RemotableNativeMethods.GetRemoteHandle(hSummaryInfo));
1114 WriteInt(requestBuf, 1, (int) uiProperty);
1115 IntPtr resp;
1116 remotingDelegate(RemoteMsiFunctionId.MsiSummaryInfoGetProperty, requestBuf, out resp);
1117 unchecked
1118 {
1119 uint ret = (uint) ReadInt(resp, 0);
1120 if (ret == 0)
1121 {
1122 uiDataType = (uint) ReadInt(resp, 1);
1123 switch ((VarEnum) uiDataType)
1124 {
1125 case VarEnum.VT_I2:
1126 case VarEnum.VT_I4:
1127 iValue = ReadInt(resp, 2);
1128 break;
1129
1130 case VarEnum.VT_FILETIME:
1131 uint ftHigh = (uint) ReadInt(resp, 2);
1132 uint ftLow = (uint) ReadInt(resp, 3);
1133 ftValue = ((long) ftHigh) << 32 | ((long) ftLow);
1134 iValue = 0;
1135 break;
1136
1137 case VarEnum.VT_LPSTR:
1138 ReadString(resp, 2, szValueBuf, ref cchValueBuf);
1139 iValue = 0;
1140 break;
1141
1142 default:
1143 iValue = 0;
1144 break;
1145 }
1146 }
1147 else
1148 {
1149 uiDataType = 0;
1150 iValue = 0;
1151 }
1152 return ret;
1153 }
1154 }
1155 }
1156
1157 internal static uint MsiVerifyDiskSpace(int hInstall)
1158 {
1159 if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall))
1160 return NativeMethods.MsiVerifyDiskSpace(hInstall);
1161 else
1162 {
1163 return RemotableNativeMethods.MsiFunc_III(
1164 RemoteMsiFunctionId.MsiVerifyDiskSpace,
1165 RemotableNativeMethods.GetRemoteHandle(hInstall),
1166 0,
1167 0);
1168 }
1169 }
1170 }
1171}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Session.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Session.cs
new file mode 100644
index 00000000..875e49a6
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Session.cs
@@ -0,0 +1,946 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Runtime.InteropServices;
10 using System.Diagnostics.CodeAnalysis;
11
12 /// <summary>
13 /// The Session object controls the installation process. It opens the
14 /// install database, which contains the installation tables and data.
15 /// </summary>
16 /// <remarks><p>
17 /// This object is associated with a standard set of action functions,
18 /// each performing particular operations on data from one or more tables. Additional
19 /// custom actions may be added for particular product installations. The basic engine
20 /// function is a sequencer that fetches sequential records from a designated sequence
21 /// table, evaluates any specified condition expression, and executes the designated
22 /// action. Actions not recognized by the engine are deferred to the UI handler object
23 /// for processing, usually dialog box sequences.
24 /// </p><p>
25 /// Note that only one Session object can be opened by a single process.
26 /// </p></remarks>
27 public sealed class Session : InstallerHandle, IFormatProvider
28 {
29 private Database database;
30 private CustomActionData customActionData;
31 private bool sessionAccessValidated = false;
32
33 internal Session(IntPtr handle, bool ownsHandle)
34 : base(handle, ownsHandle)
35 {
36 }
37
38 /// <summary>
39 /// Gets the Database for the install session.
40 /// </summary>
41 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
42 /// <exception cref="InstallerException">the Database cannot be accessed</exception>
43 /// <remarks><p>
44 /// Normally there is no need to close this Database object. The same object can be
45 /// used throughout the lifetime of the Session, and it will be closed when the Session
46 /// is closed.
47 /// </p><p>
48 /// Win32 MSI API:
49 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetactivedatabase.asp">MsiGetActiveDatabase</a>
50 /// </p></remarks>
51 [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")]
52 public Database Database
53 {
54 get
55 {
56 if (this.database == null || this.database.IsClosed)
57 {
58 lock (this.Sync)
59 {
60 if (this.database == null || this.database.IsClosed)
61 {
62 this.ValidateSessionAccess();
63
64 int hDb = RemotableNativeMethods.MsiGetActiveDatabase((int) this.Handle);
65 if (hDb == 0)
66 {
67 throw new InstallerException();
68 }
69 this.database = new Database((IntPtr) hDb, true, "", DatabaseOpenMode.ReadOnly);
70 }
71 }
72 }
73 return this.database;
74 }
75 }
76
77 /// <summary>
78 /// Gets the numeric language ID used by the current install session.
79 /// </summary>
80 /// <remarks><p>
81 /// Win32 MSI API:
82 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetlanguage.asp">MsiGetLanguage</a>
83 /// </p></remarks>
84 public int Language
85 {
86 get
87 {
88 return (int) RemotableNativeMethods.MsiGetLanguage((int) this.Handle);
89 }
90 }
91
92 /// <summary>
93 /// Gets or sets the string value of a named installer property, as maintained by the
94 /// Session object in the in-memory Property table, or, if it is prefixed with a percent
95 /// sign (%), the value of a system environment variable for the current process.
96 /// </summary>
97 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
98 /// <remarks><p>
99 /// Win32 MSI APIs:
100 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetproperty.asp">MsiGetProperty</a>,
101 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetproperty.asp">MsiSetProperty</a>
102 /// </p></remarks>
103 public string this[string property]
104 {
105 get
106 {
107 if (String.IsNullOrEmpty(property))
108 {
109 throw new ArgumentNullException("property");
110 }
111
112 if (!this.sessionAccessValidated &&
113 !Session.NonImmediatePropertyNames.Contains(property))
114 {
115 this.ValidateSessionAccess();
116 }
117
118 StringBuilder buf = new StringBuilder();
119 uint bufSize = 0;
120 uint ret = RemotableNativeMethods.MsiGetProperty((int) this.Handle, property, buf, ref bufSize);
121 if (ret == (uint) NativeMethods.Error.MORE_DATA)
122 {
123 buf.Capacity = (int) ++bufSize;
124 ret = RemotableNativeMethods.MsiGetProperty((int) this.Handle, property, buf, ref bufSize);
125 }
126
127 if (ret != 0)
128 {
129 throw InstallerException.ExceptionFromReturnCode(ret);
130 }
131 return buf.ToString();
132 }
133
134 set
135 {
136 if (String.IsNullOrEmpty(property))
137 {
138 throw new ArgumentNullException("property");
139 }
140
141 this.ValidateSessionAccess();
142
143 if (value == null)
144 {
145 value = String.Empty;
146 }
147
148 uint ret = RemotableNativeMethods.MsiSetProperty((int) this.Handle, property, value);
149 if (ret != 0)
150 {
151 throw InstallerException.ExceptionFromReturnCode(ret);
152 }
153 }
154 }
155
156 /// <summary>
157 /// Creates a new Session object from an integer session handle.
158 /// </summary>
159 /// <param name="handle">Integer session handle</param>
160 /// <param name="ownsHandle">true to close the handle when this object is disposed or finalized</param>
161 /// <remarks><p>
162 /// This method is only provided for interop purposes. A Session object
163 /// should normally be obtained by calling <see cref="Installer.OpenPackage(Database,bool)"/>
164 /// or <see cref="Installer.OpenProduct"/>.
165 /// </p></remarks>
166 public static Session FromHandle(IntPtr handle, bool ownsHandle)
167 {
168 return new Session(handle, ownsHandle);
169 }
170
171 /// <summary>
172 /// Performs any enabled logging operations and defers execution to the UI handler
173 /// object associated with the engine.
174 /// </summary>
175 /// <param name="messageType">Type of message to be processed</param>
176 /// <param name="record">Contains message-specific fields</param>
177 /// <returns>A message-dependent return value</returns>
178 /// <exception cref="InvalidHandleException">the Session or Record handle is invalid</exception>
179 /// <exception cref="ArgumentOutOfRangeException">an invalid message kind is specified</exception>
180 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
181 /// <exception cref="InstallerException">the message-handler failed for an unknown reason</exception>
182 /// <remarks><p>
183 /// Logging may be selectively enabled for the various message types.
184 /// See the <see cref="Installer.EnableLog(InstallLogModes,string)"/> method.
185 /// </p><p>
186 /// If record field 0 contains a formatting string, it is used to format the data in
187 /// the other fields. Else if the message is an error, warning, or user message, an attempt
188 /// is made to find a message template in the Error table for the current database using the
189 /// error number found in field 1 of the record for message types and return values.
190 /// </p><p>
191 /// The <paramref name="messageType"/> parameter may also include message-box flags from
192 /// the following enumerations: System.Windows.Forms.MessageBoxButtons,
193 /// System.Windows.Forms.MessageBoxDefaultButton, System.Windows.Forms.MessageBoxIcon. These
194 /// flags can be combined with the InstallMessage with a bitwise OR.
195 /// </p><p>
196 /// Note, this method never returns Cancel or Error values. Instead, appropriate
197 /// exceptions are thrown in those cases.
198 /// </p><p>
199 /// Win32 MSI API:
200 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiprocessmessage.asp">MsiProcessMessage</a>
201 /// </p></remarks>
202 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
203 public MessageResult Message(InstallMessage messageType, Record record)
204 {
205 if (record == null)
206 {
207 throw new ArgumentNullException("record");
208 }
209
210 int ret = RemotableNativeMethods.MsiProcessMessage((int) this.Handle, (uint) messageType, (int) record.Handle);
211 if (ret < 0)
212 {
213 throw new InstallerException();
214 }
215 else if (ret == (int) MessageResult.Cancel)
216 {
217 throw new InstallCanceledException();
218 }
219 return (MessageResult) ret;
220 }
221
222 /// <summary>
223 /// Writes a message to the log, if logging is enabled.
224 /// </summary>
225 /// <param name="msg">The line to be written to the log</param>
226 /// <remarks><p>
227 /// Win32 MSI API:
228 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiprocessmessage.asp">MsiProcessMessage</a>
229 /// </p></remarks>
230 public void Log(string msg)
231 {
232 if (msg == null)
233 {
234 throw new ArgumentNullException("msg");
235 }
236
237 using (Record rec = new Record(0))
238 {
239 rec.FormatString = msg;
240 this.Message(InstallMessage.Info, rec);
241 }
242 }
243
244 /// <summary>
245 /// Writes a formatted message to the log, if logging is enabled.
246 /// </summary>
247 /// <param name="format">The line to be written to the log, containing 0 or more format specifiers</param>
248 /// <param name="args">An array containing 0 or more objects to be formatted</param>
249 /// <remarks><p>
250 /// Win32 MSI API:
251 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiprocessmessage.asp">MsiProcessMessage</a>
252 /// </p></remarks>
253 public void Log(string format, params object[] args)
254 {
255 this.Log(String.Format(CultureInfo.InvariantCulture, format, args));
256 }
257
258 /// <summary>
259 /// Evaluates a logical expression containing symbols and values.
260 /// </summary>
261 /// <param name="condition">conditional expression</param>
262 /// <returns>The result of the condition evaluation</returns>
263 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
264 /// <exception cref="ArgumentNullException">the condition is null or empty</exception>
265 /// <exception cref="InvalidOperationException">the conditional expression is invalid</exception>
266 /// <remarks><p>
267 /// Win32 MSI API:
268 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msievaluatecondition.asp">MsiEvaluateCondition</a>
269 /// </p></remarks>
270 public bool EvaluateCondition(string condition)
271 {
272 if (String.IsNullOrEmpty(condition))
273 {
274 throw new ArgumentNullException("condition");
275 }
276
277 uint value = RemotableNativeMethods.MsiEvaluateCondition((int) this.Handle, condition);
278 if (value == 0)
279 {
280 return false;
281 }
282 else if (value == 1)
283 {
284 return true;
285 }
286 else
287 {
288 throw new InvalidOperationException();
289 }
290 }
291
292 /// <summary>
293 /// Evaluates a logical expression containing symbols and values, specifying a default
294 /// value to be returned in case the condition is empty.
295 /// </summary>
296 /// <param name="condition">conditional expression</param>
297 /// <param name="defaultValue">value to return if the condition is empty</param>
298 /// <returns>The result of the condition evaluation</returns>
299 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
300 /// <exception cref="InvalidOperationException">the conditional expression is invalid</exception>
301 /// <remarks><p>
302 /// Win32 MSI API:
303 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msievaluatecondition.asp">MsiEvaluateCondition</a>
304 /// </p></remarks>
305 public bool EvaluateCondition(string condition, bool defaultValue)
306 {
307 if (condition == null)
308 {
309 throw new ArgumentNullException("condition");
310 }
311 else if (condition.Length == 0)
312 {
313 return defaultValue;
314 }
315 else
316 {
317 this.ValidateSessionAccess();
318 return this.EvaluateCondition(condition);
319 }
320 }
321
322 /// <summary>
323 /// Formats a string containing installer properties.
324 /// </summary>
325 /// <param name="format">A format string containing property tokens</param>
326 /// <returns>A formatted string containing property data</returns>
327 /// <exception cref="InvalidHandleException">the Record handle is invalid</exception>
328 /// <remarks><p>
329 /// Win32 MSI API:
330 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
331 /// </p></remarks>
332 [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames")]
333 public string Format(string format)
334 {
335 if (format == null)
336 {
337 throw new ArgumentNullException("format");
338 }
339
340 using (Record formatRec = new Record(0))
341 {
342 formatRec.FormatString = format;
343 return formatRec.ToString(this);
344 }
345 }
346
347 /// <summary>
348 /// Returns a formatted string from record data.
349 /// </summary>
350 /// <param name="record">Record object containing a template and data to be formatted.
351 /// The template string must be set in field 0 followed by any referenced data parameters.</param>
352 /// <returns>A formatted string containing the record data</returns>
353 /// <exception cref="InvalidHandleException">the Record handle is invalid</exception>
354 /// <remarks><p>
355 /// Win32 MSI API:
356 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
357 /// </p></remarks>
358 public string FormatRecord(Record record)
359 {
360 if (record == null)
361 {
362 throw new ArgumentNullException("record");
363 }
364
365 return record.ToString(this);
366 }
367
368 /// <summary>
369 /// Returns a formatted string from record data using a specified format.
370 /// </summary>
371 /// <param name="record">Record object containing a template and data to be formatted</param>
372 /// <param name="format">Format string to be used instead of field 0 of the Record</param>
373 /// <returns>A formatted string containing the record data</returns>
374 /// <exception cref="InvalidHandleException">the Record handle is invalid</exception>
375 /// <remarks><p>
376 /// Win32 MSI API:
377 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
378 /// </p></remarks>
379 [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the Record's " +
380 "FormatString property separately before calling the FormatRecord() override that takes only the Record parameter.")]
381 public string FormatRecord(Record record, string format)
382 {
383 if (record == null)
384 {
385 throw new ArgumentNullException("record");
386 }
387
388 return record.ToString(format, this);
389 }
390
391 /// <summary>
392 /// Retrieves product properties (not session properties) from the product database.
393 /// </summary>
394 /// <returns>Value of the property, or an empty string if the property is not set.</returns>
395 /// <remarks><p>
396 /// Note this is not the correct method for getting ordinary session properties. For that,
397 /// see the indexer on the Session class.
398 /// </p><p>
399 /// Win32 MSI API:
400 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetproductproperty.asp">MsiGetProductProperty</a>
401 /// </p></remarks>
402 public string GetProductProperty(string property)
403 {
404 if (String.IsNullOrEmpty(property))
405 {
406 throw new ArgumentNullException("property");
407 }
408
409 this.ValidateSessionAccess();
410
411 StringBuilder buf = new StringBuilder();
412 uint bufSize = (uint) buf.Capacity;
413 uint ret = NativeMethods.MsiGetProductProperty((int) this.Handle, property, buf, ref bufSize);
414
415 if (ret == (uint) NativeMethods.Error.MORE_DATA)
416 {
417 buf.Capacity = (int) ++bufSize;
418 ret = NativeMethods.MsiGetProductProperty((int) this.Handle, property, buf, ref bufSize);
419 }
420
421 if (ret != 0)
422 {
423 throw InstallerException.ExceptionFromReturnCode(ret);
424 }
425 return buf.ToString();
426 }
427
428 /// <summary>
429 /// Gets an accessor for components in the current session.
430 /// </summary>
431 public ComponentInfoCollection Components
432 {
433 get
434 {
435 this.ValidateSessionAccess();
436 return new ComponentInfoCollection(this);
437 }
438 }
439
440 /// <summary>
441 /// Gets an accessor for features in the current session.
442 /// </summary>
443 public FeatureInfoCollection Features
444 {
445 get
446 {
447 this.ValidateSessionAccess();
448 return new FeatureInfoCollection(this);
449 }
450 }
451
452 /// <summary>
453 /// Checks to see if sufficient disk space is present for the current installation.
454 /// </summary>
455 /// <returns>True if there is sufficient disk space; false otherwise.</returns>
456 /// <remarks><p>
457 /// Win32 MSI API:
458 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiverifydiskspace.asp">MsiVerifyDiskSpace</a>
459 /// </p></remarks>
460 public bool VerifyDiskSpace()
461 {
462 this.ValidateSessionAccess();
463
464 uint ret = RemotableNativeMethods.MsiVerifyDiskSpace((int)this.Handle);
465 if (ret == (uint) NativeMethods.Error.DISK_FULL)
466 {
467 return false;
468 }
469 else if (ret != 0)
470 {
471 throw InstallerException.ExceptionFromReturnCode(ret);
472 }
473 return true;
474 }
475
476 /// <summary>
477 /// Gets the total disk space per drive required for the installation.
478 /// </summary>
479 /// <returns>A list of InstallCost structures, specifying the cost for each drive</returns>
480 /// <remarks><p>
481 /// Win32 MSI API:
482 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msienumcomponentcosts.asp">MsiEnumComponentCosts</a>
483 /// </p></remarks>
484 [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
485 public IList<InstallCost> GetTotalCost()
486 {
487 this.ValidateSessionAccess();
488
489 IList<InstallCost> costs = new List<InstallCost>();
490 StringBuilder driveBuf = new StringBuilder(20);
491 for (uint i = 0; true; i++)
492 {
493 int cost, tempCost;
494 uint driveBufSize = (uint) driveBuf.Capacity;
495 uint ret = RemotableNativeMethods.MsiEnumComponentCosts(
496 (int) this.Handle,
497 null,
498 i,
499 (int) InstallState.Default,
500 driveBuf,
501 ref driveBufSize,
502 out cost,
503 out tempCost);
504 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break;
505 if (ret == (uint) NativeMethods.Error.MORE_DATA)
506 {
507 driveBuf.Capacity = (int) ++driveBufSize;
508 ret = RemotableNativeMethods.MsiEnumComponentCosts(
509 (int) this.Handle,
510 null,
511 i,
512 (int) InstallState.Default,
513 driveBuf,
514 ref driveBufSize,
515 out cost,
516 out tempCost);
517 }
518
519 if (ret != 0)
520 {
521 throw InstallerException.ExceptionFromReturnCode(ret);
522 }
523 costs.Add(new InstallCost(driveBuf.ToString(), cost * 512L, tempCost * 512L));
524 }
525 return costs;
526 }
527
528 /// <summary>
529 /// Gets the designated mode flag for the current install session.
530 /// </summary>
531 /// <param name="mode">The type of mode to be checked.</param>
532 /// <returns>The value of the designated mode flag.</returns>
533 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
534 /// <exception cref="ArgumentOutOfRangeException">an invalid mode flag was specified</exception>
535 /// <remarks><p>
536 /// Note that only the following run modes are available to read from
537 /// a deferred custom action:<list type="bullet">
538 /// <item><description><see cref="InstallRunMode.Scheduled"/></description></item>
539 /// <item><description><see cref="InstallRunMode.Rollback"/></description></item>
540 /// <item><description><see cref="InstallRunMode.Commit"/></description></item>
541 /// </list>
542 /// </p><p>
543 /// Win32 MSI API:
544 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetmode.asp">MsiGetMode</a>
545 /// </p></remarks>
546 public bool GetMode(InstallRunMode mode)
547 {
548 return RemotableNativeMethods.MsiGetMode((int) this.Handle, (uint) mode);
549 }
550
551 /// <summary>
552 /// Sets the designated mode flag for the current install session.
553 /// </summary>
554 /// <param name="mode">The type of mode to be set.</param>
555 /// <param name="value">The desired value of the mode.</param>
556 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
557 /// <exception cref="ArgumentOutOfRangeException">an invalid mode flag was specified</exception>
558 /// <exception cref="InvalidOperationException">the mode cannot not be set</exception>
559 /// <remarks><p>
560 /// Win32 MSI API:
561 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetmode.asp">MsiSetMode</a>
562 /// </p></remarks>
563 public void SetMode(InstallRunMode mode, bool value)
564 {
565 this.ValidateSessionAccess();
566
567 uint ret = RemotableNativeMethods.MsiSetMode((int) this.Handle, (uint) mode, value);
568 if (ret != 0)
569 {
570 if (ret == (uint) NativeMethods.Error.ACCESS_DENIED)
571 {
572 throw new InvalidOperationException();
573 }
574 else
575 {
576 throw InstallerException.ExceptionFromReturnCode(ret);
577 }
578 }
579 }
580
581 /// <summary>
582 /// Gets the full path to the designated folder on the source media or server image.
583 /// </summary>
584 /// <exception cref="ArgumentException">the folder was not found in the Directory table</exception>
585 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
586 /// <remarks><p>
587 /// Win32 MSI API:
588 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetsourcepath.asp">MsiGetSourcePath</a>
589 /// </p></remarks>
590 public string GetSourcePath(string directory)
591 {
592 if (String.IsNullOrEmpty(directory))
593 {
594 throw new ArgumentNullException("directory");
595 }
596
597 this.ValidateSessionAccess();
598
599 StringBuilder buf = new StringBuilder();
600 uint bufSize = 0;
601 uint ret = RemotableNativeMethods.MsiGetSourcePath((int) this.Handle, directory, buf, ref bufSize);
602 if (ret == (uint) NativeMethods.Error.MORE_DATA)
603 {
604 buf.Capacity = (int) ++bufSize;
605 ret = ret = RemotableNativeMethods.MsiGetSourcePath((int) this.Handle, directory, buf, ref bufSize);
606 }
607
608 if (ret != 0)
609 {
610 if (ret == (uint) NativeMethods.Error.DIRECTORY)
611 {
612 throw InstallerException.ExceptionFromReturnCode(ret, directory);
613 }
614 else
615 {
616 throw InstallerException.ExceptionFromReturnCode(ret);
617 }
618 }
619 return buf.ToString();
620 }
621
622 /// <summary>
623 /// Gets the full path to the designated folder on the installation target drive.
624 /// </summary>
625 /// <exception cref="ArgumentException">the folder was not found in the Directory table</exception>
626 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
627 /// <remarks><p>
628 /// Win32 MSI API:
629 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigettargetpath.asp">MsiGetTargetPath</a>
630 /// </p></remarks>
631 public string GetTargetPath(string directory)
632 {
633 if (String.IsNullOrEmpty(directory))
634 {
635 throw new ArgumentNullException("directory");
636 }
637
638 this.ValidateSessionAccess();
639
640 StringBuilder buf = new StringBuilder();
641 uint bufSize = 0;
642 uint ret = RemotableNativeMethods.MsiGetTargetPath((int) this.Handle, directory, buf, ref bufSize);
643 if (ret == (uint) NativeMethods.Error.MORE_DATA)
644 {
645 buf.Capacity = (int) ++bufSize;
646 ret = ret = RemotableNativeMethods.MsiGetTargetPath((int) this.Handle, directory, buf, ref bufSize);
647 }
648
649 if (ret != 0)
650 {
651 if (ret == (uint) NativeMethods.Error.DIRECTORY)
652 {
653 throw InstallerException.ExceptionFromReturnCode(ret, directory);
654 }
655 else
656 {
657 throw InstallerException.ExceptionFromReturnCode(ret);
658 }
659 }
660 return buf.ToString();
661 }
662
663 /// <summary>
664 /// Sets the full path to the designated folder on the installation target drive.
665 /// </summary>
666 /// <exception cref="ArgumentException">the folder was not found in the Directory table</exception>
667 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
668 /// <remarks><p>
669 /// Setting the target path of a directory changes the path specification for the directory
670 /// in the in-memory Directory table. Also, the path specifications of all other path objects
671 /// in the table that are either subordinate or equivalent to the changed path are updated
672 /// to reflect the change. The properties for each affected path are also updated.
673 /// </p><p>
674 /// If an error occurs in this function, all updated paths and properties revert to
675 /// their previous values. Therefore, it is safe to treat errors returned by this function
676 /// as non-fatal.
677 /// </p><p>
678 /// Do not attempt to configure the target path if the components using those paths
679 /// are already installed for the current user or for a different user. Check the
680 /// ProductState property before setting the target path to determine if the product
681 /// containing this component is installed.
682 /// </p><p>
683 /// Win32 MSI API:
684 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisettargetpath.asp">MsiSetTargetPath</a>
685 /// </p></remarks>
686 public void SetTargetPath(string directory, string value)
687 {
688 if (String.IsNullOrEmpty(directory))
689 {
690 throw new ArgumentNullException("directory");
691 }
692
693 if (value == null)
694 {
695 throw new ArgumentNullException("value");
696 }
697
698 this.ValidateSessionAccess();
699
700 uint ret = RemotableNativeMethods.MsiSetTargetPath((int) this.Handle, directory, value);
701 if (ret != 0)
702 {
703 if (ret == (uint) NativeMethods.Error.DIRECTORY)
704 {
705 throw InstallerException.ExceptionFromReturnCode(ret, directory);
706 }
707 else
708 {
709 throw InstallerException.ExceptionFromReturnCode(ret);
710 }
711 }
712 }
713
714 /// <summary>
715 /// Sets the install level for the current installation to a specified value and
716 /// recalculates the Select and Installed states for all features in the Feature
717 /// table. Also sets the Action state of each component in the Component table based
718 /// on the new level.
719 /// </summary>
720 /// <param name="installLevel">New install level</param>
721 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
722 /// <remarks><p>
723 /// The SetInstallLevel method sets the following:<list type="bullet">
724 /// <item><description>The installation level for the current installation to a specified value</description></item>
725 /// <item><description>The Select and Installed states for all features in the Feature table</description></item>
726 /// <item><description>The Action state of each component in the Component table, based on the new level</description></item>
727 /// </list>
728 /// If 0 or a negative number is passed in the ilnstallLevel parameter,
729 /// the current installation level does not change, but all features are still
730 /// updated based on the current installation level.
731 /// </p><p>
732 /// Win32 MSI API:
733 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisetinstalllevel.asp">MsiSetInstallLevel</a>
734 /// </p></remarks>
735 public void SetInstallLevel(int installLevel)
736 {
737 this.ValidateSessionAccess();
738
739 uint ret = RemotableNativeMethods.MsiSetInstallLevel((int) this.Handle, installLevel);
740 if (ret != 0)
741 {
742 throw InstallerException.ExceptionFromReturnCode(ret);
743 }
744 }
745
746 /// <summary>
747 /// Executes a built-in action, custom action, or user-interface wizard action.
748 /// </summary>
749 /// <param name="action">Name of the action to execute. Case-sensitive.</param>
750 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
751 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
752 /// <remarks><p>
753 /// The DoAction method executes the action that corresponds to the name supplied. If the
754 /// name is not recognized by the installer as a built-in action or as a custom action in
755 /// the CustomAction table, the name is passed to the user-interface handler object, which
756 /// can invoke a function or a dialog box. If a null action name is supplied, the installer
757 /// uses the upper-case value of the ACTION property as the action to perform. If no property
758 /// value is defined, the default action is performed, defined as "INSTALL".
759 /// </p><p>
760 /// Actions that update the system, such as the InstallFiles and WriteRegistryValues
761 /// actions, cannot be run by calling MsiDoAction. The exception to this rule is if DoAction
762 /// is called from a custom action that is scheduled in the InstallExecuteSequence table
763 /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the
764 /// system, such as AppSearch or CostInitialize, can be called.
765 /// </p><p>
766 /// Win32 MSI API:
767 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidoaction.asp">MsiDoAction</a>
768 /// </p></remarks>
769 public void DoAction(string action)
770 {
771 this.DoAction(action, null);
772 }
773
774 /// <summary>
775 /// Executes a built-in action, custom action, or user-interface wizard action.
776 /// </summary>
777 /// <param name="action">Name of the action to execute. Case-sensitive.</param>
778 /// <param name="actionData">Optional data to be passed to a deferred custom action.</param>
779 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
780 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
781 /// <remarks><p>
782 /// The DoAction method executes the action that corresponds to the name supplied. If the
783 /// name is not recognized by the installer as a built-in action or as a custom action in
784 /// the CustomAction table, the name is passed to the user-interface handler object, which
785 /// can invoke a function or a dialog box. If a null action name is supplied, the installer
786 /// uses the upper-case value of the ACTION property as the action to perform. If no property
787 /// value is defined, the default action is performed, defined as "INSTALL".
788 /// </p><p>
789 /// Actions that update the system, such as the InstallFiles and WriteRegistryValues
790 /// actions, cannot be run by calling MsiDoAction. The exception to this rule is if DoAction
791 /// is called from a custom action that is scheduled in the InstallExecuteSequence table
792 /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the
793 /// system, such as AppSearch or CostInitialize, can be called.
794 /// </p><p>
795 /// If the called action is a deferred, rollback, or commit custom action, then the supplied
796 /// <paramref name="actionData"/> will be available via the <see cref="CustomActionData"/>
797 /// property of that custom action's session.
798 /// </p><p>
799 /// Win32 MSI API:
800 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msidoaction.asp">MsiDoAction</a>
801 /// </p></remarks>
802 public void DoAction(string action, CustomActionData actionData)
803 {
804 if (String.IsNullOrEmpty(action))
805 {
806 throw new ArgumentNullException("action");
807 }
808
809 this.ValidateSessionAccess();
810
811 if (actionData != null)
812 {
813 this[action] = actionData.ToString();
814 }
815
816 uint ret = RemotableNativeMethods.MsiDoAction((int) this.Handle, action);
817 if (ret != 0)
818 {
819 throw InstallerException.ExceptionFromReturnCode(ret);
820 }
821 }
822
823 /// <summary>
824 /// Executes an action sequence described in the specified table.
825 /// </summary>
826 /// <param name="sequenceTable">Name of the table containing the action sequence.</param>
827 /// <exception cref="InvalidHandleException">the Session handle is invalid</exception>
828 /// <exception cref="InstallCanceledException">the user exited the installation</exception>
829 /// <remarks><p>
830 /// This method queries the specified table, ordering the actions by the numbers in the Sequence column.
831 /// For each row retrieved, an action is executed, provided that any supplied condition expression does
832 /// not evaluate to FALSE.
833 /// </p><p>
834 /// An action sequence containing any actions that update the system, such as the InstallFiles and
835 /// WriteRegistryValues actions, cannot be run by calling DoActionSequence. The exception to this rule is if
836 /// DoActionSequence is called from a custom action that is scheduled in the InstallExecuteSequence table
837 /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the system, such
838 /// as AppSearch or CostInitialize, can be called.
839 /// </p><p>
840 /// Win32 MSI API:
841 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisequence.asp">MsiSequence</a>
842 /// </p></remarks>
843 public void DoActionSequence(string sequenceTable)
844 {
845 if (String.IsNullOrEmpty(sequenceTable))
846 {
847 throw new ArgumentNullException("sequenceTable");
848 }
849
850 this.ValidateSessionAccess();
851
852 uint ret = RemotableNativeMethods.MsiSequence((int) this.Handle, sequenceTable, 0);
853 if (ret != 0)
854 {
855 throw InstallerException.ExceptionFromReturnCode(ret);
856 }
857 }
858
859 /// <summary>
860 /// Gets custom action data for the session that was supplied by the caller.
861 /// </summary>
862 /// <seealso cref="DoAction(string,CustomActionData)"/>
863 public CustomActionData CustomActionData
864 {
865 get
866 {
867 if (this.customActionData == null)
868 {
869 this.customActionData = new CustomActionData(this[CustomActionData.PropertyName]);
870 }
871
872 return this.customActionData;
873 }
874 }
875
876 /// <summary>
877 /// Implements formatting for <see cref="Record" /> data.
878 /// </summary>
879 /// <param name="formatType">Type of format object to get.</param>
880 /// <returns>The the current instance, if <paramref name="formatType"/> is the same type
881 /// as the current instance; otherwise, null.</returns>
882 object IFormatProvider.GetFormat(Type formatType)
883 {
884 return formatType == typeof(Session) ? this : null;
885 }
886
887 /// <summary>
888 /// Closes the session handle. Also closes the active database handle, if it is open.
889 /// After closing a handle, further method calls may throw an <see cref="InvalidHandleException"/>.
890 /// </summary>
891 /// <param name="disposing">If true, the method has been called directly
892 /// or indirectly by a user's code, so managed and unmanaged resources will
893 /// be disposed. If false, only unmanaged resources will be disposed.</param>
894 protected override void Dispose(bool disposing)
895 {
896 try
897 {
898 if (disposing)
899 {
900 if (this.database != null)
901 {
902 this.database.Dispose();
903 this.database = null;
904 }
905 }
906 }
907 finally
908 {
909 base.Dispose(disposing);
910 }
911 }
912
913 /// <summary>
914 /// Gets the (short) list of properties that are available from non-immediate custom actions.
915 /// </summary>
916 private static IList<string> NonImmediatePropertyNames
917 {
918 get
919 {
920 return new string[] {
921 CustomActionData.PropertyName,
922 "ProductCode",
923 "UserSID"
924 };
925 }
926 }
927
928 /// <summary>
929 /// Throws an exception if the custom action is not able to access immedate session details.
930 /// </summary>
931 private void ValidateSessionAccess()
932 {
933 if (!this.sessionAccessValidated)
934 {
935 if (this.GetMode(InstallRunMode.Scheduled) ||
936 this.GetMode(InstallRunMode.Rollback) ||
937 this.GetMode(InstallRunMode.Commit))
938 {
939 throw new InstallerException("Cannot access session details from a non-immediate custom action");
940 }
941
942 this.sessionAccessValidated = true;
943 }
944 }
945 }
946}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ShortcutTarget.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ShortcutTarget.cs
new file mode 100644
index 00000000..4c043bf2
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ShortcutTarget.cs
@@ -0,0 +1,104 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 /// <summary>
6 /// Holds information about the target of a shortcut file.
7 /// </summary>
8 public struct ShortcutTarget
9 {
10 private string productCode;
11 private string feature;
12 private string componentCode;
13
14 internal ShortcutTarget(string productCode, string feature, string componentCode)
15 {
16 this.productCode = productCode;
17 this.feature = feature;
18 this.componentCode = componentCode;
19 }
20
21 /// <summary>
22 /// Gets the target product code of the shortcut, or null if not available.
23 /// </summary>
24 public string ProductCode
25 {
26 get
27 {
28 return this.productCode;
29 }
30 }
31
32 /// <summary>
33 /// Gets the name of the target feature of the shortcut, or null if not available.
34 /// </summary>
35 public string Feature
36 {
37 get
38 {
39 return this.feature;
40 }
41 }
42
43 /// <summary>
44 /// Gets the target component code of the shortcut, or null if not available.
45 /// </summary>
46 public string ComponentCode
47 {
48 get
49 {
50 return this.componentCode;
51 }
52 }
53
54 /// <summary>
55 /// Tests whether two shortcut targets have the same product code, feature, and/or component code.
56 /// </summary>
57 /// <param name="st1">The first shortcut target to compare.</param>
58 /// <param name="st2">The second shortcut target to compare.</param>
59 /// <returns>True if all parts of the targets are the same, else false.</returns>
60 public static bool operator ==(ShortcutTarget st1, ShortcutTarget st2)
61 {
62 return st1.Equals(st2);
63 }
64
65 /// <summary>
66 /// Tests whether two shortcut targets have the same product code, feature, and/or component code.
67 /// </summary>
68 /// <param name="st1">The first shortcut target to compare.</param>
69 /// <param name="st2">The second shortcut target to compare.</param>
70 /// <returns>True if any parts of the targets are different, else false.</returns>
71 public static bool operator !=(ShortcutTarget st1, ShortcutTarget st2)
72 {
73 return !st1.Equals(st2);
74 }
75
76 /// <summary>
77 /// Tests whether two shortcut targets have the same product code, feature, and/or component code.
78 /// </summary>
79 /// <param name="obj">The shortcut target to compare to the current object.</param>
80 /// <returns>True if <paramref name="obj"/> is a shortcut target and all parts of the targets are the same, else false.</returns>
81 public override bool Equals(object obj)
82 {
83 if (obj == null || obj.GetType() != typeof(ShortcutTarget))
84 {
85 return false;
86 }
87 ShortcutTarget st = (ShortcutTarget) obj;
88 return this.productCode == st.productCode
89 && this.feature == st.feature
90 && this.componentCode == st.componentCode;
91 }
92
93 /// <summary>
94 /// Generates a hash code using all parts of the shortcut target.
95 /// </summary>
96 /// <returns>An integer suitable for hashing the shortcut target.</returns>
97 public override int GetHashCode()
98 {
99 return (this.productCode != null ? this.productCode.GetHashCode() : 0)
100 ^ (this.feature != null ? this.feature.GetHashCode() : 0)
101 ^ (this.componentCode != null ? this.componentCode.GetHashCode() : 0);
102 }
103 }
104}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/SourceList.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/SourceList.cs
new file mode 100644
index 00000000..16ec22e8
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/SourceList.cs
@@ -0,0 +1,525 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// A list of sources for an installed product or patch.
13 /// </summary>
14 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
15 public class SourceList : ICollection<string>
16 {
17 private Installation installation;
18 private SourceMediaList mediaList;
19
20 internal SourceList(Installation installation)
21 {
22 this.installation = installation;
23 }
24
25 /// <summary>
26 /// Gets the list of disks registered for the media source of
27 /// the patch or product installation.
28 /// </summary>
29 public SourceMediaList MediaList
30 {
31 get
32 {
33 if (this.mediaList == null)
34 {
35 this.mediaList = new SourceMediaList(this.installation);
36 }
37 return this.mediaList;
38 }
39 }
40
41 /// <summary>
42 /// Gets the number of network and URL sources in the list.
43 /// </summary>
44 public int Count
45 {
46 get
47 {
48 int count = 0;
49 IEnumerator<string> e = this.GetEnumerator();
50 while (e.MoveNext())
51 {
52 count++;
53 }
54 return count;
55 }
56 }
57
58 /// <summary>
59 /// Gets a boolean value indicating whether the list is read-only.
60 /// A SourceList is never read-only.
61 /// </summary>
62 /// <value>read-only status of the list</value>
63 public bool IsReadOnly
64 {
65 get { return false; }
66 }
67
68 /// <summary>
69 /// Adds a network or URL source to the source list of the installed product.
70 /// </summary>
71 /// <param name="item">Path to the source to be added. This parameter is
72 /// expected to contain only the path without the filename.</param>
73 /// <remarks><p>
74 /// If this method is called with a new source, the installer adds the source
75 /// to the end of the source list.
76 /// </p><p>
77 /// If this method is called with a source already existing in the source
78 /// list, it has no effect.
79 /// </p><p>
80 /// Win32 MSI APIs:
81 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistaddsource.asp">MsiSourceListAddSource</a>,
82 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistaddsourceex.asp">MsiSourceListAddSourceEx</a>
83 /// </p></remarks>
84 /// <seealso cref="Insert"/>
85 public void Add(string item)
86 {
87 if (!this.Contains(item))
88 {
89 this.Insert(item, 0);
90 }
91 }
92
93 /// <summary>
94 /// Adds or reorders a network or URL source for the product or patch.
95 /// </summary>
96 /// <param name="item">Path to the source to be added. This parameter is
97 /// expected to contain only the path without the filename.</param>
98 /// <param name="index">Specifies the priority order in which the source
99 /// will be inserted</param>
100 /// <remarks><p>
101 /// If this method is called with a new source and <paramref name="index"/>
102 /// is set to 0, the installer adds the source to the end of the source list.
103 /// </p><p>
104 /// If this method is called with a source already existing in the source
105 /// list and <paramref name="index"/> is set to 0, the installer retains the
106 /// source's existing index.
107 /// </p><p>
108 /// If the method is called with an existing source in the source list
109 /// and <paramref name="index"/> is set to a non-zero value, the source is
110 /// removed from its current location in the list and inserted at the position
111 /// specified by Index, before any source that already exists at that position.
112 /// </p><p>
113 /// If the method is called with a new source and Index is set to a
114 /// non-zero value, the source is inserted at the position specified by
115 /// <paramref name="index"/>, before any source that already exists at
116 /// that position. The index value for all sources in the list after the
117 /// index specified by Index are updated to ensure unique index values and
118 /// the pre-existing order is guaranteed to remain unchanged.
119 /// </p><p>
120 /// If <paramref name="index"/> is greater than the number of sources
121 /// in the list, the source is placed at the end of the list with an index
122 /// value one larger than any existing source.
123 /// </p><p>
124 /// Win32 MSI API:
125 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistaddsourceex.asp">MsiSourceListAddSourceEx</a>
126 /// </p></remarks>
127 public void Insert(string item, int index)
128 {
129 if (item == null)
130 {
131 throw new ArgumentNullException("item");
132 }
133
134 NativeMethods.SourceType type = item.Contains("://") ? NativeMethods.SourceType.Url : NativeMethods.SourceType.Network;
135
136 uint ret = NativeMethods.MsiSourceListAddSourceEx(
137 this.installation.InstallationCode,
138 this.installation.UserSid,
139 this.installation.Context,
140 (uint) type | (uint) this.installation.InstallationType,
141 item,
142 (uint) index);
143 if (ret != 0)
144 {
145 throw InstallerException.ExceptionFromReturnCode(ret);
146 }
147 }
148
149 /// <summary>
150 /// Clears sources of all types: network, url, and media.
151 /// </summary>
152 /// <remarks><p>
153 /// Win32 MSI API:
154 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistclearall.asp">MsiSourceListClearAll</a>
155 /// </p></remarks>
156 public void Clear()
157 {
158 this.ClearSourceType(NativeMethods.SourceType.Url);
159 this.ClearSourceType(NativeMethods.SourceType.Network);
160 this.MediaList.Clear();
161 }
162
163 /// <summary>
164 /// Removes all network sources from the list. URL sources are not affected.
165 /// </summary>
166 /// <remarks><p>
167 /// Win32 MSI API:
168 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistclearallex.asp">MsiSourceListClearAllEx</a>
169 /// </p></remarks>
170 public void ClearNetworkSources()
171 {
172 this.ClearSourceType(NativeMethods.SourceType.Network);
173 }
174
175 /// <summary>
176 /// Removes all URL sources from the list. Network sources are not affected.
177 /// </summary>
178 /// <remarks><p>
179 /// Win32 MSI API:
180 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistclearallex.asp">MsiSourceListClearAllEx</a>
181 /// </p></remarks>
182 public void ClearUrlSources()
183 {
184 this.ClearSourceType(NativeMethods.SourceType.Url);
185 }
186
187 /// <summary>
188 /// Checks if the specified source exists in the list.
189 /// </summary>
190 /// <param name="item">case-insensitive source to look for</param>
191 /// <returns>true if the source exists in the list, false otherwise</returns>
192 public bool Contains(string item)
193 {
194 if (String.IsNullOrEmpty(item))
195 {
196 throw new ArgumentNullException("item");
197 }
198
199 foreach (string s in this)
200 {
201 if (s.Equals(item, StringComparison.OrdinalIgnoreCase))
202 {
203 return true;
204 }
205 }
206 return false;
207 }
208
209 /// <summary>
210 /// Copies the network and URL sources from this list into an array.
211 /// </summary>
212 /// <param name="array">destination array to be filed</param>
213 /// <param name="arrayIndex">offset into the destination array where copying begins</param>
214 public void CopyTo(string[] array, int arrayIndex)
215 {
216 foreach (string source in this)
217 {
218 array[arrayIndex++] = source;
219 }
220 }
221
222 /// <summary>
223 /// Removes a network or URL source.
224 /// </summary>
225 /// <remarks><p>
226 /// Win32 MSI API:
227 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistclearsource.asp">MsiSourceListClearSource</a>
228 /// </p></remarks>
229 public bool Remove(string item)
230 {
231 if (String.IsNullOrEmpty(item))
232 {
233 throw new ArgumentNullException("item");
234 }
235
236 NativeMethods.SourceType type = item.Contains("://") ? NativeMethods.SourceType.Url : NativeMethods.SourceType.Network;
237
238 uint ret = NativeMethods.MsiSourceListClearSource(
239 this.installation.InstallationCode,
240 this.installation.UserSid,
241 this.installation.Context,
242 (uint) type | (uint) this.installation.InstallationType,
243 item);
244 if (ret != 0)
245 {
246 // TODO: Figure out when to return false.
247 throw InstallerException.ExceptionFromReturnCode(ret);
248 }
249 return true;
250 }
251
252 /// <summary>
253 /// Enumerates the network and URL sources in the source list of the patch or product installation.
254 /// </summary>
255 /// <remarks><p>
256 /// Win32 MSI API:
257 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistenumsources.asp">MsiSourceListEnumSources</a>
258 /// </p></remarks>
259 public IEnumerator<string> GetEnumerator()
260 {
261 StringBuilder sourceBuf = new StringBuilder(256);
262 uint sourceBufSize = (uint) sourceBuf.Capacity;
263 for (uint i = 0; true; i++)
264 {
265 uint ret = this.EnumSources(sourceBuf, i, NativeMethods.SourceType.Network);
266 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS)
267 {
268 break;
269 }
270 else if (ret != 0)
271 {
272 throw InstallerException.ExceptionFromReturnCode(ret);
273 }
274 else
275 {
276 yield return sourceBuf.ToString();
277 }
278 }
279
280 for (uint i = 0; true; i++)
281 {
282 uint ret = this.EnumSources(sourceBuf, i, NativeMethods.SourceType.Url);
283 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS)
284 {
285 break;
286 }
287 else if (ret != 0)
288 {
289 throw InstallerException.ExceptionFromReturnCode(ret);
290 }
291 else
292 {
293 yield return sourceBuf.ToString();
294 }
295 }
296 }
297
298 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
299 {
300 return this.GetEnumerator();
301 }
302
303 /// <summary>
304 /// Forces the installer to search the source list for a valid
305 /// source the next time a source is required. For example, when the
306 /// installer performs an installation or reinstallation, or when it
307 /// requires the path for a component that is set to run from source.
308 /// </summary>
309 /// <remarks><p>
310 /// Win32 MSI APIs:
311 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistforceresolution.asp">MsiSourceListForceResolution</a>,
312 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistforceresolutionex.asp">MsiSourceListForceResolutionEx</a>
313 /// </p></remarks>
314 public void ForceResolution()
315 {
316 uint ret = NativeMethods.MsiSourceListForceResolutionEx(
317 this.installation.InstallationCode,
318 this.installation.UserSid,
319 this.installation.Context,
320 (uint) this.installation.InstallationType);
321 if (ret != 0)
322 {
323 throw InstallerException.ExceptionFromReturnCode(ret);
324 }
325 }
326
327 /// <summary>
328 /// Gets or sets the path relative to the root of the installation media.
329 /// </summary>
330 public string MediaPackagePath
331 {
332 get
333 {
334 return this["MediaPackagePath"];
335 }
336 set
337 {
338 this["MediaPackagePath"] = value;
339 }
340 }
341
342 /// <summary>
343 /// Gets or sets the prompt template that is used when prompting the user
344 /// for installation media.
345 /// </summary>
346 public string DiskPrompt
347 {
348 get
349 {
350 return this["DiskPrompt"];
351 }
352 set
353 {
354 this["DiskPrompt"] = value;
355 }
356 }
357
358 /// <summary>
359 /// Gets or sets the most recently used source location for the product.
360 /// </summary>
361 public string LastUsedSource
362 {
363 get
364 {
365 return this["LastUsedSource"];
366 }
367 set
368 {
369 this["LastUsedSource"] = value;
370 }
371 }
372
373 /// <summary>
374 /// Gets or sets the name of the Windows Installer package or patch package
375 /// on the source.
376 /// </summary>
377 public string PackageName
378 {
379 get
380 {
381 return this["PackageName"];
382 }
383 set
384 {
385 this["PackageName"] = value;
386 }
387 }
388
389 /// <summary>
390 /// Gets the type of the last-used source.
391 /// </summary>
392 /// <remarks><p>
393 /// <ul>
394 /// <li>&quot;n&quot; = network location</li>
395 /// <li>&quot;u&quot; = URL location</li>
396 /// <li>&quot;m&quot; = media location</li>
397 /// <li>(empty string) = no last used source</li>
398 /// </ul>
399 /// </p></remarks>
400 public string LastUsedType
401 {
402 get
403 {
404 return this["LastUsedType"];
405 }
406 }
407
408 /// <summary>
409 /// Gets or sets source list information properties of a product or patch installation.
410 /// </summary>
411 /// <param name="property">The source list information property name.</param>
412 /// <exception cref="ArgumentOutOfRangeException">An unknown product, patch, or property was requested</exception>
413 /// <remarks><p>
414 /// Win32 MSI API:
415 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistgetinfo.asp">MsiSourceListGetInfo</a>
416 /// </p></remarks>
417 public string this[string property]
418 {
419 get
420 {
421 StringBuilder buf = new StringBuilder("");
422 uint bufSize = 0;
423 uint ret = NativeMethods.MsiSourceListGetInfo(
424 this.installation.InstallationCode,
425 this.installation.UserSid,
426 this.installation.Context,
427 (uint) this.installation.InstallationType,
428 property,
429 buf,
430 ref bufSize);
431 if (ret != 0)
432 {
433 if (ret == (uint) NativeMethods.Error.MORE_DATA)
434 {
435 buf.Capacity = (int) ++bufSize;
436 ret = NativeMethods.MsiSourceListGetInfo(
437 this.installation.InstallationCode,
438 this.installation.UserSid,
439 this.installation.Context,
440 (uint) this.installation.InstallationType,
441 property,
442 buf,
443 ref bufSize);
444 }
445
446 if (ret != 0)
447 {
448 if (ret == (uint) NativeMethods.Error.UNKNOWN_PRODUCT ||
449 ret == (uint) NativeMethods.Error.UNKNOWN_PROPERTY)
450 {
451 throw new ArgumentOutOfRangeException("property");
452 }
453 else
454 {
455 throw InstallerException.ExceptionFromReturnCode(ret);
456 }
457 }
458 }
459 return buf.ToString();
460 }
461 set
462 {
463 uint ret = NativeMethods.MsiSourceListSetInfo(
464 this.installation.InstallationCode,
465 this.installation.UserSid,
466 this.installation.Context,
467 (uint) this.installation.InstallationType,
468 property,
469 value);
470 if (ret != 0)
471 {
472 if (ret == (uint) NativeMethods.Error.UNKNOWN_PRODUCT ||
473 ret == (uint) NativeMethods.Error.UNKNOWN_PROPERTY)
474 {
475 throw new ArgumentOutOfRangeException("property");
476 }
477 else
478 {
479 throw InstallerException.ExceptionFromReturnCode(ret);
480 }
481 }
482 }
483 }
484
485 private void ClearSourceType(NativeMethods.SourceType type)
486 {
487 uint ret = NativeMethods.MsiSourceListClearAllEx(
488 this.installation.InstallationCode,
489 this.installation.UserSid,
490 this.installation.Context,
491 (uint) type | (uint) this.installation.InstallationType);
492 if (ret != 0)
493 {
494 throw InstallerException.ExceptionFromReturnCode(ret);
495 }
496 }
497
498 private uint EnumSources(StringBuilder sourceBuf, uint i, NativeMethods.SourceType sourceType)
499 {
500 int enumType = (this.installation.InstallationType | (int) sourceType);
501 uint sourceBufSize = (uint) sourceBuf.Capacity;
502 uint ret = NativeMethods.MsiSourceListEnumSources(
503 this.installation.InstallationCode,
504 this.installation.UserSid,
505 this.installation.Context,
506 (uint) enumType,
507 i,
508 sourceBuf,
509 ref sourceBufSize);
510 if (ret == (uint) NativeMethods.Error.MORE_DATA)
511 {
512 sourceBuf.Capacity = (int) ++sourceBufSize;
513 ret = NativeMethods.MsiSourceListEnumSources(
514 this.installation.InstallationCode,
515 this.installation.UserSid,
516 this.installation.Context,
517 (uint) enumType,
518 i,
519 sourceBuf,
520 ref sourceBufSize);
521 }
522 return ret;
523 }
524 }
525}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/SourceMediaList.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/SourceMediaList.cs
new file mode 100644
index 00000000..cf7b7ec5
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/SourceMediaList.cs
@@ -0,0 +1,229 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Text;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// A list of source media for an installed product or patch.
13 /// </summary>
14 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
15 public class SourceMediaList : ICollection<MediaDisk>
16 {
17 private Installation installation;
18
19 internal SourceMediaList(Installation installation)
20 {
21 this.installation = installation;
22 }
23
24 /// <summary>
25 /// Gets the number of source media in the list.
26 /// </summary>
27 public int Count
28 {
29 get
30 {
31 int count = 0;
32 IEnumerator<MediaDisk> e = this.GetEnumerator();
33 while (e.MoveNext())
34 {
35 count++;
36 }
37 return count;
38 }
39 }
40
41 /// <summary>
42 /// Gets a boolean value indicating whether the list is read-only.
43 /// A SourceMediaList is never read-only.
44 /// </summary>
45 /// <value>read-only status of the list</value>
46 public bool IsReadOnly
47 {
48 get
49 {
50 return false;
51 }
52 }
53
54 /// <summary>
55 /// Adds or updates a disk of the media source for the product or patch.
56 /// </summary>
57 /// <remarks><p>
58 /// Win32 MSI API:
59 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistaddmediadisk.asp">MsiSourceListAddMediaDisk</a>
60 /// </p></remarks>
61 public void Add(MediaDisk item)
62 {
63 uint ret = NativeMethods.MsiSourceListAddMediaDisk(
64 this.installation.InstallationCode,
65 this.installation.UserSid,
66 this.installation.Context,
67 (uint) this.installation.InstallationType,
68 (uint) item.DiskId,
69 item.VolumeLabel,
70 item.DiskPrompt);
71
72 if (ret != 0)
73 {
74 throw InstallerException.ExceptionFromReturnCode(ret);
75 }
76 }
77
78 /// <summary>
79 /// Removes all source media from the list.
80 /// </summary>
81 /// <remarks><p>
82 /// Win32 MSI API:
83 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistclearallex.asp">MsiSourceListClearAllEx</a>
84 /// </p></remarks>
85 public void Clear()
86 {
87 uint ret = NativeMethods.MsiSourceListClearAllEx(
88 this.installation.InstallationCode,
89 this.installation.UserSid,
90 this.installation.Context,
91 (uint) NativeMethods.SourceType.Media | (uint) this.installation.InstallationType);
92
93 if (ret != 0)
94 {
95 throw InstallerException.ExceptionFromReturnCode(ret);
96 }
97 }
98
99 /// <summary>
100 /// Checks if the specified media disk id exists in the list.
101 /// </summary>
102 /// <param name="diskId">disk id of the media to look for</param>
103 /// <returns>true if the media exists in the list, false otherwise</returns>
104 public bool Contains(int diskId)
105 {
106 foreach (MediaDisk mediaDisk in this)
107 {
108 if (mediaDisk.DiskId == diskId)
109 {
110 return true;
111 }
112 }
113 return false;
114 }
115
116 bool ICollection<MediaDisk>.Contains(MediaDisk mediaDisk)
117 {
118 return this.Contains(mediaDisk.DiskId);
119 }
120
121 /// <summary>
122 /// Copies the source media info from this list into an array.
123 /// </summary>
124 /// <param name="array">destination array to be filed</param>
125 /// <param name="arrayIndex">offset into the destination array where copying begins</param>
126 public void CopyTo(MediaDisk[] array, int arrayIndex)
127 {
128 foreach (MediaDisk mediaDisk in this)
129 {
130 array[arrayIndex++] = mediaDisk;
131 }
132 }
133
134 /// <summary>
135 /// Removes a specified disk from the set of registered disks.
136 /// </summary>
137 /// <param name="diskId">ID of the disk to remove</param>
138 /// <remarks><p>
139 /// Win32 MSI API:
140 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistclearmediadisk.asp">MsiSourceListClearMediaDisk</a>
141 /// </p></remarks>
142 public bool Remove(int diskId)
143 {
144 uint ret = NativeMethods.MsiSourceListClearMediaDisk(
145 this.installation.InstallationCode,
146 this.installation.UserSid,
147 this.installation.Context,
148 (uint) this.installation.InstallationType,
149 (uint) diskId);
150
151 if (ret != 0)
152 {
153 // TODO: Figure out when to return false.
154 throw InstallerException.ExceptionFromReturnCode(ret);
155 }
156 return true;
157 }
158
159 bool ICollection<MediaDisk>.Remove(MediaDisk mediaDisk)
160 {
161 return this.Remove(mediaDisk.DiskId);
162 }
163
164 /// <summary>
165 /// Enumerates the source media in the source list of the patch or product installation.
166 /// </summary>
167 /// <remarks><p>
168 /// Win32 MSI API:
169 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisourcelistenummediadisks.asp">MsiSourceListEnumMediaDisks</a>
170 /// </p></remarks>
171 public IEnumerator<MediaDisk> GetEnumerator()
172 {
173 uint diskId;
174 StringBuilder volumeBuf = new StringBuilder(40);
175 uint volumeBufSize = (uint) volumeBuf.Capacity;
176 StringBuilder promptBuf = new StringBuilder(80);
177 uint promptBufSize = (uint) promptBuf.Capacity;
178 for (uint i = 0; true; i++)
179 {
180 uint ret = NativeMethods.MsiSourceListEnumMediaDisks(
181 this.installation.InstallationCode,
182 this.installation.UserSid,
183 this.installation.Context,
184 (uint) this.installation.InstallationType,
185 i,
186 out diskId,
187 volumeBuf,
188 ref volumeBufSize,
189 promptBuf,
190 ref promptBufSize);
191
192 if (ret == (uint) NativeMethods.Error.MORE_DATA)
193 {
194 volumeBuf.Capacity = (int) ++volumeBufSize;
195 promptBuf.Capacity = (int) ++promptBufSize;
196
197 ret = NativeMethods.MsiSourceListEnumMediaDisks(
198 this.installation.InstallationCode,
199 this.installation.UserSid,
200 this.installation.Context,
201 (uint) this.installation.InstallationType,
202 i,
203 out diskId,
204 volumeBuf,
205 ref volumeBufSize,
206 promptBuf,
207 ref promptBufSize);
208 }
209
210 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS)
211 {
212 break;
213 }
214
215 if (ret != 0)
216 {
217 throw InstallerException.ExceptionFromReturnCode(ret);
218 }
219
220 yield return new MediaDisk((int) diskId, volumeBuf.ToString(), promptBuf.ToString());
221 }
222 }
223
224 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
225 {
226 return this.GetEnumerator();
227 }
228 }
229}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/SummaryInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/SummaryInfo.cs
new file mode 100644
index 00000000..4dbff93f
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/SummaryInfo.cs
@@ -0,0 +1,612 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Globalization;
9 using System.Runtime.InteropServices;
10
11 /// <summary>
12 /// Provides access to summary information of a Windows Installer database.
13 /// </summary>
14 public class SummaryInfo : InstallerHandle
15 {
16 internal const int MAX_PROPERTIES = 20;
17
18 /// <summary>
19 /// Gets a SummaryInfo object that can be used to examine, update, and add
20 /// properties to the summary information stream of a package or transform.
21 /// </summary>
22 /// <param name="packagePath">Path to the package (database) or transform</param>
23 /// <param name="enableWrite">True to reserve resources for writing summary information properties.</param>
24 /// <exception cref="FileNotFoundException">the package does not exist or could not be read</exception>
25 /// <exception cref="InstallerException">the package is an invalid format</exception>
26 /// <remarks><p>
27 /// The SummaryInfo object should be <see cref="InstallerHandle.Close"/>d after use.
28 /// It is best that the handle be closed manually as soon as it is no longer
29 /// needed, as leaving lots of unused handles open can degrade performance.
30 /// </p><p>
31 /// Win32 MSI API:
32 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msigetsummaryinformation.asp">MsiGetSummaryInformation</a>
33 /// </p></remarks>
34 public SummaryInfo(string packagePath, bool enableWrite)
35 : base((IntPtr) SummaryInfo.OpenSummaryInfo(packagePath, enableWrite), true)
36 {
37 }
38
39 internal SummaryInfo(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle)
40 {
41 }
42
43 /// <summary>Gets or sets the Title summary information property.</summary>
44 /// <remarks><p>
45 /// The Title summary information property briefly describes the type of installer package. Phrases
46 /// such as "Installation Database" or "Transform" or "Patch" may be used for this property.
47 /// </p><p>
48 /// Win32 MSI APIs:
49 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
50 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
51 /// </p></remarks>
52 public string Title
53 {
54 get { return this[2]; }
55 set { this[2] = value; }
56 }
57
58 /// <summary>Gets or sets the Subject summary information property.</summary>
59 /// <remarks><p>
60 /// The Subject summary information property conveys to a file browser the product that can be installed using
61 /// the logic and data in this installer database. For example, the value of the summary property for
62 /// Microsoft Office 97 would be "Microsoft Office 97 Professional". This value is typically set from the
63 /// installer property ProductName.
64 /// </p><p>
65 /// Win32 MSI APIs:
66 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
67 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
68 /// </p></remarks>
69 public string Subject
70 {
71 get { return this[3]; }
72 set { this[3] = value; }
73 }
74
75 /// <summary>Gets or sets the Author summary information property.</summary>
76 /// <remarks><p>
77 /// The Author summary information property conveys to a file browser the manufacturer of the installation
78 /// database. This value is typically set from the installer property Manufacturer.
79 /// </p><p>
80 /// Win32 MSI APIs:
81 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
82 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
83 /// </p></remarks>
84 public string Author
85 {
86 get { return this[4]; }
87 set { this[4] = value; }
88 }
89
90 /// <summary>Gets or sets the Keywords summary information property.</summary>
91 /// <remarks><p>
92 /// The Keywords summary information property is used by file browsers to hold keywords that permit the
93 /// database file to be found in a keyword search. The set of keywords typically includes "Installer" as
94 /// well as product-specific keywords, and may be localized.
95 /// </p><p>
96 /// Win32 MSI APIs:
97 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
98 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
99 /// </p></remarks>
100 public string Keywords
101 {
102 get { return this[5]; }
103 set { this[5] = value; }
104 }
105
106 /// <summary>Gets or sets the Comments summary information property.</summary>
107 /// <remarks><p>
108 /// The Comments summary information property conveys the general purpose of the installer database. By convention,
109 /// the value for this summary property is set to the following:
110 /// </p><p>
111 /// "This installer database contains the logic and data required to install &lt;product name&gt;."
112 /// </p><p>
113 /// where &lt;product name&gt; is the name of the product being installed. In general the value for this summary
114 /// property only changes in the product name, nothing else.
115 /// </p><p>
116 /// Win32 MSI APIs:
117 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
118 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
119 /// </p></remarks>
120 public string Comments
121 {
122 get { return this[6]; }
123 set { this[6] = value; }
124 }
125
126 /// <summary>Gets or sets the Template summary information property.</summary>
127 /// <remarks><p>
128 /// The Template summary information propery indicates the platform and language versions supported by the database.
129 /// </p><p>
130 /// The syntax of the Template Summary property information is:
131 /// [platform property][,platform property][,...];[language id][,language id][,...]
132 /// </p><p>
133 /// For example, the following are all valid values for the Template Summary property:
134 /// <list type="bullet">
135 /// <item>Intel;1033</item>
136 /// <item>Intel64;1033</item>
137 /// <item>;1033</item>
138 /// <item>;</item>
139 /// <item>Intel ;1033,2046</item>
140 /// <item>Intel64;1033,2046</item>
141 /// <item>Intel;0</item>
142 /// </list>
143 /// </p><p>
144 /// If this is a 64-bit Windows Installer, enter Intel64 in the Template summary information property. Note that an
145 /// installation package cannot have both the Intel and Intel64 properties set.
146 /// </p><p>
147 /// If the current platform does not match one of the platforms specified then the installer will not process the
148 /// package. Not specifying a platform implies that the package is platform-independent.
149 /// </p><p>
150 /// Entering 0 in the language ID field of the Template summary information property, or leaving this field empty,
151 /// indicates that the package is language neutral.
152 /// </p><p>
153 /// There are variations of this property depending on whether it is in a source installer database or a transform.
154 /// </p><p>
155 /// Source Installer Database - Only one language can be specified in a source installer database. Merge Modules are
156 /// the only packages that may have multiple languages. For more information, see Multiple Language Merge Modules.
157 /// </p><p>
158 /// Transform - In a transform file, only one language may be specified. The specified platform and language determine
159 /// whether a transform can be applied to a particular database. The platform property and the language property can
160 /// be left blank if no transform restriction relies on them to validate the transform.
161 /// </p><p>
162 /// This summary property is REQUIRED.
163 /// </p><p>
164 /// Win32 MSI APIs:
165 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
166 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
167 /// </p></remarks>
168 public string Template
169 {
170 get { return this[7]; }
171 set { this[7] = value; }
172 }
173
174 /// <summary>Gets or sets the LastSavedBy summary information property.</summary>
175 /// <remarks><p>
176 /// The installer sets the Last Saved By summary information property to the value of the LogonUser property during
177 /// an administrative installation. The installer never uses this property and a user never needs to modify it.
178 /// Developers of a database editing tool may use this property to track the last person to modify the database.
179 /// This property should be left set to null in a final shipping database.
180 /// </p><p>
181 /// In a transform, this summary property contains the platform and language ID(s) that a database should have
182 /// after it has been transformed. The property specifies to what the Template should be set in the new database.
183 /// </p><p>
184 /// Win32 MSI APIs:
185 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
186 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
187 /// </p></remarks>
188 public string LastSavedBy
189 {
190 get { return this[8]; }
191 set { this[8] = value; }
192 }
193
194 /// <summary>Gets or sets the RevisionNumber summary information property.</summary>
195 /// <remarks><p>
196 /// The Revision Number summary information property contains the package code for the installer package. The
197 /// package code is a unique identifier of the installer package.
198 /// </p><p>
199 /// The Revision Number summary information property of a patch package specifies the GUID patch code for
200 /// the patch. This is followed by a list of patch code GUIDs for obsolete patches that are removed when this
201 /// patch is applied. The patch codes are concatenated with no delimiters separating GUIDs in the list.
202 /// </p><p>
203 /// The Revision Number summary information property of a transform package lists the product code GUIDs
204 /// and version of the new and original products and the upgrade code GUID. The list is separated with
205 /// semicolons as follows.
206 /// </p><p>
207 /// Original-Product-Code Original-Product-Version ; New-Product Code New-Product-Version; Upgrade-Code
208 /// </p><p>
209 /// This summary property is REQUIRED.
210 /// </p><p>
211 /// Win32 MSI APIs:
212 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
213 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
214 /// </p></remarks>
215 public string RevisionNumber
216 {
217 get { return this[9]; }
218 set { this[9] = value; }
219 }
220
221 /// <summary>Gets or sets the CreatingApp summary information property.</summary>
222 /// <remarks><p>
223 /// The CreatingApp summary information property conveys which application created the installer database.
224 /// In general the value for this summary property is the name of the software used to author this database.
225 /// </p><p>
226 /// Win32 MSI APIs:
227 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
228 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
229 /// </p></remarks>
230 public string CreatingApp
231 {
232 get { return this[18]; }
233 set { this[18] = value; }
234 }
235
236 /// <summary>Gets or sets the LastPrintTime summary information property.</summary>
237 /// <remarks><p>
238 /// The LastPrintTime summary information property can be set to the date and time during an administrative
239 /// installation to record when the administrative image was created. For non-administrative installations
240 /// this property is the same as the CreateTime summary information property.
241 /// </p><p>
242 /// Win32 MSI APIs:
243 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
244 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
245 /// </p></remarks>
246 public DateTime LastPrintTime
247 {
248 get { return (DateTime) this[11, typeof(DateTime)]; }
249 set { this[11, typeof(DateTime)] = value; }
250 }
251
252 /// <summary>Gets or sets the CreateTime summary information property.</summary>
253 /// <remarks><p>
254 /// The CreateTime summary information property conveys when the installer database was created.
255 /// </p><p>
256 /// Win32 MSI APIs:
257 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
258 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
259 /// </p></remarks>
260 public DateTime CreateTime
261 {
262 get { return (DateTime) this[12, typeof(DateTime)]; }
263 set { this[12, typeof(DateTime)] = value; }
264 }
265
266 /// <summary>Gets or sets the LastSaveTime summary information property.</summary>
267 /// <remarks><p>
268 /// The LastSaveTime summary information property conveys when the last time the installer database was
269 /// modified. Each time a user changes an installation the value for this summary property is updated to
270 /// the current system time/date at the time the installer database was saved. Initially the value for
271 /// this summary property is set to null to indicate that no changes have yet been made.
272 /// </p><p>
273 /// Win32 MSI APIs:
274 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
275 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
276 /// </p></remarks>
277 public DateTime LastSaveTime
278 {
279 get { return (DateTime) this[13, typeof(DateTime)]; }
280 set { this[13, typeof(DateTime)] = value; }
281 }
282
283 /// <summary>Gets or sets the CodePage summary information property.</summary>
284 /// <remarks><p>
285 /// The Codepage summary information property is the numeric value of the ANSI code page used for any
286 /// strings that are stored in the summary information. Note that this is not the same code page for
287 /// strings in the installation database. The Codepage summary information property is used to translate
288 /// the strings in the summary information into Unicode when calling the Unicode API functions. The
289 /// Codepage summary information property must be set before any string properties are set in the
290 /// summary information.
291 /// </p><p>
292 /// Win32 MSI APIs:
293 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
294 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
295 /// </p></remarks>
296 public short CodePage
297 {
298 get { return (short) this[1, typeof(short)]; }
299 set { this[1, typeof(short)] = value; }
300 }
301
302 /// <summary>Gets or sets the PageCount summary information property.</summary>
303 /// <remarks><p>
304 /// For an installation package, the PageCount summary information property contains the minimum
305 /// installer version required. For Windows Installer version 1.0, this property must be set to the
306 /// integer 100. For 64-bit Windows Installer Packages, this property must be set to the integer 200.
307 /// </p><p>
308 /// For a transform package, the PageCount summary information property contains minimum installer
309 /// version required to process the transform. Set to the greater of the two PageCount summary information
310 /// property values belonging to the databases used to generate the transform.
311 /// </p><p>
312 /// The PageCount summary information property is set to null in patch packages.
313 /// </p><p>
314 /// This summary property is REQUIRED.
315 /// </p><p>
316 /// Win32 MSI APIs:
317 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
318 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
319 /// </p></remarks>
320 public int PageCount
321 {
322 get { return (int) this[14, typeof(int)]; }
323 set { this[14, typeof(int)] = value; }
324 }
325
326 /// <summary>Gets or sets the WordCount summary information property.</summary>
327 /// <remarks><p>
328 /// The WordCount summary information property indicates the type of source file image. If this property is
329 /// not present, it defaults to 0. Note that this property is stored in place of the standard Count property.
330 /// </p><p>
331 /// This property is a bit field. New bits may be added in the future. At present the following bits are
332 /// available:
333 /// <list type="bullet">
334 /// <item>Bit 0: 0 = long file names, 1 = short file names</item>
335 /// <item>Bit 1: 0 = source is uncompressed, 1 = source is compressed</item>
336 /// <item>Bit 2: 0 = source is original media, 1 = source is administrative installation</item>
337 /// <item>[MSI 4.0] Bit 3: 0 = elevated privileges can be required to install, 1 = elevated privileges are not required to install</item>
338 /// </list>
339 /// </p><p>
340 /// These are combined to give the WordCount summary information property one of the following values
341 /// indicating a type of source file image:
342 /// <list type="bullet">
343 /// <item>0 - Original source using long file names. Matches tree in Directory table.</item>
344 /// <item>1 - Original source using short file names. Matches tree in Directory table.</item>
345 /// <item>2 - Compressed source files using long file names. Matches cabinets and files in the Media table.</item>
346 /// <item>3 - Compressed source files using short file names. Matches cabinets and files in the Media table.</item>
347 /// <item>4 - Administrative image using long file names. Matches tree in Directory table.</item>
348 /// <item>5 - Administrative image using short file names. Matches tree in Directory table.</item>
349 /// </list>
350 /// </p><p>
351 /// Note that if the package is marked as compressed (bit 1 is set), the installer only installs files
352 /// located at the root of the source. In this case, even files marked as uncompressed in the File table must
353 /// be located at the root to be installed. To specify a source image that has both a cabinet file (compressed
354 /// files) and uncompressed files that match the tree in the Directory table, mark the package as uncompressed
355 /// by leaving bit 1 unset (value=0) in the WordCount summary information property and set
356 /// <see cref="FileAttributes.Compressed"/> (value=16384) in the Attributes column of the File table
357 /// for each file in the cabinet.
358 /// </p><p>
359 /// For a patch package, the WordCount summary information property specifies the patch engine that was used
360 /// to create the patch files. The default value is 1 and indicates that MSPATCH was used to create the patch
361 /// A value of "2" means that the patch is using smaller, optimized, files available only with Windows Installer
362 /// version 1.2 or later. A patch with a WordCount of "2" fails immediately if used with a Windows Installer
363 /// version earlier than 1.2. A patch with a WordCount of "3" fails immediately if used with a Windows Installer
364 /// version earlier than 2.0.
365 /// </p><p>
366 /// This summary property is REQUIRED.
367 /// </p><p>
368 /// Win32 MSI APIs:
369 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
370 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
371 /// </p></remarks>
372 public int WordCount
373 {
374 get { return (int) this[15, typeof(int)]; }
375 set { this[15, typeof(int)] = value; }
376 }
377
378 /// <summary>Gets or sets the CharacterCount summary information property.</summary>
379 /// <remarks><p>
380 /// The CharacterCount summary information property is only used in transforms. This part of the summary
381 /// information stream is divided into two 16-bit words. The upper word contains the transform validation
382 /// flags. The lower word contains the transform error condition flags.
383 /// </p><p>
384 /// Win32 MSI APIs:
385 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
386 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
387 /// </p></remarks>
388 public int CharacterCount
389 {
390 get { return (int) this[16, typeof(int)]; }
391 set { this[16, typeof(int)] = value; }
392 }
393
394 /// <summary>Gets or sets the Security summary information property.</summary>
395 /// <remarks><p>
396 /// The Security summary information property conveys whether the package should be opened as read-only. The database
397 /// editing tool should not modify a read-only enforced database and should issue a warning at attempts to modify a
398 /// read-only recommended database. The following values of this property are applicable to Windows Installer files:
399 /// <list type="bullet">
400 /// <item>0 - no restriction</item>
401 /// <item>2 - read only recommended</item>
402 /// <item>4 - read only enforced</item>
403 /// </list>
404 /// </p><p>
405 /// This property should be set to read-only recommended (2) for an installation database and to read-only
406 /// enforced (4) for a transform or patch.
407 /// </p><p>
408 /// Win32 MSI APIs:
409 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfogetproperty.asp">MsiSummaryInfoGetProperty</a>,
410 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfosetproperty.asp">MsiSummaryInfoSetProperty</a>
411 /// </p></remarks>
412 public int Security
413 {
414 get { return (int) this[19, typeof(int)]; }
415 set { this[19, typeof(int)] = value; }
416 }
417
418 private object this[uint property, Type type]
419 {
420 get
421 {
422 uint dataType;
423 StringBuilder stringValue = new StringBuilder("");
424 uint bufSize = 0;
425 int intValue;
426 long timeValue = 0;
427
428 uint ret = RemotableNativeMethods.MsiSummaryInfoGetProperty(
429 (int) this.Handle,
430 property,
431 out dataType,
432 out intValue,
433 ref timeValue,
434 stringValue,
435 ref bufSize);
436 if (ret != 0 && dataType != (uint) VarEnum.VT_LPSTR)
437 {
438 throw InstallerException.ExceptionFromReturnCode(ret);
439 }
440
441 switch ((VarEnum) dataType)
442 {
443 case VarEnum.VT_EMPTY:
444 {
445 if (type == typeof(DateTime))
446 {
447 return DateTime.MinValue;
448 }
449 else if (type == typeof(string))
450 {
451 return String.Empty;
452 }
453 else if (type == typeof(short))
454 {
455 return (short) 0;
456 }
457 else
458 {
459 return (int) 0;
460 }
461 }
462
463 case VarEnum.VT_LPSTR:
464 {
465 if (ret == (uint) NativeMethods.Error.MORE_DATA)
466 {
467 stringValue.Capacity = (int) ++bufSize;
468 ret = RemotableNativeMethods.MsiSummaryInfoGetProperty(
469 (int) this.Handle,
470 property,
471 out dataType,
472 out intValue,
473 ref timeValue,
474 stringValue,
475 ref bufSize);
476 }
477 if (ret != 0)
478 {
479 throw InstallerException.ExceptionFromReturnCode(ret);
480 }
481 return stringValue.ToString();
482 }
483
484 case VarEnum.VT_I2:
485 case VarEnum.VT_I4:
486 {
487 if (type == typeof(string))
488 {
489 return intValue.ToString(CultureInfo.InvariantCulture);
490 }
491 else if (type == typeof(short))
492 {
493 return (short) intValue;
494 }
495 else
496 {
497 return intValue;
498 }
499 }
500
501 case VarEnum.VT_FILETIME:
502 {
503 if (type == typeof(string))
504 {
505 return DateTime.FromFileTime(timeValue).ToString(CultureInfo.InvariantCulture);
506 }
507 else
508 {
509 return DateTime.FromFileTime(timeValue);
510 }
511 }
512
513 default:
514 {
515 throw new InstallerException();
516 }
517 }
518 }
519
520 set
521 {
522 uint dataType = (uint) VarEnum.VT_NULL;
523 string stringValue = "";
524 int intValue = 0;
525 long timeValue = 0;
526
527 if (type == typeof(short))
528 {
529 dataType = (uint) VarEnum.VT_I2;
530 intValue = (int)(short) value; // Double cast because value is a *boxed* short.
531 }
532 else if (type == typeof(int))
533 {
534 dataType = (uint) VarEnum.VT_I4;
535 intValue = (int) value;
536 }
537 else if (type == typeof(string))
538 {
539 dataType = (uint) VarEnum.VT_LPSTR;
540 stringValue = (string) value;
541 }
542 else // (type == typeof(DateTime))
543 {
544 dataType = (uint) VarEnum.VT_FILETIME;
545 timeValue = ((DateTime) value).ToFileTime();
546 }
547
548 uint ret = NativeMethods.MsiSummaryInfoSetProperty(
549 (int) this.Handle,
550 property,
551 dataType,
552 intValue,
553 ref timeValue,
554 stringValue);
555 if (ret != 0)
556 {
557 throw InstallerException.ExceptionFromReturnCode(ret);
558 }
559 }
560 }
561
562 private string this[uint property]
563 {
564 get { return (string) this[property, typeof(string)]; }
565 set { this[property, typeof(string)] = value; }
566 }
567
568 /// <summary>
569 /// Formats and writes the previously stored properties into the standard summary information stream.
570 /// </summary>
571 /// <exception cref="InstallerException">The stream cannot be successfully written.</exception>
572 /// <remarks><p>
573 /// This method may only be called once after all the property values have been set. Properties may
574 /// still be read after the stream is written.
575 /// </p><p>
576 /// Win32 MSI API:
577 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msisummaryinfopersist.asp">MsiSummaryInfoPersist</a>
578 /// </p></remarks>
579 public void Persist()
580 {
581 uint ret = NativeMethods.MsiSummaryInfoPersist((int) this.Handle);
582 if (ret != 0)
583 {
584 throw InstallerException.ExceptionFromReturnCode(ret);
585 }
586 }
587
588 private static int OpenSummaryInfo(string packagePath, bool enableWrite)
589 {
590 int summaryInfoHandle;
591 int maxProperties = !enableWrite ? 0 : SummaryInfo.MAX_PROPERTIES;
592 uint ret = RemotableNativeMethods.MsiGetSummaryInformation(
593 0,
594 packagePath,
595 (uint) maxProperties,
596 out summaryInfoHandle);
597 if (ret != 0)
598 {
599 if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND ||
600 ret == (uint) NativeMethods.Error.ACCESS_DENIED)
601 {
602 throw new FileNotFoundException(null, packagePath);
603 }
604 else
605 {
606 throw InstallerException.ExceptionFromReturnCode(ret);
607 }
608 }
609 return summaryInfoHandle;
610 }
611 }
612}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/TableCollection.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/TableCollection.cs
new file mode 100644
index 00000000..95176ea0
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/TableCollection.cs
@@ -0,0 +1,192 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Text;
8
9 /// <summary>
10 /// Contains information about all the tables in a Windows Installer database.
11 /// </summary>
12 public class TableCollection : ICollection<TableInfo>
13 {
14 private Database db;
15
16 internal TableCollection(Database db)
17 {
18 this.db = db;
19 }
20
21 /// <summary>
22 /// Gets the number of tables in the database.
23 /// </summary>
24 public int Count
25 {
26 get
27 {
28 return this.GetTables().Count;
29 }
30 }
31
32 /// <summary>
33 /// Gets a boolean value indicating whether the collection is read-only.
34 /// A TableCollection is read-only when the database is read-only.
35 /// </summary>
36 /// <value>read-only status of the collection</value>
37 public bool IsReadOnly
38 {
39 get
40 {
41 return this.db.IsReadOnly;
42 }
43 }
44
45 /// <summary>
46 /// Gets information about a given table.
47 /// </summary>
48 /// <param name="table">case-sensitive name of the table</param>
49 /// <returns>information about the requested table, or null if the table does not exist in the database</returns>
50 public TableInfo this[string table]
51 {
52 get
53 {
54 if (String.IsNullOrEmpty(table))
55 {
56 throw new ArgumentNullException("table");
57 }
58
59 if (!this.Contains(table))
60 {
61 return null;
62 }
63
64 return new TableInfo(this.db, table);
65 }
66 }
67
68 /// <summary>
69 /// Adds a new table to the database.
70 /// </summary>
71 /// <param name="item">information about the table to be added</param>
72 /// <exception cref="InvalidOperationException">a table with the same name already exists in the database</exception>
73 public void Add(TableInfo item)
74 {
75 if (item == null)
76 {
77 throw new ArgumentNullException("item");
78 }
79
80 if (this.Contains(item.Name))
81 {
82 throw new InvalidOperationException();
83 }
84
85 this.db.Execute(item.SqlCreateString);
86 }
87
88 /// <summary>
89 /// Removes all tables (and all data) from the database.
90 /// </summary>
91 public void Clear()
92 {
93 foreach (string table in this.GetTables())
94 {
95 this.Remove(table);
96 }
97 }
98
99 /// <summary>
100 /// Checks if the database contains a table with the given name.
101 /// </summary>
102 /// <param name="item">case-sensitive name of the table to search for</param>
103 /// <returns>True if the table exists, false otherwise.</returns>
104 public bool Contains(string item)
105 {
106 if (String.IsNullOrEmpty(item))
107 {
108 throw new ArgumentNullException("item");
109 }
110 uint ret = RemotableNativeMethods.MsiDatabaseIsTablePersistent((int) this.db.Handle, item);
111 if (ret == 3) // MSICONDITION_ERROR
112 {
113 throw new InstallerException();
114 }
115 return ret != 2; // MSICONDITION_NONE
116 }
117
118 bool ICollection<TableInfo>.Contains(TableInfo item)
119 {
120 return this.Contains(item.Name);
121 }
122
123 /// <summary>
124 /// Copies the table information from this collection into an array.
125 /// </summary>
126 /// <param name="array">destination array to be filed</param>
127 /// <param name="arrayIndex">offset into the destination array where copying begins</param>
128 public void CopyTo(TableInfo[] array, int arrayIndex)
129 {
130 if (array == null)
131 {
132 throw new ArgumentNullException("array");
133 }
134
135 foreach (string table in this.GetTables())
136 {
137 array[arrayIndex++] = new TableInfo(this.db, table);
138 }
139 }
140
141 /// <summary>
142 /// Removes a table from the database.
143 /// </summary>
144 /// <param name="item">case-sensitive name of the table to be removed</param>
145 /// <returns>true if the table was removed, false if the table did not exist</returns>
146 public bool Remove(string item)
147 {
148 if (String.IsNullOrEmpty(item))
149 {
150 throw new ArgumentNullException("item");
151 }
152
153 if (!this.Contains(item))
154 {
155 return false;
156 }
157 this.db.Execute("DROP TABLE `{0}`", item);
158 return true;
159 }
160
161 bool ICollection<TableInfo>.Remove(TableInfo item)
162 {
163 if (item == null)
164 {
165 throw new ArgumentNullException("item");
166 }
167
168 return this.Remove(item.Name);
169 }
170
171 /// <summary>
172 /// Enumerates the tables in the database.
173 /// </summary>
174 public IEnumerator<TableInfo> GetEnumerator()
175 {
176 foreach (string table in this.GetTables())
177 {
178 yield return new TableInfo(this.db, table);
179 }
180 }
181
182 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
183 {
184 return this.GetEnumerator();
185 }
186
187 private IList<string> GetTables()
188 {
189 return this.db.ExecuteStringQuery("SELECT `Name` FROM `_Tables`");
190 }
191 }
192}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/TableInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/TableInfo.cs
new file mode 100644
index 00000000..e5a850b0
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/TableInfo.cs
@@ -0,0 +1,264 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections.Generic;
8 using System.Collections.ObjectModel;
9
10 /// <summary>
11 /// Defines a table in an installation database.
12 /// </summary>
13 public class TableInfo
14 {
15 private string name;
16 private ColumnCollection columns;
17 private ReadOnlyCollection<string> primaryKeys;
18
19 /// <summary>
20 /// Creates a table definition.
21 /// </summary>
22 /// <param name="name">Name of the table.</param>
23 /// <param name="columns">Columns in the table.</param>
24 /// <param name="primaryKeys">The primary keys of the table.</param>
25 public TableInfo(string name, ICollection<ColumnInfo> columns, IList<string> primaryKeys)
26 {
27 if (String.IsNullOrEmpty(name))
28 {
29 throw new ArgumentNullException("name");
30 }
31
32 if (columns == null || columns.Count == 0)
33 {
34 throw new ArgumentNullException("columns");
35 }
36
37 if (primaryKeys == null || primaryKeys.Count == 0)
38 {
39 throw new ArgumentNullException("primaryKeys");
40 }
41
42 this.name = name;
43 this.columns = new ColumnCollection(columns);
44 this.primaryKeys = new List<string>(primaryKeys).AsReadOnly();
45 foreach (string primaryKey in this.primaryKeys)
46 {
47 if (!this.columns.Contains(primaryKey))
48 {
49 throw new ArgumentOutOfRangeException("primaryKeys");
50 }
51 }
52 }
53
54 internal TableInfo(Database db, string name)
55 {
56 if (db == null)
57 {
58 throw new ArgumentNullException("db");
59 }
60
61 if (String.IsNullOrEmpty(name))
62 {
63 throw new ArgumentNullException("name");
64 }
65
66 this.name = name;
67
68 using (View columnsView = db.OpenView("SELECT * FROM `{0}`", name))
69 {
70 this.columns = new ColumnCollection(columnsView);
71 }
72
73 this.primaryKeys = new ReadOnlyCollection<string>(
74 TableInfo.GetTablePrimaryKeys(db, name));
75 }
76
77 /// <summary>
78 /// Gets the name of the table.
79 /// </summary>
80 public string Name
81 {
82 get
83 {
84 return this.name;
85 }
86 }
87
88 /// <summary>
89 /// Gets information about the columns in this table.
90 /// </summary>
91 /// <remarks><p>
92 /// This property queries the database every time it is called,
93 /// to ensure the returned values are up-to-date. For best performance,
94 /// hold onto the returned collection if using it more than once.
95 /// </p></remarks>
96 public ColumnCollection Columns
97 {
98 get
99 {
100 return this.columns;
101 }
102 }
103
104 /// <summary>
105 /// Gets the names of the columns that are primary keys of the table.
106 /// </summary>
107 public IList<string> PrimaryKeys
108 {
109 get
110 {
111 return this.primaryKeys;
112 }
113 }
114
115 /// <summary>
116 /// Gets an SQL CREATE string that can be used to create the table.
117 /// </summary>
118 public string SqlCreateString
119 {
120 get
121 {
122 StringBuilder s = new StringBuilder("CREATE TABLE `");
123 s.Append(this.name);
124 s.Append("` (");
125 int count = 0;
126 foreach (ColumnInfo col in this.Columns)
127 {
128 if (count > 0)
129 {
130 s.Append(", ");
131 }
132 s.Append(col.SqlCreateString);
133 count++;
134 }
135 s.Append(" PRIMARY KEY ");
136 count = 0;
137 foreach (string key in this.PrimaryKeys)
138 {
139 if (count > 0)
140 {
141 s.Append(", ");
142 }
143 s.AppendFormat("`{0}`", key);
144 count++;
145 }
146 s.Append(')');
147 return s.ToString();
148 }
149 }
150
151 /// <summary>
152 /// Gets an SQL INSERT string that can be used insert a new record into the table.
153 /// </summary>
154 /// <remarks><p>
155 /// The values are expressed as question-mark tokens, to be supplied by the record.
156 /// </p></remarks>
157 public string SqlInsertString
158 {
159 get
160 {
161 StringBuilder s = new StringBuilder("INSERT INTO `");
162 s.Append(this.name);
163 s.Append("` (");
164 int count = 0;
165 foreach (ColumnInfo col in this.Columns)
166 {
167 if (count > 0)
168 {
169 s.Append(", ");
170 }
171
172 s.AppendFormat("`{0}`", col.Name);
173 count++;
174 }
175 s.Append(") VALUES (");
176 while (count > 0)
177 {
178 count--;
179 s.Append("?");
180
181 if (count > 0)
182 {
183 s.Append(", ");
184 }
185 }
186 s.Append(')');
187 return s.ToString();
188 }
189 }
190
191 /// <summary>
192 /// Gets an SQL SELECT string that can be used to select all columns of the table.
193 /// </summary>
194 /// <remarks><p>
195 /// The columns are listed explicitly in the SELECT string, as opposed to using "SELECT *".
196 /// </p></remarks>
197 public string SqlSelectString
198 {
199 get
200 {
201 StringBuilder s = new StringBuilder("SELECT ");
202 int count = 0;
203 foreach (ColumnInfo col in this.Columns)
204 {
205 if (count > 0) s.Append(", ");
206 s.AppendFormat("`{0}`", col.Name);
207 count++;
208 }
209 s.AppendFormat(" FROM `{0}`", this.Name);
210 return s.ToString();
211 }
212 }
213
214 /// <summary>
215 /// Gets a string representation of the table.
216 /// </summary>
217 /// <returns>The name of the table.</returns>
218 public override string ToString()
219 {
220 return this.name;
221 }
222
223 private static IList<string> GetTablePrimaryKeys(Database db, string table)
224 {
225 if (table == "_Tables")
226 {
227 return new string[] { "Name" };
228 }
229 else if (table == "_Columns")
230 {
231 return new string[] { "Table", "Number" };
232 }
233 else if (table == "_Storages")
234 {
235 return new string[] { "Name" };
236 }
237 else if (table == "_Streams")
238 {
239 return new string[] { "Name" };
240 }
241 else
242 {
243 int hrec;
244 uint ret = RemotableNativeMethods.MsiDatabaseGetPrimaryKeys(
245 (int) db.Handle, table, out hrec);
246 if (ret != 0)
247 {
248 throw InstallerException.ExceptionFromReturnCode(ret);
249 }
250
251 using (Record rec = new Record((IntPtr) hrec, true, null))
252 {
253 string[] keys = new string[rec.FieldCount];
254 for (int i = 0; i < keys.Length; i++)
255 {
256 keys[i] = rec.GetString(i + 1);
257 }
258
259 return keys;
260 }
261 }
262 }
263 }
264}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Transaction.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Transaction.cs
new file mode 100644
index 00000000..798dc79e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Transaction.cs
@@ -0,0 +1,201 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics.CodeAnalysis;
8 using System.Runtime.InteropServices;
9 using System.Threading;
10
11 /// <summary>
12 /// [MSI 4.5] Handle to a multi-session install transaction.
13 /// </summary>
14 /// <remarks><p>
15 /// Win32 MSI APIs:
16 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msibegintransaction.asp">MsiBeginTransaction</a>
17 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msijointransaction.asp">MsiJoinTransaction</a>
18 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiendtransaction.asp">MsiEndTransaction</a>
19 /// </p></remarks>
20 public class Transaction : InstallerHandle
21 {
22 private string name;
23 private IntPtr ownerChangeEvent;
24 private IList<EventHandler<EventArgs>> ownerChangeListeners;
25
26 /// <summary>
27 /// [MSI 4.5] Begins transaction processing of a multi-package installation.
28 /// </summary>
29 /// <param name="name">Name of the multi-package installation.</param>
30 /// <param name="attributes">Select optional behavior when beginning the transaction.</param>
31 /// <exception cref="InstallerException">The transaction could not be initialized.</exception>
32 /// <remarks><p>
33 /// Win32 MSI API:
34 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msibegintransaction.asp">MsiBeginTransaction</a>
35 /// </p></remarks>
36 public Transaction(string name, TransactionAttributes attributes)
37 : this(name, Transaction.Begin(name, attributes), true)
38 {
39 }
40
41 /// <summary>
42 /// Internal constructor.
43 /// </summary>
44 /// <remarks>
45 /// The second parameter is an array in order to receive multiple values from the initialization method.
46 /// </remarks>
47 private Transaction(string name, IntPtr[] handles, bool ownsHandle)
48 : base(handles[0], ownsHandle)
49 {
50 this.name = name;
51 this.ownerChangeEvent = handles[1];
52 this.ownerChangeListeners = new List<EventHandler<EventArgs>>();
53 }
54
55 /// <summary>
56 /// Creates a new Transaction object from an integer handle.
57 /// </summary>
58 /// <param name="handle">Integer transaction handle</param>
59 /// <param name="ownsHandle">true to close the handle when this object is disposed</param>
60 public static Transaction FromHandle(IntPtr handle, bool ownsHandle)
61 {
62 return new Transaction(handle.ToString(), new IntPtr[] { handle, IntPtr.Zero }, ownsHandle);
63 }
64
65 /// <summary>
66 /// Gets the name of the transaction.
67 /// </summary>
68 public string Name
69 {
70 get
71 {
72 return name;
73 }
74 }
75
76 /// <summary>
77 /// Notifies listeners when the process that owns the transaction changed.
78 /// </summary>
79 public event EventHandler<EventArgs> OwnerChanged
80 {
81 add
82 {
83 this.ownerChangeListeners.Add(value);
84
85 if (this.ownerChangeEvent != IntPtr.Zero && this.ownerChangeListeners.Count == 1)
86 {
87 new Thread(this.WaitForOwnerChange).Start();
88 }
89 }
90 remove
91 {
92 this.ownerChangeListeners.Remove(value);
93 }
94 }
95
96 private void OnOwnerChanged()
97 {
98 EventArgs e = new EventArgs();
99 foreach (EventHandler<EventArgs> handler in this.ownerChangeListeners)
100 {
101 handler(this, e);
102 }
103 }
104
105 private void WaitForOwnerChange()
106 {
107 int ret = NativeMethods.WaitForSingleObject(this.ownerChangeEvent, -1);
108 if (ret == 0)
109 {
110 this.OnOwnerChanged();
111 }
112 else
113 {
114 throw new InstallerException();
115 }
116 }
117
118 /// <summary>
119 /// Makes the current process the owner of the multi-package installation transaction.
120 /// </summary>
121 /// <param name="attributes">Select optional behavior when joining the transaction.</param>
122 /// <exception cref="InvalidHandleException">The transaction handle is not valid.</exception>
123 /// <exception cref="InstallerException">The transaction could not be joined.</exception>
124 /// <remarks><p>
125 /// Win32 MSI API:
126 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msijointransaction.asp">MsiJoinTransaction</a>
127 /// </p></remarks>
128 public void Join(TransactionAttributes attributes)
129 {
130 IntPtr hChangeOfOwnerEvent;
131 uint ret = NativeMethods.MsiJoinTransaction((int) this.Handle, (int) attributes, out hChangeOfOwnerEvent);
132 if (ret != 0)
133 {
134 throw InstallerException.ExceptionFromReturnCode(ret);
135 }
136
137 this.ownerChangeEvent = hChangeOfOwnerEvent;
138 if (this.ownerChangeEvent != IntPtr.Zero && this.ownerChangeListeners.Count >= 1)
139 {
140 new Thread(this.WaitForOwnerChange).Start();
141 }
142 }
143
144 /// <summary>
145 /// Ends the install transaction and commits all changes to the system belonging to the transaction.
146 /// </summary>
147 /// <exception cref="InstallerException">The transaction could not be committed.</exception>
148 /// <remarks><p>
149 /// Runs any Commit Custom Actions and commits to the system any changes to Win32 or common language
150 /// runtime assemblies. Deletes the rollback script, and after using this option, the transaction's
151 /// changes can no longer be undone with a Rollback Installation.
152 /// </p><p>
153 /// This method can only be called by the current owner of the transaction.
154 /// </p><p>
155 /// Win32 MSI API:
156 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiendtransaction.asp">MsiEndTransaction</a>
157 /// </p></remarks>
158 public void Commit()
159 {
160 this.End(true);
161 }
162
163 /// <summary>
164 /// Ends the install transaction and undoes changes to the system belonging to the transaction.
165 /// </summary>
166 /// <exception cref="InstallerException">The transaction could not be rolled back.</exception>
167 /// <remarks><p>
168 /// This method can only be called by the current owner of the transaction.
169 /// </p><p>
170 /// Win32 MSI API:
171 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiendtransaction.asp">MsiEndTransaction</a>
172 /// </p></remarks>
173 public void Rollback()
174 {
175 this.End(false);
176 }
177
178 private static IntPtr[] Begin(string transactionName, TransactionAttributes attributes)
179 {
180 int hTransaction;
181 IntPtr hChangeOfOwnerEvent;
182 uint ret = NativeMethods.MsiBeginTransaction(transactionName, (int) attributes, out hTransaction, out hChangeOfOwnerEvent);
183 if (ret != 0)
184 {
185 throw InstallerException.ExceptionFromReturnCode(ret);
186 }
187
188 return new IntPtr[] { (IntPtr) hTransaction, hChangeOfOwnerEvent };
189 }
190
191 [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
192 private void End(bool commit)
193 {
194 uint ret = NativeMethods.MsiEndTransaction(commit ? 1 : 0);
195 if (ret != 0)
196 {
197 throw InstallerException.ExceptionFromReturnCode(ret);
198 }
199 }
200 }
201}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/ValidationErrorInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/ValidationErrorInfo.cs
new file mode 100644
index 00000000..3f75326e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/ValidationErrorInfo.cs
@@ -0,0 +1,46 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System.Diagnostics.CodeAnalysis;
6
7 /// <summary>
8 /// Contains specific information about an error encountered by the <see cref="View.Validate"/>,
9 /// <see cref="View.ValidateNew"/>, or <see cref="View.ValidateFields"/> methods of the
10 /// <see cref="View"/> class.
11 /// </summary>
12 [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")]
13 public struct ValidationErrorInfo
14 {
15 private ValidationError error;
16 private string column;
17
18 internal ValidationErrorInfo(ValidationError error, string column)
19 {
20 this.error = error;
21 this.column = column;
22 }
23
24 /// <summary>
25 /// Gets the type of validation error encountered.
26 /// </summary>
27 public ValidationError Error
28 {
29 get
30 {
31 return this.error;
32 }
33 }
34
35 /// <summary>
36 /// Gets the column containing the error, or null if the error applies to the whole row.
37 /// </summary>
38 public string Column
39 {
40 get
41 {
42 return this.column;
43 }
44 }
45 }
46}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/View.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/View.cs
new file mode 100644
index 00000000..21e8543e
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/View.cs
@@ -0,0 +1,625 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.Text;
7 using System.Collections.Generic;
8 using System.Globalization;
9 using System.Diagnostics.CodeAnalysis;
10
11 /// <summary>
12 /// A View represents a result set obtained when processing a query using the
13 /// <see cref="WixToolset.Dtf.WindowsInstaller.Database.OpenView"/> method of a
14 /// <see cref="Database"/>. Before any data can be transferred,
15 /// the query must be executed using the <see cref="Execute(Record)"/> method, passing to
16 /// it all replaceable parameters designated within the SQL query string.
17 /// </summary>
18 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
19 public class View : InstallerHandle, IEnumerable<Record>
20 {
21 private Database database;
22 private string sql;
23 private IList<TableInfo> tables;
24 private ColumnCollection columns;
25
26 internal View(IntPtr handle, string sql, Database database)
27 : base(handle, true)
28 {
29 this.sql = sql;
30 this.database = database;
31 }
32
33 /// <summary>
34 /// Gets the Database on which this View was opened.
35 /// </summary>
36 public Database Database
37 {
38 get { return this.database; }
39 }
40
41 /// <summary>
42 /// Gets the SQL query string used to open this View.
43 /// </summary>
44 public string QueryString
45 {
46 get { return this.sql; }
47 }
48
49 /// <summary>
50 /// Gets the set of tables that were included in the SQL query for this View.
51 /// </summary>
52 public IList<TableInfo> Tables
53 {
54 get
55 {
56 if (this.tables == null)
57 {
58 if (this.sql == null)
59 {
60 return null;
61 }
62
63 // Parse the table names out of the SQL query string by looking
64 // for tokens that can come before or after the list of tables.
65
66 string parseSql = this.sql.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' ');
67 string upperSql = parseSql.ToUpper(CultureInfo.InvariantCulture);
68
69 string[] prefixes = new string[] { " FROM ", " INTO ", " TABLE " };
70 string[] suffixes = new string[] { " WHERE ", " ORDER ", " SET ", " (", " ADD " };
71
72 int index;
73
74 for (int i = 0; i < prefixes.Length; i++)
75 {
76 if ((index = upperSql.IndexOf(prefixes[i], StringComparison.Ordinal)) > 0)
77 {
78 parseSql = parseSql.Substring(index + prefixes[i].Length);
79 upperSql = upperSql.Substring(index + prefixes[i].Length);
80 }
81 }
82
83 if (upperSql.StartsWith("UPDATE ", StringComparison.Ordinal))
84 {
85 parseSql = parseSql.Substring(7);
86 upperSql = upperSql.Substring(7);
87 }
88
89 for (int i = 0; i < suffixes.Length; i++)
90 {
91 if ((index = upperSql.IndexOf(suffixes[i], StringComparison.Ordinal)) > 0)
92 {
93 parseSql = parseSql.Substring(0, index);
94 upperSql = upperSql.Substring(0, index);
95 }
96 }
97
98 if (upperSql.EndsWith(" HOLD", StringComparison.Ordinal) ||
99 upperSql.EndsWith(" FREE", StringComparison.Ordinal))
100 {
101 parseSql = parseSql.Substring(0, parseSql.Length - 5);
102 upperSql = upperSql.Substring(0, upperSql.Length - 5);
103 }
104
105 // At this point we should be left with a comma-separated list of table names,
106 // optionally quoted with grave accent marks (`).
107
108 string[] tableNames = parseSql.Split(',');
109 IList<TableInfo> tableList = new List<TableInfo>(tableNames.Length);
110 for (int i = 0; i < tableNames.Length; i++)
111 {
112 string tableName = tableNames[i].Trim(' ', '`');
113 tableList.Add(new TableInfo(this.database, tableName));
114 }
115 this.tables = tableList;
116 }
117 return new List<TableInfo>(this.tables);
118 }
119 }
120
121 /// <summary>
122 /// Gets the set of columns that were included in the query for this View,
123 /// or null if this view is not a SELECT query.
124 /// </summary>
125 /// <exception cref="InstallerException">the View is not in an active state</exception>
126 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
127 /// <remarks><p>
128 /// Win32 MSI API:
129 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewgetcolumninfo.asp">MsiViewGetColumnInfo</a>
130 /// </p></remarks>
131 public ColumnCollection Columns
132 {
133 get
134 {
135 if (this.columns == null)
136 {
137 this.columns = new ColumnCollection(this);
138 }
139 return this.columns;
140 }
141 }
142
143 /// <summary>
144 /// Executes a SQL View query and supplies any required parameters. The query uses the
145 /// question mark token to represent parameters as described in SQL Syntax. The values of
146 /// these parameters are passed in as the corresponding fields of a parameter record.
147 /// </summary>
148 /// <param name="executeParams">Optional Record that supplies the parameters. This
149 /// Record contains values to replace the parameter tokens in the SQL query.</param>
150 /// <exception cref="InstallerException">the View could not be executed</exception>
151 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
152 /// <remarks><p>
153 /// Win32 MSI API:
154 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>
155 /// </p></remarks>
156 [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Params"), SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
157 public void Execute(Record executeParams)
158 {
159 uint ret = RemotableNativeMethods.MsiViewExecute(
160 (int) this.Handle,
161 (executeParams != null ? (int) executeParams.Handle : 0));
162 if (ret == (uint) NativeMethods.Error.BAD_QUERY_SYNTAX)
163 {
164 throw new BadQuerySyntaxException(this.sql);
165 }
166 else if (ret != 0)
167 {
168 throw InstallerException.ExceptionFromReturnCode(ret);
169 }
170 }
171
172 /// <summary>
173 /// Executes a SQL View query.
174 /// </summary>
175 /// <exception cref="InstallerException">the View could not be executed</exception>
176 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
177 /// <remarks><p>
178 /// Win32 MSI API:
179 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewexecute.asp">MsiViewExecute</a>
180 /// </p></remarks>
181 public void Execute() { this.Execute(null); }
182
183 /// <summary>
184 /// Fetches the next sequential record from the view, or null if there are no more records.
185 /// </summary>
186 /// <exception cref="InstallerException">the View is not in an active state</exception>
187 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
188 /// <remarks><p>
189 /// The Record object should be <see cref="InstallerHandle.Close"/>d after use.
190 /// It is best that the handle be closed manually as soon as it is no longer
191 /// needed, as leaving lots of unused handles open can degrade performance.
192 /// </p><p>
193 /// Win32 MSI API:
194 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
195 /// </p></remarks>
196 public Record Fetch()
197 {
198 int recordHandle;
199 uint ret = RemotableNativeMethods.MsiViewFetch((int) this.Handle, out recordHandle);
200 if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS)
201 {
202 return null;
203 }
204 else if (ret != 0)
205 {
206 throw InstallerException.ExceptionFromReturnCode(ret);
207 }
208
209 Record r = new Record((IntPtr) recordHandle, true, this);
210 r.IsFormatStringInvalid = true;
211 return r;
212 }
213
214 /// <summary>
215 /// Updates a fetched Record.
216 /// </summary>
217 /// <param name="mode">specifies the modify mode</param>
218 /// <param name="record">the Record to modify</param>
219 /// <exception cref="InstallerException">the modification failed,
220 /// or a validation was requested and the data did not pass</exception>
221 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
222 /// <remarks><p>
223 /// You can update or delete a record immediately after inserting, or seeking provided you
224 /// have NOT modified the 0th field of the inserted or sought record.
225 /// </p><p>
226 /// To execute any SQL statement, a View must be created. However, a View that does not
227 /// create a result set, such as CREATE TABLE, or INSERT INTO, cannot be used with any of
228 /// the Modify methods to update tables though the view.
229 /// </p><p>
230 /// You cannot fetch a record containing binary data from one database and then use
231 /// that record to insert the data into another database. To move binary data from one database
232 /// to another, you should export the data to a file and then import it into the new database
233 /// using a query and the <see cref="Record.SetStream(int,string)"/>. This ensures that each database has
234 /// its own copy of the binary data.
235 /// </p><p>
236 /// Note that custom actions can only add, modify, or remove temporary rows, columns,
237 /// or tables from a database. Custom actions cannot modify persistent data in a database,
238 /// such as data that is a part of the database stored on disk.
239 /// </p><p>
240 /// Win32 MSI API:
241 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
242 /// </p></remarks>
243 /// <seealso cref="Refresh"/>
244 /// <seealso cref="Insert"/>
245 /// <seealso cref="Update"/>
246 /// <seealso cref="Assign"/>
247 /// <seealso cref="Replace"/>
248 /// <seealso cref="Delete"/>
249 /// <seealso cref="InsertTemporary"/>
250 /// <seealso cref="Seek"/>
251 /// <seealso cref="Merge"/>
252 /// <seealso cref="Validate"/>
253 /// <seealso cref="ValidateNew"/>
254 /// <seealso cref="ValidateFields"/>
255 /// <seealso cref="ValidateDelete"/>
256 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
257 public void Modify(ViewModifyMode mode, Record record)
258 {
259 if (record == null)
260 {
261 throw new ArgumentNullException("record");
262 }
263
264 uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) mode, (int) record.Handle);
265 if (mode == ViewModifyMode.Insert || mode == ViewModifyMode.InsertTemporary)
266 {
267 record.IsFormatStringInvalid = true;
268 }
269 if (ret != 0)
270 {
271 throw InstallerException.ExceptionFromReturnCode(ret);
272 }
273 }
274
275 /// <summary>
276 /// Refreshes the data in a Record.
277 /// </summary>
278 /// <param name="record">the Record to be refreshed</param>
279 /// <exception cref="InstallerException">the refresh failed</exception>
280 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
281 /// <remarks><p>
282 /// The Record must have been obtained by calling <see cref="Fetch"/>. Fails with
283 /// a deleted Record. Works only with read-write Records.
284 /// </p><p>
285 /// See <see cref="Modify"/> for more remarks.
286 /// </p><p>
287 /// Win32 MSI API:
288 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
289 /// </p></remarks>
290 public void Refresh(Record record) { this.Modify(ViewModifyMode.Refresh, record); }
291
292 /// <summary>
293 /// Inserts a Record into the view.
294 /// </summary>
295 /// <param name="record">the Record to be inserted</param>
296 /// <exception cref="InstallerException">the insertion failed</exception>
297 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
298 /// <remarks><p>
299 /// Fails if a row with the same primary keys exists. Fails with a read-only database.
300 /// This method cannot be used with a View containing joins.
301 /// </p><p>
302 /// See <see cref="Modify"/> for more remarks.
303 /// </p><p>
304 /// Win32 MSI API:
305 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
306 /// </p></remarks>
307 public void Insert(Record record) { this.Modify(ViewModifyMode.Insert, record); }
308
309 /// <summary>
310 /// Updates the View with new data from the Record.
311 /// </summary>
312 /// <param name="record">the new data</param>
313 /// <exception cref="InstallerException">the update failed</exception>
314 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
315 /// <remarks><p>
316 /// Only non-primary keys can be updated. The Record must have been obtained by calling
317 /// <see cref="Fetch"/>. Fails with a deleted Record. Works only with read-write Records.
318 /// </p><p>
319 /// See <see cref="Modify"/> for more remarks.
320 /// </p><p>
321 /// Win32 MSI API:
322 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
323 /// </p></remarks>
324 public void Update(Record record) { this.Modify(ViewModifyMode.Update, record); }
325
326 /// <summary>
327 /// Updates or inserts a Record into the View.
328 /// </summary>
329 /// <param name="record">the Record to be assigned</param>
330 /// <exception cref="InstallerException">the assignment failed</exception>
331 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
332 /// <remarks><p>
333 /// Updates record if the primary keys match an existing row and inserts if they do not match.
334 /// Fails with a read-only database. This method cannot be used with a View containing joins.
335 /// </p><p>
336 /// See <see cref="Modify"/> for more remarks.
337 /// </p><p>
338 /// Win32 MSI API:
339 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
340 /// </p></remarks>
341 public void Assign(Record record) { this.Modify(ViewModifyMode.Assign, record); }
342
343 /// <summary>
344 /// Updates or deletes and inserts a Record into the View.
345 /// </summary>
346 /// <param name="record">the Record to be replaced</param>
347 /// <exception cref="InstallerException">the replacement failed</exception>
348 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
349 /// <remarks><p>
350 /// The Record must have been obtained by calling <see cref="Fetch"/>. Updates record if the
351 /// primary keys are unchanged. Deletes old row and inserts new if primary keys have changed.
352 /// Fails with a read-only database. This method cannot be used with a View containing joins.
353 /// </p><p>
354 /// See <see cref="Modify"/> for more remarks.
355 /// </p><p>
356 /// Win32 MSI API:
357 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
358 /// </p></remarks>
359 public void Replace(Record record) { this.Modify(ViewModifyMode.Replace, record); }
360
361 /// <summary>
362 /// Deletes a Record from the View.
363 /// </summary>
364 /// <param name="record">the Record to be deleted</param>
365 /// <exception cref="InstallerException">the deletion failed</exception>
366 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
367 /// <remarks><p>
368 /// The Record must have been obtained by calling <see cref="Fetch"/>. Fails if the row has been
369 /// deleted. Works only with read-write records. This method cannot be used with a View containing joins.
370 /// </p><p>
371 /// See <see cref="Modify"/> for more remarks.
372 /// </p><p>
373 /// Win32 MSI API:
374 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
375 /// </p></remarks>
376 public void Delete(Record record) { this.Modify(ViewModifyMode.Delete, record); }
377
378 /// <summary>
379 /// Inserts a Record into the View. The inserted data is not persistent.
380 /// </summary>
381 /// <param name="record">the Record to be inserted</param>
382 /// <exception cref="InstallerException">the insertion failed</exception>
383 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
384 /// <remarks><p>
385 /// Fails if a row with the same primary key exists. Works only with read-write records.
386 /// This method cannot be used with a View containing joins.
387 /// </p><p>
388 /// See <see cref="Modify"/> for more remarks.
389 /// </p><p>
390 /// Win32 MSI API:
391 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
392 /// </p></remarks>
393 public void InsertTemporary(Record record) { this.Modify(ViewModifyMode.InsertTemporary, record); }
394
395 /// <summary>
396 /// Refreshes the information in the supplied record without changing the position
397 /// in the result set and without affecting subsequent fetch operations.
398 /// </summary>
399 /// <param name="record">the Record to be filled with the result of the seek</param>
400 /// <exception cref="InstallerException">the seek failed</exception>
401 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
402 /// <remarks><p>
403 /// After seeking, the Record may then be used for subsequent Update, Delete, and Refresh
404 /// operations. All primary key columns of the table must be in the query and the Record must
405 /// have at least as many fields as the query. Seek cannot be used with multi-table queries.
406 /// This method cannot be used with a View containing joins.
407 /// </p><p>
408 /// See <see cref="Modify"/> for more remarks.
409 /// </p><p>
410 /// Win32 MSI API:
411 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
412 /// </p></remarks>
413 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
414 public bool Seek(Record record)
415 {
416 if (record == null)
417 {
418 throw new ArgumentNullException("record");
419 }
420
421 uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) ViewModifyMode.Seek, (int) record.Handle);
422 record.IsFormatStringInvalid = true;
423 if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED)
424 {
425 return false;
426 }
427 else if (ret != 0)
428 {
429 throw InstallerException.ExceptionFromReturnCode(ret);
430 }
431
432 return true;
433 }
434
435 /// <summary>
436 /// Inserts or validates a record.
437 /// </summary>
438 /// <param name="record">the Record to be merged</param>
439 /// <returns>true if the record was inserted or validated, false if there is an existing
440 /// record with the same primary keys that is not identical</returns>
441 /// <exception cref="InstallerException">the merge failed (for a reason other than invalid data)</exception>
442 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
443 /// <remarks><p>
444 /// Works only with read-write records. This method cannot be used with a
445 /// View containing joins.
446 /// </p><p>
447 /// See <see cref="Modify"/> for more remarks.
448 /// </p><p>
449 /// Win32 MSI API:
450 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>
451 /// </p></remarks>
452 [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")]
453 public bool Merge(Record record)
454 {
455 if (record == null)
456 {
457 throw new ArgumentNullException("record");
458 }
459
460 uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) ViewModifyMode.Merge, (int) record.Handle);
461 if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED)
462 {
463 return false;
464 }
465 else if (ret != 0)
466 {
467 throw InstallerException.ExceptionFromReturnCode(ret);
468 }
469 return true;
470 }
471
472 /// <summary>
473 /// Validates a record, returning information about any errors.
474 /// </summary>
475 /// <param name="record">the Record to be validated</param>
476 /// <returns>null if the record was validated; if there is an existing record with
477 /// the same primary keys that has conflicting data then error information is returned</returns>
478 /// <exception cref="InstallerException">the validation failed (for a reason other than invalid data)</exception>
479 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
480 /// <remarks><p>
481 /// The Record must have been obtained by calling <see cref="Fetch"/>.
482 /// Works with read-write and read-only records. This method cannot be used
483 /// with a View containing joins.
484 /// </p><p>
485 /// See <see cref="Modify"/> for more remarks.
486 /// </p><p>
487 /// Win32 MSI APIs:
488 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>,
489 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewgeterror.asp">MsiViewGetError</a>
490 /// </p></remarks>
491 public ICollection<ValidationErrorInfo> Validate(Record record) { return this.InternalValidate(ViewModifyMode.Validate, record); }
492
493 /// <summary>
494 /// Validates a new record, returning information about any errors.
495 /// </summary>
496 /// <param name="record">the Record to be validated</param>
497 /// <returns>null if the record was validated; if there is an existing
498 /// record with the same primary keys then error information is returned</returns>
499 /// <exception cref="InstallerException">the validation failed (for a reason other than invalid data)</exception>
500 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
501 /// <remarks><p>
502 /// Checks for duplicate keys. The Record must have been obtained by
503 /// calling <see cref="Fetch"/>. Works with read-write and read-only records.
504 /// This method cannot be used with a View containing joins.
505 /// </p><p>
506 /// See <see cref="Modify"/> for more remarks.
507 /// </p><p>
508 /// Win32 MSI APIs:
509 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>,
510 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewgeterror.asp">MsiViewGetError</a>
511 /// </p></remarks>
512 [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
513 public ICollection<ValidationErrorInfo> ValidateNew(Record record) { return this.InternalValidate(ViewModifyMode.ValidateNew, record); }
514
515 /// <summary>
516 /// Validates fields of a fetched or new record, returning information about any errors.
517 /// Can validate one or more fields of an incomplete record.
518 /// </summary>
519 /// <param name="record">the Record to be validated</param>
520 /// <returns>null if the record was validated; if there is an existing record with
521 /// the same primary keys that has conflicting data then error information is returned</returns>
522 /// <exception cref="InstallerException">the validation failed (for a reason other than invalid data)</exception>
523 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
524 /// <remarks><p>
525 /// Works with read-write and read-only records. This method cannot be used with
526 /// a View containing joins.
527 /// </p><p>
528 /// See <see cref="Modify"/> for more remarks.
529 /// </p><p>
530 /// Win32 MSI APIs:
531 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>,
532 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewgeterror.asp">MsiViewGetError</a>
533 /// </p></remarks>
534 public ICollection<ValidationErrorInfo> ValidateFields(Record record) { return this.InternalValidate(ViewModifyMode.ValidateField, record); }
535
536 /// <summary>
537 /// Validates a record that will be deleted later, returning information about any errors.
538 /// </summary>
539 /// <param name="record">the Record to be validated</param>
540 /// <returns>null if the record is safe to delete; if another row refers to
541 /// the primary keys of this row then error information is returned</returns>
542 /// <exception cref="InstallerException">the validation failed (for a reason other than invalid data)</exception>
543 /// <exception cref="InvalidHandleException">the View handle is invalid</exception>
544 /// <remarks><p>
545 /// Validation does not check for the existence of the primary keys of this row in properties
546 /// or strings. Does not check if a column is a foreign key to multiple tables. Works with
547 /// read-write and read-only records. This method cannot be used with a View containing joins.
548 /// </p><p>
549 /// See <see cref="Modify"/> for more remarks.
550 /// </p><p>
551 /// Win32 MSI APIs:
552 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewmodify.asp">MsiViewModify</a>,
553 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewgeterror.asp">MsiViewGetError</a>
554 /// </p></remarks>
555 public ICollection<ValidationErrorInfo> ValidateDelete(Record record) { return this.InternalValidate(ViewModifyMode.ValidateDelete, record); }
556
557 /// <summary>
558 /// Enumerates over the Records retrieved by the View.
559 /// </summary>
560 /// <returns>An enumerator of Record objects.</returns>
561 /// <exception cref="InstallerException">The View was not <see cref="Execute(Record)"/>d before attempting the enumeration.</exception>
562 /// <remarks><p>
563 /// Each Record object should be <see cref="InstallerHandle.Close"/>d after use.
564 /// It is best that the handle be closed manually as soon as it is no longer
565 /// needed, as leaving lots of unused handles open can degrade performance.
566 /// However, note that it is not necessary to complete the enumeration just
567 /// for the purpose of closing handles, because Records are fetched lazily
568 /// on each step of the enumeration.
569 /// </p><p>
570 /// Win32 MSI API:
571 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiviewfetch.asp">MsiViewFetch</a>
572 /// </p></remarks>
573 public IEnumerator<Record> GetEnumerator()
574 {
575 Record rec;
576 while ((rec = this.Fetch()) != null)
577 {
578 yield return rec;
579 }
580 }
581
582 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
583 {
584 return this.GetEnumerator();
585 }
586
587 private ICollection<ValidationErrorInfo> InternalValidate(ViewModifyMode mode, Record record)
588 {
589 uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) mode, (int) record.Handle);
590 if (ret == (uint) NativeMethods.Error.INVALID_DATA)
591 {
592 ICollection<ValidationErrorInfo> errorInfo = new List<ValidationErrorInfo>();
593 while (true)
594 {
595 uint bufSize = 40;
596 StringBuilder column = new StringBuilder("", (int) bufSize);
597 int error = RemotableNativeMethods.MsiViewGetError((int) this.Handle, column, ref bufSize);
598 if (error == -2 /*MSIDBERROR_MOREDATA*/)
599 {
600 column.Capacity = (int) ++bufSize;
601 error = RemotableNativeMethods.MsiViewGetError((int) this.Handle, column, ref bufSize);
602 }
603
604 if (error == -3 /*MSIDBERROR_INVALIDARG*/)
605 {
606 throw InstallerException.ExceptionFromReturnCode((uint) NativeMethods.Error.INVALID_PARAMETER);
607 }
608 else if (error == 0 /*MSIDBERROR_NOERROR*/)
609 {
610 break;
611 }
612
613 errorInfo.Add(new ValidationErrorInfo((ValidationError) error, column.ToString()));
614 }
615
616 return errorInfo;
617 }
618 else if (ret != 0)
619 {
620 throw InstallerException.ExceptionFromReturnCode(ret);
621 }
622 return null;
623 }
624 }
625}
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/WindowsInstaller.cd b/src/dtf/WixToolset.Dtf.WindowsInstaller/WindowsInstaller.cd
new file mode 100644
index 00000000..01b68153
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/WindowsInstaller.cd
@@ -0,0 +1,943 @@
1<?xml version="1.0" encoding="utf-8"?>
2<ClassDiagram MajorVersion="1" MinorVersion="1">
3 <Comment CommentText="Database classes">
4 <Position X="3.433" Y="7.717" Height="0.26" Width="1.159" />
5 </Comment>
6 <Comment CommentText="Custom action classes">
7 <Position X="5.791" Y="1.774" Height="0.312" Width="1.465" />
8 </Comment>
9 <Comment CommentText="Inventory classes">
10 <Position X="12.323" Y="2.386" Height="0.292" Width="1.183" />
11 </Comment>
12 <Comment CommentText="Exceptions">
13 <Position X="11.75" Y="9.25" Height="0.285" Width="0.798" />
14 </Comment>
15 <Comment CommentText="Callback delegates">
16 <Position X="17.25" Y="8" Height="0.281" Width="1.25" />
17 </Comment>
18 <Comment CommentText="API enums">
19 <Position X="21.992" Y="0.5" Height="0.283" Width="2.008" />
20 </Comment>
21 <Comment CommentText="Database column enums">
22 <Position X="24.25" Y="0.5" Height="0.26" Width="1.983" />
23 </Comment>
24 <Class Name="WixToolset.Dtf.WindowsInstaller.Database">
25 <Position X="0.5" Y="5" Width="2.25" />
26 <Members>
27 <Method Name="Database" Hidden="true" />
28 <Field Name="deleteOnClose" Hidden="true" />
29 <Method Name="Dispose" Hidden="true" />
30 <Method Name="ExecutePropertyQuery" Hidden="true" />
31 <Field Name="filePath" Hidden="true" />
32 <Method Name="Open" Hidden="true" />
33 <Field Name="openMode" Hidden="true" />
34 <Field Name="summaryInfo" Hidden="true" />
35 <Field Name="tables" Hidden="true" />
36 </Members>
37 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerHandle" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true">
38 <Path>
39 <Point X="3.312" Y="2.119" />
40 <Point X="3.312" Y="2.689" />
41 <Point X="2.699" Y="2.689" />
42 <Point X="2.699" Y="5" />
43 </Path>
44 </InheritanceLine>
45 <AssociationLine Name="Tables" Type="WixToolset.Dtf.WindowsInstaller.TableInfo" FixedFromPoint="true" FixedToPoint="true">
46 <Path>
47 <Point X="2.75" Y="8.125" />
48 <Point X="3.75" Y="8.125" />
49 <Point X="3.75" Y="8.5" />
50 </Path>
51 <MemberNameLabel ManuallyPlaced="true">
52 <Position X="0.293" Y="0.152" />
53 </MemberNameLabel>
54 </AssociationLine>
55 <AssociationLine Name="SummaryInfo" Type="WixToolset.Dtf.WindowsInstaller.SummaryInfo" FixedFromPoint="true">
56 <Path>
57 <Point X="1.318" Y="5" />
58 <Point X="1.318" Y="4.535" />
59 </Path>
60 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
61 <Position X="-1.155" Y="0.279" Height="0.16" Width="1.095" />
62 </MemberNameLabel>
63 </AssociationLine>
64 <TypeIdentifier>
65 <HashCode>QABACUgAACAAIAVUIAgQREACAAIAk0ABAIhmoEAAAAA=</HashCode>
66 <FileName>Database.cs</FileName>
67 </TypeIdentifier>
68 <ShowAsAssociation>
69 <Property Name="SummaryInfo" />
70 </ShowAsAssociation>
71 <ShowAsCollectionAssociation>
72 <Property Name="Tables" />
73 </ShowAsCollectionAssociation>
74 </Class>
75 <Class Name="WixToolset.Dtf.WindowsInstaller.InstallerHandle">
76 <Position X="3" Y="0.5" Width="2" />
77 <Members>
78 <Method Name="Dispose" Hidden="true" />
79 <Method Name="Equals" Hidden="true" />
80 <Method Name="GetHashCode" Hidden="true" />
81 <Field Name="handle" Hidden="true" />
82 <Method Name="InstallerHandle" Hidden="true" />
83 <Property Name="Sync" Hidden="true" />
84 </Members>
85 <TypeIdentifier>
86 <HashCode>AAAAAAAAACAAAACAgAAAAAAAAAAAAMIAAAACAACAAAA=</HashCode>
87 <FileName>Handle.cs</FileName>
88 </TypeIdentifier>
89 <Lollipop Position="0.2" />
90 </Class>
91 <Class Name="WixToolset.Dtf.WindowsInstaller.Installer">
92 <Position X="19.25" Y="0.5" Width="2.5" />
93 <Members>
94 <Method Name="CheckInstallResult" Hidden="true" />
95 <Field Name="errorResources" Hidden="true" />
96 <Property Name="ErrorResources" Hidden="true" />
97 <Field Name="externalUIHandlers" Hidden="true" />
98 <Method Name="GetMessageFromModule" Hidden="true" />
99 <Method Name="GetPatchStringDataType" Hidden="true" />
100 <Field Name="rebootInitiated" Hidden="true" />
101 <Field Name="rebootRequired" Hidden="true" />
102 </Members>
103 <TypeIdentifier>
104 <HashCode>IAQBiIgAgAKAAAAELEAAPBgJAUAAYAAAAOQDagAgAQE=</HashCode>
105 <FileName>ExternalUIHandler.cs</FileName>
106 </TypeIdentifier>
107 </Class>
108 <Class Name="WixToolset.Dtf.WindowsInstaller.Record">
109 <Position X="5.25" Y="3" Width="2" />
110 <Members>
111 <Method Name="CheckRange" Hidden="true" />
112 <Method Name="ExtractSubStorage" Hidden="true" />
113 <Method Name="FindColumn" Hidden="true" />
114 <Method Name="Record" Hidden="true" />
115 <Field Name="view" Hidden="true" />
116 </Members>
117 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerHandle" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true">
118 <Path>
119 <Point X="3.688" Y="2.119" />
120 <Point X="3.688" Y="2.672" />
121 <Point X="5.4" Y="2.672" />
122 <Point X="5.4" Y="3" />
123 </Path>
124 </InheritanceLine>
125 <TypeIdentifier>
126 <HashCode>AIAAAAAAAAAAAIAEiEIAJAAAAQgIEIAIIQCAAABIAAA=</HashCode>
127 <FileName>Record.cs</FileName>
128 </TypeIdentifier>
129 </Class>
130 <Class Name="WixToolset.Dtf.WindowsInstaller.Session">
131 <Position X="7.5" Y="0.5" Width="2.25" />
132 <Members>
133 <Field Name="database" Hidden="true" />
134 <Method Name="Dispose" Hidden="true" />
135 <Method Name="IFormatProvider.GetFormat" Hidden="true" />
136 <Method Name="Session" Hidden="true" />
137 </Members>
138 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerHandle" FixedFromPoint="true">
139 <Path>
140 <Point X="3.875" Y="2.119" />
141 <Point X="3.875" Y="2.395" />
142 <Point X="7.5" Y="2.395" />
143 </Path>
144 </InheritanceLine>
145 <AssociationLine Name="Database" Type="WixToolset.Dtf.WindowsInstaller.Database" FixedFromPoint="true" FixedToPoint="true">
146 <Path>
147 <Point X="7.562" Y="4.696" />
148 <Point X="7.562" Y="10.375" />
149 <Point X="2.75" Y="10.375" />
150 </Path>
151 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
152 <Position X="4.869" Y="5.46" Height="0.16" Width="0.806" />
153 </MemberNameLabel>
154 </AssociationLine>
155 <AssociationLine Name="Components" Type="WixToolset.Dtf.WindowsInstaller.ComponentInfo" FixedFromPoint="true">
156 <Path>
157 <Point X="8.562" Y="4.696" />
158 <Point X="8.562" Y="5.5" />
159 </Path>
160 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
161 <Position X="-1.021" Y="0.317" Height="0.16" Width="0.986" />
162 </MemberNameLabel>
163 </AssociationLine>
164 <AssociationLine Name="Features" Type="WixToolset.Dtf.WindowsInstaller.FeatureInfo">
165 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
166 <Position X="0.016" Y="2.568" Height="0.16" Width="0.785" />
167 </MemberNameLabel>
168 </AssociationLine>
169 <TypeIdentifier>
170 <HashCode>AAAAEAAAKGAAAoDADIBBAAAAAAgIEABBEIAAIAAACAA=</HashCode>
171 <FileName>Session.cs</FileName>
172 </TypeIdentifier>
173 <ShowAsAssociation>
174 <Property Name="Database" />
175 </ShowAsAssociation>
176 <ShowAsCollectionAssociation>
177 <Property Name="Components" />
178 <Property Name="Features" />
179 </ShowAsCollectionAssociation>
180 <Lollipop Position="0.2" />
181 </Class>
182 <Class Name="WixToolset.Dtf.WindowsInstaller.SummaryInfo">
183 <Position X="0.5" Y="0.5" Width="2" />
184 <Members>
185 <Field Name="MAX_PROPERTIES" Hidden="true" />
186 <Method Name="OpenSummaryInfo" Hidden="true" />
187 <Method Name="SummaryInfo" Hidden="true" />
188 <Property Name="this" Hidden="true" />
189 </Members>
190 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerHandle" FixedFromPoint="true">
191 <Path>
192 <Point X="3.125" Y="2.119" />
193 <Point X="3.125" Y="2.404" />
194 <Point X="2.5" Y="2.404" />
195 </Path>
196 </InheritanceLine>
197 <TypeIdentifier>
198 <HashCode>AQAAAAAAAEAAAEAAjMgAAJIAAAEAAAAAAAAEhAAAoCI=</HashCode>
199 <FileName>SummaryInfo.cs</FileName>
200 </TypeIdentifier>
201 </Class>
202 <Class Name="WixToolset.Dtf.WindowsInstaller.View">
203 <Position X="3" Y="3" Width="2" />
204 <Members>
205 <Field Name="columns" Hidden="true" />
206 <Field Name="database" Hidden="true" />
207 <Method Name="InternalValidate" Hidden="true" />
208 <Field Name="sql" Hidden="true" />
209 <Method Name="System.Collections.IEnumerable.GetEnumerator" Hidden="true" />
210 <Field Name="tables" Hidden="true" />
211 <Method Name="View" Hidden="true" />
212 </Members>
213 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerHandle" FixedFromPoint="true" FixedToPoint="true">
214 <Path>
215 <Point X="3.5" Y="2.119" />
216 <Point X="3.5" Y="3" />
217 </Path>
218 </InheritanceLine>
219 <AssociationLine Name="Database" Type="WixToolset.Dtf.WindowsInstaller.Database" FixedFromPoint="true" FixedToPoint="true">
220 <Path>
221 <Point X="3.75" Y="7.196" />
222 <Point X="3.75" Y="7.562" />
223 <Point X="2.75" Y="7.562" />
224 </Path>
225 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
226 <Position X="0.168" Y="0.16" Height="0.16" Width="0.831" />
227 </MemberNameLabel>
228 </AssociationLine>
229 <AssociationLine Name="Columns" Type="WixToolset.Dtf.WindowsInstaller.ColumnInfo" FixedFromPoint="true" FixedToPoint="true">
230 <Path>
231 <Point X="4.25" Y="7.196" />
232 <Point X="4.25" Y="7.562" />
233 <Point X="5.25" Y="7.562" />
234 </Path>
235 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
236 <Position X="0.168" Y="0.152" Height="0.16" Width="0.787" />
237 </MemberNameLabel>
238 </AssociationLine>
239 <TypeIdentifier>
240 <HashCode>ICQAIEQAAEDgABRACQEAQECBAAEAAQAACAAAAAgAACA=</HashCode>
241 <FileName>View.cs</FileName>
242 </TypeIdentifier>
243 <ShowAsAssociation>
244 <Property Name="Database" />
245 </ShowAsAssociation>
246 <ShowAsCollectionAssociation>
247 <Property Name="Columns" />
248 </ShowAsCollectionAssociation>
249 <Lollipop Position="0.638" />
250 </Class>
251 <Class Name="WixToolset.Dtf.WindowsInstaller.TableInfo">
252 <Position X="3" Y="8.5" Width="2" />
253 <Members>
254 <Field Name="columns" Hidden="true" />
255 <Method Name="GetTablePrimaryKeys" Hidden="true" />
256 <Field Name="name" Hidden="true" />
257 <Field Name="primaryKeys" Hidden="true" />
258 <Method Name="TableInfo" Hidden="true" />
259 <Method Name="ToString" Hidden="true" />
260 </Members>
261 <AssociationLine Name="Columns" Type="WixToolset.Dtf.WindowsInstaller.ColumnInfo" FixedFromPoint="true" FixedToPoint="true">
262 <Path>
263 <Point X="4.25" Y="8.5" />
264 <Point X="4.25" Y="8.125" />
265 <Point X="5.25" Y="8.125" />
266 </Path>
267 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
268 <Position X="0.143" Y="-0.338" Height="0.16" Width="0.787" />
269 </MemberNameLabel>
270 </AssociationLine>
271 <TypeIdentifier>
272 <HashCode>AAAAAAAAAEAAAAAEACAAAAQAQAAEAAAASAAAAAgAAAA=</HashCode>
273 <FileName>TableInfo.cs</FileName>
274 </TypeIdentifier>
275 <ShowAsCollectionAssociation>
276 <Property Name="Columns" />
277 </ShowAsCollectionAssociation>
278 </Class>
279 <Class Name="WixToolset.Dtf.WindowsInstaller.ColumnInfo">
280 <Position X="5.25" Y="7.25" Width="2" />
281 <Members>
282 <Method Name="ColumnInfo" Hidden="true" />
283 <Field Name="isLocalizable" Hidden="true" />
284 <Field Name="isRequired" Hidden="true" />
285 <Field Name="isTemporary" Hidden="true" />
286 <Field Name="name" Hidden="true" />
287 <Field Name="size" Hidden="true" />
288 <Method Name="ToString" Hidden="true" />
289 <Field Name="type" Hidden="true" />
290 </Members>
291 <TypeIdentifier>
292 <HashCode>AgAAAAAAAAAAgCAEEIAAABQgQAAFIAAAAQAAAAIAEAA=</HashCode>
293 <FileName>ColumnInfo.cs</FileName>
294 </TypeIdentifier>
295 </Class>
296 <Class Name="WixToolset.Dtf.WindowsInstaller.CustomActionAttribute">
297 <Position X="5.25" Y="0.5" Width="2" />
298 <Members>
299 <Method Name="CustomActionAttribute" Hidden="true" />
300 <Field Name="name" Hidden="true" />
301 </Members>
302 <TypeIdentifier>
303 <HashCode>AAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAAAAAAAAAAAA=</HashCode>
304 <FileName>CustomActionAttribute.cs</FileName>
305 </TypeIdentifier>
306 </Class>
307 <Class Name="WixToolset.Dtf.WindowsInstaller.Installation">
308 <Position X="12.5" Y="0.5" Width="2" />
309 <Members>
310 <Field Name="context" Hidden="true" />
311 <Method Name="Installation" Hidden="true" />
312 <Field Name="installationCode" Hidden="true" />
313 <Property Name="InstallationCode" Hidden="true" />
314 <Property Name="InstallationType" Hidden="true" />
315 <Field Name="sourceList" Hidden="true" />
316 <Field Name="userSid" Hidden="true" />
317 </Members>
318 <AssociationLine Name="SourceList" Type="WixToolset.Dtf.WindowsInstaller.SourceList" FixedFromPoint="true" FixedToPoint="true">
319 <Path>
320 <Point X="13.75" Y="1.95" />
321 <Point X="13.75" Y="2.298" />
322 <Point X="14.75" Y="2.298" />
323 </Path>
324 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
325 <Position X="0.076" Y="0.114" Height="0.16" Width="0.868" />
326 </MemberNameLabel>
327 </AssociationLine>
328 <TypeIdentifier>
329 <HashCode>AAACIhAAAoAQAACACAAAAABAAAAAAAAAAAAEAAAAAAA=</HashCode>
330 <FileName>Installation.cs</FileName>
331 </TypeIdentifier>
332 <ShowAsAssociation>
333 <Property Name="SourceList" />
334 </ShowAsAssociation>
335 </Class>
336 <Class Name="WixToolset.Dtf.WindowsInstaller.InstallationPart">
337 <Position X="17" Y="3" Width="2" />
338 <Members>
339 <Field Name="id" Hidden="true" />
340 <Property Name="Id" Hidden="true" />
341 <Method Name="InstallationPart" Hidden="true" />
342 <Field Name="productCode" Hidden="true" />
343 <Property Name="ProductCode" Hidden="true" />
344 </Members>
345 <AssociationLine Name="Product" Type="WixToolset.Dtf.WindowsInstaller.ProductInstallation" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true">
346 <Path>
347 <Point X="17.611" Y="3.967" />
348 <Point X="17.611" Y="4.519" />
349 <Point X="14.76" Y="4.519" />
350 <Point X="14.76" Y="6.438" />
351 <Point X="12.25" Y="6.438" />
352 </Path>
353 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
354 <Position X="4.65" Y="2.262" Height="0.16" Width="0.703" />
355 </MemberNameLabel>
356 </AssociationLine>
357 <TypeIdentifier>
358 <HashCode>AAAiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAIgA=</HashCode>
359 <FileName>InstallationPart.cs</FileName>
360 </TypeIdentifier>
361 <ShowAsAssociation>
362 <Property Name="Product" />
363 </ShowAsAssociation>
364 </Class>
365 <Class Name="WixToolset.Dtf.WindowsInstaller.ProductInstallation">
366 <Position X="10" Y="0.5" Width="2.25" />
367 <Members>
368 <Method Name="EnumFeatures" Hidden="true" />
369 <Method Name="EnumProducts" Hidden="true" />
370 <Method Name="EnumRelatedProducts" Hidden="true" />
371 <Property Name="InstallationType" Hidden="true" />
372 <Field Name="properties" Hidden="true" />
373 <Property Name="State" Hidden="true" />
374 </Members>
375 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.Installation" FixedFromPoint="true" FixedToPoint="true">
376 <Path>
377 <Point X="13.312" Y="1.95" />
378 <Point X="13.312" Y="2.312" />
379 <Point X="12.25" Y="2.312" />
380 </Path>
381 </InheritanceLine>
382 <AssociationLine Name="AllProducts" Type="WixToolset.Dtf.WindowsInstaller.ProductInstallation" FixedFromPoint="true" FixedToPoint="true">
383 <Path>
384 <Point X="10.5" Y="7.273" />
385 <Point X="10.5" Y="7.523" />
386 <Point X="11.562" Y="7.523" />
387 <Point X="11.562" Y="7.273" />
388 </Path>
389 <MemberNameLabel ManuallyPlaced="true">
390 <Position X="0.068" Y="0.052" />
391 </MemberNameLabel>
392 </AssociationLine>
393 <AssociationLine Name="Features" Type="WixToolset.Dtf.WindowsInstaller.FeatureInstallation" FixedToPoint="true">
394 <Path>
395 <Point X="12.25" Y="6.813" />
396 <Point X="15" Y="6.813" />
397 </Path>
398 <MemberNameLabel ManuallyPlaced="true">
399 <Position X="1.922" Y="0.05" />
400 </MemberNameLabel>
401 </AssociationLine>
402 <TypeIdentifier>
403 <HashCode>UAIEYrAGAAAACQBAKEABAAoASA0ARQAASUIGBAAIIAA=</HashCode>
404 <FileName>ProductInstallation.cs</FileName>
405 </TypeIdentifier>
406 <ShowAsCollectionAssociation>
407 <Property Name="AllProducts" />
408 <Property Name="Features" />
409 </ShowAsCollectionAssociation>
410 </Class>
411 <Class Name="WixToolset.Dtf.WindowsInstaller.PatchInstallation">
412 <Position X="12.5" Y="2.75" Width="2" />
413 <Members>
414 <Method Name="EnumPatches" Hidden="true" />
415 <Property Name="InstallationType" Hidden="true" />
416 <Method Name="PatchInstallation" Hidden="true" />
417 <Field Name="productCode" Hidden="true" />
418 <Property Name="State" Hidden="true" />
419 </Members>
420 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.Installation" FixedFromPoint="true" FixedToPoint="true">
421 <Path>
422 <Point X="13.562" Y="1.95" />
423 <Point X="13.562" Y="2.75" />
424 </Path>
425 </InheritanceLine>
426 <AssociationLine Name="AllPatches" Type="WixToolset.Dtf.WindowsInstaller.PatchInstallation" FixedFromPoint="true" FixedToPoint="true">
427 <Path>
428 <Point X="12.938" Y="5.98" />
429 <Point X="12.938" Y="6.23" />
430 <Point X="14" Y="6.23" />
431 <Point X="14" Y="5.98" />
432 </Path>
433 <MemberNameLabel ManuallyPlaced="true">
434 <Position X="0.093" Y="0.06" />
435 </MemberNameLabel>
436 </AssociationLine>
437 <TypeIdentifier>
438 <HashCode>AAAgsgCACAAAAAAAKEAAAgAAAgAAAAAAAgIAFAAAIAA=</HashCode>
439 <FileName>PatchInstallation.cs</FileName>
440 </TypeIdentifier>
441 <ShowAsCollectionAssociation>
442 <Property Name="AllPatches" />
443 </ShowAsCollectionAssociation>
444 </Class>
445 <Class Name="WixToolset.Dtf.WindowsInstaller.ComponentInstallation">
446 <Position X="17" Y="5" Width="2" />
447 <Members>
448 <Method Name="ComponentInstallation" Hidden="true" />
449 <Method Name="EnumClients" Hidden="true" />
450 <Method Name="EnumComponents" Hidden="true" />
451 <Method Name="EnumQualifiers" Hidden="true" />
452 <Method Name="GetProductCode" Hidden="true" />
453 </Members>
454 <Compartments>
455 <Compartment Name="Nested Types" Collapsed="false" />
456 </Compartments>
457 <NestedTypes>
458 <Struct Name="WixToolset.Dtf.WindowsInstaller.ComponentInstallation.Qualifier" Collapsed="true">
459 <TypeIdentifier>
460 <NewMemberFileName>ComponentInstallation.cs</NewMemberFileName>
461 </TypeIdentifier>
462 </Struct>
463 </NestedTypes>
464 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallationPart" FixedFromPoint="true" FixedToPoint="true">
465 <Path>
466 <Point X="18.111" Y="3.967" />
467 <Point X="18.111" Y="5" />
468 </Path>
469 </InheritanceLine>
470 <AssociationLine Name="AllComponents" Type="WixToolset.Dtf.WindowsInstaller.ComponentInstallation" FixedFromPoint="true" FixedToPoint="true">
471 <Path>
472 <Point X="17.375" Y="7.443" />
473 <Point X="17.375" Y="7.693" />
474 <Point X="18.625" Y="7.693" />
475 <Point X="18.625" Y="7.443" />
476 </Path>
477 <MemberNameLabel ManuallyPlaced="true">
478 <Position X="0.068" Y="0.052" />
479 </MemberNameLabel>
480 </AssociationLine>
481 <AssociationLine Name="ClientProducts" Type="WixToolset.Dtf.WindowsInstaller.ProductInstallation" FixedFromPoint="true">
482 <Path>
483 <Point X="17" Y="7.188" />
484 <Point X="12.25" Y="7.188" />
485 </Path>
486 <MemberNameLabel ManuallyPlaced="true" ManuallySized="true">
487 <Position X="3.658" Y="-0.195" Height="0.16" Width="1.087" />
488 </MemberNameLabel>
489 </AssociationLine>
490 <TypeIdentifier>
491 <HashCode>AUAAAAACAAAAAAIAEAgQAAQAAAAAAAAEAAAABAAAAAA=</HashCode>
492 <FileName>ComponentInstallation.cs</FileName>
493 </TypeIdentifier>
494 <ShowAsCollectionAssociation>
495 <Property Name="AllComponents" />
496 <Property Name="ClientProducts" />
497 </ShowAsCollectionAssociation>
498 </Class>
499 <Class Name="WixToolset.Dtf.WindowsInstaller.FeatureInstallation">
500 <Position X="15" Y="4.75" Width="1.75" />
501 <Members>
502 <Method Name="FeatureInstallation" Hidden="true" />
503 </Members>
504 <Compartments>
505 <Compartment Name="Nested Types" Collapsed="false" />
506 </Compartments>
507 <NestedTypes>
508 <Struct Name="WixToolset.Dtf.WindowsInstaller.FeatureInstallation.UsageData" Collapsed="true">
509 <TypeIdentifier>
510 <NewMemberFileName>FeatureInstallation.cs</NewMemberFileName>
511 </TypeIdentifier>
512 </Struct>
513 </NestedTypes>
514 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallationPart" FixedFromPoint="true" FixedToPoint="true">
515 <Path>
516 <Point X="17.889" Y="3.967" />
517 <Point X="17.889" Y="4.804" />
518 <Point X="16.75" Y="4.804" />
519 </Path>
520 </InheritanceLine>
521 <TypeIdentifier>
522 <HashCode>AAAAAAAAAAAAAAAAAAAAAAAEAAAAACAAAAAABAAAAAA=</HashCode>
523 <FileName>FeatureInstallation.cs</FileName>
524 </TypeIdentifier>
525 </Class>
526 <Class Name="WixToolset.Dtf.WindowsInstaller.SourceList">
527 <Position X="14.75" Y="0.5" Width="2" />
528 <Members>
529 <Method Name="ClearSourceType" Hidden="true" />
530 <Method Name="Contains" Hidden="true" />
531 <Method Name="CopyTo" Hidden="true" />
532 <Method Name="EnumSources" Hidden="true" />
533 <Field Name="installation" Hidden="true" />
534 <Field Name="mediaList" Hidden="true" />
535 <Method Name="SourceList" Hidden="true" />
536 <Method Name="System.Collections.IEnumerable.GetEnumerator" Hidden="true" />
537 </Members>
538 <AssociationLine Name="MediaList" Type="WixToolset.Dtf.WindowsInstaller.MediaDisk" FixedFromPoint="true" FixedToPoint="true">
539 <Path>
540 <Point X="16.75" Y="2.375" />
541 <Point X="17.667" Y="2.375" />
542 <Point X="17.667" Y="1.789" />
543 </Path>
544 <MemberNameLabel ManuallyPlaced="true">
545 <Position X="0.029" Y="0.377" />
546 </MemberNameLabel>
547 </AssociationLine>
548 <TypeIdentifier>
549 <HashCode>BCIAAEAAEABAABgQCAAABIQAAFAQAAAAAAAABARwIgA=</HashCode>
550 <FileName>SourceList.cs</FileName>
551 </TypeIdentifier>
552 <ShowAsCollectionAssociation>
553 <Property Name="MediaList" />
554 </ShowAsCollectionAssociation>
555 <Lollipop Position="0.2" />
556 </Class>
557 <Class Name="WixToolset.Dtf.WindowsInstaller.ComponentInfo">
558 <Position X="8" Y="5.5" Width="1.5" />
559 <Members>
560 <Method Name="ComponentInfo" Hidden="true" />
561 <Field Name="name" Hidden="true" />
562 <Field Name="session" Hidden="true" />
563 </Members>
564 <TypeIdentifier>
565 <HashCode>AQAAAAIAAAAAAAAAAAEAACQAAAAEAAAAAAAAAAAAAAA=</HashCode>
566 <FileName>ComponentInfo.cs</FileName>
567 </TypeIdentifier>
568 </Class>
569 <Class Name="WixToolset.Dtf.WindowsInstaller.FeatureInfo">
570 <Position X="8" Y="7.5" Width="1.75" />
571 <Members>
572 <Method Name="FeatureInfo" Hidden="true" />
573 <Field Name="name" Hidden="true" />
574 <Field Name="session" Hidden="true" />
575 </Members>
576 <TypeIdentifier>
577 <HashCode>AQAAAAIAAAggAEAAAAEAACQQAAAEAAAAAAAAAAAAAAA=</HashCode>
578 <FileName>FeatureInfo.cs</FileName>
579 </TypeIdentifier>
580 </Class>
581 <Class Name="WixToolset.Dtf.WindowsInstaller.InstallerException">
582 <Position X="11.75" Y="7.5" Width="2.25" />
583 <Members>
584 <Method Name="Combine" Hidden="true" />
585 <Field Name="errorCode" Hidden="true" />
586 <Field Name="errorData" Hidden="true" />
587 <Method Name="ExceptionFromReturnCode" Hidden="true" />
588 <Method Name="GetObjectData" Hidden="true" />
589 <Method Name="GetSystemMessage" Hidden="true" />
590 <Method Name="InstallerException" Hidden="true" />
591 <Method Name="SaveErrorRecord" Hidden="true" />
592 </Members>
593 <TypeIdentifier>
594 <HashCode>hAAAAAAAAAAAAAQAAAAAAAAgAAgIAAAIAAABIAAAQAA=</HashCode>
595 <FileName>Exceptions.cs</FileName>
596 </TypeIdentifier>
597 </Class>
598 <Class Name="WixToolset.Dtf.WindowsInstaller.BadQuerySyntaxException" Collapsed="true">
599 <Position X="11.75" Y="9.75" Width="2.25" />
600 <Members>
601 <Method Name="BadQuerySyntaxException" Hidden="true" />
602 </Members>
603 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerException" FixedFromPoint="true" FixedToPoint="true">
604 <Path>
605 <Point X="12.938" Y="9.119" />
606 <Point X="12.938" Y="9.75" />
607 </Path>
608 </InheritanceLine>
609 <TypeIdentifier>
610 <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
611 <FileName>Exceptions.cs</FileName>
612 </TypeIdentifier>
613 </Class>
614 <Class Name="WixToolset.Dtf.WindowsInstaller.InstallCanceledException" Collapsed="true">
615 <Position X="14.25" Y="9.75" Width="2.25" />
616 <Members>
617 <Method Name="InstallCanceledException" Hidden="true" />
618 </Members>
619 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerException" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true">
620 <Path>
621 <Point X="13.375" Y="9.119" />
622 <Point X="13.375" Y="9.631" />
623 <Point X="14.25" Y="9.631" />
624 <Point X="14.25" Y="9.813" />
625 </Path>
626 </InheritanceLine>
627 <TypeIdentifier>
628 <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
629 <FileName>Exceptions.cs</FileName>
630 </TypeIdentifier>
631 </Class>
632 <Class Name="WixToolset.Dtf.WindowsInstaller.InvalidHandleException" Collapsed="true">
633 <Position X="14.25" Y="9" Width="2.25" />
634 <Members>
635 <Method Name="InvalidHandleException" Hidden="true" />
636 </Members>
637 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerException" FixedFromPoint="true" FixedToPoint="true">
638 <Path>
639 <Point X="13.812" Y="9.119" />
640 <Point X="13.812" Y="9.375" />
641 <Point X="14.25" Y="9.375" />
642 </Path>
643 </InheritanceLine>
644 <TypeIdentifier>
645 <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
646 <FileName>Exceptions.cs</FileName>
647 </TypeIdentifier>
648 </Class>
649 <Class Name="WixToolset.Dtf.WindowsInstaller.MergeException">
650 <Position X="14.25" Y="7.5" Width="2.25" />
651 <Members>
652 <Field Name="conflictCounts" Hidden="true" />
653 <Field Name="conflictTables" Hidden="true" />
654 <Method Name="GetObjectData" Hidden="true" />
655 <Method Name="MergeException" Hidden="true" />
656 </Members>
657 <InheritanceLine Type="WixToolset.Dtf.WindowsInstaller.InstallerException" FixedFromPoint="true">
658 <Path>
659 <Point X="14" Y="8.813" />
660 <Point X="14.25" Y="8.813" />
661 </Path>
662 </InheritanceLine>
663 <TypeIdentifier>
664 <HashCode>AAAAAAAAAAAAAEAAEABAABAgAAAAAAAAAAAAIAAAAAA=</HashCode>
665 <FileName>Exceptions.cs</FileName>
666 </TypeIdentifier>
667 </Class>
668 <Struct Name="WixToolset.Dtf.WindowsInstaller.MediaDisk">
669 <Position X="17" Y="0.5" Width="2" />
670 <Members>
671 <Field Name="diskId" Hidden="true" />
672 <Field Name="diskPrompt" Hidden="true" />
673 <Method Name="MediaDisk" Hidden="true" />
674 <Field Name="volumeLabel" Hidden="true" />
675 </Members>
676 <TypeIdentifier>
677 <HashCode>AgAAAAAAAAAAAAAAAgAAIAAAACAAAAAEAAAABAAAAAA=</HashCode>
678 <FileName>MediaDisk.cs</FileName>
679 </TypeIdentifier>
680 </Struct>
681 <Struct Name="WixToolset.Dtf.WindowsInstaller.InstallCost">
682 <Position X="10" Y="8" Width="1.25" />
683 <Members>
684 <Field Name="cost" Hidden="true" />
685 <Field Name="driveName" Hidden="true" />
686 <Method Name="InstallCost" Hidden="true" />
687 <Field Name="tempCost" Hidden="true" />
688 </Members>
689 <TypeIdentifier>
690 <HashCode>AAAAAAAAAgAAAAIAAAAACBAAAAgQAAAAAAAAAAAAAAA=</HashCode>
691 <FileName>InstallCost.cs</FileName>
692 </TypeIdentifier>
693 </Struct>
694 <Struct Name="WixToolset.Dtf.WindowsInstaller.ShortcutTarget">
695 <Position X="19.5" Y="7.75" Width="1.5" />
696 <Members>
697 <Field Name="componentCode" Hidden="true" />
698 <Method Name="Equals" Hidden="true" />
699 <Field Name="feature" Hidden="true" />
700 <Method Name="GetHashCode" Hidden="true" />
701 <Method Name="operator !=" Hidden="true" />
702 <Method Name="operator ==" Hidden="true" />
703 <Field Name="productCode" Hidden="true" />
704 <Method Name="ShortcutTarget" Hidden="true" />
705 </Members>
706 <TypeIdentifier>
707 <HashCode>AAAgAAACAAAAAAAAgAAAAAAAAAAAAIAIAAIACAAAIiA=</HashCode>
708 <FileName>ShortcutTarget.cs</FileName>
709 </TypeIdentifier>
710 </Struct>
711 <Enum Name="WixToolset.Dtf.WindowsInstaller.ActionResult" Collapsed="true">
712 <Position X="22" Y="1" Width="2" />
713 <TypeIdentifier>
714 <HashCode>AAAAAAAAAAAAAAQgAAAAAAAAAgAAAACAAAAAAAACAAA=</HashCode>
715 <FileName>Enums.cs</FileName>
716 </TypeIdentifier>
717 </Enum>
718 <Enum Name="WixToolset.Dtf.WindowsInstaller.ControlAttributes" Collapsed="true">
719 <Position X="24.25" Y="1" Width="2" />
720 <TypeIdentifier>
721 <HashCode>AADAAlQAAAEgAoNFAAgAACAgAQAAAooMAAAAgCkCAkA=</HashCode>
722 <FileName>ColumnEnums.cs</FileName>
723 </TypeIdentifier>
724 </Enum>
725 <Enum Name="WixToolset.Dtf.WindowsInstaller.CustomActionTypes" Collapsed="true">
726 <Position X="24.25" Y="1.5" Width="2" />
727 <TypeIdentifier>
728 <HashCode>AABCCFAAAAAhJAAHAAAAAoAAAAAAAAABCAIEEQEAgAA=</HashCode>
729 <FileName>ColumnEnums.cs</FileName>
730 </TypeIdentifier>
731 </Enum>
732 <Enum Name="WixToolset.Dtf.WindowsInstaller.DatabaseOpenMode" Collapsed="true">
733 <Position X="22" Y="1.5" Width="2" />
734 <TypeIdentifier>
735 <HashCode>AAAKAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAYAAAAAAA=</HashCode>
736 <FileName>Enums.cs</FileName>
737 </TypeIdentifier>
738 </Enum>
739 <Enum Name="WixToolset.Dtf.WindowsInstaller.InstallLogModes" Collapsed="true">
740 <Position X="22" Y="2" Width="2" />
741 <TypeIdentifier>
742 <HashCode>AAABgEBAAIAAAQBAAAAAABEAAQAAABSAgAAEAEWQAAA=</HashCode>
743 <FileName>Enums.cs</FileName>
744 </TypeIdentifier>
745 </Enum>
746 <Enum Name="WixToolset.Dtf.WindowsInstaller.InstallMessage" Collapsed="true">
747 <Position X="22" Y="2.5" Width="2" />
748 <TypeIdentifier>
749 <HashCode>AAABgABAAAAAAQBAAAAAABEAAQAAABSAgAAEAECAAAA=</HashCode>
750 <FileName>Enums.cs</FileName>
751 </TypeIdentifier>
752 </Enum>
753 <Enum Name="WixToolset.Dtf.WindowsInstaller.InstallMode" Collapsed="true">
754 <Position X="22" Y="3" Width="2" />
755 <TypeIdentifier>
756 <HashCode>AAAABAAAQAAAAAAAAAAAIAAAACAAAAAAAAAAAAAAAAA=</HashCode>
757 <FileName>Enums.cs</FileName>
758 </TypeIdentifier>
759 </Enum>
760 <Enum Name="WixToolset.Dtf.WindowsInstaller.InstallRunMode" Collapsed="true">
761 <Position X="22" Y="3.5" Width="2" />
762 <TypeIdentifier>
763 <HashCode>AAgAIEACAAQAAAQCAAAAAQAACEAABAQAgAECAAAAAgA=</HashCode>
764 <FileName>Enums.cs</FileName>
765 </TypeIdentifier>
766 </Enum>
767 <Enum Name="WixToolset.Dtf.WindowsInstaller.InstallState" Collapsed="true">
768 <Position X="22" Y="4" Width="2" />
769 <TypeIdentifier>
770 <HashCode>AAAAABAAAgAAAFACgACQJAAAAABQAAQAAAAAAAAAABA=</HashCode>
771 <FileName>Enums.cs</FileName>
772 </TypeIdentifier>
773 </Enum>
774 <Enum Name="WixToolset.Dtf.WindowsInstaller.InstallType" Collapsed="true">
775 <Position X="22" Y="4.5" Width="2" />
776 <TypeIdentifier>
777 <HashCode>AAAAAAAAAAAAAAAAIAAAIAAAAAAAAAAAAgAAAAAAAAA=</HashCode>
778 <FileName>Enums.cs</FileName>
779 </TypeIdentifier>
780 </Enum>
781 <Enum Name="WixToolset.Dtf.WindowsInstaller.InstallUIOptions" Collapsed="true">
782 <Position X="22" Y="5" Width="2" />
783 <TypeIdentifier>
784 <HashCode>AAAAAAAIACAAAAAAQAAAIACEAAAAAAAAAgIAAAGAAAA=</HashCode>
785 <FileName>Enums.cs</FileName>
786 </TypeIdentifier>
787 </Enum>
788 <Enum Name="WixToolset.Dtf.WindowsInstaller.MessageResult" Collapsed="true">
789 <Position X="22" Y="5.5" Width="2" />
790 <TypeIdentifier>
791 <HashCode>AAAAAAAASAAQASBAAAAAAAAAACAAAAAAAAABAAEAAAA=</HashCode>
792 <FileName>Enums.cs</FileName>
793 </TypeIdentifier>
794 </Enum>
795 <Enum Name="WixToolset.Dtf.WindowsInstaller.ReinstallModes" Collapsed="true">
796 <Position X="22" Y="6" Width="2" />
797 <TypeIdentifier>
798 <HashCode>IAAAAAAAAACAAAAAAAAQAAAQBAAAABAAAAEAAgAIABA=</HashCode>
799 <FileName>Enums.cs</FileName>
800 </TypeIdentifier>
801 </Enum>
802 <Enum Name="WixToolset.Dtf.WindowsInstaller.TransformErrors" Collapsed="true">
803 <Position X="22" Y="6.5" Width="2" />
804 <TypeIdentifier>
805 <HashCode>AAIEAAAAAAABAAAAAAAAAAQAAAAAAAACAAAgAAEAABA=</HashCode>
806 <FileName>Enums.cs</FileName>
807 </TypeIdentifier>
808 </Enum>
809 <Enum Name="WixToolset.Dtf.WindowsInstaller.TransformValidations" Collapsed="true">
810 <Position X="22" Y="7" Width="2" />
811 <TypeIdentifier>
812 <HashCode>AAACAAAACAQIAAAAAAAAAAAAAEgAAAQAAIAQAQEAAAQ=</HashCode>
813 <FileName>Enums.cs</FileName>
814 </TypeIdentifier>
815 </Enum>
816 <Enum Name="WixToolset.Dtf.WindowsInstaller.UserContexts" Collapsed="true">
817 <Position X="22" Y="7.5" Width="2" />
818 <TypeIdentifier>
819 <HashCode>AAAAAAgAAAAAAAAEAAAAAAIAAAAAAAAAAIAAQAEAAAA=</HashCode>
820 <FileName>Enums.cs</FileName>
821 </TypeIdentifier>
822 </Enum>
823 <Enum Name="WixToolset.Dtf.WindowsInstaller.ValidationError" Collapsed="true">
824 <Position X="22" Y="8" Width="2" />
825 <TypeIdentifier>
826 <HashCode>oCMBAAABEAMAAAAAAAAQAAIASigAABKBABgCgEEAAA4=</HashCode>
827 <FileName>Enums.cs</FileName>
828 </TypeIdentifier>
829 </Enum>
830 <Enum Name="WixToolset.Dtf.WindowsInstaller.ViewModifyMode" Collapsed="true">
831 <Position X="22" Y="8.5" Width="2" />
832 <TypeIdentifier>
833 <HashCode>IAQAAEQAAADgAAAACAAAAACAAAEAAQAAAAAAAAAAACA=</HashCode>
834 <FileName>Enums.cs</FileName>
835 </TypeIdentifier>
836 </Enum>
837 <Enum Name="WixToolset.Dtf.WindowsInstaller.ComponentAttributes" Collapsed="true">
838 <Position X="24.25" Y="2" Width="2" />
839 <TypeIdentifier>
840 <HashCode>AAABAAAAIAAAAAAAEAAACAAAIAAAgQCAAAAgAAEAAAA=</HashCode>
841 <FileName>ColumnEnums.cs</FileName>
842 </TypeIdentifier>
843 </Enum>
844 <Enum Name="WixToolset.Dtf.WindowsInstaller.DialogAttributes" Collapsed="true">
845 <Position X="24.25" Y="2.5" Width="2" />
846 <TypeIdentifier>
847 <HashCode>AAAIAAQAAAAAAQIEAAAAACAAEAAAAAAIgAAggAAAAgA=</HashCode>
848 <FileName>ColumnEnums.cs</FileName>
849 </TypeIdentifier>
850 </Enum>
851 <Enum Name="WixToolset.Dtf.WindowsInstaller.FeatureAttributes" Collapsed="true">
852 <Position X="24.25" Y="3" Width="2" />
853 <TypeIdentifier>
854 <HashCode>AAAAQEAAAEAAAAAAAAAAAAAAEAAAAAABABAAAAEAAAA=</HashCode>
855 <FileName>ColumnEnums.cs</FileName>
856 </TypeIdentifier>
857 </Enum>
858 <Enum Name="WixToolset.Dtf.WindowsInstaller.FileAttributes" Collapsed="true">
859 <Position X="24.25" Y="3.5" Width="2" />
860 <TypeIdentifier>
861 <HashCode>AAAAAABAAIAIAAAAAAAAAgAAAAAEAAAAAAEIAAEEAAA=</HashCode>
862 <FileName>ColumnEnums.cs</FileName>
863 </TypeIdentifier>
864 </Enum>
865 <Enum Name="WixToolset.Dtf.WindowsInstaller.IniFileAction" Collapsed="true">
866 <Position X="24.25" Y="4" Width="2" />
867 <TypeIdentifier>
868 <HashCode>AggAAAAAAAAAEAEAAAAAAAAAAAABAAAAAAAAAAAAAAA=</HashCode>
869 <FileName>ColumnEnums.cs</FileName>
870 </TypeIdentifier>
871 </Enum>
872 <Enum Name="WixToolset.Dtf.WindowsInstaller.LocatorTypes" Collapsed="true">
873 <Position X="24.25" Y="4.5" Width="2" />
874 <TypeIdentifier>
875 <HashCode>ABAAABAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAABAAAA=</HashCode>
876 <FileName>ColumnEnums.cs</FileName>
877 </TypeIdentifier>
878 </Enum>
879 <Enum Name="WixToolset.Dtf.WindowsInstaller.RegistryRoot" Collapsed="true">
880 <Position X="24.25" Y="5" Width="2" />
881 <TypeIdentifier>
882 <HashCode>AACgACAAAIAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAA=</HashCode>
883 <FileName>ColumnEnums.cs</FileName>
884 </TypeIdentifier>
885 </Enum>
886 <Enum Name="WixToolset.Dtf.WindowsInstaller.RemoveFileModes" Collapsed="true">
887 <Position X="24.25" Y="5.5" Width="2" />
888 <TypeIdentifier>
889 <HashCode>AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAACAAAEAAAA=</HashCode>
890 <FileName>ColumnEnums.cs</FileName>
891 </TypeIdentifier>
892 </Enum>
893 <Enum Name="WixToolset.Dtf.WindowsInstaller.ServiceAttributes" Collapsed="true">
894 <Position X="24.25" Y="6" Width="2" />
895 <TypeIdentifier>
896 <HashCode>AQCAAAAAQAAAAAAAAACAAAgAAAAAAAAAAgAAABEAAAI=</HashCode>
897 <FileName>ColumnEnums.cs</FileName>
898 </TypeIdentifier>
899 </Enum>
900 <Enum Name="WixToolset.Dtf.WindowsInstaller.ServiceControlEvents" Collapsed="true">
901 <Position X="24.25" Y="6.5" Width="2" />
902 <TypeIdentifier>
903 <HashCode>AAAAAAAAACAAAAAAAAAAEAAAAAEAAgAAJAAAAAEAAAA=</HashCode>
904 <FileName>ColumnEnums.cs</FileName>
905 </TypeIdentifier>
906 </Enum>
907 <Enum Name="WixToolset.Dtf.WindowsInstaller.TextStyles" Collapsed="true">
908 <Position X="24.25" Y="7" Width="2" />
909 <TypeIdentifier>
910 <HashCode>AAAAAAAAAAAAACAAAIAAAAAAAAACAAAAAAAAAAAAAAE=</HashCode>
911 <FileName>ColumnEnums.cs</FileName>
912 </TypeIdentifier>
913 </Enum>
914 <Enum Name="WixToolset.Dtf.WindowsInstaller.UpgradeAttributes" Collapsed="true">
915 <Position X="24.25" Y="7.5" Width="2" />
916 <TypeIdentifier>
917 <HashCode>AEAAAAAAAAAAAAAQAAAAAAAAAAgIAAAAABAAAAAAIAA=</HashCode>
918 <FileName>ColumnEnums.cs</FileName>
919 </TypeIdentifier>
920 </Enum>
921 <Delegate Name="WixToolset.Dtf.WindowsInstaller.InapplicablePatchHandler" Collapsed="true">
922 <Position X="16.75" Y="9.75" Width="2.25" />
923 <TypeIdentifier>
924 <HashCode>AAAAAAAAAACAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAA=</HashCode>
925 <FileName>Installer.cs</FileName>
926 </TypeIdentifier>
927 </Delegate>
928 <Delegate Name="WixToolset.Dtf.WindowsInstaller.ExternalUIHandler" Collapsed="true">
929 <Position X="16.75" Y="8.5" Width="2.25" />
930 <TypeIdentifier>
931 <HashCode>AAAEAAAAAAAAABAAAAAABAAAAACBAAAAAAAAAAAAAAA=</HashCode>
932 <FileName>ExternalUIHandler.cs</FileName>
933 </TypeIdentifier>
934 </Delegate>
935 <Delegate Name="WixToolset.Dtf.WindowsInstaller.ExternalUIRecordHandler" Collapsed="true">
936 <Position X="16.75" Y="9" Width="2.25" />
937 <TypeIdentifier>
938 <HashCode>AAAEAAAAAAAAAAAAAAAABAAAAACBAAAAAAAAAAAAAAA=</HashCode>
939 <FileName>ExternalUIHandler.cs</FileName>
940 </TypeIdentifier>
941 </Delegate>
942 <Font Name="Verdana" Size="8" />
943</ClassDiagram> \ No newline at end of file
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/WixToolset.Dtf.WindowsInstaller.csproj b/src/dtf/WixToolset.Dtf.WindowsInstaller/WixToolset.Dtf.WindowsInstaller.csproj
new file mode 100644
index 00000000..515e609b
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/WixToolset.Dtf.WindowsInstaller.csproj
@@ -0,0 +1,30 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project Sdk="Microsoft.NET.Sdk">
5 <PropertyGroup>
6 <RootNamespace>WixToolset.Dtf.WindowsInstaller</RootNamespace>
7 <AssemblyName>WixToolset.Dtf.WindowsInstaller</AssemblyName>
8 <TargetFrameworks>netstandard2.0;net20</TargetFrameworks>
9 <Description>Managed libraries for Windows Installer</Description>
10 <CreateDocumentationFile>true</CreateDocumentationFile>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <EmbeddedResource Include="Errors.resources" />
15 </ItemGroup>
16
17 <ItemGroup>
18 <None Include="Errors.txt" />
19 <None Include="WindowsInstaller.cd" />
20 </ItemGroup>
21
22 <ItemGroup>
23 <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
24 <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" />
25 </ItemGroup>
26
27 <ItemGroup Condition=" '$(TargetFramework)'=='net20' ">
28 <Reference Include="System.Configuration" />
29 </ItemGroup>
30</Project>
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs
new file mode 100644
index 00000000..88a0295d
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs
@@ -0,0 +1,469 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Xml;
8 using System.Xml.Serialization;
9 using System.Text;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// Contains a collection of key-value pairs suitable for passing between
16 /// immediate and deferred/rollback/commit custom actions.
17 /// </summary>
18 /// <remarks>
19 /// Call the <see cref="CustomActionData.ToString" /> method to get a string
20 /// suitable for storing in a property and reconstructing the custom action data later.
21 /// </remarks>
22 /// <seealso cref="Session.CustomActionData"/>
23 /// <seealso cref="Session.DoAction(string,CustomActionData)"/>
24 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
25 public sealed class CustomActionData : IDictionary<string, string>
26 {
27 /// <summary>
28 /// "CustomActionData" literal property name.
29 /// </summary>
30 public const string PropertyName = "CustomActionData";
31
32 private const char DataSeparator = ';';
33 private const char KeyValueSeparator = '=';
34
35 private IDictionary<string, string> data;
36
37 /// <summary>
38 /// Creates a new empty custom action data object.
39 /// </summary>
40 public CustomActionData() : this(null)
41 {
42 }
43
44 /// <summary>
45 /// Reconstructs a custom action data object from data that was previously
46 /// persisted in a string.
47 /// </summary>
48 /// <param name="keyValueList">Previous output from <see cref="CustomActionData.ToString" />.</param>
49 public CustomActionData(string keyValueList)
50 {
51 this.data = new Dictionary<string, string>();
52
53 if (keyValueList != null)
54 {
55 this.Parse(keyValueList);
56 }
57 }
58
59 /// <summary>
60 /// Adds a key and value to the data collection.
61 /// </summary>
62 /// <param name="key">Case-sensitive data key.</param>
63 /// <param name="value">Data value (may be null).</param>
64 /// <exception cref="ArgumentException">the key does not consist solely of letters,
65 /// numbers, and the period, underscore, and space characters.</exception>
66 public void Add(string key, string value)
67 {
68 CustomActionData.ValidateKey(key);
69 this.data.Add(key, value);
70 }
71
72 /// <summary>
73 /// Adds a value to the data collection, using XML serialization to persist the object as a string.
74 /// </summary>
75 /// <param name="key">Case-sensitive data key.</param>
76 /// <param name="value">Data value (may be null).</param>
77 /// <exception cref="ArgumentException">the key does not consist solely of letters,
78 /// numbers, and the period, underscore, and space characters.</exception>
79 /// <exception cref="NotSupportedException">The value type does not support XML serialization.</exception>
80 /// <exception cref="InvalidOperationException">The value could not be serialized.</exception>
81 public void AddObject<T>(string key, T value)
82 {
83 if (value == null)
84 {
85 this.Add(key, null);
86 }
87 else if (typeof(T) == typeof(string) ||
88 typeof(T) == typeof(CustomActionData)) // Serialize nested CustomActionData
89 {
90 this.Add(key, value.ToString());
91 }
92 else
93 {
94 string valueString = CustomActionData.Serialize<T>(value);
95 this.Add(key, valueString);
96 }
97 }
98
99 /// <summary>
100 /// Gets a value from the data collection, using XML serialization to load the object from a string.
101 /// </summary>
102 /// <param name="key">Case-sensitive data key.</param>
103 /// <exception cref="InvalidOperationException">The value could not be deserialized.</exception>
104 [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
105 public T GetObject<T>(string key)
106 {
107 string value = this[key];
108 if (value == null)
109 {
110 return default(T);
111 }
112 else if (typeof(T) == typeof(string))
113 {
114 // Funny casting because the compiler doesn't know T is string here.
115 return (T) (object) value;
116 }
117 else if (typeof(T) == typeof(CustomActionData))
118 {
119 // Deserialize nested CustomActionData.
120 return (T) (object) new CustomActionData(value);
121 }
122 else if (value.Length == 0)
123 {
124 return default(T);
125 }
126 else
127 {
128 return CustomActionData.Deserialize<T>(value);
129 }
130 }
131
132 /// <summary>
133 /// Determines whether the data contains an item with the specified key.
134 /// </summary>
135 /// <param name="key">Case-sensitive data key.</param>
136 /// <returns>true if the data contains an item with the key; otherwise, false</returns>
137 public bool ContainsKey(string key)
138 {
139 return this.data.ContainsKey(key);
140 }
141
142 /// <summary>
143 /// Gets a collection object containing all the keys of the data.
144 /// </summary>
145 public ICollection<string> Keys
146 {
147 get
148 {
149 return this.data.Keys;
150 }
151 }
152
153 /// <summary>
154 /// Removes the item with the specified key from the data.
155 /// </summary>
156 /// <param name="key">Case-sensitive data key.</param>
157 /// <returns>true if the item was successfully removed from the data;
158 /// false if an item with the specified key was not found</returns>
159 public bool Remove(string key)
160 {
161 return this.data.Remove(key);
162 }
163
164 /// <summary>
165 /// Gets the value with the specified key.
166 /// </summary>
167 /// <param name="key">Case-sensitive data key.</param>
168 /// <param name="value">Value associated with the specified key, or
169 /// null if an item with the specified key was not found</param>
170 /// <returns>true if the data contains an item with the specified key; otherwise, false.</returns>
171 public bool TryGetValue(string key, out string value)
172 {
173 return this.data.TryGetValue(key, out value);
174 }
175
176 /// <summary>
177 /// Gets a collection containing all the values of the data.
178 /// </summary>
179 public ICollection<string> Values
180 {
181 get
182 {
183 return this.data.Values;
184 }
185 }
186
187 /// <summary>
188 /// Gets or sets a data value with a specified key.
189 /// </summary>
190 /// <param name="key">Case-sensitive data key.</param>
191 /// <exception cref="ArgumentException">the key does not consist solely of letters,
192 /// numbers, and the period, underscore, and space characters.</exception>
193 public string this[string key]
194 {
195 get
196 {
197 return this.data[key];
198 }
199 set
200 {
201 CustomActionData.ValidateKey(key);
202 this.data[key] = value;
203 }
204 }
205
206 /// <summary>
207 /// Adds an item with key and value to the data collection.
208 /// </summary>
209 /// <param name="item">Case-sensitive data key, with a data value that may be null.</param>
210 /// <exception cref="ArgumentException">the key does not consist solely of letters,
211 /// numbers, and the period, underscore, and space characters.</exception>
212 public void Add(KeyValuePair<string, string> item)
213 {
214 CustomActionData.ValidateKey(item.Key);
215 this.data.Add(item);
216 }
217
218 /// <summary>
219 /// Removes all items from the data.
220 /// </summary>
221 public void Clear()
222 {
223 if (this.data.Count > 0)
224 {
225 this.data.Clear();
226 }
227 }
228
229 /// <summary>
230 /// Determines whether the data contains a specified item.
231 /// </summary>
232 /// <param name="item">The data item to locate.</param>
233 /// <returns>true if the data contains the item; otherwise, false</returns>
234 public bool Contains(KeyValuePair<string, string> item)
235 {
236 return this.data.Contains(item);
237 }
238
239 /// <summary>
240 /// Copies the data to an array, starting at a particular array index.
241 /// </summary>
242 /// <param name="array">Destination array.</param>
243 /// <param name="arrayIndex">Index in the array at which copying begins.</param>
244 public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
245 {
246 this.data.CopyTo(array, arrayIndex);
247 }
248
249 /// <summary>
250 /// Gets the number of items in the data.
251 /// </summary>
252 public int Count
253 {
254 get
255 {
256 return this.data.Count;
257 }
258 }
259
260 /// <summary>
261 /// Gets a value indicating whether the data is read-only.
262 /// </summary>
263 public bool IsReadOnly
264 {
265 get
266 {
267 return false;
268 }
269 }
270
271 /// <summary>
272 /// Removes an item from the data.
273 /// </summary>
274 /// <param name="item">The item to remove.</param>
275 /// <returns>true if the item was successfully removed from the data;
276 /// false if the item was not found</returns>
277 public bool Remove(KeyValuePair<string, string> item)
278 {
279 return this.data.Remove(item);
280 }
281
282 /// <summary>
283 /// Returns an enumerator that iterates through the collection.
284 /// </summary>
285 /// <returns>An enumerator that can be used to iterate through the collection.</returns>
286 public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
287 {
288 return this.data.GetEnumerator();
289 }
290
291 /// <summary>
292 /// Returns an enumerator that iterates through the collection.
293 /// </summary>
294 /// <returns>An enumerator that can be used to iterate through the collection.</returns>
295 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
296 {
297 return ((System.Collections.IEnumerable) this.data).GetEnumerator();
298 }
299
300 /// <summary>
301 /// Gets a string representation of the data suitable for persisting in a property.
302 /// </summary>
303 /// <returns>Data string in the form "Key1=Value1;Key2=Value2"</returns>
304 public override string ToString()
305 {
306 StringBuilder buf = new StringBuilder();
307
308 foreach (KeyValuePair<string, string> item in this.data)
309 {
310 if (buf.Length > 0)
311 {
312 buf.Append(CustomActionData.DataSeparator);
313 }
314
315 buf.Append(item.Key);
316
317 if (item.Value != null)
318 {
319 buf.Append(CustomActionData.KeyValueSeparator);
320 buf.Append(CustomActionData.Escape(item.Value));
321 }
322 }
323
324 return buf.ToString();
325 }
326
327 /// <summary>
328 /// Ensures that a key contains valid characters.
329 /// </summary>
330 /// <param name="key">key to be validated</param>
331 /// <exception cref="ArgumentException">the key does not consist solely of letters,
332 /// numbers, and the period, underscore, and space characters.</exception>
333 private static void ValidateKey(string key)
334 {
335 if (String.IsNullOrEmpty(key))
336 {
337 throw new ArgumentNullException("key");
338 }
339
340 for (int i = 0; i < key.Length; i++)
341 {
342 char c = key[i];
343 if (!Char.IsLetterOrDigit(c) && c != '_' && c != '.' &&
344 !(i > 0 && i < key.Length - 1 && c == ' '))
345 {
346 throw new ArgumentOutOfRangeException("key");
347 }
348 }
349 }
350
351 /// <summary>
352 /// Serializes a value into an XML string.
353 /// </summary>
354 /// <typeparam name="T">Type of the value.</typeparam>
355 /// <param name="value">Value to be serialized.</param>
356 /// <returns>Serialized value data as a string.</returns>
357 private static string Serialize<T>(T value)
358 {
359 XmlWriterSettings xws = new XmlWriterSettings();
360 xws.OmitXmlDeclaration = true;
361
362 StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
363 using (XmlWriter xw = XmlWriter.Create(sw, xws))
364 {
365 XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
366 ns.Add(string.Empty, String.Empty); // Prevent output of any namespaces
367
368 XmlSerializer ser = new XmlSerializer(typeof(T));
369 ser.Serialize(xw, value, ns);
370
371 return sw.ToString();
372 }
373 }
374
375 /// <summary>
376 /// Deserializes a value from an XML string.
377 /// </summary>
378 /// <typeparam name="T">Expected type of the value.</typeparam>
379 /// <param name="value">Serialized value data.</param>
380 /// <returns>Deserialized value object.</returns>
381 private static T Deserialize<T>(string value)
382 {
383 StringReader sr = new StringReader(value);
384 using (XmlReader xr = XmlReader.Create(sr))
385 {
386 XmlSerializer ser = new XmlSerializer(typeof(T));
387 return (T) ser.Deserialize(xr);
388 }
389 }
390
391 /// <summary>
392 /// Escapes a value string by doubling any data-separator (semicolon) characters.
393 /// </summary>
394 /// <param name="value"></param>
395 /// <returns>Escaped value string</returns>
396 private static string Escape(string value)
397 {
398 value = value.Replace(String.Empty + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator);
399 return value;
400 }
401
402 /// <summary>
403 /// Unescapes a value string by undoubling any doubled data-separator (semicolon) characters.
404 /// </summary>
405 /// <param name="value"></param>
406 /// <returns>Unescaped value string</returns>
407 private static string Unescape(string value)
408 {
409 value = value.Replace(String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator);
410 return value;
411 }
412
413 /// <summary>
414 /// Loads key-value pairs from a string into the data collection.
415 /// </summary>
416 /// <param name="keyValueList">key-value pair list of the form returned by <see cref="ToString"/></param>
417 private void Parse(string keyValueList)
418 {
419 int itemStart = 0;
420 while (itemStart < keyValueList.Length)
421 {
422 // Find the next non-escaped data separator.
423 int semi = itemStart - 2;
424 do
425 {
426 semi = keyValueList.IndexOf(CustomActionData.DataSeparator, semi + 2);
427 }
428 while (semi >= 0 && semi < keyValueList.Length - 1 && keyValueList[semi + 1] == CustomActionData.DataSeparator);
429
430 if (semi < 0)
431 {
432 semi = keyValueList.Length;
433 }
434
435 // Find the next non-escaped key-value separator.
436 int equals = itemStart - 2;
437 do
438 {
439 equals = keyValueList.IndexOf(CustomActionData.KeyValueSeparator, equals + 2);
440 }
441 while (equals >= 0 && equals < keyValueList.Length - 1 && keyValueList[equals + 1] == CustomActionData.KeyValueSeparator);
442
443 if (equals < 0 || equals > semi)
444 {
445 equals = semi;
446 }
447
448 string key = keyValueList.Substring(itemStart, equals - itemStart);
449 string value = null;
450
451 // If there's a key-value separator before the next data separator, then the item has a value.
452 if (equals < semi)
453 {
454 value = keyValueList.Substring(equals + 1, semi - (equals + 1));
455 value = CustomActionData.Unescape(value);
456 }
457
458 // Add non-duplicate items to the collection.
459 if (key.Length > 0 && !this.data.ContainsKey(key))
460 {
461 this.data.Add(key, value);
462 }
463
464 // Move past the data separator to the next item.
465 itemStart = semi + 1;
466 }
467 }
468 }
469}
diff --git a/src/dtf/WixToolset.Dtf.sln b/src/dtf/WixToolset.Dtf.sln
new file mode 100644
index 00000000..7e58ad8d
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.sln
@@ -0,0 +1,281 @@
1
2Microsoft Visual Studio Solution File, Format Version 12.00
3# Visual Studio 15
4VisualStudioVersion = 15.0.26730.8
5MinimumVisualStudioVersion = 15.0.26124.0
6Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolset.Dtf.Compression", "src\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj", "{2D62850C-9F81-4BE9-BDF3-9379389C8F7B}"
7EndProject
8Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolset.Dtf.Compression.Cab", "src\WixToolset.Dtf.Compression.Cab\WixToolset.Dtf.Compression.Cab.csproj", "{15895FD1-DD68-407B-8717-08F6DD14F02C}"
9EndProject
10Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolset.Dtf.Compression.Zip", "src\WixToolset.Dtf.Compression.Zip\WixToolset.Dtf.Compression.Zip.csproj", "{261F2857-B521-42A4-A3E0-B5165F225E50}"
11EndProject
12Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolset.Dtf.Resources", "src\WixToolset.Dtf.Resources\WixToolset.Dtf.Resources.csproj", "{44931ECB-8D6F-4C12-A872-64E261B6A98E}"
13EndProject
14Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolset.Dtf.WindowsInstaller", "src\WixToolset.Dtf.WindowsInstaller\WixToolset.Dtf.WindowsInstaller.csproj", "{24121677-0ED0-41B5-833F-1B9A18E87BF4}"
15EndProject
16Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolset.Dtf.WindowsInstaller.Linq", "src\WixToolset.Dtf.WindowsInstaller.Linq\WixToolset.Dtf.WindowsInstaller.Linq.csproj", "{CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}"
17EndProject
18Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolset.Dtf.WindowsInstaller.Package", "src\WixToolset.Dtf.WindowsInstaller.Package\WixToolset.Dtf.WindowsInstaller.Package.csproj", "{1A9940A7-3E29-4428-B753-C4CC66058F1A}"
19EndProject
20Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolsetTests.Dtf.Compression", "src\WixToolsetTests.Dtf.Compression\WixToolsetTests.Dtf.Compression.csproj", "{F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}"
21EndProject
22Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolsetTests.Dtf.Compression.Cab", "src\WixToolsetTests.Dtf.Compression.Cab\WixToolsetTests.Dtf.Compression.Cab.csproj", "{4544158C-2D63-4146-85FF-62169280144E}"
23EndProject
24Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolsetTests.Dtf.Compression.Zip", "src\WixToolsetTests.Dtf.Compression.Zip\WixToolsetTests.Dtf.Compression.Zip.csproj", "{328799BB-7B03-4B28-8180-4132211FD07D}"
25EndProject
26Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolsetTests.Dtf.WindowsInstaller", "src\WixToolsetTests.Dtf.WindowsInstaller\WixToolsetTests.Dtf.WindowsInstaller.csproj", "{16F5202F-9276-4166-975C-C9654BAF8012}"
27EndProject
28Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolsetTests.Dtf.WindowsInstaller.CustomActions", "src\WixToolsetTests.Dtf.WindowsInstaller.CustomActions\WixToolsetTests.Dtf.WindowsInstaller.CustomActions.csproj", "{137D376B-989F-4FEA-9A67-01D8D38CA0DE}"
29EndProject
30Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WixToolsetTests.Dtf.WindowsInstaller.Linq", "src\WixToolsetTests.Dtf.WindowsInstaller.Linq\WixToolsetTests.Dtf.WindowsInstaller.Linq.csproj", "{4F55F9B8-D8B6-41EB-8796-221B4CD98324}"
31EndProject
32Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{A988A768-200F-408F-AE3B-5D9B14AA48EE}"
33EndProject
34Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SfxCA", "src\Tools\SfxCA\SfxCA.vcxproj", "{55D5BA28-D427-4F53-80C2-FE9EF23C1553}"
35EndProject
36Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MakeSfxCA", "src\Tools\MakeSfxCA\MakeSfxCA.csproj", "{3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}"
37EndProject
38Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{4A47EC94-8234-4479-87BC-3D924DB7AA4E}"
39EndProject
40Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ManagedCA", "src\Samples\ManagedCA\ManagedCA.csproj", "{DB9E5F02-8241-440A-9B60-980EB5B42B13}"
41EndProject
42Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmbeddedUI", "src\Samples\EmbeddedUI\EmbeddedUI.csproj", "{864B8C50-7895-4485-AC89-900D86FD8C0D}"
43EndProject
44Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WixToolset.Dtf.MSBuild", "src\WixToolset.Dtf.MSBuild\WixToolset.Dtf.MSBuild.csproj", "{E7A00377-A0B5-400F-8337-C0814AAC7153}"
45EndProject
46Global
47 GlobalSection(SolutionConfigurationPlatforms) = preSolution
48 Debug|Any CPU = Debug|Any CPU
49 Debug|x64 = Debug|x64
50 Debug|x86 = Debug|x86
51 Release|Any CPU = Release|Any CPU
52 Release|x64 = Release|x64
53 Release|x86 = Release|x86
54 EndGlobalSection
55 GlobalSection(ProjectConfigurationPlatforms) = postSolution
56 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Debug|Any CPU.Build.0 = Debug|Any CPU
58 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Debug|x64.ActiveCfg = Debug|Any CPU
59 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Debug|x64.Build.0 = Debug|Any CPU
60 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Debug|x86.ActiveCfg = Debug|Any CPU
61 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Debug|x86.Build.0 = Debug|Any CPU
62 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Release|Any CPU.ActiveCfg = Release|Any CPU
63 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Release|Any CPU.Build.0 = Release|Any CPU
64 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Release|x64.ActiveCfg = Release|Any CPU
65 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Release|x64.Build.0 = Release|Any CPU
66 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Release|x86.ActiveCfg = Release|Any CPU
67 {2D62850C-9F81-4BE9-BDF3-9379389C8F7B}.Release|x86.Build.0 = Release|Any CPU
68 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
69 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Debug|Any CPU.Build.0 = Debug|Any CPU
70 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Debug|x64.ActiveCfg = Debug|Any CPU
71 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Debug|x64.Build.0 = Debug|Any CPU
72 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Debug|x86.ActiveCfg = Debug|Any CPU
73 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Debug|x86.Build.0 = Debug|Any CPU
74 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Release|Any CPU.ActiveCfg = Release|Any CPU
75 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Release|Any CPU.Build.0 = Release|Any CPU
76 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Release|x64.ActiveCfg = Release|Any CPU
77 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Release|x64.Build.0 = Release|Any CPU
78 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Release|x86.ActiveCfg = Release|Any CPU
79 {15895FD1-DD68-407B-8717-08F6DD14F02C}.Release|x86.Build.0 = Release|Any CPU
80 {261F2857-B521-42A4-A3E0-B5165F225E50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
81 {261F2857-B521-42A4-A3E0-B5165F225E50}.Debug|Any CPU.Build.0 = Debug|Any CPU
82 {261F2857-B521-42A4-A3E0-B5165F225E50}.Debug|x64.ActiveCfg = Debug|Any CPU
83 {261F2857-B521-42A4-A3E0-B5165F225E50}.Debug|x64.Build.0 = Debug|Any CPU
84 {261F2857-B521-42A4-A3E0-B5165F225E50}.Debug|x86.ActiveCfg = Debug|Any CPU
85 {261F2857-B521-42A4-A3E0-B5165F225E50}.Debug|x86.Build.0 = Debug|Any CPU
86 {261F2857-B521-42A4-A3E0-B5165F225E50}.Release|Any CPU.ActiveCfg = Release|Any CPU
87 {261F2857-B521-42A4-A3E0-B5165F225E50}.Release|Any CPU.Build.0 = Release|Any CPU
88 {261F2857-B521-42A4-A3E0-B5165F225E50}.Release|x64.ActiveCfg = Release|Any CPU
89 {261F2857-B521-42A4-A3E0-B5165F225E50}.Release|x64.Build.0 = Release|Any CPU
90 {261F2857-B521-42A4-A3E0-B5165F225E50}.Release|x86.ActiveCfg = Release|Any CPU
91 {261F2857-B521-42A4-A3E0-B5165F225E50}.Release|x86.Build.0 = Release|Any CPU
92 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
93 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Debug|Any CPU.Build.0 = Debug|Any CPU
94 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Debug|x64.ActiveCfg = Debug|Any CPU
95 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Debug|x64.Build.0 = Debug|Any CPU
96 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Debug|x86.ActiveCfg = Debug|Any CPU
97 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Debug|x86.Build.0 = Debug|Any CPU
98 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Release|Any CPU.ActiveCfg = Release|Any CPU
99 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Release|Any CPU.Build.0 = Release|Any CPU
100 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Release|x64.ActiveCfg = Release|Any CPU
101 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Release|x64.Build.0 = Release|Any CPU
102 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Release|x86.ActiveCfg = Release|Any CPU
103 {44931ECB-8D6F-4C12-A872-64E261B6A98E}.Release|x86.Build.0 = Release|Any CPU
104 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
105 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
106 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Debug|x64.ActiveCfg = Debug|Any CPU
107 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Debug|x64.Build.0 = Debug|Any CPU
108 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Debug|x86.ActiveCfg = Debug|Any CPU
109 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Debug|x86.Build.0 = Debug|Any CPU
110 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
111 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Release|Any CPU.Build.0 = Release|Any CPU
112 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Release|x64.ActiveCfg = Release|Any CPU
113 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Release|x64.Build.0 = Release|Any CPU
114 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Release|x86.ActiveCfg = Release|Any CPU
115 {24121677-0ED0-41B5-833F-1B9A18E87BF4}.Release|x86.Build.0 = Release|Any CPU
116 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
117 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Debug|Any CPU.Build.0 = Debug|Any CPU
118 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Debug|x64.ActiveCfg = Debug|Any CPU
119 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Debug|x64.Build.0 = Debug|Any CPU
120 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Debug|x86.ActiveCfg = Debug|Any CPU
121 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Debug|x86.Build.0 = Debug|Any CPU
122 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Release|Any CPU.ActiveCfg = Release|Any CPU
123 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Release|Any CPU.Build.0 = Release|Any CPU
124 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Release|x64.ActiveCfg = Release|Any CPU
125 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Release|x64.Build.0 = Release|Any CPU
126 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Release|x86.ActiveCfg = Release|Any CPU
127 {CD7A37D8-9D8C-41BD-B78F-DB5E0C299D2E}.Release|x86.Build.0 = Release|Any CPU
128 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
129 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
130 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Debug|x64.ActiveCfg = Debug|Any CPU
131 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Debug|x64.Build.0 = Debug|Any CPU
132 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Debug|x86.ActiveCfg = Debug|Any CPU
133 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Debug|x86.Build.0 = Debug|Any CPU
134 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
135 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Release|Any CPU.Build.0 = Release|Any CPU
136 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Release|x64.ActiveCfg = Release|Any CPU
137 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Release|x64.Build.0 = Release|Any CPU
138 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Release|x86.ActiveCfg = Release|Any CPU
139 {1A9940A7-3E29-4428-B753-C4CC66058F1A}.Release|x86.Build.0 = Release|Any CPU
140 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
141 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
142 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Debug|x64.ActiveCfg = Debug|Any CPU
143 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Debug|x64.Build.0 = Debug|Any CPU
144 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Debug|x86.ActiveCfg = Debug|Any CPU
145 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Debug|x86.Build.0 = Debug|Any CPU
146 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
147 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Release|Any CPU.Build.0 = Release|Any CPU
148 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Release|x64.ActiveCfg = Release|Any CPU
149 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Release|x64.Build.0 = Release|Any CPU
150 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Release|x86.ActiveCfg = Release|Any CPU
151 {F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}.Release|x86.Build.0 = Release|Any CPU
152 {4544158C-2D63-4146-85FF-62169280144E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
153 {4544158C-2D63-4146-85FF-62169280144E}.Debug|Any CPU.Build.0 = Debug|Any CPU
154 {4544158C-2D63-4146-85FF-62169280144E}.Debug|x64.ActiveCfg = Debug|Any CPU
155 {4544158C-2D63-4146-85FF-62169280144E}.Debug|x64.Build.0 = Debug|Any CPU
156 {4544158C-2D63-4146-85FF-62169280144E}.Debug|x86.ActiveCfg = Debug|Any CPU
157 {4544158C-2D63-4146-85FF-62169280144E}.Debug|x86.Build.0 = Debug|Any CPU
158 {4544158C-2D63-4146-85FF-62169280144E}.Release|Any CPU.ActiveCfg = Release|Any CPU
159 {4544158C-2D63-4146-85FF-62169280144E}.Release|Any CPU.Build.0 = Release|Any CPU
160 {4544158C-2D63-4146-85FF-62169280144E}.Release|x64.ActiveCfg = Release|Any CPU
161 {4544158C-2D63-4146-85FF-62169280144E}.Release|x64.Build.0 = Release|Any CPU
162 {4544158C-2D63-4146-85FF-62169280144E}.Release|x86.ActiveCfg = Release|Any CPU
163 {4544158C-2D63-4146-85FF-62169280144E}.Release|x86.Build.0 = Release|Any CPU
164 {328799BB-7B03-4B28-8180-4132211FD07D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
165 {328799BB-7B03-4B28-8180-4132211FD07D}.Debug|Any CPU.Build.0 = Debug|Any CPU
166 {328799BB-7B03-4B28-8180-4132211FD07D}.Debug|x64.ActiveCfg = Debug|Any CPU
167 {328799BB-7B03-4B28-8180-4132211FD07D}.Debug|x64.Build.0 = Debug|Any CPU
168 {328799BB-7B03-4B28-8180-4132211FD07D}.Debug|x86.ActiveCfg = Debug|Any CPU
169 {328799BB-7B03-4B28-8180-4132211FD07D}.Debug|x86.Build.0 = Debug|Any CPU
170 {328799BB-7B03-4B28-8180-4132211FD07D}.Release|Any CPU.ActiveCfg = Release|Any CPU
171 {328799BB-7B03-4B28-8180-4132211FD07D}.Release|Any CPU.Build.0 = Release|Any CPU
172 {328799BB-7B03-4B28-8180-4132211FD07D}.Release|x64.ActiveCfg = Release|Any CPU
173 {328799BB-7B03-4B28-8180-4132211FD07D}.Release|x64.Build.0 = Release|Any CPU
174 {328799BB-7B03-4B28-8180-4132211FD07D}.Release|x86.ActiveCfg = Release|Any CPU
175 {328799BB-7B03-4B28-8180-4132211FD07D}.Release|x86.Build.0 = Release|Any CPU
176 {16F5202F-9276-4166-975C-C9654BAF8012}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
177 {16F5202F-9276-4166-975C-C9654BAF8012}.Debug|Any CPU.Build.0 = Debug|Any CPU
178 {16F5202F-9276-4166-975C-C9654BAF8012}.Debug|x64.ActiveCfg = Debug|Any CPU
179 {16F5202F-9276-4166-975C-C9654BAF8012}.Debug|x64.Build.0 = Debug|Any CPU
180 {16F5202F-9276-4166-975C-C9654BAF8012}.Debug|x86.ActiveCfg = Debug|Any CPU
181 {16F5202F-9276-4166-975C-C9654BAF8012}.Debug|x86.Build.0 = Debug|Any CPU
182 {16F5202F-9276-4166-975C-C9654BAF8012}.Release|Any CPU.ActiveCfg = Release|Any CPU
183 {16F5202F-9276-4166-975C-C9654BAF8012}.Release|Any CPU.Build.0 = Release|Any CPU
184 {16F5202F-9276-4166-975C-C9654BAF8012}.Release|x64.ActiveCfg = Release|Any CPU
185 {16F5202F-9276-4166-975C-C9654BAF8012}.Release|x64.Build.0 = Release|Any CPU
186 {16F5202F-9276-4166-975C-C9654BAF8012}.Release|x86.ActiveCfg = Release|Any CPU
187 {16F5202F-9276-4166-975C-C9654BAF8012}.Release|x86.Build.0 = Release|Any CPU
188 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
189 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
190 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Debug|x64.ActiveCfg = Debug|Any CPU
191 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Debug|x64.Build.0 = Debug|Any CPU
192 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Debug|x86.ActiveCfg = Debug|Any CPU
193 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Debug|x86.Build.0 = Debug|Any CPU
194 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
195 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Release|Any CPU.Build.0 = Release|Any CPU
196 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Release|x64.ActiveCfg = Release|Any CPU
197 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Release|x64.Build.0 = Release|Any CPU
198 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Release|x86.ActiveCfg = Release|Any CPU
199 {137D376B-989F-4FEA-9A67-01D8D38CA0DE}.Release|x86.Build.0 = Release|Any CPU
200 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
201 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Debug|Any CPU.Build.0 = Debug|Any CPU
202 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Debug|x64.ActiveCfg = Debug|Any CPU
203 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Debug|x64.Build.0 = Debug|Any CPU
204 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Debug|x86.ActiveCfg = Debug|Any CPU
205 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Debug|x86.Build.0 = Debug|Any CPU
206 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Release|Any CPU.ActiveCfg = Release|Any CPU
207 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Release|Any CPU.Build.0 = Release|Any CPU
208 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Release|x64.ActiveCfg = Release|Any CPU
209 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Release|x64.Build.0 = Release|Any CPU
210 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Release|x86.ActiveCfg = Release|Any CPU
211 {4F55F9B8-D8B6-41EB-8796-221B4CD98324}.Release|x86.Build.0 = Release|Any CPU
212 {55D5BA28-D427-4F53-80C2-FE9EF23C1553}.Debug|Any CPU.ActiveCfg = Debug|Win32
213 {55D5BA28-D427-4F53-80C2-FE9EF23C1553}.Debug|x64.ActiveCfg = Debug|Win32
214 {55D5BA28-D427-4F53-80C2-FE9EF23C1553}.Debug|x86.ActiveCfg = Debug|Win32
215 {55D5BA28-D427-4F53-80C2-FE9EF23C1553}.Debug|x86.Build.0 = Debug|Win32
216 {55D5BA28-D427-4F53-80C2-FE9EF23C1553}.Release|Any CPU.ActiveCfg = Release|Win32
217 {55D5BA28-D427-4F53-80C2-FE9EF23C1553}.Release|x64.ActiveCfg = Release|x64
218 {55D5BA28-D427-4F53-80C2-FE9EF23C1553}.Release|x86.ActiveCfg = Release|Win32
219 {55D5BA28-D427-4F53-80C2-FE9EF23C1553}.Release|x86.Build.0 = Release|Win32
220 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
221 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Debug|Any CPU.Build.0 = Debug|Any CPU
222 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Debug|x64.ActiveCfg = Debug|Any CPU
223 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Debug|x64.Build.0 = Debug|Any CPU
224 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Debug|x86.ActiveCfg = Debug|Any CPU
225 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Debug|x86.Build.0 = Debug|Any CPU
226 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Release|Any CPU.ActiveCfg = Release|Any CPU
227 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Release|Any CPU.Build.0 = Release|Any CPU
228 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Release|x64.ActiveCfg = Release|Any CPU
229 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Release|x64.Build.0 = Release|Any CPU
230 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Release|x86.ActiveCfg = Release|Any CPU
231 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B}.Release|x86.Build.0 = Release|Any CPU
232 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
233 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Debug|Any CPU.Build.0 = Debug|Any CPU
234 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Debug|x64.ActiveCfg = Debug|Any CPU
235 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Debug|x64.Build.0 = Debug|Any CPU
236 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Debug|x86.ActiveCfg = Debug|Any CPU
237 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Debug|x86.Build.0 = Debug|Any CPU
238 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Release|Any CPU.ActiveCfg = Release|Any CPU
239 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Release|Any CPU.Build.0 = Release|Any CPU
240 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Release|x64.ActiveCfg = Release|Any CPU
241 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Release|x64.Build.0 = Release|Any CPU
242 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Release|x86.ActiveCfg = Release|Any CPU
243 {DB9E5F02-8241-440A-9B60-980EB5B42B13}.Release|x86.Build.0 = Release|Any CPU
244 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
245 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
246 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Debug|x64.ActiveCfg = Debug|Any CPU
247 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Debug|x64.Build.0 = Debug|Any CPU
248 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Debug|x86.ActiveCfg = Debug|Any CPU
249 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Debug|x86.Build.0 = Debug|Any CPU
250 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
251 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Release|Any CPU.Build.0 = Release|Any CPU
252 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Release|x64.ActiveCfg = Release|Any CPU
253 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Release|x64.Build.0 = Release|Any CPU
254 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Release|x86.ActiveCfg = Release|Any CPU
255 {864B8C50-7895-4485-AC89-900D86FD8C0D}.Release|x86.Build.0 = Release|Any CPU
256 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
257 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Debug|Any CPU.Build.0 = Debug|Any CPU
258 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Debug|x64.ActiveCfg = Debug|Any CPU
259 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Debug|x64.Build.0 = Debug|Any CPU
260 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Debug|x86.ActiveCfg = Debug|Any CPU
261 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Debug|x86.Build.0 = Debug|Any CPU
262 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Release|Any CPU.ActiveCfg = Release|Any CPU
263 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Release|Any CPU.Build.0 = Release|Any CPU
264 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Release|x64.ActiveCfg = Release|Any CPU
265 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Release|x64.Build.0 = Release|Any CPU
266 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Release|x86.ActiveCfg = Release|Any CPU
267 {E7A00377-A0B5-400F-8337-C0814AAC7153}.Release|x86.Build.0 = Release|Any CPU
268 EndGlobalSection
269 GlobalSection(SolutionProperties) = preSolution
270 HideSolutionNode = FALSE
271 EndGlobalSection
272 GlobalSection(NestedProjects) = preSolution
273 {55D5BA28-D427-4F53-80C2-FE9EF23C1553} = {A988A768-200F-408F-AE3B-5D9B14AA48EE}
274 {3F246CE0-153D-4AC3-B6AC-5EAD8E2AD04B} = {A988A768-200F-408F-AE3B-5D9B14AA48EE}
275 {DB9E5F02-8241-440A-9B60-980EB5B42B13} = {4A47EC94-8234-4479-87BC-3D924DB7AA4E}
276 {864B8C50-7895-4485-AC89-900D86FD8C0D} = {4A47EC94-8234-4479-87BC-3D924DB7AA4E}
277 EndGlobalSection
278 GlobalSection(ExtensibilityGlobals) = postSolution
279 SolutionGuid = {BB57C98D-C0C2-4805-AED3-C19B47759DBD}
280 EndGlobalSection
281EndGlobal
diff --git a/src/dtf/WixToolsetTests.Dtf.Compression.Cab/CabTest.cs b/src/dtf/WixToolsetTests.Dtf.Compression.Cab/CabTest.cs
new file mode 100644
index 00000000..981ecc69
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.Compression.Cab/CabTest.cs
@@ -0,0 +1,1165 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Test
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Threading;
9 using System.Collections.Generic;
10 using System.Runtime.Serialization;
11 using System.Runtime.Serialization.Formatters.Binary;
12 using Microsoft.VisualStudio.TestTools.UnitTesting;
13 using WixToolset.Dtf.Compression;
14 using WixToolset.Dtf.Compression.Cab;
15
16 [TestClass]
17 public class CabTest
18 {
19 public CabTest()
20 {
21 }
22
23 [TestInitialize]
24 public void Initialize()
25 {
26 }
27
28 [TestCleanup]
29 public void Cleanup()
30 {
31 }
32
33 [TestMethod]
34 public void CabinetMultithread()
35 {
36 this.multithreadExceptions = new List<Exception>();
37
38 const int threadCount = 10;
39 IList<Thread> threads = new List<Thread>(threadCount);
40
41 for (int i = 0; i < threadCount; i++)
42 {
43 Thread thread = new Thread(new ThreadStart(this.CabinetMultithreadWorker));
44 thread.Name = "CabinetMultithreadWorker_" + i;
45 threads.Add(thread);
46 }
47
48 foreach (Thread thread in threads)
49 {
50 thread.Start();
51 }
52
53 foreach (Thread thread in threads)
54 {
55 thread.Join();
56 }
57
58 foreach (Exception ex in this.multithreadExceptions)
59 {
60 Console.WriteLine();
61 Console.WriteLine(ex);
62 }
63 Assert.AreEqual<int>(0, this.multithreadExceptions.Count);
64 }
65
66 private IList<Exception> multithreadExceptions;
67
68 private void CabinetMultithreadWorker()
69 {
70 try
71 {
72 string threadName = Thread.CurrentThread.Name;
73 int threadNumber = Int32.Parse(threadName.Substring(threadName.IndexOf('_') + 1));
74 this.RunCabinetPackUnpack(100, 10240 + threadNumber, 0, 0, CompressionLevel.Normal);
75 }
76 catch (Exception ex)
77 {
78 this.multithreadExceptions.Add(ex);
79 }
80 }
81
82 [TestMethod]
83 public void CabinetFileCounts()
84 {
85 this.RunCabinetPackUnpack(0, 10, 0, 0, CompressionLevel.Normal);
86 this.RunCabinetPackUnpack(1, 10, 0, 0, CompressionLevel.Normal);
87 this.RunCabinetPackUnpack(100, 10, 0, 0, CompressionLevel.Normal);
88 }
89
90 [TestMethod]
91 [Ignore] // Takes ~5 minutes and 66000 is over the 65535 limit anyway.
92 public void CabinetExtremeFileCounts()
93 {
94 this.RunCabinetPackUnpack(66000, 10);
95 }
96
97 [TestMethod]
98 public void CabinetFileSizes()
99 {
100 this.RunCabinetPackUnpack(1, 0, 0, 0, CompressionLevel.Normal);
101 this.RunCabinetPackUnpack(1, 1, 0, 0, CompressionLevel.Normal);
102 this.RunCabinetPackUnpack(1, 2, 0, 0, CompressionLevel.Normal);
103 this.RunCabinetPackUnpack(1, 3, 0, 0, CompressionLevel.Normal);
104 this.RunCabinetPackUnpack(1, 4, 0, 0, CompressionLevel.Normal);
105 this.RunCabinetPackUnpack(1, 5, 0, 0, CompressionLevel.Normal);
106 this.RunCabinetPackUnpack(1, 6, 0, 0, CompressionLevel.Normal);
107 // Skip file sizes 7-9: see "buggy" file sizes test below.
108 this.RunCabinetPackUnpack(1, 10, 0, 0, CompressionLevel.Normal);
109 this.RunCabinetPackUnpack(1, 11, 0, 0, CompressionLevel.Normal);
110 this.RunCabinetPackUnpack(1, 12, 0, 0, CompressionLevel.Normal);
111 this.RunCabinetPackUnpack(1, 100 * 1024, 0, 0, CompressionLevel.Normal);
112 this.RunCabinetPackUnpack(1, 10 * 1024 * 1024, 0, 0, CompressionLevel.Normal);
113 }
114
115 [TestMethod]
116 public void CabinetBuggyFileSizes()
117 {
118 // Windows' cabinet.dll has a known bug (#55001 in Windows OS Bugs)
119 // LZX compression causes an AV with file sizes of 7, 8, or 9 bytes.
120 try
121 {
122 this.RunCabinetPackUnpack(1, 7, 0, 0, CompressionLevel.Normal);
123 this.RunCabinetPackUnpack(1, 8, 0, 0, CompressionLevel.Normal);
124 this.RunCabinetPackUnpack(1, 9, 0, 0, CompressionLevel.Normal);
125 }
126 catch (AccessViolationException)
127 {
128 Assert.Fail("Known 7,8,9 file size bug detected in Windows' cabinet.dll.");
129 }
130 }
131
132 [Timeout(36000000), TestMethod]
133 [Ignore] // Takes too long to run regularly.
134 public void CabinetExtremeFileSizes()
135 {
136 this.RunCabinetPackUnpack(10, 512L * 1024 * 1024); // 5GB
137 //this.RunCabinetPackUnpack(1, 5L * 1024 * 1024 * 1024); // 5GB
138 }
139
140 [TestMethod]
141 public void CabinetFolders()
142 {
143 this.RunCabinetPackUnpack(0, 10, 1, 0, CompressionLevel.Normal);
144 this.RunCabinetPackUnpack(1, 10, 1, 0, CompressionLevel.Normal);
145 this.RunCabinetPackUnpack(100, 10, 1, 0, CompressionLevel.Normal);
146
147 IList<ArchiveFileInfo> fileInfo;
148 fileInfo = this.RunCabinetPackUnpack(7, 100 * 1024, 250 * 1024, 0, CompressionLevel.None);
149 Assert.AreEqual<int>(2, ((CabFileInfo) fileInfo[fileInfo.Count - 1]).CabinetFolderNumber,
150 "Testing whether cabinet has the correct # of folders.");
151
152 fileInfo = this.RunCabinetPackUnpack(10, 100 * 1024, 250 * 1024, 0, CompressionLevel.None);
153 Assert.AreEqual<int>(3, ((CabFileInfo) fileInfo[fileInfo.Count - 1]).CabinetFolderNumber,
154 "Testing whether cabinet has the correct # of folders.");
155
156 fileInfo = this.RunCabinetPackUnpack(2, 100 * 1024, 40 * 1024, 0, CompressionLevel.None);
157 Assert.AreEqual<int>(1, ((CabFileInfo) fileInfo[fileInfo.Count - 1]).CabinetFolderNumber,
158 "Testing whether cabinet has the correct # of folders.");
159 }
160
161 [TestMethod]
162 public void CabinetArchiveCounts()
163 {
164 IList<ArchiveFileInfo> fileInfo;
165 fileInfo = this.RunCabinetPackUnpack(10, 100 * 1024, 0, 400 * 1024, CompressionLevel.None);
166 Assert.AreEqual<int>(2, fileInfo[fileInfo.Count - 1].ArchiveNumber,
167 "Testing whether archive spans the correct # of cab files.");
168
169 fileInfo = this.RunCabinetPackUnpack(2, 90 * 1024, 0, 40 * 1024, CompressionLevel.None);
170 Assert.AreEqual<int>(2, fileInfo[fileInfo.Count - 1].ArchiveNumber,
171 "Testing whether archive spans the correct # of cab files.");
172 }
173
174 [TestMethod]
175 public void CabinetProgress()
176 {
177 CompressionTestUtil.ExpectedProgress = new List<int[]>(new int[][] {
178 // StatusType, CurFile,TotalFiles,CurFolder,CurCab,TotalCabs
179 new int[] { (int) ArchiveProgressType.StartFile, 0, 15, 0, 0, 1 },
180 new int[] { (int) ArchiveProgressType.FinishFile, 0, 15, 0, 0, 1 },
181 new int[] { (int) ArchiveProgressType.StartFile, 1, 15, 0, 0, 1 },
182 new int[] { (int) ArchiveProgressType.FinishFile, 1, 15, 0, 0, 1 },
183 new int[] { (int) ArchiveProgressType.StartFile, 2, 15, 1, 0, 1 },
184 new int[] { (int) ArchiveProgressType.FinishFile, 2, 15, 1, 0, 1 },
185 new int[] { (int) ArchiveProgressType.StartFile, 3, 15, 1, 0, 1 },
186 new int[] { (int) ArchiveProgressType.FinishFile, 3, 15, 1, 0, 1 },
187 new int[] { (int) ArchiveProgressType.StartFile, 4, 15, 2, 0, 1 },
188 new int[] { (int) ArchiveProgressType.FinishFile, 4, 15, 2, 0, 1 },
189 new int[] { (int) ArchiveProgressType.StartFile, 5, 15, 2, 0, 1 },
190 new int[] { (int) ArchiveProgressType.FinishFile, 5, 15, 2, 0, 1 },
191 new int[] { (int) ArchiveProgressType.StartFile, 6, 15, 3, 0, 1 },
192 new int[] { (int) ArchiveProgressType.FinishFile, 6, 15, 3, 0, 1 },
193 new int[] { (int) ArchiveProgressType.StartFile, 7, 15, 3, 0, 1 },
194 new int[] { (int) ArchiveProgressType.FinishFile, 7, 15, 3, 0, 1 },
195 new int[] { (int) ArchiveProgressType.StartArchive, 7, 15, 3, 0, 1 },
196 new int[] { (int) ArchiveProgressType.FinishArchive, 7, 15, 3, 0, 1 },
197 new int[] { (int) ArchiveProgressType.StartFile, 8, 15, 4, 1, 2 },
198 new int[] { (int) ArchiveProgressType.FinishFile, 8, 15, 4, 1, 2 },
199 new int[] { (int) ArchiveProgressType.StartFile, 9, 15, 4, 1, 2 },
200 new int[] { (int) ArchiveProgressType.FinishFile, 9, 15, 4, 1, 2 },
201 new int[] { (int) ArchiveProgressType.StartFile, 10, 15, 5, 1, 2 },
202 new int[] { (int) ArchiveProgressType.FinishFile, 10, 15, 5, 1, 2 },
203 new int[] { (int) ArchiveProgressType.StartFile, 11, 15, 5, 1, 2 },
204 new int[] { (int) ArchiveProgressType.FinishFile, 11, 15, 5, 1, 2 },
205 new int[] { (int) ArchiveProgressType.StartFile, 12, 15, 6, 1, 2 },
206 new int[] { (int) ArchiveProgressType.FinishFile, 12, 15, 6, 1, 2 },
207 new int[] { (int) ArchiveProgressType.StartFile, 13, 15, 6, 1, 2 },
208 new int[] { (int) ArchiveProgressType.FinishFile, 13, 15, 6, 1, 2 },
209 new int[] { (int) ArchiveProgressType.StartArchive, 13, 15, 6, 1, 2 },
210 new int[] { (int) ArchiveProgressType.FinishArchive, 13, 15, 6, 1, 2 },
211 new int[] { (int) ArchiveProgressType.StartFile, 14, 15, 7, 2, 3 },
212 new int[] { (int) ArchiveProgressType.FinishFile, 14, 15, 7, 2, 3 },
213 new int[] { (int) ArchiveProgressType.StartArchive, 14, 15, 7, 2, 3 },
214 new int[] { (int) ArchiveProgressType.FinishArchive, 14, 15, 7, 2, 3 },
215 // StatusType, CurFile,TotalFiles,CurFolder,CurCab,TotalCabs
216 new int[] { (int) ArchiveProgressType.StartArchive, 0, 15, 0, 0, 3 },
217 new int[] { (int) ArchiveProgressType.StartFile, 0, 15, 0, 0, 3 },
218 new int[] { (int) ArchiveProgressType.FinishFile, 0, 15, 0, 0, 3 },
219 new int[] { (int) ArchiveProgressType.StartFile, 1, 15, 0, 0, 3 },
220 new int[] { (int) ArchiveProgressType.FinishFile, 1, 15, 0, 0, 3 },
221 new int[] { (int) ArchiveProgressType.StartFile, 2, 15, 1, 0, 3 },
222 new int[] { (int) ArchiveProgressType.FinishFile, 2, 15, 1, 0, 3 },
223 new int[] { (int) ArchiveProgressType.StartFile, 3, 15, 1, 0, 3 },
224 new int[] { (int) ArchiveProgressType.FinishFile, 3, 15, 1, 0, 3 },
225 new int[] { (int) ArchiveProgressType.StartFile, 4, 15, 2, 0, 3 },
226 new int[] { (int) ArchiveProgressType.FinishFile, 4, 15, 2, 0, 3 },
227 new int[] { (int) ArchiveProgressType.StartFile, 5, 15, 2, 0, 3 },
228 new int[] { (int) ArchiveProgressType.FinishFile, 5, 15, 2, 0, 3 },
229 new int[] { (int) ArchiveProgressType.StartFile, 6, 15, 3, 0, 3 },
230 new int[] { (int) ArchiveProgressType.FinishArchive, 6, 15, 3, 0, 3 },
231 new int[] { (int) ArchiveProgressType.StartArchive, 6, 15, 3, 1, 3 },
232 new int[] { (int) ArchiveProgressType.FinishFile, 6, 15, 3, 1, 3 },
233 new int[] { (int) ArchiveProgressType.StartFile, 7, 15, 3, 1, 3 },
234 new int[] { (int) ArchiveProgressType.FinishFile, 7, 15, 3, 1, 3 },
235 new int[] { (int) ArchiveProgressType.StartFile, 8, 15, 4, 1, 3 },
236 new int[] { (int) ArchiveProgressType.FinishFile, 8, 15, 4, 1, 3 },
237 new int[] { (int) ArchiveProgressType.StartFile, 9, 15, 4, 1, 3 },
238 new int[] { (int) ArchiveProgressType.FinishFile, 9, 15, 4, 1, 3 },
239 new int[] { (int) ArchiveProgressType.StartFile, 10, 15, 5, 1, 3 },
240 new int[] { (int) ArchiveProgressType.FinishFile, 10, 15, 5, 1, 3 },
241 new int[] { (int) ArchiveProgressType.StartFile, 11, 15, 5, 1, 3 },
242 new int[] { (int) ArchiveProgressType.FinishFile, 11, 15, 5, 1, 3 },
243 new int[] { (int) ArchiveProgressType.StartFile, 12, 15, 6, 1, 3 },
244 new int[] { (int) ArchiveProgressType.FinishArchive, 12, 15, 6, 1, 3 },
245 new int[] { (int) ArchiveProgressType.StartArchive, 12, 15, 6, 2, 3 },
246 new int[] { (int) ArchiveProgressType.FinishFile, 12, 15, 6, 2, 3 },
247 new int[] { (int) ArchiveProgressType.StartFile, 13, 15, 6, 2, 3 },
248 new int[] { (int) ArchiveProgressType.FinishFile, 13, 15, 6, 2, 3 },
249 new int[] { (int) ArchiveProgressType.StartFile, 14, 15, 7, 2, 3 },
250 new int[] { (int) ArchiveProgressType.FinishFile, 14, 15, 7, 2, 3 },
251 new int[] { (int) ArchiveProgressType.FinishArchive, 14, 15, 7, 2, 3 },
252 });
253
254 try
255 {
256 this.RunCabinetPackUnpack(15, 20 * 1024, 1 * 1024, 130 * 1024, CompressionLevel.None);
257 }
258 finally
259 {
260 CompressionTestUtil.ExpectedProgress = null;
261 }
262 }
263
264 [TestMethod]
265 public void CabArchiveSizeParam()
266 {
267 Console.WriteLine("Testing various values for the maxArchiveSize parameter.");
268 this.RunCabinetPackUnpack(5, 1024, 0, Int64.MinValue);
269 this.RunCabinetPackUnpack(5, 1024, 0, -1);
270 this.RunCabinetPackUnpack(5, 10, 0, 2);
271 this.RunCabinetPackUnpack(5, 100, 0, 256);
272 this.RunCabinetPackUnpack(5, 24000, 0, 32768);
273 this.RunCabinetPackUnpack(5, 1024, 0, Int64.MaxValue);
274 }
275
276 [TestMethod]
277 public void CabFolderSizeParam()
278 {
279 Console.WriteLine("Testing various values for the maxFolderSize parameter.");
280 this.RunCabinetPackUnpack(5, 10, Int64.MinValue, 0);
281 this.RunCabinetPackUnpack(5, 10, -1, 0);
282 this.RunCabinetPackUnpack(5, 10, 2, 0);
283 this.RunCabinetPackUnpack(5, 10, 16, 0);
284 this.RunCabinetPackUnpack(5, 10, 100, 0);
285 this.RunCabinetPackUnpack(5, 10, Int64.MaxValue, 0);
286 }
287
288 [TestMethod]
289 public void CabCompLevelParam()
290 {
291 Console.WriteLine("Testing various values for the compressionLevel parameter.");
292 this.RunCabinetPackUnpack(5, 1024, 0, 0, CompressionLevel.None);
293 this.RunCabinetPackUnpack(5, 1024, 0, 0, CompressionLevel.Min);
294 this.RunCabinetPackUnpack(5, 1024, 0, 0, CompressionLevel.Normal);
295 this.RunCabinetPackUnpack(5, 1024, 0, 0, CompressionLevel.Max);
296 this.RunCabinetPackUnpack(5, 1024, 0, 0, (CompressionLevel) ((int) CompressionLevel.None - 1));
297 this.RunCabinetPackUnpack(5, 1024, 0, 0, (CompressionLevel) ((int) CompressionLevel.Max + 1));
298 this.RunCabinetPackUnpack(5, 1024, 0, 0, (CompressionLevel) Int32.MinValue);
299 this.RunCabinetPackUnpack(5, 1024, 0, 0, (CompressionLevel) Int32.MaxValue);
300 }
301
302 [TestMethod]
303 public void CabEngineNullParams()
304 {
305 string[] testFiles = new string[] { "test.txt" };
306 ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext("test.cab", null, null);
307
308 using (CabEngine cabEngine = new CabEngine())
309 {
310 cabEngine.CompressionLevel = CompressionLevel.None;
311
312 CompressionTestUtil.TestCompressionEngineNullParams(
313 cabEngine, streamContext, testFiles);
314 }
315 }
316
317 [TestMethod]
318 public void CabBadPackStreamContexts()
319 {
320 string[] testFiles = new string[] { "test.txt" };
321 CompressionTestUtil.GenerateRandomFile(testFiles[0], 0, 20000);
322
323 using (CabEngine cabEngine = new CabEngine())
324 {
325 cabEngine.CompressionLevel = CompressionLevel.None;
326
327 CompressionTestUtil.TestBadPackStreamContexts(cabEngine, "test.cab", testFiles);
328 }
329 }
330
331 [TestMethod]
332 public void CabEngineNoTempFileTest()
333 {
334 int txtSize = 10240;
335 CompressionTestUtil.GenerateRandomFile("testnotemp.txt", 0, txtSize);
336
337 ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext("testnotemp.cab", null, null);
338
339 using (CabEngine cabEngine = new CabEngine())
340 {
341 cabEngine.UseTempFiles = false;
342 cabEngine.Pack(streamContext, new string[] { "testnotemp.txt" });
343 }
344
345 new CabInfo("testnotemp.cab").UnpackFile("testnotemp.txt", "testnotemp2.txt");
346 Assert.AreEqual(txtSize, new FileInfo("testnotemp2.txt").Length);
347 }
348
349 [TestMethod]
350 public void CabExtractorIsCabinet()
351 {
352 int txtSize = 10240;
353 CompressionTestUtil.GenerateRandomFile("test.txt", 0, txtSize);
354 new CabInfo("test.cab").PackFiles(null, new string[] { "test.txt" }, null);
355 using (CabEngine cabEngine = new CabEngine())
356 {
357 bool isCab;
358 using (Stream fileStream = File.OpenRead("test.txt"))
359 {
360 isCab = cabEngine.IsArchive(fileStream);
361 }
362 Assert.IsFalse(isCab);
363 using (Stream cabStream = File.OpenRead("test.cab"))
364 {
365 isCab = cabEngine.IsArchive(cabStream);
366 }
367 Assert.IsTrue(isCab);
368 using (Stream cabStream = File.OpenRead("test.cab"))
369 {
370 using (Stream fileStream = new FileStream("test.txt", FileMode.Open, FileAccess.ReadWrite))
371 {
372 fileStream.Seek(0, SeekOrigin.End);
373 byte[] buf = new byte[1024];
374 int count;
375 while ((count = cabStream.Read(buf, 0, buf.Length)) > 0)
376 {
377 fileStream.Write(buf, 0, count);
378 }
379 fileStream.Seek(0, SeekOrigin.Begin);
380 isCab = cabEngine.IsArchive(fileStream);
381 }
382 }
383 Assert.IsFalse(isCab);
384 using (Stream fileStream = new FileStream("test.txt", FileMode.Open, FileAccess.ReadWrite))
385 {
386 fileStream.Write(new byte[] { (byte) 'M', (byte) 'S', (byte) 'C', (byte) 'F' }, 0, 4);
387 fileStream.Seek(0, SeekOrigin.Begin);
388 isCab = cabEngine.IsArchive(fileStream);
389 }
390 Assert.IsFalse(isCab);
391 }
392 }
393
394 [TestMethod]
395 public void CabExtractorFindOffset()
396 {
397 int txtSize = 10240;
398 CompressionTestUtil.GenerateRandomFile("test.txt", 0, txtSize);
399 new CabInfo("test.cab").PackFiles(null, new string[] { "test.txt" }, null);
400 using (CabEngine cabEngine = new CabEngine())
401 {
402 long offset;
403 using (Stream fileStream = File.OpenRead("test.txt"))
404 {
405 offset = cabEngine.FindArchiveOffset(fileStream);
406 }
407 Assert.AreEqual<long>(-1, offset);
408 using (Stream cabStream = File.OpenRead("test.cab"))
409 {
410 using (Stream fileStream = new FileStream("test.txt", FileMode.Open, FileAccess.ReadWrite))
411 {
412 fileStream.Seek(0, SeekOrigin.End);
413 byte[] buf = new byte[1024];
414 int count;
415 while ((count = cabStream.Read(buf, 0, buf.Length)) > 0)
416 {
417 fileStream.Write(buf, 0, count);
418 }
419 fileStream.Seek(0, SeekOrigin.Begin);
420 offset = cabEngine.FindArchiveOffset(fileStream);
421 }
422 }
423 Assert.AreEqual<long>(txtSize, offset);
424 }
425 }
426
427 [TestMethod]
428 public void CabExtractorGetFiles()
429 {
430 IList<ArchiveFileInfo> fileInfo;
431 CabInfo cabInfo = new CabInfo("testgetfiles.cab");
432 int txtSize = 10240;
433 CompressionTestUtil.GenerateRandomFile("testgetfiles0.txt", 0, txtSize);
434 CompressionTestUtil.GenerateRandomFile("testgetfiles1.txt", 1, txtSize);
435 cabInfo.PackFiles(null, new string[] { "testgetfiles0.txt", "testgetfiles1.txt" }, null);
436 using (CabEngine cabEngine = new CabEngine())
437 {
438 IList<string> files;
439 using (Stream cabStream = File.OpenRead("testgetfiles.cab"))
440 {
441 files = cabEngine.GetFiles(cabStream);
442 }
443 Assert.IsNotNull(files);
444 Assert.AreEqual<int>(2, files.Count);
445 Assert.AreEqual<string>("testgetfiles0.txt", files[0]);
446 Assert.AreEqual<string>("testgetfiles1.txt", files[1]);
447
448 using (Stream cabStream = File.OpenRead("testgetfiles.cab"))
449 {
450 files = cabEngine.GetFiles(new ArchiveFileStreamContext("testgetfiles.cab"), null);
451 }
452 Assert.IsNotNull(files);
453 Assert.AreEqual<int>(2, files.Count);
454 Assert.AreEqual<string>("testgetfiles0.txt", files[0]);
455 Assert.AreEqual<string>("testgetfiles1.txt", files[1]);
456
457 using (Stream cabStream = File.OpenRead("testgetfiles.cab"))
458 {
459 fileInfo = cabEngine.GetFileInfo(cabStream);
460 }
461 Assert.IsNotNull(fileInfo);
462 Assert.AreEqual<int>(2, fileInfo.Count);
463 Assert.AreEqual<string>("testgetfiles0.txt", fileInfo[0].Name);
464 Assert.AreEqual<string>("testgetfiles1.txt", fileInfo[1].Name);
465 using (Stream cabStream = File.OpenRead("testgetfiles.cab"))
466 {
467 fileInfo = cabEngine.GetFileInfo(new ArchiveFileStreamContext("testgetfiles.cab"), null);
468 }
469 Assert.IsNotNull(fileInfo);
470 Assert.AreEqual<int>(2, fileInfo.Count);
471 Assert.AreEqual<string>("testgetfiles0.txt", fileInfo[0].Name);
472 Assert.AreEqual<string>("testgetfiles1.txt", fileInfo[1].Name);
473 }
474
475 fileInfo = this.RunCabinetPackUnpack(15, 20 * 1024, 1 * 1024, 130 * 1024);
476 Assert.IsNotNull(fileInfo);
477 Assert.AreEqual<int>(15, fileInfo.Count);
478 for (int i = 0; i < fileInfo.Count; i++)
479 {
480 Assert.IsNull(fileInfo[i].Archive);
481 Assert.AreEqual<string>(TEST_FILENAME_PREFIX + i + ".txt", fileInfo[i].Name);
482 Assert.IsTrue(DateTime.Now - fileInfo[i].LastWriteTime < new TimeSpan(0, 1, 0));
483 }
484 }
485
486 [TestMethod]
487 public void CabExtractorExtract()
488 {
489 int txtSize = 40960;
490 CabInfo cabInfo = new CabInfo("test.cab");
491 CompressionTestUtil.GenerateRandomFile("test0.txt", 0, txtSize);
492 CompressionTestUtil.GenerateRandomFile("test1.txt", 1, txtSize);
493 cabInfo.PackFiles(null, new string[] { "test0.txt", "test1.txt" }, null);
494 using (CabEngine cabEngine = new CabEngine())
495 {
496 using (Stream cabStream = File.OpenRead("test.cab"))
497 {
498 using (Stream exStream = cabEngine.Unpack(cabStream, "test0.txt"))
499 {
500 string str = new StreamReader(exStream).ReadToEnd();
501 string expected = new StreamReader("test0.txt").ReadToEnd();
502 Assert.AreEqual<string>(expected, str);
503 }
504 cabStream.Seek(0, SeekOrigin.Begin);
505 using (Stream exStream = cabEngine.Unpack(cabStream, "test1.txt"))
506 {
507 string str = new StreamReader(exStream).ReadToEnd();
508 string expected = new StreamReader("test1.txt").ReadToEnd();
509 Assert.AreEqual<string>(expected, str);
510 }
511 }
512 using (Stream txtStream = File.OpenRead("test0.txt"))
513 {
514 Exception caughtEx = null;
515 try
516 {
517 cabEngine.Unpack(txtStream, "test0.txt");
518 }
519 catch (Exception ex) { caughtEx = ex; }
520 Assert.IsInstanceOfType(caughtEx, typeof(CabException));
521 Assert.AreEqual<int>(2, ((CabException) caughtEx).Error);
522 Assert.AreEqual<int>(0, ((CabException) caughtEx).ErrorCode);
523 Assert.AreEqual<string>("Cabinet file does not have the correct format.", caughtEx.Message);
524 }
525 }
526 }
527
528 [TestMethod]
529 public void CabBadUnpackStreamContexts()
530 {
531 int txtSize = 40960;
532 CabInfo cabInfo = new CabInfo("test2.cab");
533 CompressionTestUtil.GenerateRandomFile("cabtest-0.txt", 0, txtSize);
534 CompressionTestUtil.GenerateRandomFile("cabtest-1.txt", 1, txtSize);
535 cabInfo.PackFiles(null, new string[] { "cabtest-0.txt", "cabtest-1.txt" }, null);
536
537 using (CabEngine cabEngine = new CabEngine())
538 {
539 CompressionTestUtil.TestBadUnpackStreamContexts(cabEngine, "test2.cab");
540 }
541 }
542
543 [TestMethod]
544 public void CabinetExtractUpdate()
545 {
546 int fileCount = 5, fileSize = 2048;
547 string dirA = String.Format("{0}-{1}-A", fileCount, fileSize);
548 if (Directory.Exists(dirA)) Directory.Delete(dirA, true);
549 Directory.CreateDirectory(dirA);
550 string dirB = String.Format("{0}-{1}-B", fileCount, fileSize);
551 if (Directory.Exists(dirB)) Directory.Delete(dirB, true);
552 Directory.CreateDirectory(dirB);
553
554 string[] files = new string[fileCount];
555 for (int iFile = 0; iFile < fileCount; iFile++)
556 {
557 files[iFile] = "€" + iFile + ".txt";
558 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[iFile]), iFile, fileSize);
559 }
560
561 CabInfo cabInfo = new CabInfo("testupdate.cab");
562 cabInfo.Pack(dirA);
563 cabInfo.Unpack(dirB);
564
565 DateTime originalTime = File.GetLastWriteTime(Path.Combine(dirA, "€1.txt"));
566 DateTime pastTime = originalTime - new TimeSpan(0, 5, 0);
567 DateTime futureTime = originalTime + new TimeSpan(0, 5, 0);
568
569 using (CabEngine cabEngine = new CabEngine())
570 {
571 string cabName = "testupdate.cab";
572 ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext(cabName, dirB, null);
573 streamContext.ExtractOnlyNewerFiles = true;
574
575 Assert.AreEqual<bool>(true, streamContext.ExtractOnlyNewerFiles);
576 Assert.IsNotNull(streamContext.ArchiveFiles);
577 Assert.AreEqual<int>(1, streamContext.ArchiveFiles.Count);
578 Assert.AreEqual<string>(cabName, streamContext.ArchiveFiles[0]);
579 Assert.AreEqual<string>(dirB, streamContext.Directory);
580
581 File.SetLastWriteTime(Path.Combine(dirB, "€1.txt"), futureTime);
582 cabEngine.Unpack(streamContext, null);
583 Assert.IsTrue(File.GetLastWriteTime(Path.Combine(dirB, "€1.txt")) - originalTime > new TimeSpan(0, 4, 55));
584
585 File.SetLastWriteTime(Path.Combine(dirB, "€1.txt"), pastTime);
586 File.SetLastWriteTime(Path.Combine(dirB, "€2.txt"), pastTime);
587 File.SetAttributes(Path.Combine(dirB, "€2.txt"), FileAttributes.ReadOnly);
588 File.SetAttributes(Path.Combine(dirB, "€2.txt"), FileAttributes.Hidden);
589 File.SetAttributes(Path.Combine(dirB, "€2.txt"), FileAttributes.System);
590
591 cabEngine.Unpack(streamContext, null);
592 Assert.IsTrue((File.GetLastWriteTime(Path.Combine(dirB, "€1.txt")) - originalTime).Duration() < new TimeSpan(0, 0, 5));
593
594 // Just test the rest of the streamContext properties here.
595 IDictionary<string, string> testMap = new Dictionary<string, string>();
596 streamContext = new ArchiveFileStreamContext(cabName, dirB, testMap);
597 Assert.AreSame(testMap, streamContext.Files);
598
599 Assert.IsFalse(streamContext.EnableOffsetOpen);
600 streamContext.EnableOffsetOpen = true;
601 Assert.IsTrue(streamContext.EnableOffsetOpen);
602 streamContext = new ArchiveFileStreamContext(cabName, ".", testMap);
603 Assert.AreEqual<string>(".", streamContext.Directory);
604 string[] testArchiveFiles = new string[] { cabName };
605 streamContext = new ArchiveFileStreamContext(testArchiveFiles, ".", testMap);
606 Assert.AreSame(testArchiveFiles, streamContext.ArchiveFiles);
607 }
608 }
609
610 [TestMethod]
611 public void CabinetOffset()
612 {
613 int txtSize = 10240;
614 CompressionTestUtil.GenerateRandomFile("test.txt", 0, txtSize);
615 CompressionTestUtil.GenerateRandomFile("base.txt", 1, 2 * txtSize + 4);
616
617 ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext("base.txt", null, null);
618 streamContext.EnableOffsetOpen = true;
619
620 using (CabEngine cabEngine = new CabEngine())
621 {
622 cabEngine.Pack(streamContext, new string[] { "test.txt" });
623 }
624
625 Assert.IsTrue(new FileInfo("base.txt").Length > 2 * txtSize + 4);
626
627 string saveText;
628 using (Stream txtStream = File.OpenRead("test.txt"))
629 {
630 saveText = new StreamReader(txtStream).ReadToEnd();
631 }
632 File.Delete("test.txt");
633
634 using (CabEngine cex = new CabEngine())
635 {
636 cex.Unpack(streamContext, null);
637 }
638 string testText;
639 using (Stream txtStream = File.OpenRead("test.txt"))
640 {
641 testText = new StreamReader(txtStream).ReadToEnd();
642 }
643 Assert.AreEqual<string>(saveText, testText);
644 }
645
646 [TestMethod]
647 public void CabinetUtfPaths()
648 {
649 string[] files = new string[]
650 {
651 "어그리먼트送信ポート1ßà_Agreement.txt",
652 "콘토소ßà_MyProfile.txt",
653 "파트너1ßà_PartnerProfile.txt",
654 };
655
656 string dirA = "utf8-A";
657 if (Directory.Exists(dirA)) Directory.Delete(dirA, true);
658 Directory.CreateDirectory(dirA);
659 string dirB = "utf8-B";
660 if (Directory.Exists(dirB)) Directory.Delete(dirB, true);
661 Directory.CreateDirectory(dirB);
662
663 int txtSize = 1024;
664 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[0]), 0, txtSize);
665 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[1]), 1, txtSize);
666 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[2]), 2, txtSize);
667
668 ArchiveFileStreamContext streamContextA = new ArchiveFileStreamContext("utf8.cab", dirA, null);
669 using (CabEngine cabEngine = new CabEngine())
670 {
671 cabEngine.Pack(streamContextA, files);
672 }
673
674 ArchiveFileStreamContext streamContextB = new ArchiveFileStreamContext("utf8.cab", dirB, null);
675 using (CabEngine cex = new CabEngine())
676 {
677 cex.Unpack(streamContextB, null);
678 }
679
680 bool directoryMatch = CompressionTestUtil.CompareDirectories(dirA, dirB);
681 Assert.IsTrue(directoryMatch,
682 "Testing whether cabinet output directory matches input directory.");
683 }
684
685 [TestMethod]
686 //[Ignore] // Requires clean environment.
687 public void CabInfoProperties()
688 {
689 Exception caughtEx;
690 CabInfo cabInfo = new CabInfo("test.cab");
691 int txtSize = 10240;
692 CompressionTestUtil.GenerateRandomFile("test00.txt", 0, txtSize);
693 CompressionTestUtil.GenerateRandomFile("test01.txt", 1, txtSize);
694 cabInfo.PackFiles(null, new string[] { "test00.txt", "test01.txt" }, null);
695
696 Assert.AreEqual<string>(new FileInfo("test.cab").Directory.FullName, cabInfo.Directory.FullName, "CabInfo.FullName");
697 Assert.AreEqual<string>(new FileInfo("test.cab").DirectoryName, cabInfo.DirectoryName, "CabInfo.DirectoryName");
698 Assert.AreEqual<long>(new FileInfo("test.cab").Length, cabInfo.Length, "CabInfo.Length");
699 Assert.AreEqual<string>("test.cab", cabInfo.Name, "CabInfo.Name");
700 Assert.AreEqual<string>(new FileInfo("test.cab").FullName, cabInfo.ToString(), "CabInfo.ToString()");
701 cabInfo.CopyTo("test3.cab");
702 caughtEx = null;
703 try
704 {
705 cabInfo.CopyTo("test3.cab");
706 }
707 catch (Exception ex) { caughtEx = ex; }
708 Assert.IsInstanceOfType(caughtEx, typeof(IOException), "CabInfo.CopyTo() caught exception: " + caughtEx);
709 cabInfo.CopyTo("test3.cab", true);
710 cabInfo.MoveTo("test4.cab");
711 Assert.AreEqual<string>("test4.cab", cabInfo.Name);
712 Assert.IsTrue(cabInfo.Exists, "CabInfo.Exists()");
713 Assert.IsTrue(cabInfo.IsValid(), "CabInfo.IsValid");
714 cabInfo.Delete();
715 Assert.IsFalse(cabInfo.Exists, "!CabInfo.Exists()");
716 }
717
718 [TestMethod]
719 //[Ignore] // Requires clean environment.
720 public void CabInfoNullParams()
721 {
722 int fileCount = 10, fileSize = 1024;
723 string dirA = String.Format("{0}-{1}-A", fileCount, fileSize);
724 if (Directory.Exists(dirA)) Directory.Delete(dirA, true);
725 Directory.CreateDirectory(dirA);
726 string dirB = String.Format("{0}-{1}-B", fileCount, fileSize);
727 if (Directory.Exists(dirB)) Directory.Delete(dirB, true);
728 Directory.CreateDirectory(dirB);
729
730 string[] files = new string[fileCount];
731 for (int iFile = 0; iFile < fileCount; iFile++)
732 {
733 files[iFile] = "cabinfo-" + iFile + ".txt";
734 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[iFile]), iFile, fileSize);
735 }
736
737 CabInfo cabInfo = new CabInfo("testnull.cab");
738
739 CompressionTestUtil.TestArchiveInfoNullParams(cabInfo, dirA, dirB, files);
740 }
741
742 [TestMethod]
743 public void CabInfoGetFiles()
744 {
745 IList<CabFileInfo> fileInfo;
746 CabInfo cabInfo = new CabInfo("test.cab");
747 int txtSize = 10240;
748 CompressionTestUtil.GenerateRandomFile("testinfo0.txt", 0, txtSize);
749 CompressionTestUtil.GenerateRandomFile("testinfo1.txt", 1, txtSize);
750 cabInfo.PackFiles(null, new string[] { "testinfo0.txt", "testinfo1.txt" }, null);
751
752 fileInfo = cabInfo.GetFiles();
753 Assert.IsNotNull(fileInfo);
754 Assert.AreEqual<int>(2, fileInfo.Count);
755 Assert.AreEqual<string>("testinfo0.txt", fileInfo[0].Name);
756 Assert.AreEqual<string>("testinfo1.txt", fileInfo[1].Name);
757
758 fileInfo = cabInfo.GetFiles("*.txt");
759 Assert.IsNotNull(fileInfo);
760 Assert.AreEqual<int>(2, fileInfo.Count);
761 Assert.AreEqual<string>("testinfo0.txt", fileInfo[0].Name);
762 Assert.AreEqual<string>("testinfo1.txt", fileInfo[1].Name);
763
764 fileInfo = cabInfo.GetFiles("testinfo1.txt");
765 Assert.IsNotNull(fileInfo);
766 Assert.AreEqual<int>(1, fileInfo.Count);
767 Assert.AreEqual<string>("testinfo1.txt", fileInfo[0].Name);
768 }
769
770 [TestMethod]
771 public void CabInfoCompressExtract()
772 {
773 int fileCount = 10, fileSize = 1024;
774 string dirA = String.Format("{0}-{1}-A", fileCount, fileSize);
775 if (Directory.Exists(dirA)) Directory.Delete(dirA, true);
776 Directory.CreateDirectory(dirA);
777 Directory.CreateDirectory(Path.Combine(dirA, "sub"));
778 string dirB = String.Format("{0}-{1}-B", fileCount, fileSize);
779 if (Directory.Exists(dirB)) Directory.Delete(dirB, true);
780 Directory.CreateDirectory(dirB);
781
782 string[] files = new string[fileCount];
783 for (int iFile = 0; iFile < fileCount; iFile++)
784 {
785 files[iFile] = "€" + iFile + ".txt";
786 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[iFile]), iFile, fileSize);
787 }
788 CompressionTestUtil.GenerateRandomFile(Path.Combine(Path.Combine(dirA, "sub"), "€-.txt"), fileCount + 1, fileSize);
789
790 CabInfo cabInfo = new CabInfo("test.cab");
791 cabInfo.Pack(dirA);
792 cabInfo.Unpack(dirB);
793 bool directoryMatch = CompressionTestUtil.CompareDirectories(dirA, dirB);
794 Assert.IsFalse(directoryMatch,
795 "Testing whether cabinet output directory matches input directory.");
796 Directory.Delete(dirB, true);
797 Directory.CreateDirectory(dirB);
798 cabInfo.Pack(dirA, true, CompressionLevel.Normal, null);
799 cabInfo.Unpack(dirB);
800 directoryMatch = CompressionTestUtil.CompareDirectories(dirA, dirB);
801 Assert.IsTrue(directoryMatch,
802 "Testing whether cabinet output directory matches input directory.");
803 Directory.Delete(dirB, true);
804 Directory.Delete(Path.Combine(dirA, "sub"), true);
805 Directory.CreateDirectory(dirB);
806 cabInfo.Delete();
807
808 cabInfo.PackFiles(dirA, files, null);
809 cabInfo.UnpackFiles(files, dirB, null);
810 directoryMatch = CompressionTestUtil.CompareDirectories(dirA, dirB);
811 Assert.IsTrue(directoryMatch,
812 "Testing whether cabinet output directory matches input directory.");
813 Directory.Delete(dirB, true);
814 Directory.CreateDirectory(dirB);
815 cabInfo.Delete();
816
817 IDictionary<string, string> testMap = new Dictionary<string, string>(files.Length);
818 for (int iFile = 0; iFile < fileCount; iFile++)
819 {
820 testMap[files[iFile] + ".key"] = files[iFile];
821 }
822 cabInfo.PackFileSet(dirA, testMap);
823 cabInfo.UnpackFileSet(testMap, dirB);
824 directoryMatch = CompressionTestUtil.CompareDirectories(dirA, dirB);
825 Assert.IsTrue(directoryMatch,
826 "Testing whether cabinet output directory matches input directory.");
827 Directory.Delete(dirB, true);
828 Directory.CreateDirectory(dirB);
829
830 testMap.Remove(files[1] + ".key");
831 cabInfo.UnpackFileSet(testMap, dirB);
832 directoryMatch = CompressionTestUtil.CompareDirectories(dirA, dirB);
833 Assert.IsFalse(directoryMatch,
834 "Testing whether cabinet output directory matches input directory.");
835 Directory.Delete(dirB, true);
836 Directory.CreateDirectory(dirB);
837 cabInfo.Delete();
838
839 cabInfo.PackFiles(dirA, files, null);
840 cabInfo.UnpackFile("€2.txt", Path.Combine(dirB, "test.txt"));
841 Assert.IsTrue(File.Exists(Path.Combine(dirB, "test.txt")));
842 Assert.AreEqual<int>(1, Directory.GetFiles(dirB).Length);
843 }
844
845 [TestMethod]
846 //[Ignore] // Requires clean environment.
847 public void CabFileInfoProperties()
848 {
849 CabInfo cabInfo = new CabInfo("test.cab");
850 int txtSize = 10240;
851 CompressionTestUtil.GenerateRandomFile("test00.txt", 0, txtSize);
852 CompressionTestUtil.GenerateRandomFile("test01.txt", 1, txtSize);
853 File.SetAttributes("test01.txt", FileAttributes.ReadOnly | FileAttributes.Archive);
854 DateTime testTime = File.GetLastWriteTime("test01.txt");
855 cabInfo.PackFiles(null, new string[] { "test00.txt", "test01.txt" }, null);
856 File.SetAttributes("test01.txt", FileAttributes.Archive);
857
858 CabFileInfo cfi = new CabFileInfo(cabInfo, "test01.txt");
859 Assert.AreEqual(cabInfo.FullName, cfi.CabinetName);
860 Assert.AreEqual<int>(0, ((CabFileInfo) cfi).CabinetFolderNumber);
861 Assert.AreEqual<string>(Path.Combine(cabInfo.FullName, "test01.txt"), cfi.FullName);
862 cfi = new CabFileInfo(cabInfo, "test01.txt");
863 Assert.IsTrue(cfi.Exists);
864 cfi = new CabFileInfo(cabInfo, "test01.txt");
865 Assert.AreEqual<long>(txtSize, cfi.Length);
866 cfi = new CabFileInfo(cabInfo, "test00.txt");
867 Assert.AreEqual<FileAttributes>(FileAttributes.Archive, cfi.Attributes);
868 cfi = new CabFileInfo(cabInfo, "test01.txt");
869 Assert.AreEqual<FileAttributes>(FileAttributes.ReadOnly | FileAttributes.Archive, cfi.Attributes);
870 cfi = new CabFileInfo(cabInfo, "test01.txt");
871 Assert.IsTrue((testTime - cfi.LastWriteTime).Duration() < new TimeSpan(0, 0, 5));
872 Assert.AreEqual<string>(Path.Combine(cabInfo.FullName, "test01.txt"), cfi.ToString());
873 cfi.CopyTo("testcopy.txt");
874 Assert.IsTrue(File.Exists("testCopy.txt"));
875 Assert.AreEqual<long>(cfi.Length, new FileInfo("testCopy.txt").Length);
876
877 Exception caughtEx = null;
878 try
879 {
880 cfi.CopyTo("testcopy.txt", false);
881 }
882 catch (Exception ex) { caughtEx = ex; }
883 Assert.IsInstanceOfType(caughtEx, typeof(IOException));
884 }
885
886 [TestMethod]
887 public void CabFileInfoOpenText()
888 {
889 CabInfo cabInfo = new CabInfo("test.cab");
890 int txtSize = 10240;
891 CompressionTestUtil.GenerateRandomFile("test00.txt", 0, txtSize);
892 CompressionTestUtil.GenerateRandomFile("test01.txt", 1, txtSize);
893
894 string expectedText = File.ReadAllText("test01.txt");
895
896 cabInfo.PackFiles(null, new string[] { "test00.txt", "test01.txt" }, null);
897
898 CabFileInfo cfi = new CabFileInfo(cabInfo, "test01.txt");
899 using (StreamReader cabFileReader = cfi.OpenText())
900 {
901 string text = cabFileReader.ReadToEnd();
902 Assert.AreEqual(expectedText, text);
903
904 // Check the assumption that the cab can't be deleted while a stream is open.
905 Exception caughtEx = null;
906 try
907 {
908 File.Delete(cabInfo.FullName);
909 }
910 catch (Exception ex)
911 {
912 caughtEx = ex;
913 }
914
915 Assert.IsInstanceOfType(caughtEx, typeof(IOException));
916 }
917
918 // Ensure all streams are closed after disposing of the StreamReader returned by OpenText.
919 File.Delete(cabInfo.FullName);
920 }
921
922 [TestMethod]
923 public void CabFileInfoNullParams()
924 {
925 Exception caughtEx;
926 CabInfo cabInfo = new CabInfo("test.cab");
927 int txtSize = 10240;
928 CompressionTestUtil.GenerateRandomFile("test00.txt", 0, txtSize);
929 CompressionTestUtil.GenerateRandomFile("test01.txt", 1, txtSize);
930 cabInfo.PackFiles(null, new string[] { "test00.txt", "test01.txt" }, null);
931 CabFileInfo cfi = new CabFileInfo(cabInfo, "test01.txt");
932
933 caughtEx = null;
934 try
935 {
936 new CabFileInfo(null, "test00.txt");
937 }
938 catch (Exception ex) { caughtEx = ex; }
939 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException));
940 caughtEx = null;
941 try
942 {
943 new CabFileInfo(cabInfo, null);
944 }
945 catch (Exception ex) { caughtEx = ex; }
946 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException));
947 caughtEx = null;
948 try
949 {
950 cfi.CopyTo(null);
951 }
952 catch (Exception ex) { caughtEx = ex; }
953 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException));
954 }
955
956 [TestMethod]
957 public void CabInfoSerialization()
958 {
959 CabInfo cabInfo = new CabInfo("testser.cab");
960 int txtSize = 10240;
961 CompressionTestUtil.GenerateRandomFile("testser00.txt", 0, txtSize);
962 CompressionTestUtil.GenerateRandomFile("testser01.txt", 1, txtSize);
963 cabInfo.PackFiles(null, new string[] { "testser00.txt", "testser01.txt" }, null);
964 ArchiveFileInfo cfi = cabInfo.GetFiles()[1];
965
966 MemoryStream memStream = new MemoryStream();
967
968 BinaryFormatter formatter = new BinaryFormatter();
969
970 memStream.Seek(0, SeekOrigin.Begin);
971 formatter.Serialize(memStream, cabInfo);
972 memStream.Seek(0, SeekOrigin.Begin);
973 CabInfo cabInfo2 = (CabInfo) formatter.Deserialize(memStream);
974 Assert.AreEqual<string>(cabInfo.FullName, cabInfo2.FullName);
975
976 memStream.Seek(0, SeekOrigin.Begin);
977 formatter.Serialize(memStream, cfi);
978 memStream.Seek(0, SeekOrigin.Begin);
979 CabFileInfo cfi2 = (CabFileInfo) formatter.Deserialize(memStream);
980 Assert.AreEqual<string>(cfi.FullName, cfi2.FullName);
981 Assert.AreEqual<long>(cfi.Length, cfi2.Length);
982
983 CabException cabEx = new CabException();
984 memStream.Seek(0, SeekOrigin.Begin);
985 formatter.Serialize(memStream, cabEx);
986 memStream.Seek(0, SeekOrigin.Begin);
987 formatter.Deserialize(memStream);
988
989 cabEx = new CabException("Test exception.", null);
990 Assert.AreEqual<string>("Test exception.", cabEx.Message);
991 }
992
993 [TestMethod]
994 public void CabFileStreamContextNullParams()
995 {
996 ArchiveFileStreamContext streamContext = null;
997 Exception caughtEx = null;
998 try
999 {
1000 streamContext = new ArchiveFileStreamContext(null);
1001 }
1002 catch (Exception ex) { caughtEx = ex; }
1003 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Passing null to constructor.");
1004 caughtEx = null;
1005 try
1006 {
1007 streamContext = new ArchiveFileStreamContext(new string[] { }, "testDir", new Dictionary<string, string>());
1008 }
1009 catch (Exception ex) { caughtEx = ex; }
1010 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Passing 0-length array to constructor.");
1011 caughtEx = null;
1012 try
1013 {
1014 streamContext = new ArchiveFileStreamContext(new string[] { "test.cab" }, null, null);
1015 }
1016 catch (Exception ex) { caughtEx = ex; }
1017 Assert.IsNull(caughtEx);
1018 }
1019
1020 [TestMethod]
1021 public void CabinetTruncateOnCreate()
1022 {
1023 CabInfo cabInfo = new CabInfo("testtruncate.cab");
1024 int txtSize = 20240;
1025 CompressionTestUtil.GenerateRandomFile("testtruncate0.txt", 0, txtSize);
1026 CompressionTestUtil.GenerateRandomFile("testtruncate1.txt", 1, txtSize);
1027 cabInfo.PackFiles(null, new string[] { "testtruncate0.txt", "testtruncate1.txt" }, null);
1028
1029 long size1 = cabInfo.Length;
1030
1031 txtSize /= 5;
1032 CompressionTestUtil.GenerateRandomFile("testtruncate2.txt", 2, txtSize);
1033 CompressionTestUtil.GenerateRandomFile("testtruncate3.txt", 3, txtSize);
1034 cabInfo.PackFiles(null, new string[] { "testtruncate2.txt", "testtruncate3.txt" }, null);
1035
1036 // The newly created cab file should be smaller than before.
1037 Assert.AreNotEqual<long>(size1, cabInfo.Length, "Checking that cabinet file got truncated when creating a smaller cab in-place.");
1038 }
1039
1040 [TestMethod]
1041 public void CabTruncatedArchive()
1042 {
1043 CabInfo cabInfo = new CabInfo("test-t.cab");
1044 CompressionTestUtil.GenerateRandomFile("cabtest-0.txt", 0, 5);
1045 CompressionTestUtil.GenerateRandomFile("cabtest-1.txt", 1, 5);
1046 cabInfo.PackFiles(null, new string[] { "cabtest-0.txt", "cabtest-1.txt" }, null);
1047
1048 CompressionTestUtil.TestTruncatedArchive(cabInfo, typeof(CabException));
1049 }
1050 private const string TEST_FILENAME_PREFIX = "\x20AC";
1051
1052 private IList<ArchiveFileInfo> RunCabinetPackUnpack(int fileCount, long fileSize)
1053 {
1054 return RunCabinetPackUnpack(fileCount, fileSize, 0, 0);
1055 }
1056 private IList<ArchiveFileInfo> RunCabinetPackUnpack(int fileCount, long fileSize,
1057 long maxFolderSize, long maxArchiveSize)
1058 {
1059 return this.RunCabinetPackUnpack(fileCount, fileSize, maxFolderSize, maxArchiveSize, CompressionLevel.Normal);
1060 }
1061 private IList<ArchiveFileInfo> RunCabinetPackUnpack(int fileCount, long fileSize,
1062 long maxFolderSize, long maxArchiveSize, CompressionLevel compLevel)
1063 {
1064 Console.WriteLine("Creating cabinet with {0} files of size {1}",
1065 fileCount, fileSize);
1066 Console.WriteLine("MaxFolderSize={0}, MaxArchiveSize={1}, CompressionLevel={2}",
1067 maxFolderSize, maxArchiveSize, compLevel);
1068
1069 string dirA = String.Format("{0}-{1}-A", fileCount, fileSize);
1070 if (Directory.Exists(dirA)) Directory.Delete(dirA, true);
1071 Directory.CreateDirectory(dirA);
1072 string dirB = String.Format("{0}-{1}-B", fileCount, fileSize);
1073 if (Directory.Exists(dirB)) Directory.Delete(dirB, true);
1074 Directory.CreateDirectory(dirB);
1075
1076 string[] files = new string[fileCount];
1077 for (int iFile = 0; iFile < fileCount; iFile++)
1078 {
1079 files[iFile] = TEST_FILENAME_PREFIX + iFile + ".txt";
1080 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[iFile]), iFile, fileSize);
1081 }
1082
1083 string[] archiveNames = new string[100];
1084 for (int i = 0; i < archiveNames.Length; i++)
1085 {
1086 archiveNames[i] = String.Format("{0}-{1}{2}{3}.cab", fileCount, fileSize,
1087 (i == 0 ? "" : "-"), (i == 0 ? "" : i.ToString()));
1088 }
1089
1090 string progressTextFile = String.Format("progress_{0}-{1}.txt", fileCount, fileSize);
1091 CompressionTestUtil testUtil = new CompressionTestUtil(progressTextFile);
1092
1093 IList<ArchiveFileInfo> fileInfo;
1094 using (CabEngine cabEngine = new CabEngine())
1095 {
1096 cabEngine.CompressionLevel = compLevel;
1097
1098 File.AppendAllText(progressTextFile,
1099 "\r\n\r\n====================================================\r\nCREATE\r\n\r\n");
1100 cabEngine.Progress += testUtil.PrintArchiveProgress;
1101
1102 OptionStreamContext streamContext = new OptionStreamContext(archiveNames, dirA, null);
1103 if (maxFolderSize == 1)
1104 {
1105 streamContext.OptionHandler =
1106 delegate(string optionName, object[] parameters)
1107 {
1108 if (optionName == "nextFolder") return true;
1109 return null;
1110 };
1111 }
1112 else if (maxFolderSize > 1)
1113 {
1114 streamContext.OptionHandler =
1115 delegate(string optionName, object[] parameters)
1116 {
1117 if (optionName == "maxFolderSize") return maxFolderSize;
1118 return null;
1119 };
1120 }
1121 cabEngine.Pack(streamContext, files, maxArchiveSize);
1122
1123 IList<string> createdArchiveNames = new List<string>(archiveNames.Length);
1124 for (int i = 0; i < archiveNames.Length; i++)
1125 {
1126 if (File.Exists(archiveNames[i]))
1127 {
1128 createdArchiveNames.Add(archiveNames[i]);
1129 }
1130 else
1131 {
1132 break;
1133 }
1134 }
1135
1136 Console.WriteLine("Listing cabinet with {0} files of size {1}",
1137 fileCount, fileSize);
1138 File.AppendAllText(progressTextFile, "\r\n\r\nLIST\r\n\r\n");
1139 fileInfo = cabEngine.GetFileInfo(
1140 new ArchiveFileStreamContext(createdArchiveNames, null, null), null);
1141
1142 Assert.AreEqual<int>(fileCount, fileInfo.Count);
1143 if (fileCount > 0)
1144 {
1145 int folders = ((CabFileInfo) fileInfo[fileInfo.Count - 1]).CabinetFolderNumber + 1;
1146 if (maxFolderSize == 1)
1147 {
1148 Assert.AreEqual<int>(fileCount, folders);
1149 }
1150 }
1151
1152 Console.WriteLine("Extracting cabinet with {0} files of size {1}",
1153 fileCount, fileSize);
1154 File.AppendAllText(progressTextFile, "\r\n\r\nEXTRACT\r\n\r\n");
1155 cabEngine.Unpack(new ArchiveFileStreamContext(createdArchiveNames, dirB, null), null);
1156 }
1157
1158 bool directoryMatch = CompressionTestUtil.CompareDirectories(dirA, dirB);
1159 Assert.IsTrue(directoryMatch,
1160 "Testing whether cabinet output directory matches input directory.");
1161
1162 return fileInfo;
1163 }
1164 }
1165}
diff --git a/src/dtf/WixToolsetTests.Dtf.Compression.Cab/WixToolsetTests.Dtf.Compression.Cab.csproj b/src/dtf/WixToolsetTests.Dtf.Compression.Cab/WixToolsetTests.Dtf.Compression.Cab.csproj
new file mode 100644
index 00000000..4c269d55
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.Compression.Cab/WixToolsetTests.Dtf.Compression.Cab.csproj
@@ -0,0 +1,32 @@
1<?xml version="1.0" encoding="utf-8" ?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
5 <PropertyGroup>
6 <ProjectGuid>{4544158C-2D63-4146-85FF-62169280144E}</ProjectGuid>
7 <OutputType>Library</OutputType>
8 <RootNamespace>WixToolsetTests.Dtf.Cab</RootNamespace>
9 <AssemblyName>WixToolsetTests.Dtf.Cab</AssemblyName>
10 <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
11 <CreateDocumentation>false</CreateDocumentation>
12 </PropertyGroup>
13
14 <ItemGroup>
15 <Compile Include="CabTest.cs" />
16 </ItemGroup>
17
18 <ItemGroup>
19 <Reference Include="System" />
20 <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
21 <Reference Include="System.Data" />
22 <Reference Include="System.Xml" />
23 </ItemGroup>
24
25 <ItemGroup>
26 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
27 <ProjectReference Include="..\WixToolset.Dtf.Compression.Cab\WixToolset.Dtf.Compression.Cab.csproj" />
28 <ProjectReference Include="..\WixToolsetTests.Dtf.Compression\WixToolsetTests.Dtf.Compression.csproj" />
29 </ItemGroup>
30
31 <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
32</Project>
diff --git a/src/dtf/WixToolsetTests.Dtf.Compression.Zip/WixToolsetTests.Dtf.Compression.Zip.csproj b/src/dtf/WixToolsetTests.Dtf.Compression.Zip/WixToolsetTests.Dtf.Compression.Zip.csproj
new file mode 100644
index 00000000..c65563b6
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.Compression.Zip/WixToolsetTests.Dtf.Compression.Zip.csproj
@@ -0,0 +1,30 @@
1<?xml version="1.0" encoding="utf-8" ?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
5 <PropertyGroup>
6 <ProjectGuid>{328799BB-7B03-4B28-8180-4132211FD07D}</ProjectGuid>
7 <OutputType>Library</OutputType>
8 <RootNamespace>WixToolsetTests.Dtf</RootNamespace>
9 <AssemblyName>WixToolsetTests.Dtf.Zip</AssemblyName>
10 <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
11 <CreateDocumentation>false</CreateDocumentation>
12 </PropertyGroup>
13
14 <ItemGroup>
15 <Compile Include="ZipTest.cs" />
16 </ItemGroup>
17
18 <ItemGroup>
19 <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
20 <Reference Include="System" />
21 </ItemGroup>
22
23 <ItemGroup>
24 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
25 <ProjectReference Include="..\WixToolset.Dtf.Compression.Zip\WixToolset.Dtf.Compression.Zip.csproj" />
26 <ProjectReference Include="..\WixToolsetTests.Dtf.Compression\WixToolsetTests.Dtf.Compression.csproj" />
27 </ItemGroup>
28
29 <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
30</Project>
diff --git a/src/dtf/WixToolsetTests.Dtf.Compression.Zip/ZipTest.cs b/src/dtf/WixToolsetTests.Dtf.Compression.Zip/ZipTest.cs
new file mode 100644
index 00000000..b264ad5b
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.Compression.Zip/ZipTest.cs
@@ -0,0 +1,518 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Test
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections.Generic;
9 using Microsoft.VisualStudio.TestTools.UnitTesting;
10 using WixToolset.Dtf.Compression;
11 using WixToolset.Dtf.Compression.Zip;
12
13 [TestClass]
14 public class ZipTest
15 {
16 public ZipTest()
17 {
18 }
19
20 [TestInitialize]
21 public void Initialize()
22 {
23 }
24
25 [TestCleanup]
26 public void Cleanup()
27 {
28 }
29
30 [TestMethod]
31 public void ZipFileCounts()
32 {
33 this.RunZipPackUnpack(0, 10, 0);
34 this.RunZipPackUnpack(0, 100000, 0);
35 this.RunZipPackUnpack(1, 10, 0);
36 this.RunZipPackUnpack(100, 10, 0);
37 }
38
39 [TestMethod]
40 [Ignore] // Takes too long to run regularly.
41 public void ZipExtremeFileCounts()
42 {
43 this.RunZipPackUnpack(66000, 10, 0);
44 }
45
46 [TestMethod]
47 public void ZipFileSizes()
48 {
49 this.RunZipPackUnpack(1, 0, 0);
50 for (int n = 1; n <= 33; n++)
51 {
52 this.RunZipPackUnpack(1, n, 0);
53 }
54 this.RunZipPackUnpack(1, 100 * 1024, 0);
55 this.RunZipPackUnpack(1, 10 * 1024 * 1024, 0);
56 }
57
58 [Timeout(36000000), TestMethod]
59 [Ignore] // Takes too long to run regularly.
60 public void ZipExtremeFileSizes()
61 {
62 //this.RunZipPackUnpack(10, 512L * 1024 * 1024, 0); // 5GB
63 this.RunZipPackUnpack(1, 5L * 1024 * 1024 * 1024, 0, CompressionLevel.None); // 5GB
64 }
65
66 [TestMethod]
67 public void ZipArchiveCounts()
68 {
69 IList<ArchiveFileInfo> fileInfo;
70 fileInfo = this.RunZipPackUnpack(10, 100 * 1024, 400 * 1024, CompressionLevel.None);
71 Assert.AreEqual<int>(2, fileInfo[fileInfo.Count - 1].ArchiveNumber,
72 "Testing whether archive spans the correct # of zip files.");
73
74 fileInfo = this.RunZipPackUnpack(2, 90 * 1024, 40 * 1024, CompressionLevel.None);
75 Assert.AreEqual<int>(2, fileInfo[fileInfo.Count - 1].ArchiveNumber,
76 "Testing whether archive spans the correct # of zip files.");
77 }
78
79 [TestMethod]
80 public void ZipProgress()
81 {
82 CompressionTestUtil.ExpectedProgress = new List<int[]>(new int[][] {
83 // StatusType, CurFile,TotalFiles,CurFolder,CurArchive,TotalArchives
84 new int[] { (int) ArchiveProgressType.StartArchive, 0, 15, 0, 0, 1 },
85 new int[] { (int) ArchiveProgressType.StartFile, 0, 15, 0, 0, 1 },
86 new int[] { (int) ArchiveProgressType.FinishFile, 0, 15, 0, 0, 1 },
87 new int[] { (int) ArchiveProgressType.StartFile, 1, 15, 0, 0, 1 },
88 new int[] { (int) ArchiveProgressType.FinishFile, 1, 15, 0, 0, 1 },
89 new int[] { (int) ArchiveProgressType.StartFile, 2, 15, 0, 0, 1 },
90 new int[] { (int) ArchiveProgressType.FinishFile, 2, 15, 0, 0, 1 },
91 new int[] { (int) ArchiveProgressType.StartFile, 3, 15, 0, 0, 1 },
92 new int[] { (int) ArchiveProgressType.FinishFile, 3, 15, 0, 0, 1 },
93 new int[] { (int) ArchiveProgressType.StartFile, 4, 15, 0, 0, 1 },
94 new int[] { (int) ArchiveProgressType.FinishFile, 4, 15, 0, 0, 1 },
95 new int[] { (int) ArchiveProgressType.StartFile, 5, 15, 0, 0, 1 },
96 new int[] { (int) ArchiveProgressType.FinishFile, 5, 15, 0, 0, 1 },
97 new int[] { (int) ArchiveProgressType.StartFile, 6, 15, 0, 0, 1 },
98 new int[] { (int) ArchiveProgressType.FinishArchive, 6, 15, 0, 0, 1 },
99 new int[] { (int) ArchiveProgressType.StartArchive, 6, 15, 0, 1, 2 },
100 new int[] { (int) ArchiveProgressType.FinishFile, 6, 15, 0, 1, 2 },
101 new int[] { (int) ArchiveProgressType.StartFile, 7, 15, 0, 1, 2 },
102 new int[] { (int) ArchiveProgressType.FinishFile, 7, 15, 0, 1, 2 },
103 new int[] { (int) ArchiveProgressType.StartFile, 8, 15, 0, 1, 2 },
104 new int[] { (int) ArchiveProgressType.FinishFile, 8, 15, 0, 1, 2 },
105 new int[] { (int) ArchiveProgressType.StartFile, 9, 15, 0, 1, 2 },
106 new int[] { (int) ArchiveProgressType.FinishFile, 9, 15, 0, 1, 2 },
107 new int[] { (int) ArchiveProgressType.StartFile, 10, 15, 0, 1, 2 },
108 new int[] { (int) ArchiveProgressType.FinishFile, 10, 15, 0, 1, 2 },
109 new int[] { (int) ArchiveProgressType.StartFile, 11, 15, 0, 1, 2 },
110 new int[] { (int) ArchiveProgressType.FinishFile, 11, 15, 0, 1, 2 },
111 new int[] { (int) ArchiveProgressType.StartFile, 12, 15, 0, 1, 2 },
112 new int[] { (int) ArchiveProgressType.FinishArchive, 12, 15, 0, 1, 2 },
113 new int[] { (int) ArchiveProgressType.StartArchive, 12, 15, 0, 2, 3 },
114 new int[] { (int) ArchiveProgressType.FinishFile, 12, 15, 0, 2, 3 },
115 new int[] { (int) ArchiveProgressType.StartFile, 13, 15, 0, 2, 3 },
116 new int[] { (int) ArchiveProgressType.FinishFile, 13, 15, 0, 2, 3 },
117 new int[] { (int) ArchiveProgressType.StartFile, 14, 15, 0, 2, 3 },
118 new int[] { (int) ArchiveProgressType.FinishFile, 14, 15, 0, 2, 3 },
119 new int[] { (int) ArchiveProgressType.FinishArchive, 14, 15, 0, 2, 3 },
120 // StatusType, CurFile,TotalFiles,CurFolder,CurArchive,TotalArchives
121 new int[] { (int) ArchiveProgressType.StartArchive, 0, 15, 0, 0, 3 },
122 new int[] { (int) ArchiveProgressType.StartFile, 0, 15, 0, 0, 3 },
123 new int[] { (int) ArchiveProgressType.FinishFile, 0, 15, 0, 0, 3 },
124 new int[] { (int) ArchiveProgressType.StartFile, 1, 15, 0, 0, 3 },
125 new int[] { (int) ArchiveProgressType.FinishFile, 1, 15, 0, 0, 3 },
126 new int[] { (int) ArchiveProgressType.StartFile, 2, 15, 0, 0, 3 },
127 new int[] { (int) ArchiveProgressType.FinishFile, 2, 15, 0, 0, 3 },
128 new int[] { (int) ArchiveProgressType.StartFile, 3, 15, 0, 0, 3 },
129 new int[] { (int) ArchiveProgressType.FinishFile, 3, 15, 0, 0, 3 },
130 new int[] { (int) ArchiveProgressType.StartFile, 4, 15, 0, 0, 3 },
131 new int[] { (int) ArchiveProgressType.FinishFile, 4, 15, 0, 0, 3 },
132 new int[] { (int) ArchiveProgressType.StartFile, 5, 15, 0, 0, 3 },
133 new int[] { (int) ArchiveProgressType.FinishFile, 5, 15, 0, 0, 3 },
134 new int[] { (int) ArchiveProgressType.StartFile, 6, 15, 0, 0, 3 },
135 new int[] { (int) ArchiveProgressType.FinishArchive, 6, 15, 0, 0, 3 },
136 new int[] { (int) ArchiveProgressType.StartArchive, 6, 15, 0, 1, 3 },
137 new int[] { (int) ArchiveProgressType.FinishFile, 6, 15, 0, 1, 3 },
138 new int[] { (int) ArchiveProgressType.StartFile, 7, 15, 0, 1, 3 },
139 new int[] { (int) ArchiveProgressType.FinishFile, 7, 15, 0, 1, 3 },
140 new int[] { (int) ArchiveProgressType.StartFile, 8, 15, 0, 1, 3 },
141 new int[] { (int) ArchiveProgressType.FinishFile, 8, 15, 0, 1, 3 },
142 new int[] { (int) ArchiveProgressType.StartFile, 9, 15, 0, 1, 3 },
143 new int[] { (int) ArchiveProgressType.FinishFile, 9, 15, 0, 1, 3 },
144 new int[] { (int) ArchiveProgressType.StartFile, 10, 15, 0, 1, 3 },
145 new int[] { (int) ArchiveProgressType.FinishFile, 10, 15, 0, 1, 3 },
146 new int[] { (int) ArchiveProgressType.StartFile, 11, 15, 0, 1, 3 },
147 new int[] { (int) ArchiveProgressType.FinishFile, 11, 15, 0, 1, 3 },
148 new int[] { (int) ArchiveProgressType.StartFile, 12, 15, 0, 1, 3 },
149 new int[] { (int) ArchiveProgressType.FinishArchive, 12, 15, 0, 1, 3 },
150 new int[] { (int) ArchiveProgressType.StartArchive, 12, 15, 0, 2, 3 },
151 new int[] { (int) ArchiveProgressType.FinishFile, 12, 15, 0, 2, 3 },
152 new int[] { (int) ArchiveProgressType.StartFile, 13, 15, 0, 2, 3 },
153 new int[] { (int) ArchiveProgressType.FinishFile, 13, 15, 0, 2, 3 },
154 new int[] { (int) ArchiveProgressType.StartFile, 14, 15, 0, 2, 3 },
155 new int[] { (int) ArchiveProgressType.FinishFile, 14, 15, 0, 2, 3 },
156 new int[] { (int) ArchiveProgressType.FinishArchive, 14, 15, 0, 2, 3 },
157 });
158 CompressionTestUtil.ExpectedProgress = null;
159
160 try
161 {
162 this.RunZipPackUnpack(15, 20 * 1024, 130 * 1024, CompressionLevel.None);
163 }
164 finally
165 {
166 CompressionTestUtil.ExpectedProgress = null;
167 }
168 }
169
170 [TestMethod]
171 //[Ignore] // Requires clean environment.
172 public void ZipArchiveSizes()
173 {
174 Console.WriteLine("Testing various values for the maxArchiveSize parameter.");
175 this.RunZipPackUnpack(5, 1024, Int64.MinValue);
176 this.RunZipPackUnpack(5, 1024, -1);
177 this.RunZipPackUnpack(2, 10, 0);
178
179 this.RunZipPackUnpack(1, 10, 1);
180 this.RunZipPackUnpack(2, 10, 2);
181 this.RunZipPackUnpack(2, 10, 3);
182 this.RunZipPackUnpack(2, 10, 4);
183 this.RunZipPackUnpack(2, 10, 5);
184 this.RunZipPackUnpack(2, 10, 6);
185 this.RunZipPackUnpack(2, 10, 7);
186 this.RunZipPackUnpack(5, 10, 8);
187 this.RunZipPackUnpack(5, 10, 9);
188 this.RunZipPackUnpack(5, 10, 10);
189 this.RunZipPackUnpack(5, 10, 11);
190 this.RunZipPackUnpack(5, 10, 12);
191
192 this.RunZipPackUnpack(5, 101, 255);
193 this.RunZipPackUnpack(5, 102, 256);
194 this.RunZipPackUnpack(5, 103, 257);
195 this.RunZipPackUnpack(5, 24000, 32768);
196 this.RunZipPackUnpack(5, 1024, Int64.MaxValue);
197 }
198
199 [TestMethod]
200 public void ZipCompLevelParam()
201 {
202 Console.WriteLine("Testing various values for the compressionLevel parameter.");
203 this.RunZipPackUnpack(5, 1024, 0, CompressionLevel.None);
204 this.RunZipPackUnpack(5, 1024, 0, CompressionLevel.Min);
205 this.RunZipPackUnpack(5, 1024, 0, CompressionLevel.Normal);
206 this.RunZipPackUnpack(5, 1024, 0, CompressionLevel.Max);
207 this.RunZipPackUnpack(5, 1024, 0, (CompressionLevel) ((int) CompressionLevel.None - 1));
208 this.RunZipPackUnpack(5, 1024, 0, (CompressionLevel) ((int) CompressionLevel.Max + 1));
209 this.RunZipPackUnpack(5, 1024, 0, (CompressionLevel) Int32.MinValue);
210 this.RunZipPackUnpack(5, 1024, 0, (CompressionLevel) Int32.MaxValue);
211 }
212
213 [TestMethod]
214 public void ZipInfoGetFiles()
215 {
216 IList<ZipFileInfo> fileInfos;
217 ZipInfo zipInfo = new ZipInfo("testgetfiles.zip");
218
219 int txtSize = 10240;
220 CompressionTestUtil.GenerateRandomFile("testinfo0.txt", 0, txtSize);
221 CompressionTestUtil.GenerateRandomFile("testinfo1.txt", 1, txtSize);
222 CompressionTestUtil.GenerateRandomFile("testinfo2.ini", 2, txtSize);
223 zipInfo.PackFiles(null, new string[] { "testinfo0.txt", "testinfo1.txt", "testinfo2.ini" }, null);
224
225 fileInfos = zipInfo.GetFiles();
226 Assert.IsNotNull(fileInfos);
227 Assert.AreEqual<int>(3, fileInfos.Count);
228 Assert.AreEqual<string>("testinfo0.txt", fileInfos[0].Name);
229 Assert.AreEqual<string>("testinfo1.txt", fileInfos[1].Name);
230 Assert.AreEqual<string>("testinfo2.ini", fileInfos[2].Name);
231
232 fileInfos = zipInfo.GetFiles("*.txt");
233 Assert.IsNotNull(fileInfos);
234 Assert.AreEqual<int>(2, fileInfos.Count);
235 Assert.AreEqual<string>("testinfo0.txt", fileInfos[0].Name);
236 Assert.AreEqual<string>("testinfo1.txt", fileInfos[1].Name);
237
238 fileInfos = zipInfo.GetFiles("testinfo1.txt");
239 Assert.IsNotNull(fileInfos);
240 Assert.AreEqual<int>(1, fileInfos.Count);
241 Assert.AreEqual<string>("testinfo1.txt", fileInfos[0].Name);
242 Assert.IsTrue(DateTime.Now - fileInfos[0].LastWriteTime < TimeSpan.FromMinutes(1),
243 "Checking ZipFileInfo.LastWriteTime is current.");
244 }
245
246 [TestMethod]
247 //[Ignore] // Requires clean environment.
248 public void ZipInfoNullParams()
249 {
250 int fileCount = 10, fileSize = 1024;
251 string dirA = String.Format("{0}-{1}-A", fileCount, fileSize);
252 if (Directory.Exists(dirA)) Directory.Delete(dirA, true);
253 Directory.CreateDirectory(dirA);
254 string dirB = String.Format("{0}-{1}-B", fileCount, fileSize);
255 if (Directory.Exists(dirB)) Directory.Delete(dirB, true);
256 Directory.CreateDirectory(dirB);
257
258 string[] files = new string[fileCount];
259 for (int iFile = 0; iFile < fileCount; iFile++)
260 {
261 files[iFile] = "zipinfo-" + iFile + ".txt";
262 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[iFile]), iFile, fileSize);
263 }
264
265 ZipInfo zipInfo = new ZipInfo("testnull.zip");
266
267 CompressionTestUtil.TestArchiveInfoNullParams(zipInfo, dirA, dirB, files);
268 }
269
270 [TestMethod]
271 public void ZipFileInfoNullParams()
272 {
273 Exception caughtEx;
274 ZipInfo zipInfo = new ZipInfo("test.zip");
275 int txtSize = 10240;
276 CompressionTestUtil.GenerateRandomFile("test00.txt", 0, txtSize);
277 CompressionTestUtil.GenerateRandomFile("test01.txt", 1, txtSize);
278 zipInfo.PackFiles(null, new string[] { "test00.txt", "test01.txt" }, null);
279 ZipFileInfo zfi = new ZipFileInfo(zipInfo, "test01.txt");
280
281 caughtEx = null;
282 try
283 {
284 new ZipFileInfo(null, "test00.txt");
285 }
286 catch (Exception ex) { caughtEx = ex; }
287 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException));
288 caughtEx = null;
289 try
290 {
291 new ZipFileInfo(zipInfo, null);
292 }
293 catch (Exception ex) { caughtEx = ex; }
294 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException));
295 caughtEx = null;
296 try
297 {
298 zfi.CopyTo(null);
299 }
300 catch (Exception ex) { caughtEx = ex; }
301 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException));
302 }
303
304 [TestMethod]
305 public void ZipEngineNullParams()
306 {
307 string[] testFiles = new string[] { "test.txt" };
308 ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext("test.zip", null, null);
309
310 using (ZipEngine zipEngine = new ZipEngine())
311 {
312 zipEngine.CompressionLevel = CompressionLevel.None;
313
314 CompressionTestUtil.TestCompressionEngineNullParams(zipEngine, streamContext, testFiles);
315 }
316 }
317
318 [TestMethod]
319 public void ZipBadPackStreamContexts()
320 {
321 string[] testFiles = new string[] { "test.txt" };
322 CompressionTestUtil.GenerateRandomFile(testFiles[0], 0, 20000);
323
324 using (ZipEngine zipEngine = new ZipEngine())
325 {
326 zipEngine.CompressionLevel = CompressionLevel.None;
327
328 CompressionTestUtil.TestBadPackStreamContexts(zipEngine, "test.zip", testFiles);
329 }
330 }
331
332 [TestMethod]
333 public void ZipBadUnpackStreamContexts()
334 {
335 int txtSize = 40960;
336 ZipInfo zipInfo = new ZipInfo("test2.zip");
337 CompressionTestUtil.GenerateRandomFile("ziptest-0.txt", 0, txtSize);
338 CompressionTestUtil.GenerateRandomFile("ziptest-1.txt", 1, txtSize);
339 zipInfo.PackFiles(null, new string[] { "ziptest-0.txt", "ziptest-1.txt" }, null);
340
341 using (ZipEngine zipEngine = new ZipEngine())
342 {
343 CompressionTestUtil.TestBadUnpackStreamContexts(zipEngine, "test2.zip");
344 }
345 }
346
347 [TestMethod]
348 [Ignore] // Failed on build server, need to investigate.
349 public void ZipTruncatedArchive()
350 {
351 ZipInfo zipInfo = new ZipInfo("test-t.zip");
352 CompressionTestUtil.GenerateRandomFile("ziptest-0.txt", 0, 5);
353 CompressionTestUtil.GenerateRandomFile("ziptest-1.txt", 1, 5);
354 zipInfo.PackFiles(null, new string[] { "ziptest-0.txt", "ziptest-1.txt" }, null);
355
356 CompressionTestUtil.TestTruncatedArchive(zipInfo, typeof(ZipException));
357 }
358
359 /*
360 [TestMethod]
361 public void ZipUnpack()
362 {
363 IList<ZipFileInfo> fileInfos;
364 foreach (FileInfo zipFile in new DirectoryInfo("D:\\temp").GetFiles("*.zip"))
365 {
366 Console.WriteLine("=====================================================");
367 Console.WriteLine(zipFile.FullName);
368 Console.WriteLine("=====================================================");
369 ZipInfo zipTest = new ZipInfo(zipFile.FullName);
370 fileInfos = zipTest.GetFiles();
371 Assert.AreNotEqual<int>(0, fileInfos.Count);
372 foreach (ArchiveFileInfo file in fileInfos)
373 {
374 Console.WriteLine("{0}\t{1}\t{2}", Path.Combine(file.Path, file.Name), file.Length, file.LastWriteTime);
375 }
376
377 Directory.CreateDirectory(Path.GetFileNameWithoutExtension(zipFile.Name));
378 zipTest.Unpack(Path.GetFileNameWithoutExtension(zipFile.Name));
379 }
380 }
381 */
382
383 /*
384 [TestMethod]
385 public void ZipUnpackSelfExtractor()
386 {
387 ZipInfo zipTest = new ZipInfo(@"C:\temp\testzip.exe");
388 IList<ZipFileInfo> fileInfos = zipTest.GetFiles();
389 Assert.AreNotEqual<int>(0, fileInfos.Count);
390 foreach (ArchiveFileInfo file in fileInfos)
391 {
392 Console.WriteLine("{0}\t{1}\t{2}", Path.Combine(file.Path, file.Name), file.Length, file.LastWriteTime);
393 }
394
395 string extractDir = Path.GetFileNameWithoutExtension(zipTest.Name);
396 Directory.CreateDirectory(extractDir);
397 zipTest.Unpack(extractDir);
398 }
399 */
400
401 private const string TEST_FILENAME_PREFIX = "\x20AC";
402
403 private IList<ArchiveFileInfo> RunZipPackUnpack(int fileCount, long fileSize,
404 long maxArchiveSize)
405 {
406 return this.RunZipPackUnpack(fileCount, fileSize, maxArchiveSize, CompressionLevel.Normal);
407 }
408
409 private IList<ArchiveFileInfo> RunZipPackUnpack(int fileCount, long fileSize,
410 long maxArchiveSize, CompressionLevel compLevel)
411 {
412 Console.WriteLine("Creating zip archive with {0} files of size {1}",
413 fileCount, fileSize);
414 Console.WriteLine("MaxArchiveSize={0}, CompressionLevel={1}", maxArchiveSize, compLevel);
415
416 string dirA = String.Format("{0}-{1}-A", fileCount, fileSize);
417 if (Directory.Exists(dirA)) Directory.Delete(dirA, true);
418 Directory.CreateDirectory(dirA);
419 string dirB = String.Format("{0}-{1}-B", fileCount, fileSize);
420 if (Directory.Exists(dirB)) Directory.Delete(dirB, true);
421 Directory.CreateDirectory(dirB);
422
423 string[] files = new string[fileCount];
424 for (int iFile = 0; iFile < fileCount; iFile++)
425 {
426 files[iFile] = TEST_FILENAME_PREFIX + iFile + ".txt";
427 CompressionTestUtil.GenerateRandomFile(Path.Combine(dirA, files[iFile]), iFile, fileSize);
428 }
429
430 string[] archiveNames = new string[1000];
431 for (int i = 0; i < archiveNames.Length; i++)
432 {
433 if (i < 100)
434 {
435 archiveNames[i] = String.Format(
436 (i == 0 ? "{0}-{1}.zip" : "{0}-{1}.z{2:d02}"),
437 fileCount, fileSize, i);
438 }
439 else
440 {
441 archiveNames[i] = String.Format(
442 "{0}-{1}.{2:d03}", fileCount, fileSize, i);
443 }
444 }
445
446 string progressTextFile = String.Format("progress_{0}-{1}.txt", fileCount, fileSize);
447 CompressionTestUtil testUtil = new CompressionTestUtil(progressTextFile);
448
449 IList<ArchiveFileInfo> fileInfo;
450 using (ZipEngine zipEngine = new ZipEngine())
451 {
452 zipEngine.CompressionLevel = compLevel;
453
454 File.AppendAllText(progressTextFile,
455 "\r\n\r\n====================================================\r\nCREATE\r\n\r\n");
456 zipEngine.Progress += testUtil.PrintArchiveProgress;
457
458 OptionStreamContext streamContext = new OptionStreamContext(archiveNames, dirA, null);
459 streamContext.OptionHandler =
460 delegate(string optionName, object[] parameters)
461 {
462 // For testing purposes, force zip64 for only moderately large files.
463 switch (optionName)
464 {
465 case "forceZip64":
466 return fileSize > UInt16.MaxValue;
467 default:
468 return null;
469 }
470 };
471
472 zipEngine.Pack(streamContext, files, maxArchiveSize);
473
474 string checkArchiveName = archiveNames[0];
475 if (File.Exists(archiveNames[1])) checkArchiveName = archiveNames[1];
476 using (Stream archiveStream = File.OpenRead(checkArchiveName))
477 {
478 bool isArchive = zipEngine.IsArchive(archiveStream);
479 Assert.IsTrue(isArchive, "Checking that created archive appears valid.");
480 }
481
482 IList<string> createdArchiveNames = new List<string>(archiveNames.Length);
483 for (int i = 0; i < archiveNames.Length; i++)
484 {
485 if (File.Exists(archiveNames[i]))
486 {
487 createdArchiveNames.Add(archiveNames[i]);
488 }
489 else
490 {
491 break;
492 }
493 }
494
495 Assert.AreNotEqual<int>(0, createdArchiveNames.Count);
496
497 Console.WriteLine("Listing zip archive with {0} files of size {1}",
498 fileCount, fileSize);
499 File.AppendAllText(progressTextFile, "\r\n\r\nLIST\r\n\r\n");
500 fileInfo = zipEngine.GetFileInfo(
501 new ArchiveFileStreamContext(createdArchiveNames, null, null), null);
502
503 Assert.AreEqual<int>(fileCount, fileInfo.Count);
504
505 Console.WriteLine("Extracting zip archive with {0} files of size {1}",
506 fileCount, fileSize);
507 File.AppendAllText(progressTextFile, "\r\n\r\nEXTRACT\r\n\r\n");
508 zipEngine.Unpack(new ArchiveFileStreamContext(createdArchiveNames, dirB, null), null);
509 }
510
511 bool directoryMatch = CompressionTestUtil.CompareDirectories(dirA, dirB);
512 Assert.IsTrue(directoryMatch,
513 "Testing whether zip output directory matches input directory.");
514
515 return fileInfo;
516 }
517 }
518}
diff --git a/src/dtf/WixToolsetTests.Dtf.Compression/CompressionTestUtil.cs b/src/dtf/WixToolsetTests.Dtf.Compression/CompressionTestUtil.cs
new file mode 100644
index 00000000..e7a5373d
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.Compression/CompressionTestUtil.cs
@@ -0,0 +1,649 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3using System;
4using System.IO;
5using System.Text;
6using System.Collections.Generic;
7using System.Security.Cryptography;
8using Microsoft.VisualStudio.TestTools.UnitTesting;
9using WixToolset.Dtf.Compression;
10
11namespace WixToolset.Dtf.Test
12{
13 public class CompressionTestUtil
14 {
15 private static MD5 md5 = new MD5CryptoServiceProvider();
16
17 private string progressTextFile;
18
19 public CompressionTestUtil(string progressTextFile)
20 {
21 this.progressTextFile = progressTextFile;
22 }
23
24 public static IList<int[]> ExpectedProgress
25 {
26 get { return CompressionTestUtil.expectedProgress; }
27 set { CompressionTestUtil.expectedProgress = value; }
28 }
29 private static IList<int[]> expectedProgress;
30
31 public void PrintArchiveProgress(object source, ArchiveProgressEventArgs e)
32 {
33 switch (e.ProgressType)
34 {
35 case ArchiveProgressType.StartFile:
36 {
37 Console.WriteLine("StartFile: {0}", e.CurrentFileName);
38 } break;
39 case ArchiveProgressType.FinishFile:
40 {
41 Console.WriteLine("FinishFile: {0}", e.CurrentFileName);
42 } break;
43 case ArchiveProgressType.StartArchive:
44 {
45 Console.WriteLine("StartArchive: {0} : {1}", e.CurrentArchiveNumber, e.CurrentArchiveName);
46 } break;
47 case ArchiveProgressType.FinishArchive:
48 {
49 Console.WriteLine("FinishArchive: {0} : {1}", e.CurrentArchiveNumber, e.CurrentArchiveName);
50 } break;
51 }
52
53 File.AppendAllText(this.progressTextFile, e.ToString().Replace("\n", Environment.NewLine));
54
55 if (CompressionTestUtil.expectedProgress != null &&
56 e.ProgressType != ArchiveProgressType.PartialFile &&
57 e.ProgressType != ArchiveProgressType.PartialArchive)
58 {
59 Assert.AreNotEqual<int>(0, CompressionTestUtil.expectedProgress.Count);
60 int[] expected = CompressionTestUtil.expectedProgress[0];
61 CompressionTestUtil.expectedProgress.RemoveAt(0);
62 Assert.AreEqual<ArchiveProgressType>((ArchiveProgressType) expected[0], e.ProgressType, "Checking ProgressType.");
63 Assert.AreEqual<int>(expected[1], e.CurrentFileNumber, "Checking CurrentFileNumber.");
64 Assert.AreEqual<int>(expected[2], e.TotalFiles, "Checking TotalFiles.");
65 Assert.AreEqual<int>(expected[4], e.CurrentArchiveNumber, "Checking CurrentArchiveNumber.");
66 Assert.AreEqual<int>(expected[5], e.TotalArchives, "Checking TotalArchives.");
67 }
68 }
69
70 public static bool CompareDirectories(string dirA, string dirB)
71 {
72 bool difference = false;
73 Console.WriteLine("Comparing directories {0}, {1}", dirA, dirB);
74
75 string[] filesA = Directory.GetFiles(dirA);
76 string[] filesB = Directory.GetFiles(dirB);
77 for (int iA = 0; iA < filesA.Length; iA++)
78 {
79 filesA[iA] = Path.GetFileName(filesA[iA]);
80 }
81 for (int iB = 0; iB < filesB.Length; iB++)
82 {
83 filesB[iB] = Path.GetFileName(filesB[iB]);
84 }
85 Array.Sort(filesA);
86 Array.Sort(filesB);
87
88 for (int iA = 0, iB = 0; iA < filesA.Length || iB < filesB.Length; )
89 {
90 int comp;
91 if (iA == filesA.Length)
92 {
93 comp = 1;
94 }
95 else if (iB == filesB.Length)
96 {
97 comp = -1;
98 }
99 else
100 {
101 comp = String.Compare(filesA[iA], filesB[iB]);
102 }
103 if (comp < 0)
104 {
105 Console.WriteLine("< " + filesA[iA]);
106 difference = true;
107 iA++;
108 }
109 else if (comp > 0)
110 {
111 Console.WriteLine("> " + filesB[iB]);
112 difference = true;
113 iB++;
114 }
115 else
116 {
117 string fileA = Path.Combine(dirA, filesA[iA]);
118 string fileB = Path.Combine(dirB, filesB[iB]);
119
120 byte[] hashA;
121 byte[] hashB;
122
123 lock (CompressionTestUtil.md5)
124 {
125 using (Stream fileAStream = File.OpenRead(fileA))
126 {
127 hashA = CompressionTestUtil.md5.ComputeHash(fileAStream);
128 }
129 using (Stream fileBStream = File.OpenRead(fileB))
130 {
131 hashB = CompressionTestUtil.md5.ComputeHash(fileBStream);
132 }
133 }
134
135 for (int i = 0; i < hashA.Length; i++)
136 {
137 if (hashA[i] != hashB[i])
138 {
139 Console.WriteLine("~ " + filesA[iA]);
140 difference = true;
141 break;
142 }
143 }
144
145 iA++;
146 iB++;
147 }
148 }
149
150 string[] dirsA = Directory.GetDirectories(dirA);
151 string[] dirsB = Directory.GetDirectories(dirB);
152 for (int iA = 0; iA < dirsA.Length; iA++)
153 {
154 dirsA[iA] = Path.GetFileName(dirsA[iA]);
155 }
156 for (int iB = 0; iB < dirsB.Length; iB++)
157 {
158 dirsB[iB] = Path.GetFileName(dirsB[iB]);
159 }
160 Array.Sort(dirsA);
161 Array.Sort(dirsB);
162
163 for (int iA = 0, iB = 0; iA < dirsA.Length || iB < dirsB.Length; )
164 {
165 int comp;
166 if (iA == dirsA.Length)
167 {
168 comp = 1;
169 }
170 else if (iB == dirsB.Length)
171 {
172 comp = -1;
173 }
174 else
175 {
176 comp = String.Compare(dirsA[iA], dirsB[iB]);
177 }
178 if (comp < 0)
179 {
180 Console.WriteLine("< {0}\\", dirsA[iA]);
181 difference = true;
182 iA++;
183 }
184 else if (comp > 0)
185 {
186 Console.WriteLine("> {1}\\", dirsB[iB]);
187 difference = true;
188 iB++;
189 }
190 else
191 {
192 string subDirA = Path.Combine(dirA, dirsA[iA]);
193 string subDirB = Path.Combine(dirB, dirsB[iB]);
194 if (!CompressionTestUtil.CompareDirectories(subDirA, subDirB))
195 {
196 difference = true;
197 }
198 iA++;
199 iB++;
200 }
201 }
202
203 return !difference;
204 }
205
206
207 public static void GenerateRandomFile(string path, int seed, long size)
208 {
209 Console.WriteLine("Generating random file {0} (seed={1}, size={2})",
210 path, seed, size);
211 Random random = new Random(seed);
212 bool easy = random.Next(2) == 1;
213 int chunk = 1024 * random.Next(1, 100);
214 using (TextWriter tw = new StreamWriter(
215 File.Create(path, 4096), Encoding.ASCII))
216 {
217 for (long count = 0; count < size; count++)
218 {
219 char c = (char) (easy ? random.Next('a', 'b' + 1)
220 : random.Next(32, 127));
221 tw.Write(c);
222 if (--chunk == 0)
223 {
224 chunk = 1024 * random.Next(1, 101);
225 easy = random.Next(2) == 1;
226 }
227 }
228 }
229 }
230
231 public static void TestArchiveInfoNullParams(
232 ArchiveInfo archiveInfo,
233 string dirA,
234 string dirB,
235 string[] files)
236 {
237 Exception caughtEx = null;
238 try
239 {
240 archiveInfo.PackFiles(null, null, files);
241 }
242 catch (Exception ex) { caughtEx = ex; }
243 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
244 caughtEx = null;
245 try
246 {
247 archiveInfo.PackFiles(null, files, new string[] { });
248 }
249 catch (Exception ex) { caughtEx = ex; }
250 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentOutOfRangeException), "Caught exception: " + caughtEx);
251 caughtEx = null;
252 try
253 {
254 archiveInfo.PackFileSet(dirA, null);
255 }
256 catch (Exception ex) { caughtEx = ex; }
257 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
258 caughtEx = null;
259 try
260 {
261 archiveInfo.PackFiles(null, files, files);
262 }
263 catch (Exception ex) { caughtEx = ex; }
264 Assert.IsInstanceOfType(caughtEx, typeof(FileNotFoundException), "Caught exception: " + caughtEx);
265 caughtEx = null;
266 try
267 {
268 archiveInfo.PackFiles(dirA, null, files);
269 }
270 catch (Exception ex) { caughtEx = ex; }
271 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
272 caughtEx = null;
273 try
274 {
275 archiveInfo.PackFiles(dirA, files, null);
276 }
277 catch (Exception ex) { caughtEx = ex; }
278 Assert.IsNull(caughtEx, "Caught exception: " + caughtEx);
279
280 caughtEx = null;
281 try
282 {
283 archiveInfo.CopyTo(null);
284 }
285 catch (Exception ex) { caughtEx = ex; }
286 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
287 caughtEx = null;
288 try
289 {
290 archiveInfo.CopyTo(null, true);
291 }
292 catch (Exception ex) { caughtEx = ex; }
293 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
294 caughtEx = null;
295 try
296 {
297 archiveInfo.MoveTo(null);
298 }
299 catch (Exception ex) { caughtEx = ex; }
300 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
301 caughtEx = null;
302 try
303 {
304 archiveInfo.GetFiles(null);
305 }
306 catch (Exception ex) { caughtEx = ex; }
307 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
308 caughtEx = null;
309 try
310 {
311 archiveInfo.UnpackFile(null, "test.txt");
312 }
313 catch (Exception ex) { caughtEx = ex; }
314 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
315 caughtEx = null;
316 try
317 {
318 archiveInfo.UnpackFile("test.txt", null);
319 }
320 catch (Exception ex) { caughtEx = ex; }
321 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
322 caughtEx = null;
323 try
324 {
325 archiveInfo.UnpackFiles(null, dirB, files);
326 }
327 catch (Exception ex) { caughtEx = ex; }
328 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
329 caughtEx = null;
330 try
331 {
332 archiveInfo.UnpackFiles(files, null, null);
333 }
334 catch (Exception ex) { caughtEx = ex; }
335 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
336 caughtEx = null;
337 try
338 {
339 archiveInfo.UnpackFiles(files, null, files);
340 }
341 catch (Exception ex) { caughtEx = ex; }
342 Assert.IsNull(caughtEx, "Caught exception: " + caughtEx);
343 caughtEx = null;
344 try
345 {
346 archiveInfo.UnpackFiles(files, dirB, null);
347 }
348 catch (Exception ex) { caughtEx = ex; }
349 Assert.IsNull(caughtEx, "Caught exception: " + caughtEx);
350 caughtEx = null;
351 try
352 {
353 archiveInfo.UnpackFiles(files, dirB, new string[] { });
354 }
355 catch (Exception ex) { caughtEx = ex; }
356 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentOutOfRangeException), "Caught exception: " + caughtEx);
357 caughtEx = null;
358 try
359 {
360 archiveInfo.UnpackFileSet(null, dirB);
361 }
362 catch (Exception ex) { caughtEx = ex; }
363 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
364 }
365
366 public static void TestCompressionEngineNullParams(
367 CompressionEngine engine,
368 ArchiveFileStreamContext streamContext,
369 string[] testFiles)
370 {
371 Exception caughtEx;
372
373 Console.WriteLine("Testing null streamContext.");
374 caughtEx = null;
375 try
376 {
377 engine.Pack(null, testFiles);
378 }
379 catch (Exception ex) { caughtEx = ex; }
380 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
381 caughtEx = null;
382 try
383 {
384 engine.Pack(null, testFiles, 0);
385 }
386 catch (Exception ex) { caughtEx = ex; }
387 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
388
389 Console.WriteLine("Testing null files.");
390 caughtEx = null;
391 try
392 {
393 engine.Pack(streamContext, null);
394 }
395 catch (Exception ex) { caughtEx = ex; }
396 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
397
398 Console.WriteLine("Testing null files.");
399 caughtEx = null;
400 try
401 {
402 engine.Pack(streamContext, null, 0);
403 }
404 catch (Exception ex) { caughtEx = ex; }
405 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
406
407
408 Console.WriteLine("Testing null stream.");
409 caughtEx = null;
410 try
411 {
412 engine.IsArchive(null);
413 }
414 catch (Exception ex) { caughtEx = ex; }
415 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
416 caughtEx = null;
417 try
418 {
419 engine.FindArchiveOffset(null);
420 }
421 catch (Exception ex) { caughtEx = ex; }
422 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
423 caughtEx = null;
424 try
425 {
426 engine.GetFiles(null);
427 }
428 catch (Exception ex) { caughtEx = ex; }
429 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
430 caughtEx = null;
431 try
432 {
433 engine.GetFileInfo(null);
434 }
435 catch (Exception ex) { caughtEx = ex; }
436 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
437 caughtEx = null;
438 try
439 {
440 engine.Unpack(null, "testUnpack.txt");
441 }
442 catch (Exception ex) { caughtEx = ex; }
443 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
444 Console.WriteLine("Testing null streamContext.");
445 caughtEx = null;
446 try
447 {
448 engine.GetFiles(null, null);
449 }
450 catch (Exception ex) { caughtEx = ex; }
451 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
452 caughtEx = null;
453 try
454 {
455 engine.GetFileInfo(null, null);
456 }
457 catch (Exception ex) { caughtEx = ex; }
458 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
459 caughtEx = null;
460 try
461 {
462 engine.Unpack((IUnpackStreamContext) null, null);
463 }
464 catch (Exception ex) { caughtEx = ex; }
465 Assert.IsInstanceOfType(caughtEx, typeof(ArgumentNullException), "Caught exception: " + caughtEx);
466 }
467
468 public static void TestBadPackStreamContexts(
469 CompressionEngine engine, string archiveName, string[] testFiles)
470 {
471 Exception caughtEx;
472
473 Console.WriteLine("Testing streamContext that returns null from GetName.");
474 caughtEx = null;
475 try
476 {
477 engine.Pack(
478 new MisbehavingStreamContext(archiveName, null, null, false, false, true, true, true, true),
479 testFiles);
480 }
481 catch (Exception ex) { caughtEx = ex; }
482 Assert.IsTrue(caughtEx is FileNotFoundException, "Caught exception: " + caughtEx);
483 Console.WriteLine("Testing streamContext that returns null from OpenArchive.");
484 caughtEx = null;
485 try
486 {
487 engine.Pack(
488 new MisbehavingStreamContext(archiveName, null, null, false, true, false, true, true, true),
489 testFiles);
490 }
491 catch (Exception ex) { caughtEx = ex; }
492 Assert.IsTrue(caughtEx is FileNotFoundException, "Caught exception: " + caughtEx);
493 Console.WriteLine("Testing streamContext that returns null from OpenFile.");
494 caughtEx = null;
495 try
496 {
497 engine.Pack(
498 new MisbehavingStreamContext(archiveName, null, null, false, true, true, true, false, true),
499 testFiles);
500 }
501 catch (Exception ex) { caughtEx = ex; }
502 Assert.IsNull(caughtEx, "Caught exception: " + caughtEx);
503 Console.WriteLine("Testing streamContext that throws on GetName.");
504 caughtEx = null;
505 try
506 {
507 engine.Pack(
508 new MisbehavingStreamContext(archiveName, null, null, true, false, true, true, true, true),
509 testFiles);
510 }
511 catch (Exception ex) { caughtEx = ex; }
512 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
513 Console.WriteLine("Testing streamContext that throws on OpenArchive.");
514 caughtEx = null;
515 try
516 {
517 engine.Pack(
518 new MisbehavingStreamContext(archiveName, null, null, true, true, false, true, true, true),
519 testFiles);
520 }
521 catch (Exception ex) { caughtEx = ex; }
522 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
523 Console.WriteLine("Testing streamContext that throws on CloseArchive.");
524 caughtEx = null;
525 try
526 {
527 engine.Pack(
528 new MisbehavingStreamContext(archiveName, null, null, true, true, true, false, true, true),
529 testFiles);
530 }
531 catch (Exception ex) { caughtEx = ex; }
532 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
533 Console.WriteLine("Testing streamContext that throws on OpenFile.");
534 caughtEx = null;
535 try
536 {
537 engine.Pack(
538 new MisbehavingStreamContext(archiveName, null, null, true, true, true, true, false, true),
539 testFiles);
540 }
541 catch (Exception ex) { caughtEx = ex; }
542 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
543 Console.WriteLine("Testing streamContext that throws on CloseFile.");
544 caughtEx = null;
545 try
546 {
547 engine.Pack(
548 new MisbehavingStreamContext(archiveName, null, null, true, true, true, true, true, false),
549 testFiles);
550 }
551 catch (Exception ex) { caughtEx = ex; }
552 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
553 }
554
555 public static void TestBadUnpackStreamContexts(
556 CompressionEngine engine, string archiveName)
557 {
558 Exception caughtEx;
559
560 Console.WriteLine("Testing streamContext that returns null from OpenArchive.");
561 caughtEx = null;
562 try
563 {
564 engine.Unpack(new MisbehavingStreamContext(archiveName, null, null, false, true, false, true, true, true), null);
565 }
566 catch (Exception ex) { caughtEx = ex; }
567 Assert.IsInstanceOfType(caughtEx, typeof(FileNotFoundException), "Caught exception: " + caughtEx);
568 Console.WriteLine("Testing streamContext that returns null from OpenFile.");
569 caughtEx = null;
570 try
571 {
572 engine.Unpack(new MisbehavingStreamContext(archiveName, null, null, false, true, true, true, false, true), null);
573 }
574 catch (Exception ex) { caughtEx = ex; }
575 Assert.IsNull(caughtEx, "Caught exception: " + caughtEx);
576 Console.WriteLine("Testing streamContext that throws on OpenArchive.");
577 caughtEx = null;
578 try
579 {
580 engine.Unpack(new MisbehavingStreamContext(archiveName, null, null, true, true, false, true, true, true), null);
581 }
582 catch (Exception ex) { caughtEx = ex; }
583 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
584 Console.WriteLine("Testing streamContext that throws on CloseArchive.");
585 caughtEx = null;
586 try
587 {
588 engine.Unpack(new MisbehavingStreamContext(archiveName, null, null, true, true, true, false, true, true), null);
589 }
590 catch (Exception ex) { caughtEx = ex; }
591 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
592 Console.WriteLine("Testing streamContext that throws on OpenFile.");
593 caughtEx = null;
594 try
595 {
596 engine.Unpack(new MisbehavingStreamContext(archiveName, null, null, true, true, true, true, false, true), null);
597 }
598 catch (Exception ex) { caughtEx = ex; }
599 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
600 Console.WriteLine("Testing streamContext that throws on CloseFile.");
601 caughtEx = null;
602 try
603 {
604 engine.Unpack(new MisbehavingStreamContext(archiveName, null, null, true, true, true, true, true, false), null);
605 }
606 catch (Exception ex) { caughtEx = ex; }
607 Assert.IsTrue(caughtEx != null && caughtEx.Message == MisbehavingStreamContext.EXCEPTION, "Caught exception: " + caughtEx);
608 }
609
610 public static void TestTruncatedArchive(
611 ArchiveInfo archiveInfo, Type expectedExceptionType)
612 {
613 for (long len = archiveInfo.Length - 1; len >= 0; len--)
614 {
615 string testArchive = String.Format("{0}.{1:d06}",
616 archiveInfo.FullName, len);
617 if (File.Exists(testArchive))
618 {
619 File.Delete(testArchive);
620 }
621
622 archiveInfo.CopyTo(testArchive);
623 using (FileStream truncateStream =
624 File.Open(testArchive, FileMode.Open, FileAccess.ReadWrite))
625 {
626 truncateStream.SetLength(len);
627 }
628
629 ArchiveInfo testArchiveInfo = (ArchiveInfo) archiveInfo.GetType()
630 .GetConstructor(new Type[] { typeof(string) }).Invoke(new object[] { testArchive });
631
632 Exception caughtEx = null;
633 try
634 {
635 testArchiveInfo.GetFiles();
636 }
637 catch (Exception ex) { caughtEx = ex; }
638 File.Delete(testArchive);
639
640 if (caughtEx != null)
641 {
642 Assert.IsInstanceOfType(caughtEx, expectedExceptionType,
643 String.Format("Caught exception listing archive truncated to {0}/{1} bytes",
644 len, archiveInfo.Length));
645 }
646 }
647 }
648 }
649}
diff --git a/src/dtf/WixToolsetTests.Dtf.Compression/MisbehavingStreamContext.cs b/src/dtf/WixToolsetTests.Dtf.Compression/MisbehavingStreamContext.cs
new file mode 100644
index 00000000..2531f3bc
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.Compression/MisbehavingStreamContext.cs
@@ -0,0 +1,202 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3using System;
4using System.IO;
5using System.Collections.Generic;
6using WixToolset.Dtf.Compression;
7
8namespace WixToolset.Dtf.Test
9{
10 public class MisbehavingStreamContext : ArchiveFileStreamContext
11 {
12 public const string EXCEPTION = "Test exception.";
13
14 private bool throwEx;
15 private bool getName;
16 private bool openArchive;
17 private bool closeArchive;
18 private bool openFile;
19 private bool closeFile;
20 private int closeFileCount;
21
22 public MisbehavingStreamContext(
23 string cabinetFile,
24 string directory,
25 IDictionary<string, string> files,
26 bool throwEx,
27 bool getName,
28 bool openArchive,
29 bool closeArchive,
30 bool openFile,
31 bool closeFile)
32 : base(cabinetFile, directory, files)
33 {
34 this.throwEx = throwEx;
35 this.getName = getName;
36 this.openArchive = openArchive;
37 this.closeArchive = closeArchive;
38 this.openFile = openFile;
39 this.closeFile = closeFile;
40 }
41
42 public override string GetArchiveName(int archiveNumber)
43 {
44 if (!this.getName)
45 {
46 if (throwEx)
47 {
48 throw new Exception(EXCEPTION);
49 }
50 else
51 {
52 return null;
53 }
54 }
55 return base.GetArchiveName(archiveNumber);
56 }
57
58 public override Stream OpenArchiveWriteStream(
59 int archiveNumber,
60 string archiveName,
61 bool truncate,
62 CompressionEngine compressionEngine)
63 {
64 if (!this.openArchive)
65 {
66 if (throwEx)
67 {
68 throw new Exception(EXCEPTION);
69 }
70 else
71 {
72 return null;
73 }
74 }
75 return base.OpenArchiveWriteStream(
76 archiveNumber, archiveName, truncate, compressionEngine);
77 }
78
79 public override void CloseArchiveWriteStream(
80 int archiveNumber,
81 string archiveName,
82 Stream stream)
83 {
84 if (!this.closeArchive)
85 {
86 if (throwEx)
87 {
88 this.closeArchive = true;
89 throw new Exception(EXCEPTION);
90 }
91 return;
92 }
93 base.CloseArchiveWriteStream(archiveNumber, archiveName, stream);
94 }
95
96 public override Stream OpenFileReadStream(
97 string path,
98 out FileAttributes attributes,
99 out DateTime lastWriteTime)
100 {
101 if (!this.openFile)
102 {
103 if (throwEx)
104 {
105 throw new Exception(EXCEPTION);
106 }
107 else
108 {
109 attributes = FileAttributes.Normal;
110 lastWriteTime = DateTime.MinValue;
111 return null;
112 }
113 }
114 return base.OpenFileReadStream(path, out attributes, out lastWriteTime);
115 }
116
117 public override void CloseFileReadStream(string path, Stream stream)
118 {
119 if (!this.closeFile && ++closeFileCount == 2)
120 {
121 if (throwEx)
122 {
123 throw new Exception(EXCEPTION);
124 }
125 return;
126 }
127 base.CloseFileReadStream(path, stream);
128 }
129
130 public override Stream OpenArchiveReadStream(
131 int archiveNumber,
132 string archiveName,
133 CompressionEngine compressionEngine)
134 {
135 if (!this.openArchive)
136 {
137 if (throwEx)
138 {
139 throw new Exception(EXCEPTION);
140 }
141 else
142 {
143 return null;
144 }
145 }
146 return base.OpenArchiveReadStream(archiveNumber, archiveName, compressionEngine);
147 }
148
149 public override void CloseArchiveReadStream(
150 int archiveNumber,
151 string archiveName,
152 Stream stream)
153 {
154 if (!this.closeArchive)
155 {
156 if (throwEx)
157 {
158 this.closeArchive = true;
159 throw new Exception(EXCEPTION);
160 }
161 return;
162 }
163 base.CloseArchiveReadStream(archiveNumber, archiveName, stream);
164 }
165
166 public override Stream OpenFileWriteStream(
167 string path,
168 long fileSize,
169 DateTime lastWriteTime)
170 {
171 if (!this.openFile)
172 {
173 if (throwEx)
174 {
175 throw new Exception(EXCEPTION);
176 }
177 else
178 {
179 return null;
180 }
181 }
182 return base.OpenFileWriteStream(path, fileSize, lastWriteTime);
183 }
184
185 public override void CloseFileWriteStream(
186 string path,
187 Stream stream,
188 FileAttributes attributes,
189 DateTime lastWriteTime)
190 {
191 if (!this.closeFile && ++closeFileCount == 2)
192 {
193 if (throwEx)
194 {
195 throw new Exception(EXCEPTION);
196 }
197 return;
198 }
199 base.CloseFileWriteStream(path, stream, attributes, lastWriteTime);
200 }
201 }
202}
diff --git a/src/dtf/WixToolsetTests.Dtf.Compression/OptionStreamContext.cs b/src/dtf/WixToolsetTests.Dtf.Compression/OptionStreamContext.cs
new file mode 100644
index 00000000..98354d97
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.Compression/OptionStreamContext.cs
@@ -0,0 +1,42 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3using System;
4using System.Collections.Generic;
5using WixToolset.Dtf.Compression;
6
7namespace WixToolset.Dtf.Test
8{
9 public class OptionStreamContext : ArchiveFileStreamContext
10 {
11 private PackOptionHandler packOptionHandler;
12
13 public OptionStreamContext(IList<string> archiveFiles, string directory, IDictionary<string, string> files)
14 : base(archiveFiles, directory, files)
15 {
16 }
17
18 public delegate object PackOptionHandler(string optionName, object[] parameters);
19
20 public PackOptionHandler OptionHandler
21 {
22 get
23 {
24 return this.packOptionHandler;
25 }
26 set
27 {
28 this.packOptionHandler = value;
29 }
30 }
31
32 public override object GetOption(string optionName, object[] parameters)
33 {
34 if (this.OptionHandler == null)
35 {
36 return null;
37 }
38
39 return this.OptionHandler(optionName, parameters);
40 }
41 }
42}
diff --git a/src/dtf/WixToolsetTests.Dtf.Compression/WixToolsetTests.Dtf.Compression.csproj b/src/dtf/WixToolsetTests.Dtf.Compression/WixToolsetTests.Dtf.Compression.csproj
new file mode 100644
index 00000000..120779ee
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.Compression/WixToolsetTests.Dtf.Compression.csproj
@@ -0,0 +1,31 @@
1<?xml version="1.0" encoding="utf-8" ?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
5 <PropertyGroup>
6 <ProjectGuid>{F045FFC1-05F9-4EA2-9F03-E1CBDB7BC4F9}</ProjectGuid>
7 <OutputType>Library</OutputType>
8 <RootNamespace>WixToolsetTests.Dtf</RootNamespace>
9 <AssemblyName>WixToolsetTests.Dtf.Compression</AssemblyName>
10 <CreateDocumentation>false</CreateDocumentation>
11 </PropertyGroup>
12
13 <ItemGroup>
14 <Compile Include="CompressionTestUtil.cs" />
15 <Compile Include="MisbehavingStreamContext.cs" />
16 <Compile Include="OptionStreamContext.cs" />
17 </ItemGroup>
18
19 <ItemGroup>
20 <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
21 <Reference Include="System" />
22 <Reference Include="System.Data" />
23 <Reference Include="System.Xml" />
24 </ItemGroup>
25
26 <ItemGroup>
27 <ProjectReference Include="..\WixToolset.Dtf.Compression\WixToolset.Dtf.Compression.csproj" />
28 </ItemGroup>
29
30 <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
31</Project>
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.CustomActions/CustomActionTest.cs b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.CustomActions/CustomActionTest.cs
new file mode 100644
index 00000000..bf843024
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.CustomActions/CustomActionTest.cs
@@ -0,0 +1,206 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Test
4{
5 using System;
6 using System.IO;
7 using System.Text;
8 using System.Collections.Generic;
9 using Microsoft.VisualStudio.TestTools.UnitTesting;
10 using WixToolset.Dtf.WindowsInstaller;
11
12 [TestClass]
13 public class CustomActionTest
14 {
15 public CustomActionTest()
16 {
17 }
18
19 [TestMethod]
20 [Ignore] // Currently fails.
21 public void CustomActionTest1()
22 {
23 InstallLogModes logEverything =
24 InstallLogModes.FatalExit |
25 InstallLogModes.Error |
26 InstallLogModes.Warning |
27 InstallLogModes.User |
28 InstallLogModes.Info |
29 InstallLogModes.ResolveSource |
30 InstallLogModes.OutOfDiskSpace |
31 InstallLogModes.ActionStart |
32 InstallLogModes.ActionData |
33 InstallLogModes.CommonData |
34 InstallLogModes.Progress |
35 InstallLogModes.Initialize |
36 InstallLogModes.Terminate |
37 InstallLogModes.ShowDialog;
38
39 Installer.SetInternalUI(InstallUIOptions.Silent);
40 ExternalUIHandler prevHandler = Installer.SetExternalUI(
41 WindowsInstallerTest.ExternalUILogger, logEverything);
42
43 try
44 {
45 string[] customActions = new string[] { "SampleCA1", "SampleCA2" };
46 #if DEBUG
47 string caDir = @"..\..\..\..\..\build\debug\x86\";
48 #else
49 string caDir = @"..\..\..\..\..\build\ship\x86\";
50 #endif
51 caDir = Path.GetFullPath(caDir);
52 string caFile = "WixToolset.Dtf.Samples.ManagedCA.dll";
53 string caProduct = "CustomActionTest.msi";
54
55 this.CreateCustomActionProduct(caProduct, caDir + caFile, customActions, false);
56
57 Exception caughtEx = null;
58 try
59 {
60 Installer.InstallProduct(caProduct, String.Empty);
61 }
62 catch (Exception ex) { caughtEx = ex; }
63 Assert.IsInstanceOfType(caughtEx, typeof(InstallCanceledException),
64 "Exception thrown while installing product: " + caughtEx);
65
66 string arch = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
67 string arch2 = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432");
68 if (arch == "AMD64" || arch2 == "AMD64")
69 {
70 caDir = caDir.Replace("x86", "x64");
71
72 this.CreateCustomActionProduct(caProduct, caDir + caFile, customActions, true);
73
74 caughtEx = null;
75 try
76 {
77 Installer.InstallProduct(caProduct, String.Empty);
78 }
79 catch (Exception ex) { caughtEx = ex; }
80 Assert.IsInstanceOfType(caughtEx, typeof(InstallCanceledException),
81 "Exception thrown while installing 64bit product: " + caughtEx);
82 }
83 }
84 finally
85 {
86 Installer.SetExternalUI(prevHandler, InstallLogModes.None);
87 }
88 }
89
90 private void CreateCustomActionProduct(
91 string msiFile, string customActionFile, IList<string> customActions, bool sixtyFourBit)
92 {
93 using (Database db = new Database(msiFile, DatabaseOpenMode.CreateDirect))
94 {
95 WindowsInstallerUtils.InitializeProductDatabase(db, sixtyFourBit);
96 WindowsInstallerUtils.CreateTestProduct(db);
97
98 if (!File.Exists(customActionFile))
99 throw new FileNotFoundException(customActionFile);
100
101 using (Record binRec = new Record(2))
102 {
103 binRec[1] = Path.GetFileName(customActionFile);
104 binRec.SetStream(2, customActionFile);
105
106 db.Execute("INSERT INTO `Binary` (`Name`, `Data`) VALUES (?, ?)", binRec);
107 }
108
109 using (Record binRec2 = new Record(2))
110 {
111 binRec2[1] = "TestData";
112 binRec2.SetStream(2, new MemoryStream(Encoding.UTF8.GetBytes("This is a test data stream.")));
113
114 db.Execute("INSERT INTO `Binary` (`Name`, `Data`) VALUES (?, ?)", binRec2);
115 }
116
117 for (int i = 0; i < customActions.Count; i++)
118 {
119 db.Execute(
120 "INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES ('{0}', 1, '{1}', '{2}')",
121 customActions[i],
122 Path.GetFileName(customActionFile),
123 customActions[i]);
124 db.Execute(
125 "INSERT INTO `InstallExecuteSequence` (`Action`, `Condition`, `Sequence`) VALUES ('{0}', '', {1})",
126 customActions[i],
127 101 + i);
128 }
129
130 db.Execute("INSERT INTO `Property` (`Property`, `Value`) VALUES ('SampleCATest', 'TestValue')");
131
132 db.Commit();
133 }
134 }
135
136 [TestMethod]
137 public void CustomActionData()
138 {
139 string dataString = "Key1=Value1;Key2=;Key3;Key4=Value=4;Key5";
140 string dataString2 = "Key1=;Key2=Value2;Key3;Key4;Key6=Value;;6=6;Key7=Value7";
141
142 CustomActionData data = new CustomActionData(dataString);
143 Assert.AreEqual<string>(dataString, data.ToString());
144
145 data["Key1"] = String.Empty;
146 data["Key2"] = "Value2";
147 data["Key4"] = null;
148 data.Remove("Key5");
149 data["Key6"] = "Value;6=6";
150 data["Key7"] = "Value7";
151
152 Assert.AreEqual<string>(dataString2, data.ToString());
153
154 MyDataClass myData = new MyDataClass();
155 myData.Member1 = "test1";
156 myData.Member2 = "test2";
157 data.AddObject("MyData", myData);
158
159 string myDataString = data.ToString();
160 CustomActionData data2 = new CustomActionData(myDataString);
161
162 MyDataClass myData2 = data2.GetObject<MyDataClass>("MyData");
163 Assert.AreEqual<MyDataClass>(myData, myData2);
164
165 List<string> myComplexDataObject = new List<string>();
166 myComplexDataObject.Add("CValue1");
167 myComplexDataObject.Add("CValue2");
168 myComplexDataObject.Add("CValue3");
169
170 CustomActionData myComplexData = new CustomActionData();
171 myComplexData.AddObject("MyComplexData", myComplexDataObject);
172 myComplexData.AddObject("NestedData", data);
173 string myComplexDataString = myComplexData.ToString();
174
175 CustomActionData myComplexData2 = new CustomActionData(myComplexDataString);
176 List<string> myComplexDataObject2 = myComplexData2.GetObject<List<string>>("MyComplexData");
177
178 Assert.AreEqual<int>(myComplexDataObject.Count, myComplexDataObject2.Count);
179 for (int i = 0; i < myComplexDataObject.Count; i++)
180 {
181 Assert.AreEqual<string>(myComplexDataObject[i], myComplexDataObject2[i]);
182 }
183
184 data2 = myComplexData2.GetObject<CustomActionData>("NestedData");
185 Assert.AreEqual<string>(data.ToString(), data2.ToString());
186 }
187
188 public class MyDataClass
189 {
190 public string Member1;
191 public string Member2;
192
193 public override bool Equals(object obj)
194 {
195 MyDataClass other = obj as MyDataClass;
196 return other != null && this.Member1 == other.Member1 && this.Member2 == other.Member2;
197 }
198
199 public override int GetHashCode()
200 {
201 return (this.Member1 != null ? this.Member1.GetHashCode() : 0) ^
202 (this.Member2 != null ? this.Member2.GetHashCode() : 0);
203 }
204 }
205 }
206}
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.CustomActions/WixToolsetTests.Dtf.WindowsInstaller.CustomActions.csproj b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.CustomActions/WixToolsetTests.Dtf.WindowsInstaller.CustomActions.csproj
new file mode 100644
index 00000000..aadd4592
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.CustomActions/WixToolsetTests.Dtf.WindowsInstaller.CustomActions.csproj
@@ -0,0 +1,28 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4
5<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
6 <PropertyGroup>
7 <ProjectGuid>{137D376B-989F-4FEA-9A67-01D8D38CA0DE}</ProjectGuid>
8 <OutputType>Library</OutputType>
9 <RootNamespace>WixToolsetTests.Dtf</RootNamespace>
10 <AssemblyName>WixToolsetTests.Dtf.WindowsInstaller.CustomActions</AssemblyName>
11 <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
12 <CreateDocumentation>false</CreateDocumentation>
13 </PropertyGroup>
14
15 <ItemGroup>
16 <Compile Include="CustomActionTest.cs" />
17 </ItemGroup>
18
19 <ItemGroup>
20 <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
21 <Reference Include="System" />
22 <Reference Include="System.Windows.Forms" />
23 <Reference Include="$(OutputPath)\WixToolset.Dtf.WindowsInstaller.dll" />
24 <ProjectReference Include="..\WixToolsetTests.Dtf.WindowsInstaller\WixToolsetTests.Dtf.WindowsInstaller.csproj" />
25 </ItemGroup>
26
27 <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
28</Project>
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.Linq/LinqTest.cs b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.Linq/LinqTest.cs
new file mode 100644
index 00000000..7776a1c3
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.Linq/LinqTest.cs
@@ -0,0 +1,509 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3using System;
4using System.Text;
5using System.Collections;
6using System.Collections.Generic;
7using System.Linq;
8using System.Linq.Expressions;
9using Microsoft.VisualStudio.TestTools.UnitTesting;
10using WixToolset.Dtf.WindowsInstaller;
11using WixToolset.Dtf.WindowsInstaller.Linq;
12
13namespace WixToolset.Dtf.Test
14{
15 [TestClass]
16 public class LinqTest
17 {
18 private void InitLinqTestDatabase(QDatabase db)
19 {
20 WindowsInstallerUtils.InitializeProductDatabase(db);
21 WindowsInstallerUtils.CreateTestProduct(db);
22
23 db.Execute(
24 "INSERT INTO `Feature` (`Feature`, `Title`, `Description`, `Level`, `Attributes`) VALUES ('{0}', '{1}', '{2}', {3}, {4})",
25 "TestFeature2",
26 "Test Feature 2",
27 "Test Feature 2 Description",
28 1,
29 (int) FeatureAttributes.None);
30
31 WindowsInstallerUtils.AddRegistryComponent(
32 db, "TestFeature2", "MyTestRegComp",
33 Guid.NewGuid().ToString("B"),
34 "SOFTWARE\\Microsoft\\DTF\\Test",
35 "MyTestRegComp", "test");
36 WindowsInstallerUtils.AddRegistryComponent(
37 db, "TestFeature2", "MyTestRegComp2",
38 Guid.NewGuid().ToString("B"),
39 "SOFTWARE\\Microsoft\\DTF\\Test",
40 "MyTestRegComp2", "test2");
41 WindowsInstallerUtils.AddRegistryComponent(
42 db, "TestFeature2", "excludeComp",
43 Guid.NewGuid().ToString("B"),
44 "SOFTWARE\\Microsoft\\DTF\\Test",
45 "MyTestRegComp3", "test3");
46
47 db.Commit();
48
49 db.Log = Console.Out;
50 }
51
52 [TestMethod]
53 public void LinqSimple()
54 {
55 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
56 {
57 this.InitLinqTestDatabase(db);
58
59 var comps = from c in db.Components
60 select c;
61
62 int count = 0;
63 foreach (var c in comps)
64 {
65 Console.WriteLine(c);
66 count++;
67 }
68
69 Assert.AreEqual<int>(4, count);
70 }
71 }
72
73 [TestMethod]
74 public void LinqWhereNull()
75 {
76 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
77 {
78 this.InitLinqTestDatabase(db);
79
80 var features = from f in db.Features
81 where f.Description != null
82 select f;
83
84 int count = 0;
85 foreach (var f in features)
86 {
87 Console.WriteLine(f);
88 Assert.AreEqual<string>("TestFeature2", f.Feature);
89 count++;
90 }
91
92 Assert.AreEqual<int>(1, count);
93
94 var features2 = from f in db.Features
95 where f.Description == null
96 select f;
97
98 count = 0;
99 foreach (var f in features2)
100 {
101 Console.WriteLine(f);
102 Assert.AreEqual<string>("TestFeature1", f.Feature);
103 count++;
104 }
105
106 Assert.AreEqual<int>(1, count);
107 }
108 }
109
110 [TestMethod]
111 public void LinqWhereOperators()
112 {
113 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
114 {
115 this.InitLinqTestDatabase(db);
116
117 for (int i = 0; i < 100; i++)
118 {
119 var newFile = db.Files.NewRecord();
120 newFile.File = "TestFile" + i;
121 newFile.Component_ = "TestComponent";
122 newFile.FileName = "TestFile" + i + ".txt";
123 newFile.FileSize = i % 10;
124 newFile.Sequence = i;
125 newFile.Insert();
126 }
127
128 var files1 = from f in db.Files where f.Sequence < 40 select f;
129 Assert.AreEqual<int>(40, files1.AsEnumerable().Count());
130
131 var files2 = from f in db.Files where f.Sequence <= 40 select f;
132 Assert.AreEqual<int>(41, files2.AsEnumerable().Count());
133
134 var files3 = from f in db.Files where f.Sequence > 40 select f;
135 Assert.AreEqual<int>(59, files3.AsEnumerable().Count());
136
137 var files4 = from f in db.Files where f.Sequence >= 40 select f;
138 Assert.AreEqual<int>(60, files4.AsEnumerable().Count());
139
140 var files5 = from f in db.Files where 40 < f.Sequence select f;
141 Assert.AreEqual<int>(59, files5.AsEnumerable().Count());
142
143 var files6 = from f in db.Files where 40 <= f.Sequence select f;
144 Assert.AreEqual<int>(60, files6.AsEnumerable().Count());
145
146 var files7 = from f in db.Files where 40 > f.Sequence select f;
147 Assert.AreEqual<int>(40, files7.AsEnumerable().Count());
148
149 var files8 = from f in db.Files where 40 >= f.Sequence select f;
150 Assert.AreEqual<int>(41, files8.AsEnumerable().Count());
151
152 var files9 = from f in db.Files where f.Sequence == 40 select f;
153 Assert.AreEqual<int>(40, files9.AsEnumerable().First().Sequence);
154
155 var files10 = from f in db.Files where f.Sequence != 40 select f;
156 Assert.AreEqual<int>(99, files10.AsEnumerable().Count());
157
158 var files11 = from f in db.Files where 40 == f.Sequence select f;
159 Assert.AreEqual<int>(40, files11.AsEnumerable().First().Sequence);
160
161 var files12 = from f in db.Files where 40 != f.Sequence select f;
162 Assert.AreEqual<int>(99, files12.AsEnumerable().Count());
163 }
164 }
165
166 [TestMethod]
167 public void LinqShapeSelect()
168 {
169 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
170 {
171 this.InitLinqTestDatabase(db);
172
173 Console.WriteLine("Running LINQ query 1.");
174 var features1 = from f in db.Features
175 select new { Name = f.Feature,
176 Desc = f.Description };
177
178 int count = 0;
179 foreach (var f in features1)
180 {
181 Console.WriteLine(f);
182 count++;
183 }
184
185 Assert.AreEqual<int>(2, count);
186
187 Console.WriteLine();
188 Console.WriteLine("Running LINQ query 2.");
189 var features2 = from f in db.Features
190 where f.Description != null
191 select new { Name = f.Feature,
192 Desc = f.Description.ToLower() };
193
194 count = 0;
195 foreach (var f in features2)
196 {
197 Console.WriteLine(f);
198 Assert.AreEqual<string>("TestFeature2", f.Name);
199 count++;
200 }
201
202 Assert.AreEqual<int>(1, count);
203 }
204 }
205
206 [TestMethod]
207 public void LinqUpdateNullableString()
208 {
209 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
210 {
211 this.InitLinqTestDatabase(db);
212
213 string newDescription = "New updated feature description.";
214
215 var features = from f in db.Features
216 where f.Description != null
217 select f;
218
219 int count = 0;
220 foreach (var f in features)
221 {
222 Console.WriteLine(f);
223 Assert.AreEqual<string>("TestFeature2", f.Feature);
224 f.Description = newDescription;
225 count++;
226 }
227
228 Assert.AreEqual<int>(1, count);
229
230 var features2 = from f in db.Features
231 where f.Description == newDescription
232 select f;
233 count = 0;
234 foreach (var f in features2)
235 {
236 Console.WriteLine(f);
237 Assert.AreEqual<string>("TestFeature2", f.Feature);
238 f.Description = null;
239 count++;
240 }
241
242 Assert.AreEqual<int>(1, count);
243
244 var features3 = from f in db.Features
245 where f.Description == null
246 select f.Feature;
247 count = 0;
248 foreach (var f in features3)
249 {
250 Console.WriteLine(f);
251 count++;
252 }
253
254 Assert.AreEqual<int>(2, count);
255
256 db.Commit();
257 }
258 }
259
260 [TestMethod]
261 public void LinqInsertDelete()
262 {
263 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
264 {
265 this.InitLinqTestDatabase(db);
266
267 var newProp = db.Properties.NewRecord();
268 newProp.Property = "TestNewProp1";
269 newProp.Value = "TestNewValue";
270 newProp.Insert();
271
272 string prop = (from p in db.Properties
273 where p.Property == "TestNewProp1"
274 select p.Value).AsEnumerable().First();
275 Assert.AreEqual<string>("TestNewValue", prop);
276
277 newProp.Delete();
278
279 int propCount = (from p in db.Properties
280 where p.Property == "TestNewProp1"
281 select p.Value).AsEnumerable().Count();
282 Assert.AreEqual<int>(0, propCount);
283
284 db.Commit();
285 }
286 }
287
288 [TestMethod]
289 public void LinqQueryQRecord()
290 {
291 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
292 {
293 this.InitLinqTestDatabase(db);
294
295 var installFilesSeq = (from a in db["InstallExecuteSequence"]
296 where a["Action"] == "InstallFiles"
297 select a["Sequence"]).AsEnumerable().First();
298 Assert.AreEqual<string>("4000", installFilesSeq);
299 }
300 }
301
302 [TestMethod]
303 public void LinqOrderBy()
304 {
305 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
306 {
307 this.InitLinqTestDatabase(db);
308
309 var actions = from a in db.InstallExecuteSequences
310 orderby a.Sequence
311 select a.Action;
312 foreach (var a in actions)
313 {
314 Console.WriteLine(a);
315 }
316
317 var files = from f in db.Files
318 orderby f.FileSize, f["Sequence"]
319 where f.Attributes == FileAttributes.None
320 select f;
321
322 foreach (var f in files)
323 {
324 Console.WriteLine(f);
325 }
326 }
327 }
328
329 [TestMethod]
330 public void LinqTwoWayJoin()
331 {
332 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
333 {
334 this.InitLinqTestDatabase(db);
335 int count;
336
337 var regs = from r in db.Registries
338 join c in db["Component"] on r.Component_ equals c["Component"]
339 where c["Component"] == "MyTestRegComp" &&
340 r.Root == RegistryRoot.UserOrMachine
341 select new { Reg = r.Registry, Dir = c["Directory_"] };
342
343 count = 0;
344 foreach (var r in regs)
345 {
346 Console.WriteLine(r);
347 count++;
348 }
349 Assert.AreEqual<int>(1, count);
350
351 var regs2 = from r in db.Registries
352 join c in db.Components on r.Component_ equals c.Component
353 where c.Component == "MyTestRegComp" &&
354 r.Root == RegistryRoot.UserOrMachine
355 select new { Reg = r, Dir = c.Directory_ };
356
357 count = 0;
358 foreach (var r in regs2)
359 {
360 Assert.IsNotNull(r.Reg.Registry);
361 Console.WriteLine(r);
362 count++;
363 }
364 Assert.AreEqual<int>(1, count);
365
366 var regs3 = from r in db.Registries
367 join c in db.Components on r.Component_ equals c.Component
368 where c.Component == "MyTestRegComp" &&
369 r.Root == RegistryRoot.UserOrMachine
370 select r;
371
372 count = 0;
373 foreach (var r in regs3)
374 {
375 Assert.IsNotNull(r.Registry);
376 Console.WriteLine(r);
377 count++;
378 }
379 Assert.AreEqual<int>(1, count);
380
381 }
382 }
383
384 [TestMethod]
385 public void LinqFourWayJoin()
386 {
387 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
388 {
389 this.InitLinqTestDatabase(db);
390 int count;
391
392 IList<string> pretest = db.ExecuteStringQuery(
393 "SELECT `Feature`.`Feature` " +
394 "FROM `Feature`, `FeatureComponents`, `Component`, `Registry` " +
395 "WHERE `Feature`.`Feature` = `FeatureComponents`.`Feature_` " +
396 "AND `FeatureComponents`.`Component_` = `Component`.`Component` " +
397 "AND `Component`.`Component` = `Registry`.`Component_` " +
398 "AND (`Registry`.`Registry` = 'MyTestRegCompReg1')");
399 Assert.AreEqual<int>(1, pretest.Count);
400
401 var features = from f in db.Features
402 join fc in db.FeatureComponents on f.Feature equals fc.Feature_
403 join c in db.Components on fc.Component_ equals c.Component
404 join r in db.Registries on c.Component equals r.Component_
405 where r.Registry == "MyTestRegCompReg1"
406 select f.Feature;
407
408 count = 0;
409 foreach (var featureName in features)
410 {
411 Console.WriteLine(featureName);
412 count++;
413 }
414 Assert.AreEqual<int>(1, count);
415
416 }
417 }
418
419 [TestMethod]
420 public void EnumTable()
421 {
422 using (QDatabase db = new QDatabase("testlinq.msi", DatabaseOpenMode.Create))
423 {
424 this.InitLinqTestDatabase(db);
425 int count = 0;
426 foreach (var comp in db.Components)
427 {
428 Console.WriteLine(comp);
429 count++;
430 }
431 Assert.AreNotEqual<int>(0, count);
432 }
433 }
434
435 [TestMethod]
436 public void DatabaseAsQueryable()
437 {
438 using (Database db = new Database("testlinq.msi", DatabaseOpenMode.Create))
439 {
440 WindowsInstallerUtils.InitializeProductDatabase(db);
441 WindowsInstallerUtils.CreateTestProduct(db);
442
443 var comps = from c in db.AsQueryable().Components
444 select c;
445
446 int count = 0;
447 foreach (var c in comps)
448 {
449 Console.WriteLine(c);
450 count++;
451 }
452
453 Assert.AreEqual<int>(1, count);
454 }
455 }
456
457 [TestMethod]
458 public void EnumProducts()
459 {
460 var products = from p in ProductInstallation.AllProducts
461 where p.Publisher == ".NET Foundation"
462 select new { Name = p.ProductName,
463 Ver = p.ProductVersion,
464 Company = p.Publisher,
465 InstallDate = p.InstallDate,
466 PackageCode = p.AdvertisedPackageCode };
467
468 foreach (var p in products)
469 {
470 Console.WriteLine(p);
471 Assert.IsTrue(p.Company == ".NET Foundation");
472 }
473 }
474
475 [TestMethod]
476 public void EnumFeatures()
477 {
478 foreach (var p in ProductInstallation.AllProducts)
479 {
480 Console.WriteLine(p.ProductName);
481
482 foreach (var f in p.Features)
483 {
484 Console.WriteLine("\t" + f.FeatureName);
485 }
486 }
487 }
488
489 [TestMethod]
490 public void EnumComponents()
491 {
492 var comps = from c in ComponentInstallation.AllComponents
493 where c.State == InstallState.Local &&
494 c.Product.Publisher == ".NET Foundation"
495 select c.Path;
496
497 int count = 0;
498 foreach (var c in comps)
499 {
500 if (++count == 100) break;
501
502 Console.WriteLine(c);
503 }
504
505 Assert.AreEqual<int>(100, count);
506 }
507 }
508
509}
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.Linq/WixToolsetTests.Dtf.WindowsInstaller.Linq.csproj b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.Linq/WixToolsetTests.Dtf.WindowsInstaller.Linq.csproj
new file mode 100644
index 00000000..0dc90860
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller.Linq/WixToolsetTests.Dtf.WindowsInstaller.Linq.csproj
@@ -0,0 +1,31 @@
1<?xml version="1.0" encoding="utf-8" ?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
5 <PropertyGroup>
6 <ProjectGuid>{4F55F9B8-D8B6-41EB-8796-221B4CD98324}</ProjectGuid>
7 <OutputType>Library</OutputType>
8 <RootNamespace>WixToolsetTests.Dtf</RootNamespace>
9 <AssemblyName>WixToolsetTests.Dtf.Linq</AssemblyName>
10 <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
11 <CreateDocumentation>false</CreateDocumentation>
12 </PropertyGroup>
13
14 <ItemGroup>
15 <Compile Include="LinqTest.cs" />
16 </ItemGroup>
17
18 <ItemGroup>
19 <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
20 <Reference Include="System" />
21 <Reference Include="System.Core" />
22 </ItemGroup>
23
24 <ItemGroup>
25 <ProjectReference Include="..\WixToolset.Dtf.WindowsInstaller\WixToolset.Dtf.WindowsInstaller.csproj" />
26 <ProjectReference Include="..\WixToolset.Dtf.WindowsInstaller.Linq\WixToolset.Dtf.WindowsInstaller.Linq.csproj" />
27 <ProjectReference Include="..\WixToolsetTests.Dtf.WindowsInstaller\WixToolsetTests.Dtf.WindowsInstaller.csproj" />
28 </ItemGroup>
29
30 <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
31</Project>
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/EmbeddedExternalUI.cs b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/EmbeddedExternalUI.cs
new file mode 100644
index 00000000..b0fc00a8
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/EmbeddedExternalUI.cs
@@ -0,0 +1,173 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Test
4{
5 using System;
6 using System.IO;
7 using System.Reflection;
8 using System.Windows.Forms;
9 using System.Globalization;
10 using System.Collections.Generic;
11 using Microsoft.VisualStudio.TestTools.UnitTesting;
12 using WixToolset.Dtf.WindowsInstaller;
13 using View = WixToolset.Dtf.WindowsInstaller.View;
14
15 [TestClass]
16 public class EmbeddedExternalUI
17 {
18 const InstallLogModes TestLogModes =
19 InstallLogModes.FatalExit |
20 InstallLogModes.Error |
21 InstallLogModes.Warning |
22 InstallLogModes.User |
23 InstallLogModes.Info |
24 InstallLogModes.ResolveSource |
25 InstallLogModes.OutOfDiskSpace |
26 InstallLogModes.ActionStart |
27 InstallLogModes.ActionData |
28 InstallLogModes.CommonData;
29
30#if DEBUG
31 const string EmbeddedUISampleBinDir = @"..\..\build\debug\";
32#else
33 const string EmbeddedUISampleBinDir = @"..\..\build\release\";
34#endif
35
36 [TestMethod]
37 [Ignore] // Requires elevation.
38 public void EmbeddedUISingleInstall()
39 {
40 string dbFile = "EmbeddedUISingleInstall.msi";
41 string productCode;
42
43 string uiDir = Path.GetFullPath(EmbeddedExternalUI.EmbeddedUISampleBinDir);
44 string uiFile = "WixToolset.Dtf.Samples.EmbeddedUI.dll";
45
46 using (Database db = new Database(dbFile, DatabaseOpenMode.CreateDirect))
47 {
48 WindowsInstallerUtils.InitializeProductDatabase(db);
49 WindowsInstallerUtils.CreateTestProduct(db);
50
51 productCode = db.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductCode'")[0];
52
53 using (Record uiRec = new Record(5))
54 {
55 uiRec[1] = "TestEmbeddedUI";
56 uiRec[2] = Path.GetFileNameWithoutExtension(uiFile) + ".Wrapper.dll";
57 uiRec[3] = 1;
58 uiRec[4] = (int) (
59 EmbeddedExternalUI.TestLogModes |
60 InstallLogModes.Progress |
61 InstallLogModes.Initialize |
62 InstallLogModes.Terminate |
63 InstallLogModes.ShowDialog);
64 uiRec.SetStream(5, Path.Combine(uiDir, uiFile));
65 db.Execute(db.Tables["MsiEmbeddedUI"].SqlInsertString, uiRec);
66 }
67
68 db.Commit();
69 }
70
71 Installer.SetInternalUI(InstallUIOptions.Full);
72
73 ProductInstallation installation = new ProductInstallation(productCode);
74 Assert.IsFalse(installation.IsInstalled, "Checking that product is not installed before starting.");
75
76 Exception caughtEx = null;
77 try
78 {
79 Installer.EnableLog(EmbeddedExternalUI.TestLogModes, "install.log");
80 Installer.InstallProduct(dbFile, String.Empty);
81 }
82 catch (Exception ex) { caughtEx = ex; }
83 Assert.IsNull(caughtEx, "Exception thrown while installing product: " + caughtEx);
84
85 Assert.IsTrue(installation.IsInstalled, "Checking that product is installed.");
86 Console.WriteLine();
87 Console.WriteLine();
88 Console.WriteLine();
89 Console.WriteLine("===================================================================");
90 Console.WriteLine();
91 Console.WriteLine();
92 Console.WriteLine();
93
94 try
95 {
96 Installer.EnableLog(EmbeddedExternalUI.TestLogModes, "uninstall.log");
97 Installer.InstallProduct(dbFile, "REMOVE=All");
98 }
99 catch (Exception ex) { caughtEx = ex; }
100 Assert.IsNull(caughtEx, "Exception thrown while uninstalling product: " + caughtEx);
101 }
102
103 // This test does not pass if run normally.
104 // It only passes when a failure is injected into the EmbeddedUI launcher.
105 ////[TestMethod]
106 public void EmbeddedUIInitializeFails()
107 {
108 string dbFile = "EmbeddedUIInitializeFails.msi";
109 string productCode;
110
111 string uiDir = Path.GetFullPath(EmbeddedExternalUI.EmbeddedUISampleBinDir);
112 string uiFile = "WixToolset.Dtf.Samples.EmbeddedUI.dll";
113
114 // A number that will be used to check whether a type 19 CA runs.
115 const string magicNumber = "3.14159265358979323846264338327950";
116
117 using (Database db = new Database(dbFile, DatabaseOpenMode.CreateDirect))
118 {
119 WindowsInstallerUtils.InitializeProductDatabase(db);
120 WindowsInstallerUtils.CreateTestProduct(db);
121
122 const string failureActionName = "EmbeddedUIInitializeFails";
123 db.Execute("INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) " +
124 "VALUES ('{0}', 19, '', 'Logging magic number: {1}')", failureActionName, magicNumber);
125
126 // This type 19 CA (launch condition) is given a condition of 'UILevel = 3' so that it only runs if the
127 // installation is running in BASIC UI mode, which is what we expect if the EmbeddedUI fails to initialize.
128 db.Execute("INSERT INTO `InstallExecuteSequence` (`Action`, `Condition`, `Sequence`) " +
129 "VALUES ('{0}', 'UILevel = 3', 1)", failureActionName);
130
131 productCode = db.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductCode'")[0];
132
133 using (Record uiRec = new Record(5))
134 {
135 uiRec[1] = "TestEmbeddedUI";
136 uiRec[2] = Path.GetFileNameWithoutExtension(uiFile) + ".Wrapper.dll";
137 uiRec[3] = 1;
138 uiRec[4] = (int)(
139 EmbeddedExternalUI.TestLogModes |
140 InstallLogModes.Progress |
141 InstallLogModes.Initialize |
142 InstallLogModes.Terminate |
143 InstallLogModes.ShowDialog);
144 uiRec.SetStream(5, Path.Combine(uiDir, uiFile));
145 db.Execute(db.Tables["MsiEmbeddedUI"].SqlInsertString, uiRec);
146 }
147
148 db.Commit();
149 }
150
151 Installer.SetInternalUI(InstallUIOptions.Full);
152
153 ProductInstallation installation = new ProductInstallation(productCode);
154 Assert.IsFalse(installation.IsInstalled, "Checking that product is not installed before starting.");
155
156 string logFile = "install.log";
157 Exception caughtEx = null;
158 try
159 {
160 Installer.EnableLog(EmbeddedExternalUI.TestLogModes, logFile);
161 Installer.InstallProduct(dbFile, String.Empty);
162 }
163 catch (Exception ex) { caughtEx = ex; }
164 Assert.IsInstanceOfType(caughtEx, typeof(InstallerException),
165 "Excpected InstallerException installing product; caught: " + caughtEx);
166
167 Assert.IsFalse(installation.IsInstalled, "Checking that product is not installed.");
168
169 string logText = File.ReadAllText(logFile);
170 Assert.IsTrue(logText.Contains(magicNumber), "Checking that the type 19 custom action ran.");
171 }
172 }
173}
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/Schema.cs b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/Schema.cs
new file mode 100644
index 00000000..26c172c9
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/Schema.cs
@@ -0,0 +1,238 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Test
4{
5 using System;
6 using System.IO;
7 using System.Collections.Generic;
8 using System.Text;
9 using WixToolset.Dtf.WindowsInstaller;
10
11 public static class Schema
12 {
13 public static IList<TableInfo> Tables
14 {
15 get
16 {
17 return new TableInfo[]
18 {
19 Binary,
20 Component,
21 CustomAction,
22 Directory,
23 EmbeddedUI,
24 Feature,
25 FeatureComponents,
26 File,
27 InstallExecuteSequence,
28 Media,
29 Property,
30 Registry
31 };
32 }
33 }
34
35 #region Table data
36
37 public static TableInfo Binary { get { return new TableInfo(
38 "Binary",
39 new ColumnInfo[]
40 {
41 new ColumnInfo("Name", typeof(String), 72, true),
42 new ColumnInfo("Data", typeof(Stream), 0, true),
43 },
44 new string[] { "Name" });
45 } }
46
47 public static TableInfo Component { get { return new TableInfo(
48 "Component",
49 new ColumnInfo[]
50 {
51 new ColumnInfo("Component", typeof(String), 72, true),
52 new ColumnInfo("ComponentId", typeof(String), 38, false),
53 new ColumnInfo("Directory_", typeof(String), 72, true),
54 new ColumnInfo("Attributes", typeof(Int16), 2, true),
55 new ColumnInfo("Condition", typeof(String), 255, false),
56 new ColumnInfo("KeyPath", typeof(String), 72, false),
57 },
58 new string[] { "Component" });
59 } }
60
61 public static TableInfo CustomAction { get { return new TableInfo(
62 "CustomAction",
63 new ColumnInfo[]
64 {
65 new ColumnInfo("Action", typeof(String), 72, true),
66 new ColumnInfo("Type", typeof(Int16), 2, true),
67 new ColumnInfo("Source", typeof(String), 64, false),
68 new ColumnInfo("Target", typeof(String), 255, false),
69 },
70 new string[] { "Action" });
71 } }
72
73 public static TableInfo Directory { get { return new TableInfo(
74 "Directory",
75 new ColumnInfo[]
76 {
77 new ColumnInfo("Directory", typeof(String), 72, true),
78 new ColumnInfo("Directory_Parent", typeof(String), 72, false),
79 new ColumnInfo("DefaultDir", typeof(String), 255, true, false, true),
80 },
81 new string[] { "Directory" });
82 } }
83
84 public static TableInfo EmbeddedUI { get { return new TableInfo(
85 "MsiEmbeddedUI",
86 new ColumnInfo[]
87 {
88 new ColumnInfo("MsiEmbeddedUI", typeof(String), 72, true),
89 new ColumnInfo("FileName", typeof(String), 72, true),
90 new ColumnInfo("Attributes", typeof(Int16), 2, true),
91 new ColumnInfo("MessageFilter", typeof(Int32), 4, false),
92 new ColumnInfo("Data", typeof(Stream), 0, true),
93 },
94 new string[] { "MsiEmbeddedUI" });
95 } }
96
97 public static TableInfo Feature { get { return new TableInfo(
98 "Feature",
99 new ColumnInfo[]
100 {
101 new ColumnInfo("Feature", typeof(String), 38, true),
102 new ColumnInfo("Feature_Parent", typeof(String), 38, false),
103 new ColumnInfo("Title", typeof(String), 64, false, false, true),
104 new ColumnInfo("Description", typeof(String), 64, false, false, true),
105 new ColumnInfo("Display", typeof(Int16), 2, false),
106 new ColumnInfo("Level", typeof(Int16), 2, true),
107 new ColumnInfo("Directory_", typeof(String), 72, false),
108 new ColumnInfo("Attributes", typeof(Int16), 2, true),
109 },
110 new string[] { "Feature" });
111 } }
112
113 public static TableInfo FeatureComponents { get { return new TableInfo(
114 "FeatureComponents",
115 new ColumnInfo[]
116 {
117 new ColumnInfo("Feature_", typeof(String), 38, true),
118 new ColumnInfo("Component_", typeof(String), 72, true),
119 },
120 new string[] { "Feature_", "Component_" });
121 } }
122
123 public static TableInfo File { get { return new TableInfo(
124 "File",
125 new ColumnInfo[]
126 {
127 new ColumnInfo("File", typeof(String), 72, true),
128 new ColumnInfo("Component_", typeof(String), 72, true),
129 new ColumnInfo("FileName", typeof(String), 255, true, false, true),
130 new ColumnInfo("FileSize", typeof(Int32), 4, true),
131 new ColumnInfo("Version", typeof(String), 72, false),
132 new ColumnInfo("Language", typeof(String), 20, false),
133 new ColumnInfo("Attributes", typeof(Int16), 2, false),
134 new ColumnInfo("Sequence", typeof(Int16), 2, true),
135 },
136 new string[] { "File" });
137 } }
138
139 public static TableInfo InstallExecuteSequence { get { return new TableInfo(
140 "InstallExecuteSequence",
141 new ColumnInfo[]
142 {
143 new ColumnInfo("Action", typeof(String), 72, true),
144 new ColumnInfo("Condition", typeof(String), 255, false),
145 new ColumnInfo("Sequence", typeof(Int16), 2, true),
146 },
147 new string[] { "Action" });
148 } }
149
150 public static TableInfo Media { get { return new TableInfo(
151 "Media",
152 new ColumnInfo[]
153 {
154 new ColumnInfo("DiskId", typeof(Int16), 2, true),
155 new ColumnInfo("LastSequence", typeof(Int16), 2, true),
156 new ColumnInfo("DiskPrompt", typeof(String), 64, false, false, true),
157 new ColumnInfo("Cabinet", typeof(String), 255, false),
158 new ColumnInfo("VolumeLabel", typeof(String), 32, false),
159 new ColumnInfo("Source", typeof(String), 32, false),
160 },
161 new string[] { "DiskId" });
162 } }
163
164 public static TableInfo Property { get { return new TableInfo(
165 "Property",
166 new ColumnInfo[]
167 {
168 new ColumnInfo("Property", typeof(String), 72, true),
169 new ColumnInfo("Value", typeof(String), 255, true),
170 },
171 new string[] { "Property" });
172 } }
173
174 public static TableInfo Registry { get { return new TableInfo(
175 "Registry",
176 new ColumnInfo[]
177 {
178 new ColumnInfo("Registry", typeof(String), 72, true),
179 new ColumnInfo("Root", typeof(Int16), 2, true),
180 new ColumnInfo("Key", typeof(String), 255, true, false, true),
181 new ColumnInfo("Name", typeof(String), 255, false, false, true),
182 new ColumnInfo("Value", typeof(String), 0, false, false, true),
183 new ColumnInfo("Component_", typeof(String), 72, true),
184 },
185 new string[] { "Registry" });
186 } }
187
188 #endregion
189
190 }
191
192 public class Action
193 {
194 public readonly string Name;
195 public readonly int Sequence;
196
197 public Action(string name, int sequence)
198 {
199 this.Name = name;
200 this.Sequence = sequence;
201 }
202
203 }
204
205 public class Sequence
206 {
207 public static IList<Action> InstallExecute
208 {
209 get
210 {
211 return new Action[]
212 {
213 new Action("CostInitialize", 800),
214 new Action("FileCost", 900),
215 new Action("CostFinalize", 1000),
216 new Action("InstallValidate", 1400),
217 new Action("InstallInitialize", 1500),
218 new Action("ProcessComponents", 1600),
219 new Action("UnpublishComponents", 1700),
220 new Action("UnpublishFeatures", 1800),
221 new Action("RemoveRegistryValues", 2600),
222 new Action("RemoveFiles", 3500),
223 new Action("RemoveFolders", 3600),
224 new Action("CreateFolders", 3700),
225 new Action("MoveFiles", 3800),
226 new Action("InstallFiles", 4000),
227 new Action("WriteRegistryValues", 5000),
228 new Action("RegisterProduct", 6100),
229 new Action("PublishComponents", 6200),
230 new Action("PublishFeatures", 6300),
231 new Action("PublishProduct", 6400),
232 new Action("InstallFinalize", 6600),
233 };
234 }
235 }
236
237 }
238}
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerTest.cs b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerTest.cs
new file mode 100644
index 00000000..f994dfef
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerTest.cs
@@ -0,0 +1,409 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Test
4{
5 using System;
6 using System.IO;
7 using System.Windows.Forms;
8 using System.Globalization;
9 using System.Collections.Generic;
10 using Microsoft.VisualStudio.TestTools.UnitTesting;
11 using WixToolset.Dtf.WindowsInstaller;
12 using View = WixToolset.Dtf.WindowsInstaller.View;
13
14 [TestClass]
15 public class WindowsInstallerTest
16 {
17 public WindowsInstallerTest()
18 {
19 }
20
21 [TestInitialize()]
22 public void Initialize()
23 {
24 }
25
26 [TestCleanup()]
27 public void Cleanup()
28 {
29 }
30
31 [TestMethod]
32 [Ignore] // Currently fails.
33 public void InstallerErrorMessages()
34 {
35 string msg3002 = Installer.GetErrorMessage(3002);
36 Console.WriteLine("3002=" + msg3002);
37 Assert.IsNotNull(msg3002);
38 Assert.IsTrue(msg3002.Length > 0);
39 }
40
41 [TestMethod]
42 public void InstallerDatabaseSchema()
43 {
44 string dbFile = "InstallerDatabaseSchema.msi";
45
46 using (Database db = new Database(dbFile, DatabaseOpenMode.CreateDirect))
47 {
48 WindowsInstallerUtils.InitializeProductDatabase(db);
49 db.Commit();
50 }
51
52 Assert.IsTrue(File.Exists(dbFile), "Checking whether created database file " + dbFile + " exists.");
53
54 using (Database db = new Database(dbFile, DatabaseOpenMode.ReadOnly))
55 {
56 TableCollection tables = db.Tables;
57 Assert.AreEqual<int>(Schema.Tables.Count, tables.Count, "Counting tables.");
58 Assert.AreEqual<int>(Schema.Property.Columns.Count, tables["Property"].Columns.Count, "Counting columns in Property table.");
59
60 foreach (TableInfo tableInfo in tables)
61 {
62 Console.WriteLine(tableInfo.Name);
63 foreach (ColumnInfo columnInfo in tableInfo.Columns)
64 {
65 Console.WriteLine("\t{0} {1}", columnInfo.Name, columnInfo.ColumnDefinitionString);
66 }
67 }
68 }
69 }
70
71 [TestMethod]
72 public void InstallerViewTables()
73 {
74 string dbFile = "InstallerViewTables.msi";
75
76 using (Database db = new Database(dbFile, DatabaseOpenMode.CreateDirect))
77 {
78 WindowsInstallerUtils.InitializeProductDatabase(db);
79 db.Commit();
80
81 using (View view1 = db.OpenView("SELECT `Property`, `Value` FROM `Property` WHERE `Value` IS NOT NULL"))
82 {
83 IList<TableInfo> viewTables = view1.Tables;
84 Assert.IsNotNull(viewTables);
85 Assert.AreEqual<int>(1, viewTables.Count);
86 Assert.AreEqual<String>("Property", viewTables[0].Name);
87 }
88
89 using (View view2 = db.OpenView("INSERT INTO `Property` (`Property`, `Value`) VALUES ('TestViewTables', 1)"))
90 {
91 IList<TableInfo> viewTables = view2.Tables;
92 Assert.IsNotNull(viewTables);
93 Assert.AreEqual<int>(1, viewTables.Count);
94 Assert.AreEqual<String>("Property", viewTables[0].Name);
95 }
96
97 using (View view3 = db.OpenView("UPDATE `Property` SET `Value` = 2 WHERE `Property` = 'TestViewTables'"))
98 {
99 IList<TableInfo> viewTables = view3.Tables;
100 Assert.IsNotNull(viewTables);
101 Assert.AreEqual<int>(1, viewTables.Count);
102 Assert.AreEqual<String>("Property", viewTables[0].Name);
103 }
104
105 using (View view4 = db.OpenView("alter table Property hold"))
106 {
107 IList<TableInfo> viewTables = view4.Tables;
108 Assert.IsNotNull(viewTables);
109 Assert.AreEqual<int>(1, viewTables.Count);
110 Assert.AreEqual<String>("Property", viewTables[0].Name);
111 }
112 }
113 }
114
115 [TestMethod]
116 public void InstallerInstallProduct()
117 {
118 string dbFile = "InstallerInstallProduct.msi";
119 string productCode;
120
121 using (Database db = new Database(dbFile, DatabaseOpenMode.CreateDirect))
122 {
123 WindowsInstallerUtils.InitializeProductDatabase(db);
124 WindowsInstallerUtils.CreateTestProduct(db);
125
126 productCode = db.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductCode'")[0];
127
128 db.Commit();
129 }
130
131 ProductInstallation installation = new ProductInstallation(productCode);
132 Assert.IsFalse(installation.IsInstalled, "Checking that product is not installed before starting.");
133
134 Installer.SetInternalUI(InstallUIOptions.Silent);
135 ExternalUIHandler prevHandler = Installer.SetExternalUI(WindowsInstallerTest.ExternalUILogger,
136 InstallLogModes.FatalExit |
137 InstallLogModes.Error |
138 InstallLogModes.Warning |
139 InstallLogModes.User |
140 InstallLogModes.Info |
141 InstallLogModes.ResolveSource |
142 InstallLogModes.OutOfDiskSpace |
143 InstallLogModes.ActionStart |
144 InstallLogModes.ActionData |
145 InstallLogModes.CommonData |
146 InstallLogModes.Progress |
147 InstallLogModes.Initialize |
148 InstallLogModes.Terminate |
149 InstallLogModes.ShowDialog);
150 Assert.IsNull(prevHandler, "Checking that returned previous UI handler is null.");
151
152 Exception caughtEx = null;
153 try
154 {
155 Installer.InstallProduct(dbFile, String.Empty);
156 }
157 catch (Exception ex) { caughtEx = ex; }
158 Assert.IsNull(caughtEx, "Exception thrown while installing product: " + caughtEx);
159
160 prevHandler = Installer.SetExternalUI(prevHandler, InstallLogModes.None);
161 Assert.AreEqual<ExternalUIHandler>(WindowsInstallerTest.ExternalUILogger, prevHandler, "Checking that previously-set UI handler is returned.");
162
163 Assert.IsTrue(installation.IsInstalled, "Checking that product is installed.");
164 Console.WriteLine();
165 Console.WriteLine();
166 Console.WriteLine();
167 Console.WriteLine("===================================================================");
168 Console.WriteLine();
169 Console.WriteLine();
170 Console.WriteLine();
171
172 ExternalUIRecordHandler prevRecHandler = Installer.SetExternalUI(WindowsInstallerTest.ExternalUIRecordLogger,
173 InstallLogModes.FatalExit |
174 InstallLogModes.Error |
175 InstallLogModes.Warning |
176 InstallLogModes.User |
177 InstallLogModes.Info |
178 InstallLogModes.ResolveSource |
179 InstallLogModes.OutOfDiskSpace |
180 InstallLogModes.ActionStart |
181 InstallLogModes.ActionData |
182 InstallLogModes.CommonData |
183 InstallLogModes.Progress |
184 InstallLogModes.Initialize |
185 InstallLogModes.Terminate |
186 InstallLogModes.ShowDialog);
187 Assert.IsNull(prevRecHandler, "Checking that returned previous UI record handler is null.");
188
189 try
190 {
191 Installer.InstallProduct(dbFile, "REMOVE=All");
192 }
193 catch (Exception ex) { caughtEx = ex; }
194 Assert.IsNull(caughtEx, "Exception thrown while installing product: " + caughtEx);
195
196 Assert.IsFalse(installation.IsInstalled, "Checking that product is not installed after removing.");
197
198 prevRecHandler = Installer.SetExternalUI(prevRecHandler, InstallLogModes.None);
199 Assert.AreEqual<ExternalUIRecordHandler>(WindowsInstallerTest.ExternalUIRecordLogger, prevRecHandler, "Checking that previously-set UI record handler is returned.");
200 }
201
202 public static MessageResult ExternalUILogger(
203 InstallMessage messageType,
204 string message,
205 MessageButtons buttons,
206 MessageIcon icon,
207 MessageDefaultButton defaultButton)
208 {
209 Console.WriteLine("{0}: {1}", messageType, message);
210 return MessageResult.None;
211 }
212
213 public static MessageResult ExternalUIRecordLogger(
214 InstallMessage messageType,
215 Record messageRecord,
216 MessageButtons buttons,
217 MessageIcon icon,
218 MessageDefaultButton defaultButton)
219 {
220 if (messageRecord != null)
221 {
222 if (messageRecord.FormatString.Length == 0 && messageRecord.FieldCount > 0)
223 {
224 messageRecord.FormatString = "1: [1] 2: [2] 3: [3] 4: [4] 5: [5]";
225 }
226 Console.WriteLine("{0}: {1}", messageType, messageRecord.ToString());
227 }
228 else
229 {
230 Console.WriteLine("{0}: (null)", messageType);
231 }
232 return MessageResult.None;
233 }
234
235 [TestMethod]
236 [Ignore] // Currently fails.
237 public void InstallerMessageResources()
238 {
239 string message1101 = Installer.GetErrorMessage(1101);
240 Console.WriteLine("Message 1101: " + message1101);
241 Assert.IsNotNull(message1101);
242 Assert.IsTrue(message1101.Contains("file"));
243
244 message1101 = Installer.GetErrorMessage(1101, CultureInfo.GetCultureInfo(1033));
245 Console.WriteLine("Message 1101: " + message1101);
246 Assert.IsNotNull(message1101);
247 Assert.IsTrue(message1101.Contains("file"));
248
249 string message2621 = Installer.GetErrorMessage(2621);
250 Console.WriteLine("Message 2621: " + message2621);
251 Assert.IsNotNull(message2621);
252 Assert.IsTrue(message2621.Contains("DLL"));
253
254 string message3002 = Installer.GetErrorMessage(3002);
255 Console.WriteLine("Message 3002: " + message3002);
256 Assert.IsNotNull(message3002);
257 Assert.IsTrue(message3002.Contains("sequencing"));
258 }
259
260 [TestMethod]
261 public void EnumComponentQualifiers()
262 {
263 foreach (ComponentInstallation comp in ComponentInstallation.AllComponents)
264 {
265 bool qualifiers = false;
266 foreach (ComponentInstallation.Qualifier qualifier in comp.Qualifiers)
267 {
268 if (!qualifiers)
269 {
270 Console.WriteLine(comp.Path);
271 qualifiers = true;
272 }
273
274 Console.WriteLine("\t{0}: {1}", qualifier.QualifierCode, qualifier.Data);
275 }
276 }
277 }
278
279 [TestMethod]
280 public void DeleteRecord()
281 {
282 string dbFile = "DeleteRecord.msi";
283
284 using (Database db = new Database(dbFile, DatabaseOpenMode.CreateDirect))
285 {
286 WindowsInstallerUtils.InitializeProductDatabase(db);
287 WindowsInstallerUtils.CreateTestProduct(db);
288
289 string query = "SELECT `Property`, `Value` FROM `Property` WHERE `Property` = 'UpgradeCode'";
290
291 using (View view = db.OpenView(query))
292 {
293 view.Execute();
294
295 Record rec = view.Fetch();
296
297 Console.WriteLine("Calling ToString() : " + rec);
298
299 view.Delete(rec);
300 }
301
302 Assert.AreEqual(0, db.ExecuteStringQuery(query).Count);
303 }
304 }
305
306 [TestMethod]
307 public void InsertRecordThenTryFormatString()
308 {
309 string dbFile = "InsertRecordThenTryFormatString.msi";
310
311 using (Database db = new Database(dbFile, DatabaseOpenMode.CreateDirect))
312 {
313 WindowsInstallerUtils.InitializeProductDatabase(db);
314 WindowsInstallerUtils.CreateTestProduct(db);
315
316 string parameterFormatString = "[1]";
317 string[] properties = new string[]
318 {
319 "SonGoku", "Over 9000",
320 };
321
322 string query = "SELECT `Property`, `Value` FROM `Property`";
323
324 using (View view = db.OpenView(query))
325 {
326 using (Record rec = new Record(2))
327 {
328 rec[1] = properties[0];
329 rec[2] = properties[1];
330 rec.FormatString = parameterFormatString;
331 Console.WriteLine("Format String before inserting: " + rec.FormatString);
332 view.Insert(rec);
333
334 Console.WriteLine("Format String after inserting: " + rec.FormatString);
335 // After inserting, the format string is invalid.
336 Assert.AreEqual(String.Empty, rec.ToString());
337
338 // Setting the format string manually makes it valid again.
339 rec.FormatString = parameterFormatString;
340 Assert.AreEqual(properties[0], rec.ToString());
341 }
342 }
343 }
344 }
345
346 [TestMethod]
347 public void SeekRecordThenTryFormatString()
348 {
349 string dbFile = "SeekRecordThenTryFormatString.msi";
350
351 using (Database db = new Database(dbFile, DatabaseOpenMode.CreateDirect))
352 {
353 WindowsInstallerUtils.InitializeProductDatabase(db);
354 WindowsInstallerUtils.CreateTestProduct(db);
355
356 string parameterFormatString = "[1]";
357 string[] properties = new string[]
358 {
359 "SonGoku", "Over 9000",
360 };
361
362 string query = "SELECT `Property`, `Value` FROM `Property`";
363
364 using (View view = db.OpenView(query))
365 {
366 using (Record rec = new Record(2))
367 {
368 rec[1] = properties[0];
369 rec[2] = properties[1];
370 rec.FormatString = parameterFormatString;
371 Console.WriteLine("Record fields before seeking: " + rec[0] + " " + rec[1] + " " + rec[2]);
372 view.Seek(rec);
373
374 //TODO: Why does view.Seek remove the record fields?
375 Console.WriteLine("Record fields after seeking: " + rec[0] + " " + rec[1] + " " + rec[2]);
376 // After inserting, the format string is invalid.
377 Assert.AreEqual(String.Empty, rec.ToString());
378 }
379 }
380 }
381 }
382
383 [TestMethod]
384 public void TestToString()
385 {
386 string defaultString = "1: ";
387 string vegetaShout = "It's OVER 9000!!";
388 string gokuPowerLevel = "9001";
389 string nappaInquiry = "Vegeta, what's the Scouter say about his power level?";
390 string parameterFormatString = "[1]";
391
392 Record rec = new Record(1);
393 Assert.AreEqual(defaultString, rec.ToString(), "Testing default FormatString");
394
395 rec.FormatString = String.Empty;
396 Assert.AreEqual(defaultString, rec.ToString(), "Explicitly set the FormatString to the empty string.");
397
398 rec.FormatString = vegetaShout;
399 Assert.AreEqual(vegetaShout, rec.ToString(), "Testing text only (empty FormatString)");
400
401 rec.FormatString = gokuPowerLevel;
402 Assert.AreEqual(gokuPowerLevel, rec.ToString(), "Testing numbers only from a record that wasn't fetched.");
403
404 Record rec2 = new Record(nappaInquiry);
405 rec2.FormatString = parameterFormatString;
406 Assert.AreEqual(nappaInquiry, rec2.ToString(), "Testing text with a FormatString set.");
407 }
408 }
409}
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerTransactions.cs b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerTransactions.cs
new file mode 100644
index 00000000..3bdf5acd
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerTransactions.cs
@@ -0,0 +1,161 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Test
4{
5 using System;
6 using System.IO;
7 using System.Windows.Forms;
8 using System.Globalization;
9 using System.Collections.Generic;
10 using Microsoft.VisualStudio.TestTools.UnitTesting;
11 using WixToolset.Dtf.WindowsInstaller;
12 using View = WixToolset.Dtf.WindowsInstaller.View;
13
14 [TestClass]
15 public class WindowsInstallerTransactions
16 {
17 [TestInitialize()]
18 public void Initialize()
19 {
20 }
21
22 [TestCleanup()]
23 public void Cleanup()
24 {
25 }
26
27 [TestMethod]
28 [Ignore] // Requires elevation.
29 public void InstallerTransactTwoProducts()
30 {
31 string dbFile1 = "InstallerTransactProduct1.msi";
32 string dbFile2 = "InstallerTransactProduct2.msi";
33 string productCode1;
34 string productCode2;
35
36 using (Database db1 = new Database(dbFile1, DatabaseOpenMode.CreateDirect))
37 {
38 WindowsInstallerUtils.InitializeProductDatabase(db1);
39 WindowsInstallerUtils.CreateTestProduct(db1);
40
41 productCode1 = db1.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductCode'")[0];
42
43 db1.Commit();
44 }
45
46 using (Database db2 = new Database(dbFile2, DatabaseOpenMode.CreateDirect))
47 {
48 WindowsInstallerUtils.InitializeProductDatabase(db2);
49 WindowsInstallerUtils.CreateTestProduct(db2);
50
51 productCode2 = db2.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = 'ProductCode'")[0];
52
53 db2.Commit();
54 }
55
56 ProductInstallation installation1 = new ProductInstallation(productCode1);
57 ProductInstallation installation2 = new ProductInstallation(productCode2);
58 Assert.IsFalse(installation1.IsInstalled, "Checking that product 1 is not installed before starting.");
59 Assert.IsFalse(installation2.IsInstalled, "Checking that product 2 is not installed before starting.");
60
61 Installer.SetInternalUI(InstallUIOptions.Silent);
62 ExternalUIHandler prevHandler = Installer.SetExternalUI(WindowsInstallerTest.ExternalUILogger,
63 InstallLogModes.FatalExit |
64 InstallLogModes.Error |
65 InstallLogModes.Warning |
66 InstallLogModes.User |
67 InstallLogModes.Info |
68 InstallLogModes.ResolveSource |
69 InstallLogModes.OutOfDiskSpace |
70 InstallLogModes.ActionStart |
71 InstallLogModes.ActionData |
72 InstallLogModes.CommonData |
73 InstallLogModes.Progress |
74 InstallLogModes.Initialize |
75 InstallLogModes.Terminate |
76 InstallLogModes.ShowDialog);
77 Assert.IsNull(prevHandler, "Checking that returned previous UI handler is null.");
78
79 Transaction transaction = new Transaction("TestInstallTransaction", TransactionAttributes.None);
80
81 Exception caughtEx = null;
82 try
83 {
84 Installer.InstallProduct(dbFile1, String.Empty);
85 }
86 catch (Exception ex) { caughtEx = ex; }
87 Assert.IsNull(caughtEx, "Exception thrown while installing product 1: " + caughtEx);
88
89 Console.WriteLine();
90 Console.WriteLine("===================================================================");
91 Console.WriteLine();
92
93 try
94 {
95 Installer.InstallProduct(dbFile2, String.Empty);
96 }
97 catch (Exception ex) { caughtEx = ex; }
98 Assert.IsNull(caughtEx, "Exception thrown while installing product 2: " + caughtEx);
99
100 transaction.Commit();
101 transaction.Close();
102
103 prevHandler = Installer.SetExternalUI(prevHandler, InstallLogModes.None);
104 Assert.AreEqual<ExternalUIHandler>(WindowsInstallerTest.ExternalUILogger, prevHandler, "Checking that previously-set UI handler is returned.");
105
106 Assert.IsTrue(installation1.IsInstalled, "Checking that product 1 is installed.");
107 Assert.IsTrue(installation2.IsInstalled, "Checking that product 2 is installed.");
108
109 Console.WriteLine();
110 Console.WriteLine();
111 Console.WriteLine();
112 Console.WriteLine("===================================================================");
113 Console.WriteLine("===================================================================");
114 Console.WriteLine();
115 Console.WriteLine();
116 Console.WriteLine();
117
118 ExternalUIRecordHandler prevRecHandler = Installer.SetExternalUI(WindowsInstallerTest.ExternalUIRecordLogger,
119 InstallLogModes.FatalExit |
120 InstallLogModes.Error |
121 InstallLogModes.Warning |
122 InstallLogModes.User |
123 InstallLogModes.Info |
124 InstallLogModes.ResolveSource |
125 InstallLogModes.OutOfDiskSpace |
126 InstallLogModes.ActionStart |
127 InstallLogModes.ActionData |
128 InstallLogModes.CommonData |
129 InstallLogModes.Progress |
130 InstallLogModes.Initialize |
131 InstallLogModes.Terminate |
132 InstallLogModes.ShowDialog);
133 Assert.IsNull(prevRecHandler, "Checking that returned previous UI record handler is null.");
134
135 transaction = new Transaction("TestUninstallTransaction", TransactionAttributes.None);
136
137 try
138 {
139 Installer.InstallProduct(dbFile1, "REMOVE=All");
140 }
141 catch (Exception ex) { caughtEx = ex; }
142 Assert.IsNull(caughtEx, "Exception thrown while removing product 1: " + caughtEx);
143
144 try
145 {
146 Installer.InstallProduct(dbFile2, "REMOVE=All");
147 }
148 catch (Exception ex) { caughtEx = ex; }
149 Assert.IsNull(caughtEx, "Exception thrown while removing product 2: " + caughtEx);
150
151 transaction.Commit();
152 transaction.Close();
153
154 Assert.IsFalse(installation1.IsInstalled, "Checking that product 1 is not installed after removing.");
155 Assert.IsFalse(installation2.IsInstalled, "Checking that product 2 is not installed after removing.");
156
157 prevRecHandler = Installer.SetExternalUI(prevRecHandler, InstallLogModes.None);
158 Assert.AreEqual<ExternalUIRecordHandler>(WindowsInstallerTest.ExternalUIRecordLogger, prevRecHandler, "Checking that previously-set UI record handler is returned.");
159 }
160 }
161}
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerUtils.cs b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerUtils.cs
new file mode 100644
index 00000000..644f1988
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WindowsInstallerUtils.cs
@@ -0,0 +1,174 @@
1// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3namespace WixToolset.Dtf.Test
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Text;
8 using WixToolset.Dtf.WindowsInstaller;
9
10 public class WindowsInstallerUtils
11 {
12 public static void InitializeProductDatabase(Database db)
13 {
14 InitializeProductDatabase(db, false);
15 }
16
17 public static void InitializeProductDatabase(Database db, bool sixtyFourBit)
18 {
19 db.SummaryInfo.CodePage = (short) Encoding.Default.CodePage;
20 db.SummaryInfo.Title = "Windows Installer Test";
21 db.SummaryInfo.Subject = db.SummaryInfo.Title;
22 db.SummaryInfo.Author = typeof(WindowsInstallerUtils).Assembly.FullName;
23 db.SummaryInfo.CreatingApp = db.SummaryInfo.Author;
24 db.SummaryInfo.Comments = typeof(WindowsInstallerUtils).FullName + ".CreateBasicDatabase()";
25 db.SummaryInfo.Keywords = "Installer,MSI,Database";
26 db.SummaryInfo.PageCount = 300;
27 db.SummaryInfo.WordCount = 0;
28 db.SummaryInfo.RevisionNumber = Guid.NewGuid().ToString("B").ToUpper();
29 db.SummaryInfo.Template = (sixtyFourBit ? "x64" : "Intel") + ";0";
30
31 foreach (TableInfo tableInfo in Schema.Tables)
32 {
33 db.Execute(tableInfo.SqlCreateString);
34 }
35
36 db.Execute("INSERT INTO `Directory` (`Directory`, `DefaultDir`) VALUES ('TARGETDIR', 'SourceDir')");
37 db.Execute("INSERT INTO `Directory` (`Directory`, `Directory_Parent`, `DefaultDir`) VALUES ('ProgramFilesFolder', 'TARGETDIR', '.')");
38
39 foreach (Action action in Sequence.InstallExecute)
40 {
41 db.Execute("INSERT INTO `InstallExecuteSequence` (`Action`, `Sequence`) VALUES ('{0}', {1})",
42 action.Name, action.Sequence);
43 }
44 }
45
46 public const string UpgradeCode = "{05955FE8-005F-4695-A81F-D559338065BB}";
47
48 public static void CreateTestProduct(Database db)
49 {
50 Guid productGuid = Guid.NewGuid();
51
52 string[] properties = new string[]
53 {
54 "ProductCode", productGuid.ToString("B").ToUpper(),
55 "UpgradeCode", UpgradeCode,
56 "ProductName", "Windows Installer Test Product " + productGuid.ToString("P").ToUpper(),
57 "ProductVersion", "1.0.0.0000",
58 };
59
60 using (View view = db.OpenView("INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)"))
61 {
62 using (Record rec = new Record(2))
63 {
64 for (int i = 0; i < properties.Length; i += 2)
65 {
66 rec[1] = properties[i];
67 rec[2] = properties[i + 1];
68 view.Execute(rec);
69 }
70 }
71 }
72
73 int randomId = new Random().Next(10000);
74 string productDir = "TestDir" + randomId;
75 db.Execute(
76 "INSERT INTO `Directory` (`Directory`, `Directory_Parent`, `DefaultDir`) " +
77 "VALUES ('TestDir', 'ProgramFilesFolder', 'TestDir|{0}:.')", productDir);
78
79 string compId = Guid.NewGuid().ToString("B").ToUpper();
80 db.Execute(
81 "INSERT INTO `Component` " +
82 "(`Component`, `ComponentId`, `Directory_`, `Attributes`, `KeyPath`) " +
83 "VALUES ('{0}', '{1}', '{2}', {3}, '{4}')",
84 "TestRegComp1",
85 compId,
86 "TestDir",
87 (int) ComponentAttributes.RegistryKeyPath,
88 "TestReg1");
89
90 string productReg = "TestReg" + randomId;
91 db.Execute(
92 "INSERT INTO `Registry` (`Registry`, `Root`, `Key`, `Component_`) VALUES ('{0}', {1}, '{2}', '{3}')",
93 "TestReg1",
94 -1,
95 @"Software\Microsoft\Windows Installer Test\" + productReg,
96 "TestRegComp1");
97
98 db.Execute(
99 "INSERT INTO `Feature` (`Feature`, `Title`, `Level`, `Attributes`) VALUES ('{0}', '{1}', {2}, {3})",
100 "TestFeature1",
101 "Test Feature 1",
102 1,
103 (int) FeatureAttributes.None);
104
105 db.Execute(
106 "INSERT INTO `FeatureComponents` (`Feature_`, `Component_`) VALUES ('{0}', '{1}')",
107 "TestFeature1",
108 "TestRegComp1");
109 }
110
111 public static void AddFeature(Database db, string featureName)
112 {
113 db.Execute(
114 "INSERT INTO `Feature` (`Feature`, `Title`, `Level`, `Attributes`) VALUES ('{0}', '{1}', {2}, {3})",
115 featureName,
116 featureName,
117 1,
118 (int) FeatureAttributes.None);
119 }
120
121 public static void AddRegistryComponent(Database db,
122 string featureName, string compName, string compId,
123 string keyName, string keyValueName, string value)
124 {
125 db.Execute(
126 "INSERT INTO `Component` " +
127 "(`Component`, `ComponentId`, `Directory_`, `Attributes`, `KeyPath`) " +
128 "VALUES ('{0}', '{1}', '{2}', {3}, '{4}')",
129 compName,
130 compId,
131 "TestDir",
132 (int) ComponentAttributes.RegistryKeyPath,
133 compName + "Reg1");
134 db.Execute(
135 "INSERT INTO `Registry` (`Registry`, `Root`, `Key`, `Name`, `Value`, `Component_`) VALUES ('{0}', {1}, '{2}', '{3}', '{4}', '{5}')",
136 compName + "Reg1",
137 -1,
138 @"Software\Microsoft\Windows Installer Test\" + keyName,
139 keyValueName,
140 value,
141 compName);
142 db.Execute(
143 "INSERT INTO `FeatureComponents` (`Feature_`, `Component_`) VALUES ('{0}', '{1}')",
144 featureName,
145 compName);
146 }
147
148 public static void AddFileComponent(Database db,
149 string featureName, string compName, string compId,
150 string fileKey, string fileName)
151 {
152 db.Execute(
153 "INSERT INTO `Component` " +
154 "(`Component`, `ComponentId`, `Directory_`, `Attributes`, `KeyPath`) " +
155 "VALUES ('{0}', '{1}', '{2}', {3}, '{4}')",
156 compName,
157 compId,
158 "TestDir",
159 (int) ComponentAttributes.None,
160 fileKey);
161 db.Execute(
162 "INSERT INTO `File` " +
163 "(`File`, `Component_`, `FileName`, `FileSize`, `Attributes`, `Sequence`) " +
164 "VALUES ('{0}', '{1}', '{2}', 1, 0, 1)",
165 fileKey,
166 compName,
167 fileName);
168 db.Execute(
169 "INSERT INTO `FeatureComponents` (`Feature_`, `Component_`) VALUES ('{0}', '{1}')",
170 featureName,
171 compName);
172 }
173 }
174}
diff --git a/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WixToolsetTests.Dtf.WindowsInstaller.csproj b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WixToolsetTests.Dtf.WindowsInstaller.csproj
new file mode 100644
index 00000000..dbddb682
--- /dev/null
+++ b/src/dtf/WixToolsetTests.Dtf.WindowsInstaller/WixToolsetTests.Dtf.WindowsInstaller.csproj
@@ -0,0 +1,34 @@
1<?xml version="1.0" encoding="utf-8"?>
2<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
3
4<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
5 <PropertyGroup>
6 <ProjectGuid>{16F5202F-9276-4166-975C-C9654BAF8012}</ProjectGuid>
7 <OutputType>Library</OutputType>
8 <RootNamespace>WixToolsetTests.Dtf</RootNamespace>
9 <AssemblyName>WixToolsetTests.Dtf.WindowsInstaller</AssemblyName>
10 <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
11 <CreateDocumentation>false</CreateDocumentation>
12 </PropertyGroup>
13
14 <ItemGroup>
15 <Compile Include="EmbeddedExternalUI.cs" />
16 <Compile Include="Schema.cs" />
17 <Compile Include="WindowsInstallerTest.cs" />
18 <Compile Include="WindowsInstallerTransactions.cs" />
19 <Compile Include="WindowsInstallerUtils.cs" />
20 </ItemGroup>
21
22 <ItemGroup>
23 <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
24 <Reference Include="System" />
25 <Reference Include="System.Windows.Forms" />
26 <Reference Include="System.Xml" />
27 </ItemGroup>
28
29 <ItemGroup>
30 <ProjectReference Include="..\WixToolset.Dtf.WindowsInstaller\WixToolset.Dtf.WindowsInstaller.csproj" />
31 </ItemGroup>
32
33 <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
34</Project>
diff --git a/src/dtf/appveyor.cmd b/src/dtf/appveyor.cmd
new file mode 100644
index 00000000..6dd50c8a
--- /dev/null
+++ b/src/dtf/appveyor.cmd
@@ -0,0 +1,28 @@
1@setlocal
2@pushd %~dp0
3@set _C=Release
4@set _P=%~dp0build\%_C%\publish
5
6nuget restore || exit /b
7
8msbuild -p:Configuration=%_C% || exit /b
9
10msbuild src\Tools\SfxCA\SfxCA.vcxproj -p:Configuration=%_C% -p:Platform="x64" -p:OutputPath="%_P%\WixToolset.Dtf.MSBuild\tools\\" || exit /b
11msbuild src\Tools\SfxCA\SfxCA.vcxproj -p:Configuration=%_C% -p:Platform="x86" -p:OutputPath="%_P%\WixToolset.Dtf.MSBuild\tools\\" || exit /b
12
13msbuild src\Tools\MakeSfxCA\MakeSfxCA.csproj -p:Configuration=%_C% -p:TargetFramework="net461" -p:OutputPath="%_P%\WixToolset.Dtf.MSBuild\tools\net461" || exit /b
14msbuild src\Tools\MakeSfxCA\MakeSfxCA.csproj -p:Configuration=%_C% -p:TargetFramework="netcoreapp3.1" -p:OutputPath="%_P%\WixToolset.Dtf.MSBuild\tools\netcoreapp3.1" || exit /b
15
16msbuild src\WixToolset.Dtf.MSBuild\WixToolset.Dtf.MSBuild.csproj -p:Configuration=%_C% -p:OutputPath="%_P%\WixToolset.Dtf.MSBuild" || exit /b
17msbuild src\WixToolset.Dtf.MSBuild\WixToolset.Dtf.MSBuild.csproj -target:Pack -p:Configuration=%_C% || exit /b
18
19msbuild -t:Pack -p:Configuration=%_C% src\WixToolset.Dtf.Compression || exit /b
20msbuild -t:Pack -p:Configuration=%_C% src\WixToolset.Dtf.Compression.Cab || exit /b
21msbuild -t:Pack -p:Configuration=%_C% src\WixToolset.Dtf.Compression.Zip || exit /b
22msbuild -t:Pack -p:Configuration=%_C% src\WixToolset.Dtf.Resources || exit /b
23msbuild -t:Pack -p:Configuration=%_C% src\WixToolset.Dtf.WindowsInstaller || exit /b
24msbuild -t:Pack -p:Configuration=%_C% src\WixToolset.Dtf.WindowsInstaller.Linq || exit /b
25msbuild -t:Pack -p:Configuration=%_C% src\WixToolset.Dtf.WindowsInstaller.Package || exit /b
26
27@popd
28@endlocal \ No newline at end of file
diff --git a/src/dtf/appveyor.yml b/src/dtf/appveyor.yml
new file mode 100644
index 00000000..522e5af3
--- /dev/null
+++ b/src/dtf/appveyor.yml
@@ -0,0 +1,42 @@
1# Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2#
3# Do NOT modify this file. Update the canonical version in Home\repo-template\src\appveyor.yml
4# then update all of the repos.
5
6branches:
7 only:
8 - master
9 - develop
10
11image: Visual Studio 2019
12
13version: 0.0.0.{build}
14configuration: Release
15
16environment:
17 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
18 DOTNET_CLI_TELEMETRY_OPTOUT: 1
19 NUGET_XMLDOC_MODE: skip
20
21build_script:
22 - appveyor.cmd
23
24pull_requests:
25 do_not_increment_build_number: true
26
27nuget:
28 disable_publish_on_pr: true
29
30skip_branch_with_pr: true
31skip_tags: true
32
33artifacts:
34- path: build\Release\**\*.nupkg
35 name: nuget
36- path: build\Release\**\*.msi
37 name: msi
38
39notifications:
40- provider: Slack
41 incoming_webhook:
42 secure: p5xuu+4x2JHfwGDMDe5KcG1k7gZxqYc4jWVwvyNZv5cvkubPD2waJs5yXMAXZNN7Z63/3PWHb7q4KoY/99AjauYa1nZ4c5qYqRPFRBKTHfA=
diff --git a/src/dtf/nuget.config b/src/dtf/nuget.config
new file mode 100644
index 00000000..6e1ad9b5
--- /dev/null
+++ b/src/dtf/nuget.config
@@ -0,0 +1,7 @@
1<?xml version="1.0" encoding="utf-8"?>
2<configuration>
3 <packageSources>
4 <clear />
5 <add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
6 </packageSources>
7</configuration> \ No newline at end of file