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