From 8cbfc326cccf8d9b3b63cb6f752fc770f7dee0fc Mon Sep 17 00:00:00 2001
From: Sean Hall <r.sean.hall@gmail.com>
Date: Tue, 29 Jun 2021 19:14:02 -0500
Subject: Expose overridable variable APIs in balutil and Mba.Core.

Fixes #4777
---
 .../WixToolset.Mba.Core/BootstrapperCommand.cs     |  52 +++++-
 src/api/burn/WixToolset.Mba.Core/BundleInfo.cs     |   5 +
 .../WixToolset.Mba.Core/IBootstrapperCommand.cs    |  16 +-
 src/api/burn/WixToolset.Mba.Core/IBundleInfo.cs    |   5 +
 src/api/burn/WixToolset.Mba.Core/IMbaCommand.cs    |  30 +++
 .../IOverridableVariableInfo.cs                    |  15 ++
 .../WixToolset.Mba.Core/IOverridableVariables.cs   |  17 ++
 src/api/burn/WixToolset.Mba.Core/MbaCommand.cs     |  33 ++++
 .../WixToolset.Mba.Core/OverridableVariableInfo.cs |  15 ++
 .../WixToolset.Mba.Core/OverridableVariables.cs    |  51 ++++++
 src/api/burn/balutil/balinfo.cpp                   | 203 +++++++++++++++++++++
 src/api/burn/balutil/inc/balinfo.h                 |  55 ++++++
 src/api/burn/balutil/precomp.h                     |   1 +
 .../BaseBootstrapperApplicationFactoryFixture.cs   |  13 +-
 14 files changed, 500 insertions(+), 11 deletions(-)
 create mode 100644 src/api/burn/WixToolset.Mba.Core/IMbaCommand.cs
 create mode 100644 src/api/burn/WixToolset.Mba.Core/IOverridableVariableInfo.cs
 create mode 100644 src/api/burn/WixToolset.Mba.Core/IOverridableVariables.cs
 create mode 100644 src/api/burn/WixToolset.Mba.Core/MbaCommand.cs
 create mode 100644 src/api/burn/WixToolset.Mba.Core/OverridableVariableInfo.cs
 create mode 100644 src/api/burn/WixToolset.Mba.Core/OverridableVariables.cs

(limited to 'src/api')

diff --git a/src/api/burn/WixToolset.Mba.Core/BootstrapperCommand.cs b/src/api/burn/WixToolset.Mba.Core/BootstrapperCommand.cs
index 65dde0f4..345e0448 100644
--- a/src/api/burn/WixToolset.Mba.Core/BootstrapperCommand.cs
+++ b/src/api/burn/WixToolset.Mba.Core/BootstrapperCommand.cs
@@ -3,6 +3,7 @@
 namespace WixToolset.Mba.Core
 {
     using System;
+    using System.Collections.Generic;
     using System.ComponentModel;
     using System.Runtime.InteropServices;
 
@@ -11,8 +12,6 @@ namespace WixToolset.Mba.Core
     /// </summary>
     public sealed class BootstrapperCommand : IBootstrapperCommand
     {
-        private readonly string commandLine;
-
         /// <summary>
         /// 
         /// </summary>
@@ -45,7 +44,7 @@ namespace WixToolset.Mba.Core
             this.Action = action;
             this.Display = display;
             this.Restart = restart;
-            this.commandLine = commandLine;
+            this.CommandLine = commandLine;
             this.CmdShow = cmdShow;
             this.Resume = resume;
             this.SplashScreen = splashScreen;
@@ -66,7 +65,7 @@ namespace WixToolset.Mba.Core
         public Restart Restart { get; }
 
         /// <inheritdoc/>
-        public string[] CommandLineArgs => GetCommandLineArgs(this.commandLine);
+        public string CommandLine { get; }
 
         /// <inheritdoc/>
         public int CmdShow { get; }
@@ -92,6 +91,49 @@ namespace WixToolset.Mba.Core
         /// <inheritdoc/>
         public string BootstrapperApplicationDataPath { get; }
 
+        /// <inheritdoc/>
+        public IMbaCommand ParseCommandLine()
+        {
+            var args = ParseCommandLineToArgs(this.CommandLine);
+            var unknownArgs = new List<string>();
+            var variables = new List<KeyValuePair<string, string>>();
+
+            foreach (var arg in args)
+            {
+                var unknownArg = false;
+
+                if (arg[0] == '-' || arg[0] == '/')
+                {
+                    unknownArg = true;
+                }
+                else
+                {
+                    var index = arg.IndexOf('=');
+                    if (index == -1)
+                    {
+                        unknownArg = true;
+                    }
+                    else
+                    {
+                        var name = arg.Substring(0, index);
+                        var value = arg.Substring(index + 1);
+                        variables.Add(new KeyValuePair<string, string>(name, value));
+                    }
+                }
+
+                if (unknownArg)
+                {
+                    unknownArgs.Add(arg);
+                }
+            }
+
+            return new MbaCommand
+            {
+                UnknownCommandLineArgs = unknownArgs.ToArray(),
+                Variables = variables.ToArray(),
+            };
+        }
+
         /// <summary>
         /// Gets the command line arguments as a string array.
         /// </summary>
@@ -102,7 +144,7 @@ namespace WixToolset.Mba.Core
         /// <remarks>
         /// This method uses the same parsing as the operating system which handles quotes and spaces correctly.
         /// </remarks>
-        public static string[] GetCommandLineArgs(string commandLine)
+        public static string[] ParseCommandLineToArgs(string commandLine)
         {
             if (null == commandLine)
             {
diff --git a/src/api/burn/WixToolset.Mba.Core/BundleInfo.cs b/src/api/burn/WixToolset.Mba.Core/BundleInfo.cs
index 3d5d535d..4a533bf9 100644
--- a/src/api/burn/WixToolset.Mba.Core/BundleInfo.cs
+++ b/src/api/burn/WixToolset.Mba.Core/BundleInfo.cs
@@ -22,6 +22,9 @@ namespace WixToolset.Mba.Core
         /// <inheritdoc/>
         public string LogVariable { get; internal set; }
 
+        /// <inheritdoc/>
+        public IOverridableVariables OverridableVariables { get; internal set; }
+
         /// <inheritdoc/>
         public IDictionary<string, IPackageInfo> Packages { get; internal set; }
 
@@ -78,6 +81,8 @@ namespace WixToolset.Mba.Core
 
             bundle.LogVariable = BootstrapperApplicationData.GetAttribute(bundleNode, "LogPathVariable");
 
+            bundle.OverridableVariables = OverridableVariablesInfo.ParseFromXml(root);
+
             bundle.Packages = PackageInfo.ParsePackagesFromXml(root);
 
             return bundle;
diff --git a/src/api/burn/WixToolset.Mba.Core/IBootstrapperCommand.cs b/src/api/burn/WixToolset.Mba.Core/IBootstrapperCommand.cs
index e861813f..b7baa55c 100644
--- a/src/api/burn/WixToolset.Mba.Core/IBootstrapperCommand.cs
+++ b/src/api/burn/WixToolset.Mba.Core/IBootstrapperCommand.cs
@@ -25,13 +25,12 @@ namespace WixToolset.Mba.Core
         Restart Restart { get; }
 
         /// <summary>
-        /// Gets the command line arguments as a string array.
+        /// Gets the command line arguments.
         /// </summary>
         /// <returns>
-        /// Array of command line arguments not handled by the engine.
+        /// Command line arguments not handled by the engine.
         /// </returns>
-        /// <exception type="Win32Exception">The command line could not be parsed into an array.</exception>
-        string[] CommandLineArgs { get; }
+        string CommandLine { get; }
 
         /// <summary>
         /// Hint for the initial visibility of the window.
@@ -72,5 +71,14 @@ namespace WixToolset.Mba.Core
         /// Gets path to BootstrapperApplicationData.xml.
         /// </summary>
         string BootstrapperApplicationDataPath { get; }
+
+        /// <summary>
+        /// Parses the command line arguments into an <see cref="IMbaCommand"/>.
+        /// </summary>
+        /// <returns>
+        /// The parsed information.
+        /// </returns>
+        /// <exception type="Win32Exception">The command line could not be parsed.</exception>
+        IMbaCommand ParseCommandLine();
     }
 }
diff --git a/src/api/burn/WixToolset.Mba.Core/IBundleInfo.cs b/src/api/burn/WixToolset.Mba.Core/IBundleInfo.cs
index f4a82f36..35decc88 100644
--- a/src/api/burn/WixToolset.Mba.Core/IBundleInfo.cs
+++ b/src/api/burn/WixToolset.Mba.Core/IBundleInfo.cs
@@ -19,6 +19,11 @@ namespace WixToolset.Mba.Core
         /// </summary>
         string Name { get; }
 
+        /// <summary>
+        /// 
+        /// </summary>
+        IOverridableVariables OverridableVariables { get; }
+
         /// <summary>
         /// 
         /// </summary>
diff --git a/src/api/burn/WixToolset.Mba.Core/IMbaCommand.cs b/src/api/burn/WixToolset.Mba.Core/IMbaCommand.cs
new file mode 100644
index 00000000..a3ad68d8
--- /dev/null
+++ b/src/api/burn/WixToolset.Mba.Core/IMbaCommand.cs
@@ -0,0 +1,30 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    using System.Collections.Generic;
+
+    /// <summary>
+    /// Command information parsed from the command line.
+    /// </summary>
+    public interface IMbaCommand
+    {
+        /// <summary>
+        /// The command line arguments not parsed into <see cref="IBootstrapperCommand"/> or <see cref="IMbaCommand"/>.
+        /// </summary>
+        string[] UnknownCommandLineArgs { get; }
+
+        /// <summary>
+        /// The variables that were parsed from the command line.
+        /// Key = variable name, Value = variable value.
+        /// </summary>
+        KeyValuePair<string, string>[] Variables { get; }
+
+        /// <summary>
+        /// Sets overridable variables from the command line.
+        /// </summary>
+        /// <param name="overridableVariables">The overridable variable information from <see cref="IBootstrapperApplicationData"/>.</param>
+        /// <param name="engine">The engine.</param>
+        void SetOverridableVariables(IOverridableVariables overridableVariables, IEngine engine);
+    }
+}
diff --git a/src/api/burn/WixToolset.Mba.Core/IOverridableVariableInfo.cs b/src/api/burn/WixToolset.Mba.Core/IOverridableVariableInfo.cs
new file mode 100644
index 00000000..d01dead3
--- /dev/null
+++ b/src/api/burn/WixToolset.Mba.Core/IOverridableVariableInfo.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    /// <summary>
+    /// Overridable variable from the BA manifest.
+    /// </summary>
+    public interface IOverridableVariableInfo
+    {
+        /// <summary>
+        /// The Variable name.
+        /// </summary>
+        string Name { get; }
+    }
+}
\ No newline at end of file
diff --git a/src/api/burn/WixToolset.Mba.Core/IOverridableVariables.cs b/src/api/burn/WixToolset.Mba.Core/IOverridableVariables.cs
new file mode 100644
index 00000000..3944913b
--- /dev/null
+++ b/src/api/burn/WixToolset.Mba.Core/IOverridableVariables.cs
@@ -0,0 +1,17 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    using System.Collections.Generic;
+
+    /// <summary>
+    /// Overridable variable information from the BA manifest.
+    /// </summary>
+    public interface IOverridableVariables
+    {
+        /// <summary>
+        /// Variable Dictionary of variable name to <see cref="IOverridableVariableInfo"/>.
+        /// </summary>
+        IDictionary<string, IOverridableVariableInfo> Variables { get; }
+    }
+}
\ No newline at end of file
diff --git a/src/api/burn/WixToolset.Mba.Core/MbaCommand.cs b/src/api/burn/WixToolset.Mba.Core/MbaCommand.cs
new file mode 100644
index 00000000..e7e49607
--- /dev/null
+++ b/src/api/burn/WixToolset.Mba.Core/MbaCommand.cs
@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    using System.Collections.Generic;
+
+    /// <summary>
+    /// Default implementation of <see cref="IMbaCommand"/>.
+    /// </summary>
+    internal sealed class MbaCommand : IMbaCommand
+    {
+        public string[] UnknownCommandLineArgs { get; internal set; }
+
+        public KeyValuePair<string, string>[] Variables { get; internal set; }
+
+        internal MbaCommand() { }
+
+        public void SetOverridableVariables(IOverridableVariables overridableVariables, IEngine engine)
+        {
+            foreach (var kvp in this.Variables)
+            {
+                if (!overridableVariables.Variables.TryGetValue(kvp.Key, out var overridableVariable))
+                {
+                    engine.Log(LogLevel.Error, string.Format("Ignoring attempt to set non-overridable variable: '{0}'.", kvp.Key));
+                }
+                else
+                {
+                    engine.SetVariableString(overridableVariable.Name, kvp.Value, false);
+                }
+            }
+        }
+    }
+}
diff --git a/src/api/burn/WixToolset.Mba.Core/OverridableVariableInfo.cs b/src/api/burn/WixToolset.Mba.Core/OverridableVariableInfo.cs
new file mode 100644
index 00000000..b8e85c34
--- /dev/null
+++ b/src/api/burn/WixToolset.Mba.Core/OverridableVariableInfo.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    /// <summary>
+    /// Default implementation of <see cref="IOverridableVariableInfo"/>.
+    /// </summary>
+    internal class OverridableVariableInfo : IOverridableVariableInfo
+    {
+        /// <inheritdoc />
+        public string Name { get; internal set; }
+
+        internal OverridableVariableInfo() { }
+    }
+}
diff --git a/src/api/burn/WixToolset.Mba.Core/OverridableVariables.cs b/src/api/burn/WixToolset.Mba.Core/OverridableVariables.cs
new file mode 100644
index 00000000..855ce9a9
--- /dev/null
+++ b/src/api/burn/WixToolset.Mba.Core/OverridableVariables.cs
@@ -0,0 +1,51 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
+
+namespace WixToolset.Mba.Core
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Xml;
+    using System.Xml.XPath;
+
+    /// <summary>
+    /// Default implementation of <see cref="IOverridableVariables"/>.
+    /// </summary>
+    public class OverridableVariablesInfo : IOverridableVariables
+    {
+        /// <inheritdoc />
+        public IDictionary<string, IOverridableVariableInfo> Variables { get; internal set; }
+
+        internal OverridableVariablesInfo() { }
+
+        /// <summary>
+        /// Parses the overridable variable info from the BA manifest.
+        /// </summary>
+        /// <param name="root">XML root</param>
+        /// <returns>The parsed information.</returns>
+        public static IOverridableVariables ParseFromXml(XPathNavigator root)
+        {
+            XmlNamespaceManager namespaceManager = new XmlNamespaceManager(root.NameTable);
+            namespaceManager.AddNamespace("p", BootstrapperApplicationData.XMLNamespace);
+            XPathNodeIterator nodes = root.Select("/p:BootstrapperApplicationData/p:WixStdbaOverridableVariable", namespaceManager);
+
+            var overridableVariables = new OverridableVariablesInfo();
+            overridableVariables.Variables = new Dictionary<string, IOverridableVariableInfo>();
+
+            foreach (XPathNavigator node in nodes)
+            {
+                var variable = new OverridableVariableInfo();
+
+                string name = BootstrapperApplicationData.GetAttribute(node, "Name");
+                if (name == null)
+                {
+                    throw new Exception("Failed to get name for overridable variable.");
+                }
+                variable.Name = name;
+
+                overridableVariables.Variables.Add(variable.Name, variable);
+            }
+
+            return overridableVariables;
+        }
+    }
+}
diff --git a/src/api/burn/balutil/balinfo.cpp b/src/api/burn/balutil/balinfo.cpp
index 3abb9286..5927ef72 100644
--- a/src/api/burn/balutil/balinfo.cpp
+++ b/src/api/burn/balutil/balinfo.cpp
@@ -11,7 +11,88 @@ static HRESULT ParseBalPackageInfoFromXml(
     __in BAL_INFO_PACKAGES* pPackages,
     __in IXMLDOMDocument* pixdManifest
     );
+static HRESULT ParseOverridableVariablesFromXml(
+    __in BAL_INFO_OVERRIDABLE_VARIABLES* pOverridableVariables,
+    __in IXMLDOMDocument* pixdManifest
+    );
+
+
+DAPI_(HRESULT) BalInfoParseCommandLine(
+    __in BAL_INFO_COMMAND* pCommand,
+    __in LPCWSTR wzCommandLine
+    )
+{
+    HRESULT hr = S_OK;
+    int argc = 0;
+    LPWSTR* argv = NULL;
+    BOOL fUnknownArg = FALSE;
+
+    BalInfoUninitializeCommandLine(pCommand);
+
+    if (!wzCommandLine || !*wzCommandLine)
+    {
+        ExitFunction();
+    }
+
+    hr = AppParseCommandLine(wzCommandLine, &argc, &argv);
+    ExitOnFailure(hr, "Failed to parse command line.");
 
+    for (int i = 0; i < argc; ++i)
+    {
+        fUnknownArg = FALSE;
+
+        if (argv[i][0] == L'-' || argv[i][0] == L'/')
+        {
+            fUnknownArg = TRUE;
+        }
+        else
+        {
+            const wchar_t* pwc = wcschr(argv[i], L'=');
+            if (!pwc)
+            {
+                fUnknownArg = TRUE;
+            }
+            else
+            {
+                hr = MemEnsureArraySizeForNewItems(reinterpret_cast<LPVOID*>(&pCommand->rgVariableNames), pCommand->cVariables, 1, sizeof(LPWSTR), 5);
+                ExitOnFailure(hr, "Failed to ensure size for variable names.");
+
+                hr = MemEnsureArraySizeForNewItems(reinterpret_cast<LPVOID*>(&pCommand->rgVariableValues), pCommand->cVariables, 1, sizeof(LPWSTR), 5);
+                ExitOnFailure(hr, "Failed to ensure size for variable values.");
+
+                LPWSTR* psczVariableName = pCommand->rgVariableNames + pCommand->cVariables;
+                LPWSTR* psczVariableValue = pCommand->rgVariableValues + pCommand->cVariables;
+                pCommand->cVariables += 1;
+
+                hr = StrAllocString(psczVariableName, argv[i], pwc - argv[i]);
+                BalExitOnFailure(hr, "Failed to copy variable name.");
+
+                hr = StrAllocString(psczVariableValue, ++pwc, 0);
+                BalExitOnFailure(hr, "Failed to copy variable value.");
+            }
+        }
+
+        if (fUnknownArg)
+        {
+            hr = MemEnsureArraySizeForNewItems(reinterpret_cast<LPVOID*>(&pCommand->rgUnknownArgs), pCommand->cUnknownArgs, 1, sizeof(LPWSTR), 5);
+            BalExitOnFailure(hr, "Failed to ensure size for unknown args.");
+
+            LPWSTR* psczArg = pCommand->rgUnknownArgs + pCommand->cUnknownArgs;
+            pCommand->cUnknownArgs += 1;
+
+            StrAllocString(psczArg, argv[i], 0);
+            BalExitOnFailure(hr, "Failed to copy unknown arg.");
+        }
+    }
+
+LExit:
+    if (argv)
+    {
+        AppFreeCommandLineArgs(argv);
+    }
+
+    return hr;
+}
 
 DAPI_(HRESULT) BalInfoParseFromXml(
     __in BAL_INFO_BUNDLE* pBundle,
@@ -45,6 +126,9 @@ DAPI_(HRESULT) BalInfoParseFromXml(
         }
     }
 
+    hr = ParseOverridableVariablesFromXml(&pBundle->overridableVariables, pixdManifest);
+    BalExitOnFailure(hr, "Failed to parse overridable variables from bootstrapper application data.");
+
     hr = ParsePackagesFromXml(&pBundle->packages, pixdManifest);
     BalExitOnFailure(hr, "Failed to parse package information from bootstrapper application data.");
 
@@ -163,12 +247,76 @@ DAPI_(void) BalInfoUninitialize(
 
     ReleaseMem(pBundle->packages.rgPackages);
 
+    for (DWORD i = 0; i < pBundle->overridableVariables.cVariables; ++i)
+    {
+        ReleaseStr(pBundle->overridableVariables.rgVariables[i].sczName);
+    }
+
+    ReleaseMem(pBundle->overridableVariables.rgVariables);
+    ReleaseDict(pBundle->overridableVariables.sdVariables);
+
     ReleaseStr(pBundle->sczName);
     ReleaseStr(pBundle->sczLogVariable);
     memset(pBundle, 0, sizeof(BAL_INFO_BUNDLE));
 }
 
 
+DAPI_(void) BalInfoUninitializeCommandLine(
+    __in BAL_INFO_COMMAND* pCommand
+    )
+{
+    for (DWORD i = 0; i < pCommand->cUnknownArgs; ++i)
+    {
+        ReleaseNullStrSecure(pCommand->rgUnknownArgs[i]);
+    }
+
+    ReleaseMem(pCommand->rgUnknownArgs);
+
+    for (DWORD i = 0; i < pCommand->cVariables; ++i)
+    {
+        ReleaseNullStrSecure(pCommand->rgVariableNames[i]);
+        ReleaseNullStrSecure(pCommand->rgVariableValues[i]);
+    }
+
+    ReleaseMem(pCommand->rgVariableNames);
+    ReleaseMem(pCommand->rgVariableValues);
+
+    memset(pCommand, 0, sizeof(BAL_INFO_COMMAND));
+}
+
+
+DAPI_(HRESULT) BalSetOverridableVariablesFromEngine(
+    __in BAL_INFO_OVERRIDABLE_VARIABLES* pOverridableVariables,
+    __in BAL_INFO_COMMAND* pCommand,
+    __in IBootstrapperEngine* pEngine
+    )
+{
+    HRESULT hr = S_OK;
+    BAL_INFO_OVERRIDABLE_VARIABLE* pOverridableVariable = NULL;
+
+    for (DWORD i = 0; i < pCommand->cVariables; ++i)
+    {
+        LPCWSTR wzVariableName = pCommand->rgVariableNames[i];
+        LPCWSTR wzVariableValue = pCommand->rgVariableValues[i];
+
+        hr = DictGetValue(pOverridableVariables->sdVariables, wzVariableName, reinterpret_cast<void**>(&pOverridableVariable));
+        if (E_NOTFOUND == hr)
+        {
+            BalLog(BOOTSTRAPPER_LOG_LEVEL_ERROR, "Ignoring attempt to set non-overridable variable: '%ls'.", wzVariableName);
+            hr = S_OK;
+            continue;
+        }
+        BalExitOnFailure(hr, "Failed to check the dictionary of overridable variables.");
+
+        hr = pEngine->SetVariableString(pOverridableVariable->sczName, wzVariableValue, FALSE);
+        BalExitOnFailure(hr, "Failed to set variable: '%ls'.", pOverridableVariable->sczName);
+    }
+
+LExit:
+    return hr;
+}
+
+
 static HRESULT ParsePackagesFromXml(
     __in BAL_INFO_PACKAGES* pPackages,
     __in IXMLDOMDocument* pixdManifest
@@ -371,3 +519,58 @@ LExit:
 
     return hr;
 }
+
+
+static HRESULT ParseOverridableVariablesFromXml(
+    __in BAL_INFO_OVERRIDABLE_VARIABLES* pOverridableVariables,
+    __in IXMLDOMDocument* pixdManifest
+    )
+{
+    HRESULT hr = S_OK;
+    IXMLDOMNode* pNode = NULL;
+    IXMLDOMNodeList* pNodes = NULL;
+    BAL_INFO_OVERRIDABLE_VARIABLE* pOverridableVariable = NULL;
+
+    // Get the list of variables users can override on the command line.
+    hr = XmlSelectNodes(pixdManifest, L"/BootstrapperApplicationData/WixStdbaOverridableVariable", &pNodes);
+    if (S_FALSE == hr)
+    {
+        ExitFunction1(hr = S_OK);
+    }
+    ExitOnFailure(hr, "Failed to select overridable variable nodes.");
+
+    hr = pNodes->get_length(reinterpret_cast<long*>(&pOverridableVariables->cVariables));
+    ExitOnFailure(hr, "Failed to get overridable variable node count.");
+
+    if (pOverridableVariables->cVariables)
+    {
+        hr = DictCreateWithEmbeddedKey(&pOverridableVariables->sdVariables, pOverridableVariables->cVariables, reinterpret_cast<void**>(&pOverridableVariables->rgVariables), offsetof(BAL_INFO_OVERRIDABLE_VARIABLE, sczName), DICT_FLAG_NONE);
+        ExitOnFailure(hr, "Failed to create the overridable variables string dictionary.");
+
+        hr = MemAllocArray(reinterpret_cast<LPVOID*>(&pOverridableVariables->rgVariables), sizeof(pOverridableVariable), pOverridableVariables->cVariables);
+        ExitOnFailure(hr, "Failed to create the overridable variables array.");
+
+        for (DWORD i = 0; i < pOverridableVariables->cVariables; ++i)
+        {
+            pOverridableVariable = pOverridableVariables->rgVariables + i;
+
+            hr = XmlNextElement(pNodes, &pNode, NULL);
+            ExitOnFailure(hr, "Failed to get next node.");
+
+            // @Name
+            hr = XmlGetAttributeEx(pNode, L"Name", &pOverridableVariable->sczName);
+            ExitOnFailure(hr, "Failed to get name for overridable variable.");
+
+            hr = DictAddValue(pOverridableVariables->sdVariables, pOverridableVariable);
+            ExitOnFailure(hr, "Failed to add \"%ls\" to the string dictionary.", pOverridableVariable->sczName);
+
+            // prepare next iteration
+            ReleaseNullObject(pNode);
+        }
+    }
+
+LExit:
+    ReleaseObject(pNode);
+    ReleaseObject(pNodes);
+    return hr;
+}
diff --git a/src/api/burn/balutil/inc/balinfo.h b/src/api/burn/balutil/inc/balinfo.h
index 8c2155e9..0fce35ec 100644
--- a/src/api/burn/balutil/inc/balinfo.h
+++ b/src/api/burn/balutil/inc/balinfo.h
@@ -47,15 +47,50 @@ typedef struct _BAL_INFO_PACKAGES
 } BAL_INFO_PACKAGES;
 
 
+typedef struct _BAL_INFO_OVERRIDABLE_VARIABLE
+{
+    LPWSTR sczName;
+} BAL_INFO_OVERRIDABLE_VARIABLE;
+
+
+typedef struct _BAL_INFO_OVERRIDABLE_VARIABLES
+{
+    BAL_INFO_OVERRIDABLE_VARIABLE* rgVariables;
+    DWORD cVariables;
+    STRINGDICT_HANDLE sdVariables;
+} BAL_INFO_OVERRIDABLE_VARIABLES;
+
+
 typedef struct _BAL_INFO_BUNDLE
 {
     BOOL fPerMachine;
     LPWSTR sczName;
     LPWSTR sczLogVariable;
     BAL_INFO_PACKAGES packages;
+    BAL_INFO_OVERRIDABLE_VARIABLES overridableVariables;
 } BAL_INFO_BUNDLE;
 
 
+typedef struct _BAL_INFO_COMMAND
+{
+    DWORD cUnknownArgs;
+    LPWSTR* rgUnknownArgs;
+    DWORD cVariables;
+    LPWSTR* rgVariableNames;
+    LPWSTR* rgVariableValues;
+} BAL_INFO_COMMAND;
+
+
+/*******************************************************************
+ BalInfoParseCommandLine - parses wzCommandLine from BOOTSTRAPPER_COMMAND.
+
+********************************************************************/
+HRESULT DAPI BalInfoParseCommandLine(
+    __in BAL_INFO_COMMAND* pCommand,
+    __in LPCWSTR wzCommandLine
+    );
+
+
 /*******************************************************************
  BalInfoParseFromXml - loads the bundle and package info from the UX
                        manifest.
@@ -100,6 +135,26 @@ DAPI_(void) BalInfoUninitialize(
     );
 
 
+/*******************************************************************
+ BalInfoUninitializeCommandLine - uninitializes BAL_INFO_COMMAND.
+
+********************************************************************/
+void DAPI BalInfoUninitializeCommandLine(
+    __in BAL_INFO_COMMAND* pCommand
+);
+
+
+/*******************************************************************
+ BalInfoSetOverridableVariablesFromEngine - sets overridable variables from command line.
+
+ ********************************************************************/
+HRESULT DAPI BalSetOverridableVariablesFromEngine(
+    __in BAL_INFO_OVERRIDABLE_VARIABLES* pOverridableVariables,
+    __in BAL_INFO_COMMAND* pCommand,
+    __in IBootstrapperEngine* pEngine
+    );
+
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/api/burn/balutil/precomp.h b/src/api/burn/balutil/precomp.h
index 15142210..64d4a6cf 100644
--- a/src/api/burn/balutil/precomp.h
+++ b/src/api/burn/balutil/precomp.h
@@ -16,6 +16,7 @@
 #include <strsafe.h>
 
 #include <dutil.h>
+#include <apputil.h>
 #include <pathutil.h>
 #include <locutil.h>
 #include <memutil.h>
diff --git a/src/api/burn/test/WixToolsetTest.Mba.Core/BaseBootstrapperApplicationFactoryFixture.cs b/src/api/burn/test/WixToolsetTest.Mba.Core/BaseBootstrapperApplicationFactoryFixture.cs
index aaf5ee29..b49b3927 100644
--- a/src/api/burn/test/WixToolsetTest.Mba.Core/BaseBootstrapperApplicationFactoryFixture.cs
+++ b/src/api/burn/test/WixToolsetTest.Mba.Core/BaseBootstrapperApplicationFactoryFixture.cs
@@ -3,6 +3,7 @@
 namespace WixToolsetTest.Mba.Core
 {
     using System;
+    using System.Collections.Generic;
     using System.Runtime.InteropServices;
     using WixToolset.Mba.Core;
     using Xunit;
@@ -17,7 +18,7 @@ namespace WixToolsetTest.Mba.Core
                 action = LaunchAction.Install,
                 cbSize = Marshal.SizeOf(typeof(TestCommand)),
                 display = Display.Full,
-                wzCommandLine = "this \"is a\" test",
+                wzCommandLine = "this \"is a\" test VariableA=AVariable =EmptyName EmptyValue=",
             };
             var pCommand = Marshal.AllocHGlobal(command.cbSize);
             try
@@ -42,7 +43,15 @@ namespace WixToolsetTest.Mba.Core
                         Assert.Equal(baFactory.BA, createResults.pBA);
                         Assert.Equal(baFactory.BA.Command.Action, command.action);
                         Assert.Equal(baFactory.BA.Command.Display, command.display);
-                        Assert.Equal(baFactory.BA.Command.CommandLineArgs, new string[] { "this", "is a", "test" });
+
+                        var mbaCommand = baFactory.BA.Command.ParseCommandLine();
+                        Assert.Equal(mbaCommand.UnknownCommandLineArgs, new string[] { "this", "is a", "test" });
+                        Assert.Equal(mbaCommand.Variables, new KeyValuePair<string, string>[]
+                        {
+                            new KeyValuePair<string, string>("VariableA", "AVariable"),
+                            new KeyValuePair<string, string>("", "EmptyName"),
+                            new KeyValuePair<string, string>("EmptyValue", ""),
+                        });
                     }
                     finally
                     {
-- 
cgit v1.2.3-55-g6feb