Skip to content

Commit

Permalink
feat: Add support for Windows Installer Patch (.msp) files
Browse files Browse the repository at this point in the history
Closes #1
  • Loading branch information
learn-more committed Oct 31, 2021
1 parent 83cf1a8 commit b4db2bb
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 20 deletions.
2 changes: 1 addition & 1 deletion src/LessMsi.Cli/ListTableCommand.cs
Expand Up @@ -31,7 +31,7 @@ public override void Run(List<string> args)

var csv = new StringBuilder();
Debug.Print("Opening msi file '{0}'.", extra[0]);
using (var msidb = new Database(extra[0], OpenDatabase.ReadOnly))
using (var msidb = MsiDatabase.Create(new LessIO.Path(extra[0])))
{
Debug.Print("Opening table '{0}'.", tableName);
var query = string.Format(CultureInfo.InvariantCulture, "SELECT * FROM `{0}`", tableName);
Expand Down
1 change: 1 addition & 0 deletions src/LessMsi.Core/LessMsi.Core.csproj
Expand Up @@ -56,6 +56,7 @@
<ItemGroup>
<Compile Include="Msi\ColumnInfo.cs" />
<Compile Include="Msi\ExternalCabNotFoundException.cs" />
<Compile Include="Msi\MsiDatabase.cs" />
<Compile Include="Msi\MsiDirectory.cs" />
<Compile Include="Msi\MsiFile.cs" />
<Compile Include="Msi\TableWrapper.cs" />
Expand Down
57 changes: 57 additions & 0 deletions src/LessMsi.Core/Msi/MsiDatabase.cs
@@ -0,0 +1,57 @@
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Copyright (c) 2021 Scott Willeke (http://scott.willeke.com)
//
// Authors:
// Scott Willeke (scott@willeke.com)
//
using Microsoft.Tools.WindowsInstallerXml.Msi;

namespace LessMsi.Msi
{
/// <summary>
/// Helper class for opening an MSI Database or MSI Patch file
/// </summary>
public static class MsiDatabase
{
/// <summary>
/// Documented flag, unlisted in Microsoft.Tools.WindowsInstallerXml.Msi.OpenDatabase
/// </summary>
const uint MSIDBOPEN_PATCHFILE = 32;

/// <summary>
/// Create a Database object from either an .msi or .mso file
/// </summary>
/// <param name="msiDatabaseFilePath">The path to the database or patch file</param>
/// <returns></returns>
public static Database Create(LessIO.Path msiDatabaseFilePath)
{
try
{
return new Database(msiDatabaseFilePath.PathString, OpenDatabase.ReadOnly);
}
catch (System.IO.IOException)
{
// retry as patchfile (.msp)
return new Database(msiDatabaseFilePath.PathString, OpenDatabase.ReadOnly | (OpenDatabase)MSIDBOPEN_PATCHFILE);
}
}
}
}
2 changes: 1 addition & 1 deletion src/LessMsi.Core/Msi/TableWrapper.cs
Expand Up @@ -48,7 +48,7 @@ public static TableRow[] GetRowsFromTable(Database msidb, string tableName)
{
if (!msidb.TableExists(tableName))
{
Trace.WriteLine(string.Format("Table name does {0} not exist Found.", tableName));
Trace.WriteLine(string.Format("Table name '{0}' does not exist.", tableName));
return new TableRow[0];
}

Expand Down
6 changes: 4 additions & 2 deletions src/LessMsi.Gui/MainForm.cs
Expand Up @@ -48,6 +48,8 @@ internal class MainForm : Form, IMainFormView
private Panel pnlStreamsBottom;
private Button btnExtractStreamFiles;
private ToolStripMenuItem searchFileToolStripMenuItem;
readonly static string[] AllowedDragDropExtensions = new[] { ".msi", ".msp" };


public MainForm(string defaultInputFile)
{
Expand Down Expand Up @@ -671,7 +673,7 @@ private void InitializeComponent()
// openMsiDialog
//
this.openMsiDialog.DefaultExt = "msi";
this.openMsiDialog.Filter = "msierablefiles|*.msi|All Files|*.*";
this.openMsiDialog.Filter = "msierablefiles|*.msi;*.msp|All Files|*.*";
//
// statusBar1
//
Expand Down Expand Up @@ -1016,7 +1018,7 @@ private static IEnumerable<string> GetDraggedFiles(DragEventArgs e)
{
var files = (string[]) e.Data.GetData(DataFormats.FileDrop);
var query = from file in files
where file != null && Path.GetExtension(file).ToLowerInvariant() == ".msi"
where file != null && AllowedDragDropExtensions.Contains(Path.GetExtension(file).ToLowerInvariant())
select file;
return query;
}
Expand Down
33 changes: 21 additions & 12 deletions src/LessMsi.Gui/MainFormPresenter.cs
Expand Up @@ -98,7 +98,7 @@ public MainForm ViewLeakedAbstraction
/// </summary>
public void ViewFiles()
{
using (var msidb = new Database(this.SelectedMsiFile.FullName, OpenDatabase.ReadOnly))
using (var msidb = MsiDatabase.Create(new LessIO.Path(this.SelectedMsiFile.FullName)))
{
ViewFiles(msidb);
ToggleSelectAllFiles(true);
Expand All @@ -120,14 +120,23 @@ private void ViewFiles(Database msidb)
{
Status();

MsiFile[] dataItems = MsiFile.CreateMsiFilesFromMSI(msidb);
MsiFileItemView[] viewItems = Array.ConvertAll<MsiFile, MsiFileItemView>(dataItems,
inItem => new MsiFileItemView(inItem)
);
fileDataSource = new SortableBindingList<MsiFileItemView>(viewItems);
ViewLeakedAbstraction.fileGrid.DataSource = fileDataSource;
View.AutoSizeFileGridColumns();
Status(fileDataSource.Count + " files found.");
ViewLeakedAbstraction.fileGrid.DataSource = null;

if (msidb.TableExists("File"))
{
MsiFile[] dataItems = MsiFile.CreateMsiFilesFromMSI(msidb);
MsiFileItemView[] viewItems = Array.ConvertAll<MsiFile, MsiFileItemView>(dataItems,
inItem => new MsiFileItemView(inItem)
);
fileDataSource = new SortableBindingList<MsiFileItemView>(viewItems);
ViewLeakedAbstraction.fileGrid.DataSource = fileDataSource;
View.AutoSizeFileGridColumns();
Status(fileDataSource.Count + " files found.");
}
else
{
Status("No files present.");
}
}
catch (Exception eUnexpected)
{
Expand All @@ -144,7 +153,7 @@ public void UpdatePropertyTabView()
try
{
MsiPropertyInfo[] props;
using (var msidb = new Database(this.SelectedMsiFile.FullName, OpenDatabase.ReadOnly))
using (var msidb = MsiDatabase.Create(new LessIO.Path(this.SelectedMsiFile.FullName)))
{
props = MsiPropertyInfo.GetPropertiesFromDatabase(msidb);
}
Expand Down Expand Up @@ -315,7 +324,7 @@ public void LoadTables()

IEnumerable<string> msiTableNames = allTableNames;

using (var msidb = new Database(this.SelectedMsiFile.FullName, OpenDatabase.ReadOnly))
using (var msidb = MsiDatabase.Create(new LessIO.Path(this.SelectedMsiFile.FullName)))
{
using (View.StartWaitCursor())
{
Expand Down Expand Up @@ -407,7 +416,7 @@ public void OnSelectedStreamChanged()
/// </summary>
public void UpdateMSiTableGrid()
{
using (var msidb = new Database(this.SelectedMsiFile.FullName, OpenDatabase.ReadOnly))
using (var msidb = MsiDatabase.Create(new LessIO.Path(this.SelectedMsiFile.FullName)))
{
string tableName = View.SelectedTableName;
UpdateMSiTableGrid(msidb, tableName);
Expand Down
2 changes: 2 additions & 0 deletions src/Lessmsi.Tests/LessMsi.Tests.csproj
Expand Up @@ -53,6 +53,7 @@
<Reference Include="System" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.XML" />
<Reference Include="WindowsBase" />
<Reference Include="wix, Version=2.0.2110.0, Culture=neutral">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\lib\wix.dll</HintPath>
Expand Down Expand Up @@ -83,6 +84,7 @@
<Compile Include="FileEntryGraph.cs" />
<Compile Include="MiscTests.cs" />
<Compile Include="MiscTestsNUnit.cs" />
<Compile Include="MspTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TestBase.cs" />
</ItemGroup>
Expand Down
30 changes: 30 additions & 0 deletions src/Lessmsi.Tests/MspTests.cs
@@ -0,0 +1,30 @@
using Xunit;

namespace LessMsi.Tests
{
public class MspTests: TestBase
{
[Fact]
public void MsXml5()
{
ExpectTables("msxml5.msp", new[] { "MsiPatchMetadata", "MsiPatchSequence" });
// Cannot test properties yet, since they are internal in LessMsi.Gui!
ExpectStreamCabFiles("msxml5.msp", true);
}

[Fact]
public void WPF2_32()
{
ExpectTables("WPF2_32.msp", new[] { "MsiPatchMetadata", "MsiPatchSequence" });
ExpectStreamCabFiles("WPF2_32.msp", true);
}

[Fact]
public void SQL2008_AS()
{
ExpectTables("SQL2008_AS.msp", new[] { "MsiPatchSequence" });
ExpectStreamCabFiles("SQL2008_AS.msp", true);
}

}
}
48 changes: 44 additions & 4 deletions src/Lessmsi.Tests/TestBase.cs
Expand Up @@ -4,7 +4,8 @@
using System.Threading;
using Xunit;
using LessIO;

using System.Linq;

namespace LessMsi.Tests
{
public class TestBase
Expand Down Expand Up @@ -242,6 +243,45 @@ protected Path AppPath
var local = new Path(codeBase.LocalPath);
return local.Parent;
}
}
}
}
}

[DebuggerHidden]
protected void ExpectTables(string sourceFileName, string[] expectedTableNames)
{
using (var msidb = Msi.MsiDatabase.Create(GetMsiTestFile(sourceFileName)))
{
Assert.NotNull(msidb);
var query = "SELECT * FROM `_Tables`";
using (var msiTable = new Msi.ViewWrapper(msidb.OpenExecuteView(query)))
{
Assert.NotNull(msiTable);

var tableNames = from record in msiTable.Records
select record[0] as string;
// Since we don't care about the order, we sort the lists
Assert.Equal(expectedTableNames.OrderBy(s => s), tableNames.OrderBy(s => s));
}
}
}

[DebuggerHidden]
protected void ExpectStreamCabFiles(string sourceFileName, bool hasCab)
{
using (var stg = new OleStorage.OleStorageFile(GetMsiTestFile(sourceFileName)))
{
var strm = stg.GetStreams().Where(elem => OleStorage.OleStorageFile.IsCabStream(elem));
if (strm != null)
{
// Rest of the CAB parsing logic is in the UI, can't extract filenames without duplicating code that we want to test..
Assert.True(hasCab);
}
else
{
// Not expecting to find a cab here
Assert.False(hasCab);
}
}
}

}
}
Binary file not shown.
Binary file added src/Lessmsi.Tests/TestFiles/MsiInput/WPF2_32.msp
Binary file not shown.
Binary file added src/Lessmsi.Tests/TestFiles/MsiInput/msxml5.msp
Binary file not shown.

0 comments on commit b4db2bb

Please sign in to comment.