Skip to content

Commit b4db2bb

Browse files
authored
feat: Add support for Windows Installer Patch (.msp) files
Closes #1
1 parent 83cf1a8 commit b4db2bb

File tree

12 files changed

+161
-20
lines changed

12 files changed

+161
-20
lines changed

src/LessMsi.Cli/ListTableCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public override void Run(List<string> args)
3131

3232
var csv = new StringBuilder();
3333
Debug.Print("Opening msi file '{0}'.", extra[0]);
34-
using (var msidb = new Database(extra[0], OpenDatabase.ReadOnly))
34+
using (var msidb = MsiDatabase.Create(new LessIO.Path(extra[0])))
3535
{
3636
Debug.Print("Opening table '{0}'.", tableName);
3737
var query = string.Format(CultureInfo.InvariantCulture, "SELECT * FROM `{0}`", tableName);

src/LessMsi.Core/LessMsi.Core.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
<ItemGroup>
5757
<Compile Include="Msi\ColumnInfo.cs" />
5858
<Compile Include="Msi\ExternalCabNotFoundException.cs" />
59+
<Compile Include="Msi\MsiDatabase.cs" />
5960
<Compile Include="Msi\MsiDirectory.cs" />
6061
<Compile Include="Msi\MsiFile.cs" />
6162
<Compile Include="Msi\TableWrapper.cs" />

src/LessMsi.Core/Msi/MsiDatabase.cs

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Permission is hereby granted, free of charge, to any person obtaining
2+
// a copy of this software and associated documentation files (the
3+
// "Software"), to deal in the Software without restriction, including
4+
// without limitation the rights to use, copy, modify, merge, publish,
5+
// distribute, sublicense, and/or sell copies of the Software, and to
6+
// permit persons to whom the Software is furnished to do so, subject to
7+
// the following conditions:
8+
//
9+
// The above copyright notice and this permission notice shall be
10+
// included in all copies or substantial portions of the Software.
11+
//
12+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
16+
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
17+
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18+
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19+
//
20+
// Copyright (c) 2021 Scott Willeke (http://scott.willeke.com)
21+
//
22+
// Authors:
23+
// Scott Willeke ([email protected])
24+
//
25+
using Microsoft.Tools.WindowsInstallerXml.Msi;
26+
27+
namespace LessMsi.Msi
28+
{
29+
/// <summary>
30+
/// Helper class for opening an MSI Database or MSI Patch file
31+
/// </summary>
32+
public static class MsiDatabase
33+
{
34+
/// <summary>
35+
/// Documented flag, unlisted in Microsoft.Tools.WindowsInstallerXml.Msi.OpenDatabase
36+
/// </summary>
37+
const uint MSIDBOPEN_PATCHFILE = 32;
38+
39+
/// <summary>
40+
/// Create a Database object from either an .msi or .mso file
41+
/// </summary>
42+
/// <param name="msiDatabaseFilePath">The path to the database or patch file</param>
43+
/// <returns></returns>
44+
public static Database Create(LessIO.Path msiDatabaseFilePath)
45+
{
46+
try
47+
{
48+
return new Database(msiDatabaseFilePath.PathString, OpenDatabase.ReadOnly);
49+
}
50+
catch (System.IO.IOException)
51+
{
52+
// retry as patchfile (.msp)
53+
return new Database(msiDatabaseFilePath.PathString, OpenDatabase.ReadOnly | (OpenDatabase)MSIDBOPEN_PATCHFILE);
54+
}
55+
}
56+
}
57+
}

src/LessMsi.Core/Msi/TableWrapper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static TableRow[] GetRowsFromTable(Database msidb, string tableName)
4848
{
4949
if (!msidb.TableExists(tableName))
5050
{
51-
Trace.WriteLine(string.Format("Table name does {0} not exist Found.", tableName));
51+
Trace.WriteLine(string.Format("Table name '{0}' does not exist.", tableName));
5252
return new TableRow[0];
5353
}
5454

src/LessMsi.Gui/MainForm.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ internal class MainForm : Form, IMainFormView
4848
private Panel pnlStreamsBottom;
4949
private Button btnExtractStreamFiles;
5050
private ToolStripMenuItem searchFileToolStripMenuItem;
51+
readonly static string[] AllowedDragDropExtensions = new[] { ".msi", ".msp" };
52+
5153

5254
public MainForm(string defaultInputFile)
5355
{
@@ -671,7 +673,7 @@ private void InitializeComponent()
671673
// openMsiDialog
672674
//
673675
this.openMsiDialog.DefaultExt = "msi";
674-
this.openMsiDialog.Filter = "msierablefiles|*.msi|All Files|*.*";
676+
this.openMsiDialog.Filter = "msierablefiles|*.msi;*.msp|All Files|*.*";
675677
//
676678
// statusBar1
677679
//
@@ -1016,7 +1018,7 @@ private static IEnumerable<string> GetDraggedFiles(DragEventArgs e)
10161018
{
10171019
var files = (string[]) e.Data.GetData(DataFormats.FileDrop);
10181020
var query = from file in files
1019-
where file != null && Path.GetExtension(file).ToLowerInvariant() == ".msi"
1021+
where file != null && AllowedDragDropExtensions.Contains(Path.GetExtension(file).ToLowerInvariant())
10201022
select file;
10211023
return query;
10221024
}

src/LessMsi.Gui/MainFormPresenter.cs

+21-12
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public MainForm ViewLeakedAbstraction
9898
/// </summary>
9999
public void ViewFiles()
100100
{
101-
using (var msidb = new Database(this.SelectedMsiFile.FullName, OpenDatabase.ReadOnly))
101+
using (var msidb = MsiDatabase.Create(new LessIO.Path(this.SelectedMsiFile.FullName)))
102102
{
103103
ViewFiles(msidb);
104104
ToggleSelectAllFiles(true);
@@ -120,14 +120,23 @@ private void ViewFiles(Database msidb)
120120
{
121121
Status();
122122

123-
MsiFile[] dataItems = MsiFile.CreateMsiFilesFromMSI(msidb);
124-
MsiFileItemView[] viewItems = Array.ConvertAll<MsiFile, MsiFileItemView>(dataItems,
125-
inItem => new MsiFileItemView(inItem)
126-
);
127-
fileDataSource = new SortableBindingList<MsiFileItemView>(viewItems);
128-
ViewLeakedAbstraction.fileGrid.DataSource = fileDataSource;
129-
View.AutoSizeFileGridColumns();
130-
Status(fileDataSource.Count + " files found.");
123+
ViewLeakedAbstraction.fileGrid.DataSource = null;
124+
125+
if (msidb.TableExists("File"))
126+
{
127+
MsiFile[] dataItems = MsiFile.CreateMsiFilesFromMSI(msidb);
128+
MsiFileItemView[] viewItems = Array.ConvertAll<MsiFile, MsiFileItemView>(dataItems,
129+
inItem => new MsiFileItemView(inItem)
130+
);
131+
fileDataSource = new SortableBindingList<MsiFileItemView>(viewItems);
132+
ViewLeakedAbstraction.fileGrid.DataSource = fileDataSource;
133+
View.AutoSizeFileGridColumns();
134+
Status(fileDataSource.Count + " files found.");
135+
}
136+
else
137+
{
138+
Status("No files present.");
139+
}
131140
}
132141
catch (Exception eUnexpected)
133142
{
@@ -144,7 +153,7 @@ public void UpdatePropertyTabView()
144153
try
145154
{
146155
MsiPropertyInfo[] props;
147-
using (var msidb = new Database(this.SelectedMsiFile.FullName, OpenDatabase.ReadOnly))
156+
using (var msidb = MsiDatabase.Create(new LessIO.Path(this.SelectedMsiFile.FullName)))
148157
{
149158
props = MsiPropertyInfo.GetPropertiesFromDatabase(msidb);
150159
}
@@ -315,7 +324,7 @@ public void LoadTables()
315324

316325
IEnumerable<string> msiTableNames = allTableNames;
317326

318-
using (var msidb = new Database(this.SelectedMsiFile.FullName, OpenDatabase.ReadOnly))
327+
using (var msidb = MsiDatabase.Create(new LessIO.Path(this.SelectedMsiFile.FullName)))
319328
{
320329
using (View.StartWaitCursor())
321330
{
@@ -407,7 +416,7 @@ public void OnSelectedStreamChanged()
407416
/// </summary>
408417
public void UpdateMSiTableGrid()
409418
{
410-
using (var msidb = new Database(this.SelectedMsiFile.FullName, OpenDatabase.ReadOnly))
419+
using (var msidb = MsiDatabase.Create(new LessIO.Path(this.SelectedMsiFile.FullName)))
411420
{
412421
string tableName = View.SelectedTableName;
413422
UpdateMSiTableGrid(msidb, tableName);

src/Lessmsi.Tests/LessMsi.Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<Reference Include="System" />
5454
<Reference Include="System.Runtime.Serialization" />
5555
<Reference Include="System.XML" />
56+
<Reference Include="WindowsBase" />
5657
<Reference Include="wix, Version=2.0.2110.0, Culture=neutral">
5758
<SpecificVersion>False</SpecificVersion>
5859
<HintPath>..\..\lib\wix.dll</HintPath>
@@ -83,6 +84,7 @@
8384
<Compile Include="FileEntryGraph.cs" />
8485
<Compile Include="MiscTests.cs" />
8586
<Compile Include="MiscTestsNUnit.cs" />
87+
<Compile Include="MspTests.cs" />
8688
<Compile Include="Properties\AssemblyInfo.cs" />
8789
<Compile Include="TestBase.cs" />
8890
</ItemGroup>

src/Lessmsi.Tests/MspTests.cs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Xunit;
2+
3+
namespace LessMsi.Tests
4+
{
5+
public class MspTests: TestBase
6+
{
7+
[Fact]
8+
public void MsXml5()
9+
{
10+
ExpectTables("msxml5.msp", new[] { "MsiPatchMetadata", "MsiPatchSequence" });
11+
// Cannot test properties yet, since they are internal in LessMsi.Gui!
12+
ExpectStreamCabFiles("msxml5.msp", true);
13+
}
14+
15+
[Fact]
16+
public void WPF2_32()
17+
{
18+
ExpectTables("WPF2_32.msp", new[] { "MsiPatchMetadata", "MsiPatchSequence" });
19+
ExpectStreamCabFiles("WPF2_32.msp", true);
20+
}
21+
22+
[Fact]
23+
public void SQL2008_AS()
24+
{
25+
ExpectTables("SQL2008_AS.msp", new[] { "MsiPatchSequence" });
26+
ExpectStreamCabFiles("SQL2008_AS.msp", true);
27+
}
28+
29+
}
30+
}

src/Lessmsi.Tests/TestBase.cs

+44-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
using System.Threading;
55
using Xunit;
66
using LessIO;
7-
7+
using System.Linq;
8+
89
namespace LessMsi.Tests
910
{
1011
public class TestBase
@@ -242,6 +243,45 @@ protected Path AppPath
242243
var local = new Path(codeBase.LocalPath);
243244
return local.Parent;
244245
}
245-
}
246-
}
247-
}
246+
}
247+
248+
[DebuggerHidden]
249+
protected void ExpectTables(string sourceFileName, string[] expectedTableNames)
250+
{
251+
using (var msidb = Msi.MsiDatabase.Create(GetMsiTestFile(sourceFileName)))
252+
{
253+
Assert.NotNull(msidb);
254+
var query = "SELECT * FROM `_Tables`";
255+
using (var msiTable = new Msi.ViewWrapper(msidb.OpenExecuteView(query)))
256+
{
257+
Assert.NotNull(msiTable);
258+
259+
var tableNames = from record in msiTable.Records
260+
select record[0] as string;
261+
// Since we don't care about the order, we sort the lists
262+
Assert.Equal(expectedTableNames.OrderBy(s => s), tableNames.OrderBy(s => s));
263+
}
264+
}
265+
}
266+
267+
[DebuggerHidden]
268+
protected void ExpectStreamCabFiles(string sourceFileName, bool hasCab)
269+
{
270+
using (var stg = new OleStorage.OleStorageFile(GetMsiTestFile(sourceFileName)))
271+
{
272+
var strm = stg.GetStreams().Where(elem => OleStorage.OleStorageFile.IsCabStream(elem));
273+
if (strm != null)
274+
{
275+
// Rest of the CAB parsing logic is in the UI, can't extract filenames without duplicating code that we want to test..
276+
Assert.True(hasCab);
277+
}
278+
else
279+
{
280+
// Not expecting to find a cab here
281+
Assert.False(hasCab);
282+
}
283+
}
284+
}
285+
286+
}
287+
}
Binary file not shown.
21.5 KB
Binary file not shown.
7.22 MB
Binary file not shown.

0 commit comments

Comments
 (0)