diff --git a/src/LessMsi.Core/Msi/MsiDatabase.cs b/src/LessMsi.Core/Msi/MsiDatabase.cs
index 94b1cf2..968f204 100644
--- a/src/LessMsi.Core/Msi/MsiDatabase.cs
+++ b/src/LessMsi.Core/Msi/MsiDatabase.cs
@@ -23,6 +23,9 @@
// Scott Willeke (scott@willeke.com)
//
using Microsoft.Tools.WindowsInstallerXml.Msi;
+using System;
+using System.IO;
+using System.IO.MemoryMappedFiles;
namespace LessMsi.Msi
{
@@ -53,5 +56,81 @@ public static Database Create(LessIO.Path msiDatabaseFilePath)
return new Database(msiDatabaseFilePath.PathString, OpenDatabase.ReadOnly | (OpenDatabase)MSIDBOPEN_PATCHFILE);
}
}
+
+
+ // 'Magic bytes' that identify the beginning of an 'StgStorage' file, which is the format used by MSI files.
+ static readonly Byte[] STG_STORAGE_magic_bytes = new byte[]{ 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1 };
+
+ public static bool TryDetectMsiHeader(LessIO.Path filePath, out long offset)
+ {
+ using (var mapped = MemoryMappedFile.CreateFromFile(filePath.PathString, System.IO.FileMode.Open, null, 0L, MemoryMappedFileAccess.Read))
+ {
+ using (var accessor = mapped.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read))
+ {
+ byte[] buffer = new byte[4096];
+ long readOffset = 0;
+ while (true)
+ {
+ var totalRemaining = accessor.Capacity - readOffset;
+ if (totalRemaining <= 0)
+ break;
+ int remaining = accessor.ReadArray(readOffset, buffer, 0, (int)Math.Min(totalRemaining, buffer.Length));
+
+ bool moveLess = false;
+ for (int n = 0; n < remaining; n++)
+ {
+ if (buffer[n] == STG_STORAGE_magic_bytes[0])
+ {
+ if (buffer.Length - n >= STG_STORAGE_magic_bytes.Length)
+ {
+ bool match = true;
+ for (int m = 1; m < STG_STORAGE_magic_bytes.Length; m++)
+ {
+ if (buffer[n + m] != STG_STORAGE_magic_bytes[m])
+ {
+ match = false;
+ break;
+ }
+ }
+ if (match)
+ {
+ offset = readOffset + n;
+ return true;
+ }
+ }
+ else
+ {
+ moveLess = true;
+ break;
+ }
+ }
+ }
+
+ if (moveLess && remaining > STG_STORAGE_magic_bytes.Length && readOffset > STG_STORAGE_magic_bytes.Length)
+ {
+ readOffset -= STG_STORAGE_magic_bytes.Length;
+ }
+ readOffset += remaining;
+ }
+
+ offset = -1;
+ return false;
+ }
+ }
+ }
+
+ public static void ExtractMsiFromExe(LessIO.Path filePath, LessIO.Path outputFile, long offset)
+ {
+ using (var mapped = MemoryMappedFile.CreateFromFile(filePath.PathString, FileMode.Open, null, 0L, MemoryMappedFileAccess.Read))
+ {
+ using (var reader = mapped.CreateViewStream(offset, 0, MemoryMappedFileAccess.Read))
+ {
+ using (var output = File.Create(outputFile.PathString))
+ {
+ reader.CopyTo(output);
+ }
+ }
+ }
+ }
}
}
diff --git a/src/LessMsi.Gui/IMainFormView.cs b/src/LessMsi.Gui/IMainFormView.cs
index 161fec1..060e74e 100644
--- a/src/LessMsi.Gui/IMainFormView.cs
+++ b/src/LessMsi.Gui/IMainFormView.cs
@@ -78,6 +78,11 @@ internal interface IMainFormView
///
void ShowUserError(string formatStr, params object[] args);
///
+ /// Displays a message to the user with a Yes/No question and returns the user's response.
+ ///
+ /// The message to display to the user.
+ bool ShowUserMessageQuestionYesNo(string message);
+ ///
/// Adds a column to the MSI table grid.
///
void AddTableViewGridColumn(string headerText);
diff --git a/src/LessMsi.Gui/MainForm.cs b/src/LessMsi.Gui/MainForm.cs
index c423ef0..f922a03 100644
--- a/src/LessMsi.Gui/MainForm.cs
+++ b/src/LessMsi.Gui/MainForm.cs
@@ -54,7 +54,7 @@ internal class MainForm : Form, IMainFormView
private Panel pnlStreamsBottom;
private Button btnExtractStreamFiles;
private ToolStripMenuItem searchFileToolStripMenuItem;
- readonly static string[] AllowedDragDropExtensions = new[] { ".msi", ".msp" };
+ readonly static string[] AllowedDragDropExtensions = new[] { ".msi", ".msp", ".exe" };
public MainForm(string defaultInputFile)
{
@@ -169,6 +169,12 @@ public void ShowUserError(string formatStr, params object[] args)
ShowUserMessageBox(string.Format(CultureInfo.CurrentUICulture, formatStr, args));
}
+ public bool ShowUserMessageQuestionYesNo(string message)
+ {
+ return MessageBox.Show(this, message, "LessMSI", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes;
+ }
+
+
#region MSI Table Grid Stuff
public void AddTableViewGridColumn(string headerText)
diff --git a/src/LessMsi.Gui/MainFormPresenter.cs b/src/LessMsi.Gui/MainFormPresenter.cs
index c65d23b..f256cc9 100644
--- a/src/LessMsi.Gui/MainFormPresenter.cs
+++ b/src/LessMsi.Gui/MainFormPresenter.cs
@@ -22,12 +22,6 @@
// Authors:
// Scott Willeke (scott@willeke.com)
//
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics;
-using System.IO;
-using System.Linq;
using LessIO;
using LessMsi.Gui.Model;
using LessMsi.Gui.Resources.Languages;
@@ -35,6 +29,13 @@
using LessMsi.Msi;
using LessMsi.OleStorage;
using Microsoft.Tools.WindowsInstallerXml.Msi;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.IO.MemoryMappedFiles;
+using System.Linq;
namespace LessMsi.Gui
{
@@ -545,6 +546,16 @@ public void LoadCurrentFile()
}
catch (Exception eCatchAll)
{
+ if (eCatchAll is IOException && this.SelectedMsiFile.Extension.ToLower() == ".exe" && MsiDatabase.TryDetectMsiHeader(new LessIO.Path(this.SelectedMsiFile.FullName), out long offset))
+ {
+ if (View.ShowUserMessageQuestionYesNo("It looks like this file contains a .msi file, do you want to extract it?"))
+ {
+ var tempFile = System.IO.Path.GetTempFileName();
+ MsiDatabase.ExtractMsiFromExe(new LessIO.Path(this.SelectedMsiFile.FullName), new LessIO.Path(tempFile), offset);
+ LoadFile(tempFile);
+ return;
+ }
+ }
isBadFile = true;
Error(Strings.OpenFileError, eCatchAll);
}
diff --git a/src/Lessmsi.Tests/MiscTests.cs b/src/Lessmsi.Tests/MiscTests.cs
index 18bd1f1..5cba954 100644
--- a/src/Lessmsi.Tests/MiscTests.cs
+++ b/src/Lessmsi.Tests/MiscTests.cs
@@ -140,5 +140,27 @@ public void MissingParentDirectoryEntry()
{
ExtractAndCompareToMaster("IviNetSharedComponents32_Fx20_1.3.0.msi");
}
+
+ ///
+ /// From https://github.com/activescott/lessmsi/pull/237
+ ///
+ [Fact]
+ public void TryDetectEmbeddedMsi()
+ {
+ const long stg_storage_offset_in_vmware_tools_setup = 0x315E3C;
+ bool containsMsi = Msi.MsiDatabase.TryDetectMsiHeader(GetMsiTestFile("vmware_tools_setup.exe"), out long offset);
+ Assert.True(containsMsi);
+ Assert.Equal(stg_storage_offset_in_vmware_tools_setup, offset);
+
+ const long stg_storage_offset_in_ivinetsharedcomponents_msi = 0;
+ containsMsi = Msi.MsiDatabase.TryDetectMsiHeader(GetMsiTestFile("IviNetSharedComponents32_Fx20_1.3.0.msi"), out offset);
+ Assert.True(containsMsi);
+ Assert.Equal(stg_storage_offset_in_ivinetsharedcomponents_msi, offset);
+
+ const long no_stg_storage_magic_found = -1;
+ containsMsi = Msi.MsiDatabase.TryDetectMsiHeader(GetMsiTestFile("msi_with_external_cab.cab"), out offset);
+ Assert.False(containsMsi);
+ Assert.Equal(no_stg_storage_magic_found, offset);
+ }
}
}
diff --git a/src/Lessmsi.Tests/TestFiles/MsiInput/vmware_tools_setup.exe b/src/Lessmsi.Tests/TestFiles/MsiInput/vmware_tools_setup.exe
new file mode 100644
index 0000000..90b34c1
Binary files /dev/null and b/src/Lessmsi.Tests/TestFiles/MsiInput/vmware_tools_setup.exe differ