Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/LessMsi.Core/Msi/MsiDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need the 4K buffer here. Since you're already in a MemoryMappedFile (good call!!), you can just use the view to scan through for the magic bits.

Minor: I might just return an offset of -1 instead of using an out param, but that's just a preference. up to you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean using the ReadByte function of the MemoryMappedViewAccessor,
or the ReadByte from the MemoryMappedViewStream ?

I am not too familiar with MMF in c#, and I didn't find another way to use it.
(In c you'd directly read from the raw memory pointer)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MMF is already memory (like C) so the extra buffer is probably redundant. It’s a nitpick. We can investigate later.

{
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it's worth to extract totalRemaining <= 0 into a separate method, since you're using this bool statement twice

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second condition is not strictly needed, if you prefer no repetition I'll just remove it.

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);
}
}
}
}
}
}
5 changes: 5 additions & 0 deletions src/LessMsi.Gui/IMainFormView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ internal interface IMainFormView
/// </summary>
void ShowUserError(string formatStr, params object[] args);
/// <summary>
/// Displays a message to the user with a Yes/No question and returns the user's response.
/// </summary>
/// <param name="message">The message to display to the user.</param>
bool ShowUserMessageQuestionYesNo(string message);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this method necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we drop a file on disk for the user (even if it is just in the temp file),
I consider it good practice to ask it.

/// <summary>
/// Adds a column to the MSI table grid.
/// </summary>
void AddTableViewGridColumn(string headerText);
Expand Down
8 changes: 7 additions & 1 deletion src/LessMsi.Gui/MainForm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
23 changes: 17 additions & 6 deletions src/LessMsi.Gui/MainFormPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,20 @@
// 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;
using LessMsi.Gui.Windows.Forms;
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
{
Expand Down Expand Up @@ -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?"))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really think we need to ask? I would assume if the dropped an exe they want us to do it, so why not just do it and assume that's what they want?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It drops a file on the users pc, and this file will be removed again later on.
I thought it would be nice, but if you have other ideas about this that works for me.

{
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);
}
Expand Down
22 changes: 22 additions & 0 deletions src/Lessmsi.Tests/MiscTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,27 @@ public void MissingParentDirectoryEntry()
{
ExtractAndCompareToMaster("IviNetSharedComponents32_Fx20_1.3.0.msi");
}

/// <summary>
/// From https://github.com/activescott/lessmsi/pull/237
/// </summary>
[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);
}
}
}
Binary file not shown.