From 6f6c485118796f044a278447722eaf18ac5bf86e Mon Sep 17 00:00:00 2001
From: Rob Mensching <rob@firegiant.com>
Date: Wed, 15 May 2019 16:09:26 -0700
Subject: Initial code commit

---
 .../ConvertTuplesCommand.cs                        | 609 +++++++++++++++++++
 .../WixToolset.Converters.Tupleizer.csproj         |  32 +
 src/WixToolset.Converters/Wix3Converter.cs         | 652 +++++++++++++++++++++
 .../WixToolset.Converters.csproj                   |  26 +
 src/deps/wix.dll                                   | Bin 0 -> 1753088 bytes
 .../ConvertTuplesFixture.cs                        | 391 ++++++++++++
 .../TestData/Integration/test.wixout               | Bin 0 -> 148559 bytes
 .../TestData/Integration/test.wixproj              |  47 ++
 .../TestData/Integration/test.wxs                  |  36 ++
 .../WixToolsetTest.Converters.Tupleizer.csproj     |  33 ++
 .../WixToolsetTest.Converters/ConverterFixture.cs  | 554 +++++++++++++++++
 .../Preprocessor/ConvertedPreprocessor.wxs         |  62 ++
 .../TestData/Preprocessor/Preprocessor.wxs         |  63 ++
 .../TestData/Preprocessor/wixcop.settings.xml      |   9 +
 .../TestData/QtExec.bad/v3.wxs                     |  65 ++
 .../TestData/QtExec.bad/v4_expected.wxs            |  64 ++
 .../TestData/QtExec/v3.wxs                         |  64 ++
 .../TestData/QtExec/v4_expected.wxs                |  63 ++
 .../TestData/SingleFile/ConvertedSingleFile.wxs    |  60 ++
 .../TestData/SingleFile/SingleFile.wxs             |  61 ++
 .../WixToolsetTest.Converters.csproj               |  39 ++
 21 files changed, 2930 insertions(+)
 create mode 100644 src/WixToolset.Converters.Tupleizer/ConvertTuplesCommand.cs
 create mode 100644 src/WixToolset.Converters.Tupleizer/WixToolset.Converters.Tupleizer.csproj
 create mode 100644 src/WixToolset.Converters/Wix3Converter.cs
 create mode 100644 src/WixToolset.Converters/WixToolset.Converters.csproj
 create mode 100644 src/deps/wix.dll
 create mode 100644 src/test/WixToolsetTest.Converters.Tupleizer/ConvertTuplesFixture.cs
 create mode 100644 src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wixout
 create mode 100644 src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wixproj
 create mode 100644 src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wxs
 create mode 100644 src/test/WixToolsetTest.Converters.Tupleizer/WixToolsetTest.Converters.Tupleizer.csproj
 create mode 100644 src/test/WixToolsetTest.Converters/ConverterFixture.cs
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/Preprocessor/ConvertedPreprocessor.wxs
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/Preprocessor/Preprocessor.wxs
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/Preprocessor/wixcop.settings.xml
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v3.wxs
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v4_expected.wxs
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/QtExec/v3.wxs
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/QtExec/v4_expected.wxs
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/SingleFile/ConvertedSingleFile.wxs
 create mode 100644 src/test/WixToolsetTest.Converters/TestData/SingleFile/SingleFile.wxs
 create mode 100644 src/test/WixToolsetTest.Converters/WixToolsetTest.Converters.csproj

(limited to 'src')

diff --git a/src/WixToolset.Converters.Tupleizer/ConvertTuplesCommand.cs b/src/WixToolset.Converters.Tupleizer/ConvertTuplesCommand.cs
new file mode 100644
index 00000000..c07dd42e
--- /dev/null
+++ b/src/WixToolset.Converters.Tupleizer/ConvertTuplesCommand.cs
@@ -0,0 +1,609 @@
+// 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.Converters.Tupleizer
+{
+    using System;
+    using System.Linq;
+    using WixToolset.Data;
+    using WixToolset.Data.Tuples;
+    using WixToolset.Data.WindowsInstaller;
+    using Wix3 = Microsoft.Tools.WindowsInstallerXml;
+
+    public class ConvertTuplesCommand
+    {
+        public Intermediate Execute(string path)
+        {
+            var output = Wix3.Output.Load(path, suppressVersionCheck: true, suppressSchema: true);
+            return this.Execute(output);
+        }
+
+        public Intermediate Execute(Wix3.Output output)
+        {
+            var section = new IntermediateSection(String.Empty, OutputType3ToSectionType4(output.Type), output.Codepage);
+
+            foreach (Wix3.Table table in output.Tables)
+            {
+                foreach (Wix3.Row row in table.Rows)
+                {
+                    var tuple = GenerateTupleFromRow(row);
+                    if (tuple != null)
+                    {
+                        section.Tuples.Add(tuple);
+                    }
+                }
+            }
+
+            return new Intermediate(String.Empty, new[] { section }, localizationsByCulture: null, embedFilePaths: null);
+        }
+
+        private static IntermediateTuple GenerateTupleFromRow(Wix3.Row row)
+        {
+            var name = row.Table.Name;
+            switch (name)
+            {
+                case "_SummaryInformation":
+                    return DefaultTupleFromRow(typeof(_SummaryInformationTuple), row, columnZeroIsId: false);
+                case "ActionText":
+                    return DefaultTupleFromRow(typeof(ActionTextTuple), row, columnZeroIsId: false);
+                case "AdvtExecuteSequence":
+                    return DefaultTupleFromRow(typeof(AdvtExecuteSequenceTuple), row, columnZeroIsId: false);
+                case "AppId":
+                    return DefaultTupleFromRow(typeof(AppIdTuple), row, columnZeroIsId: false);
+                case "AppSearch":
+                    return DefaultTupleFromRow(typeof(AppSearchTuple), row, columnZeroIsId: false);
+                case "Binary":
+                    return DefaultTupleFromRow(typeof(BinaryTuple), row, columnZeroIsId: false);
+                case "Class":
+                    return DefaultTupleFromRow(typeof(ClassTuple), row, columnZeroIsId: false);
+                case "CompLocator":
+                    return DefaultTupleFromRow(typeof(CompLocatorTuple), row, columnZeroIsId: true);
+                case "Component":
+                {
+                    var attributes = FieldAsNullableInt(row, 3);
+
+                    var location = ComponentLocation.LocalOnly;
+                    if ((attributes & WindowsInstallerConstants.MsidbComponentAttributesSourceOnly) == WindowsInstallerConstants.MsidbComponentAttributesSourceOnly)
+                    {
+                        location = ComponentLocation.SourceOnly;
+                    }
+                    else if ((attributes & WindowsInstallerConstants.MsidbComponentAttributesOptional) == WindowsInstallerConstants.MsidbComponentAttributesOptional)
+                    {
+                        location = ComponentLocation.Either;
+                    }
+
+                    var keyPathType = ComponentKeyPathType.File;
+                    if ((attributes & WindowsInstallerConstants.MsidbComponentAttributesRegistryKeyPath) == WindowsInstallerConstants.MsidbComponentAttributesRegistryKeyPath)
+                    {
+                        keyPathType = ComponentKeyPathType.Registry;
+                    }
+                    else if ((attributes & WindowsInstallerConstants.MsidbComponentAttributesODBCDataSource) == WindowsInstallerConstants.MsidbComponentAttributesODBCDataSource)
+                    {
+                        keyPathType = ComponentKeyPathType.OdbcDataSource;
+                    }
+
+                    return new ComponentTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        ComponentId = FieldAsString(row, 1),
+                        Directory_ = FieldAsString(row, 2),
+                        Condition = FieldAsString(row, 4),
+                        KeyPath = FieldAsString(row, 5),
+                        Location = location,
+                        DisableRegistryReflection = (attributes & WindowsInstallerConstants.MsidbComponentAttributesDisableRegistryReflection) == WindowsInstallerConstants.MsidbComponentAttributesDisableRegistryReflection,
+                        NeverOverwrite = (attributes & WindowsInstallerConstants.MsidbComponentAttributesNeverOverwrite) == WindowsInstallerConstants.MsidbComponentAttributesNeverOverwrite,
+                        Permanent = (attributes & WindowsInstallerConstants.MsidbComponentAttributesPermanent) == WindowsInstallerConstants.MsidbComponentAttributesPermanent,
+                        SharedDllRefCount = (attributes & WindowsInstallerConstants.MsidbComponentAttributesSharedDllRefCount) == WindowsInstallerConstants.MsidbComponentAttributesSharedDllRefCount,
+                        Shared = (attributes & WindowsInstallerConstants.MsidbComponentAttributesShared) == WindowsInstallerConstants.MsidbComponentAttributesShared,
+                        Transitive = (attributes & WindowsInstallerConstants.MsidbComponentAttributesTransitive) == WindowsInstallerConstants.MsidbComponentAttributesTransitive,
+                        UninstallWhenSuperseded = (attributes & WindowsInstallerConstants.MsidbComponentAttributesUninstallOnSupersedence) == WindowsInstallerConstants.MsidbComponentAttributesUninstallOnSupersedence,
+                        Win64 = (attributes & WindowsInstallerConstants.MsidbComponentAttributes64bit) == WindowsInstallerConstants.MsidbComponentAttributes64bit,
+                        KeyPathType = keyPathType,
+                    };
+                }
+
+                case "Condition":
+                    return DefaultTupleFromRow(typeof(ConditionTuple), row, columnZeroIsId: false);
+                case "CreateFolder":
+                    return DefaultTupleFromRow(typeof(CreateFolderTuple), row, columnZeroIsId: false);
+                case "CustomAction":
+                {
+                    var caType = FieldAsInt(row, 1);
+                    var executionType = DetermineCustomActionExecutionType(caType);
+                    var sourceType = DetermineCustomActionSourceType(caType);
+                    var targetType = DetermineCustomActionTargetType(caType);
+
+                    return new CustomActionTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        ExecutionType = executionType,
+                        SourceType = sourceType,
+                        Source = FieldAsString(row, 2),
+                        TargetType = targetType,
+                        Target = FieldAsString(row, 3),
+                        Win64 = (caType & WindowsInstallerConstants.MsidbCustomActionType64BitScript) == WindowsInstallerConstants.MsidbCustomActionType64BitScript,
+                        TSAware = (caType & WindowsInstallerConstants.MsidbCustomActionTypeTSAware) == WindowsInstallerConstants.MsidbCustomActionTypeTSAware,
+                        Impersonate = (caType & WindowsInstallerConstants.MsidbCustomActionTypeNoImpersonate) != WindowsInstallerConstants.MsidbCustomActionTypeNoImpersonate,
+                        IgnoreResult = (caType & WindowsInstallerConstants.MsidbCustomActionTypeContinue) == WindowsInstallerConstants.MsidbCustomActionTypeContinue,
+                        Hidden = (caType & WindowsInstallerConstants.MsidbCustomActionTypeHideTarget) == WindowsInstallerConstants.MsidbCustomActionTypeHideTarget,
+                        Async = (caType & WindowsInstallerConstants.MsidbCustomActionTypeAsync) == WindowsInstallerConstants.MsidbCustomActionTypeAsync,
+                    };
+                }
+
+                case "Directory":
+                    return DefaultTupleFromRow(typeof(DirectoryTuple), row, columnZeroIsId: false);
+                case "DrLocator":
+                    return DefaultTupleFromRow(typeof(DrLocatorTuple), row, columnZeroIsId: false);
+                case "Error":
+                    return DefaultTupleFromRow(typeof(ErrorTuple), row, columnZeroIsId: false);
+                case "Extension":
+                    return DefaultTupleFromRow(typeof(ExtensionTuple), row, columnZeroIsId: false);
+                case "Feature":
+                {
+                    int attributes = FieldAsInt(row, 7);
+                    var installDefault = FeatureInstallDefault.Local;
+                    if ((attributes & WindowsInstallerConstants.MsidbFeatureAttributesFollowParent) == WindowsInstallerConstants.MsidbFeatureAttributesFollowParent)
+                    {
+                        installDefault = FeatureInstallDefault.FollowParent;
+                    }
+                    else
+                    if ((attributes & WindowsInstallerConstants.MsidbFeatureAttributesFavorSource) == WindowsInstallerConstants.MsidbFeatureAttributesFavorSource)
+                    {
+                        installDefault = FeatureInstallDefault.Source;
+                    }
+
+                    return new FeatureTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        Feature_Parent = FieldAsString(row, 1),
+                        Title = FieldAsString(row, 2),
+                        Description = FieldAsString(row, 3),
+                        Display = FieldAsInt(row, 4), // BUGBUGBUG: FieldAsNullableInt(row, 4),
+                        Level = FieldAsInt(row, 5),
+                        Directory_ = FieldAsString(row, 6),
+                        DisallowAbsent = (attributes & WindowsInstallerConstants.MsidbFeatureAttributesUIDisallowAbsent) == WindowsInstallerConstants.MsidbFeatureAttributesUIDisallowAbsent,
+                        DisallowAdvertise = (attributes & WindowsInstallerConstants.MsidbFeatureAttributesDisallowAdvertise) == WindowsInstallerConstants.MsidbFeatureAttributesDisallowAdvertise,
+                        InstallDefault = installDefault,
+                        TypicalDefault = (attributes & WindowsInstallerConstants.MsidbFeatureAttributesFavorAdvertise) == WindowsInstallerConstants.MsidbFeatureAttributesFavorAdvertise ? FeatureTypicalDefault.Advertise : FeatureTypicalDefault.Install,
+                    };
+                }
+
+                case "FeatureComponents":
+                    return DefaultTupleFromRow(typeof(FeatureComponentsTuple), row, columnZeroIsId: false);
+                case "File":
+                {
+                    var attributes = FieldAsNullableInt(row, 6);
+                    var readOnly = (attributes & WindowsInstallerConstants.MsidbFileAttributesReadOnly) == WindowsInstallerConstants.MsidbFileAttributesReadOnly;
+                    var hidden = (attributes & WindowsInstallerConstants.MsidbFileAttributesHidden) == WindowsInstallerConstants.MsidbFileAttributesHidden;
+                    var system = (attributes & WindowsInstallerConstants.MsidbFileAttributesSystem) == WindowsInstallerConstants.MsidbFileAttributesSystem;
+                    var vital = (attributes & WindowsInstallerConstants.MsidbFileAttributesVital) == WindowsInstallerConstants.MsidbFileAttributesVital;
+                    var checksum = (attributes & WindowsInstallerConstants.MsidbFileAttributesChecksum) == WindowsInstallerConstants.MsidbFileAttributesChecksum;
+                    bool? compressed = null;
+                    if ((attributes & WindowsInstallerConstants.MsidbFileAttributesNoncompressed) == WindowsInstallerConstants.MsidbFileAttributesNoncompressed)
+                    {
+                        compressed = false;
+                    }
+                    else if ((attributes & WindowsInstallerConstants.MsidbFileAttributesCompressed) == WindowsInstallerConstants.MsidbFileAttributesCompressed)
+                    {
+                        compressed = true;
+                    }
+
+                    return new FileTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        Component_ = FieldAsString(row, 1),
+                        LongFileName = FieldAsString(row, 2),
+                        FileSize = FieldAsInt(row, 3),
+                        Version = FieldAsString(row, 4),
+                        Language = FieldAsString(row, 5),
+                        ReadOnly = readOnly,
+                        Hidden = hidden,
+                        System = system,
+                        Vital = vital,
+                        Checksum = checksum,
+                        Compressed = compressed,
+                    };
+                }
+
+                case "Font":
+                    return DefaultTupleFromRow(typeof(FontTuple), row, columnZeroIsId: false);
+                case "Icon":
+                    return DefaultTupleFromRow(typeof(IconTuple), row, columnZeroIsId: false);
+                case "InstallExecuteSequence":
+                    return DefaultTupleFromRow(typeof(InstallExecuteSequenceTuple), row, columnZeroIsId: false);
+                case "LockPermissions":
+                    return DefaultTupleFromRow(typeof(LockPermissionsTuple), row, columnZeroIsId: false);
+                case "Media":
+                    return DefaultTupleFromRow(typeof(MediaTuple), row, columnZeroIsId: false);
+                case "MIME":
+                    return DefaultTupleFromRow(typeof(MIMETuple), row, columnZeroIsId: false);
+                case "MoveFile":
+                    return DefaultTupleFromRow(typeof(MoveFileTuple), row, columnZeroIsId: false);
+                case "MsiAssembly":
+                    return DefaultTupleFromRow(typeof(MsiAssemblyTuple), row, columnZeroIsId: false);
+                case "MsiShortcutProperty":
+                    return DefaultTupleFromRow(typeof(MsiShortcutPropertyTuple), row, columnZeroIsId: false);
+                case "ProgId":
+                    return DefaultTupleFromRow(typeof(ProgIdTuple), row, columnZeroIsId: false);
+                case "Property":
+                    return DefaultTupleFromRow(typeof(PropertyTuple), row, columnZeroIsId: false);
+                case "PublishComponent":
+                    return DefaultTupleFromRow(typeof(PublishComponentTuple), row, columnZeroIsId: false);
+                case "Registry":
+                {
+                    var value = FieldAsString(row, 4);
+                    var valueType = RegistryValueType.String;
+                    var valueAction = RegistryValueActionType.Write;
+
+                    if (!String.IsNullOrEmpty(value))
+                    {
+                        if (value.StartsWith("#x", StringComparison.Ordinal))
+                        {
+                            valueType = RegistryValueType.Binary;
+                            value = value.Substring(2);
+                        }
+                        else if (value.StartsWith("#%", StringComparison.Ordinal))
+                        {
+                            valueType = RegistryValueType.Expandable;
+                            value = value.Substring(2);
+                        }
+                        else if (value.StartsWith("#", StringComparison.Ordinal))
+                        {
+                            valueType = RegistryValueType.Integer;
+                            value = value.Substring(1);
+                        }
+                        else if (value.StartsWith("[~]", StringComparison.Ordinal) && value.EndsWith("[~]", StringComparison.Ordinal))
+                        {
+                            value = value.Substring(3, value.Length - 6);
+                            valueType = RegistryValueType.MultiString;
+                            valueAction = RegistryValueActionType.Write;
+                        }
+                        else if (value.StartsWith("[~]", StringComparison.Ordinal))
+                        {
+                            value = value.Substring(3);
+                            valueType = RegistryValueType.MultiString;
+                            valueAction = RegistryValueActionType.Append;
+                        }
+                        else if (value.EndsWith("[~]", StringComparison.Ordinal))
+                        {
+                            value = value.Substring(0, value.Length - 3);
+                            valueType = RegistryValueType.MultiString;
+                            valueAction = RegistryValueActionType.Prepend;
+                        }
+                    }
+
+                    return new RegistryTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        Root = (RegistryRootType)FieldAsInt(row, 1),
+                        Key = FieldAsString(row, 2),
+                        Name = FieldAsString(row, 3),
+                        Value = value,
+                        Component_ = FieldAsString(row, 5),
+                        ValueAction = valueAction,
+                        ValueType = valueType,
+                    };
+                }
+
+                case "RegLocator":
+                    return DefaultTupleFromRow(typeof(RegLocatorTuple), row, columnZeroIsId: false);
+                case "RemoveFile":
+                    return DefaultTupleFromRow(typeof(RemoveFileTuple), row, columnZeroIsId: false);
+                case "RemoveRegistry":
+                {
+                    return new RemoveRegistryTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        Action = RemoveRegistryActionType.RemoveOnInstall,
+                        Root = (RegistryRootType)FieldAsInt(row, 1),
+                        Key = FieldAsString(row, 2),
+                        Name = FieldAsString(row, 3),
+                        Component_ = FieldAsString(row, 4),
+                    };
+                }
+
+                case "ReserveCost":
+                    return DefaultTupleFromRow(typeof(ReserveCostTuple), row, columnZeroIsId: false);
+                case "ServiceControl":
+                {
+                    var events = FieldAsInt(row, 2);
+                    var wait = FieldAsNullableInt(row, 4);
+                    return new ServiceControlTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        Name = FieldAsString(row, 1),
+                        Arguments = FieldAsString(row, 3),
+                        Wait = !wait.HasValue || wait.Value == 1,
+                        Component_ = FieldAsString(row, 5),
+                        InstallRemove = (events & WindowsInstallerConstants.MsidbServiceControlEventDelete) == WindowsInstallerConstants.MsidbServiceControlEventDelete,
+                        UninstallRemove = (events & WindowsInstallerConstants.MsidbServiceControlEventUninstallDelete) == WindowsInstallerConstants.MsidbServiceControlEventUninstallDelete,
+                        InstallStart = (events & WindowsInstallerConstants.MsidbServiceControlEventStart) == WindowsInstallerConstants.MsidbServiceControlEventStart,
+                        UninstallStart = (events & WindowsInstallerConstants.MsidbServiceControlEventUninstallStart) == WindowsInstallerConstants.MsidbServiceControlEventUninstallStart,
+                        InstallStop = (events & WindowsInstallerConstants.MsidbServiceControlEventStop) == WindowsInstallerConstants.MsidbServiceControlEventStop,
+                        UninstallStop = (events & WindowsInstallerConstants.MsidbServiceControlEventUninstallStop) == WindowsInstallerConstants.MsidbServiceControlEventUninstallStop,
+                    };
+                }
+
+                case "ServiceInstall":
+                    return DefaultTupleFromRow(typeof(ServiceInstallTuple), row, columnZeroIsId: true);
+                case "Shortcut":
+                    return DefaultTupleFromRow(typeof(ShortcutTuple), row, columnZeroIsId: true);
+                case "Signature":
+                    return DefaultTupleFromRow(typeof(SignatureTuple), row, columnZeroIsId: false);
+                case "Upgrade":
+                {
+                    var attributes = FieldAsInt(row, 4);
+                    return new UpgradeTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        UpgradeCode = FieldAsString(row, 0),
+                        VersionMin = FieldAsString(row, 1),
+                        VersionMax = FieldAsString(row, 2),
+                        Language = FieldAsString(row, 3),
+                        Remove = FieldAsString(row, 5),
+                        ActionProperty = FieldAsString(row, 6),
+                        MigrateFeatures = (attributes & WindowsInstallerConstants.MsidbUpgradeAttributesMigrateFeatures) == WindowsInstallerConstants.MsidbUpgradeAttributesMigrateFeatures,
+                        OnlyDetect = (attributes & WindowsInstallerConstants.MsidbUpgradeAttributesOnlyDetect) == WindowsInstallerConstants.MsidbUpgradeAttributesOnlyDetect,
+                        IgnoreRemoveFailures = (attributes & WindowsInstallerConstants.MsidbUpgradeAttributesIgnoreRemoveFailure) == WindowsInstallerConstants.MsidbUpgradeAttributesIgnoreRemoveFailure,
+                        VersionMinInclusive = (attributes & WindowsInstallerConstants.MsidbUpgradeAttributesVersionMinInclusive) == WindowsInstallerConstants.MsidbUpgradeAttributesVersionMinInclusive,
+                        VersionMaxInclusive = (attributes & WindowsInstallerConstants.MsidbUpgradeAttributesVersionMaxInclusive) == WindowsInstallerConstants.MsidbUpgradeAttributesVersionMaxInclusive,
+                        ExcludeLanguages = (attributes & WindowsInstallerConstants.MsidbUpgradeAttributesLanguagesExclusive) == WindowsInstallerConstants.MsidbUpgradeAttributesLanguagesExclusive,
+                    };
+                }
+
+                case "Verb":
+                    return DefaultTupleFromRow(typeof(VerbTuple), row, columnZeroIsId: false);
+                //case "WixAction":
+                //    return new WixActionTuple(SourceLineNumber4(row.SourceLineNumbers))
+                //    {
+                //        SequenceTable = (SequenceTable)Enum.Parse(typeof(SequenceTable), FieldAsString(row, 0)),
+                //        Action = FieldAsString(row, 1),
+                //        Condition = FieldAsString(row, 2),
+                //        Sequence = FieldAsInt(row, 3),
+                //        Before = FieldAsString(row, 4),
+                //        After = FieldAsString(row, 5),
+                //        Overridable = FieldAsNullableInt(row, 6) != 0,
+                //    };
+                case "WixFile":
+                    var assemblyAttributes3 = FieldAsNullableInt(row, 1);
+                    return new WixFileTuple(SourceLineNumber4(row.SourceLineNumbers), new Identifier(AccessModifier.Public, FieldAsString(row, 0)))
+                    {
+                        AssemblyType = assemblyAttributes3 == 0 ? FileAssemblyType.DotNetAssembly : assemblyAttributes3 == 1 ? FileAssemblyType.Win32Assembly : FileAssemblyType.NotAnAssembly,
+                        File_AssemblyManifest = FieldAsString(row, 2),
+                        File_AssemblyApplication = FieldAsString(row, 3),
+                        Directory_ = FieldAsString(row, 4),
+                        DiskId = FieldAsInt(row, 5), // TODO: BUGBUGBUG: AB#2626: DiskId is nullable in WiX v3.
+                        Source = new IntermediateFieldPathValue() { Path = FieldAsString(row, 6) },
+                        ProcessorArchitecture = FieldAsString(row, 7),
+                        PatchGroup = FieldAsInt(row, 8),
+                        Attributes = FieldAsInt(row, 9),
+                    };
+                case "WixProperty":
+                {
+                    var attributes = FieldAsInt(row, 1);
+                    return new WixPropertyTuple(SourceLineNumber4(row.SourceLineNumbers))
+                    {
+                        Property_ = FieldAsString(row, 0),
+                        Admin = (attributes & 0x1) == 0x1,
+                        Hidden = (attributes & 0x2) == 0x2,
+                        Secure = (attributes & 0x4) == 0x4,
+                    };
+                }
+
+                default:
+                    return GenericTupleFromCustomRow(row, columnZeroIsId: false);
+            }
+        }
+
+        private static CustomActionTargetType DetermineCustomActionTargetType(int type)
+        {
+            var targetType = default(CustomActionTargetType);
+
+            if ((type & WindowsInstallerConstants.MsidbCustomActionTypeVBScript) == WindowsInstallerConstants.MsidbCustomActionTypeVBScript)
+            {
+                targetType = CustomActionTargetType.VBScript;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeJScript) == WindowsInstallerConstants.MsidbCustomActionTypeJScript)
+            {
+                targetType = CustomActionTargetType.JScript;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeTextData) == WindowsInstallerConstants.MsidbCustomActionTypeTextData)
+            {
+                targetType = CustomActionTargetType.TextData;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeExe) == WindowsInstallerConstants.MsidbCustomActionTypeExe)
+            {
+                targetType = CustomActionTargetType.Exe;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeDll) == WindowsInstallerConstants.MsidbCustomActionTypeDll)
+            {
+                targetType = CustomActionTargetType.Dll;
+            }
+
+            return targetType;
+        }
+
+        private static CustomActionSourceType DetermineCustomActionSourceType(int type)
+        {
+            var sourceType = CustomActionSourceType.Binary;
+
+            if ((type & WindowsInstallerConstants.MsidbCustomActionTypeProperty) == WindowsInstallerConstants.MsidbCustomActionTypeProperty)
+            {
+                sourceType = CustomActionSourceType.Property;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeDirectory) == WindowsInstallerConstants.MsidbCustomActionTypeDirectory)
+            {
+                sourceType = CustomActionSourceType.Directory;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeSourceFile) == WindowsInstallerConstants.MsidbCustomActionTypeSourceFile)
+            {
+                sourceType = CustomActionSourceType.File;
+            }
+
+            return sourceType;
+        }
+
+        private static CustomActionExecutionType DetermineCustomActionExecutionType(int type)
+        {
+            var executionType = CustomActionExecutionType.Immediate;
+
+            if ((type & (WindowsInstallerConstants.MsidbCustomActionTypeInScript | WindowsInstallerConstants.MsidbCustomActionTypeCommit)) == (WindowsInstallerConstants.MsidbCustomActionTypeInScript | WindowsInstallerConstants.MsidbCustomActionTypeCommit))
+            {
+                executionType = CustomActionExecutionType.Commit;
+            }
+            else if ((type & (WindowsInstallerConstants.MsidbCustomActionTypeInScript | WindowsInstallerConstants.MsidbCustomActionTypeRollback)) == (WindowsInstallerConstants.MsidbCustomActionTypeInScript | WindowsInstallerConstants.MsidbCustomActionTypeRollback))
+            {
+                executionType = CustomActionExecutionType.Rollback;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeInScript) == WindowsInstallerConstants.MsidbCustomActionTypeInScript)
+            {
+                executionType = CustomActionExecutionType.Deferred;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeClientRepeat) == WindowsInstallerConstants.MsidbCustomActionTypeClientRepeat)
+            {
+                executionType = CustomActionExecutionType.ClientRepeat;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeOncePerProcess) == WindowsInstallerConstants.MsidbCustomActionTypeOncePerProcess)
+            {
+                executionType = CustomActionExecutionType.OncePerProcess;
+            }
+            else if ((type & WindowsInstallerConstants.MsidbCustomActionTypeFirstSequence) == WindowsInstallerConstants.MsidbCustomActionTypeFirstSequence)
+            {
+                executionType = CustomActionExecutionType.FirstSequence;
+            }
+
+            return executionType;
+        }
+
+        private static IntermediateFieldType ColumnType3ToIntermediateFieldType4(Wix3.ColumnType columnType)
+        {
+            switch (columnType)
+            {
+                case Wix3.ColumnType.Number:
+                    return IntermediateFieldType.Number;
+                case Wix3.ColumnType.Object:
+                    return IntermediateFieldType.Path;
+                case Wix3.ColumnType.Unknown:
+                case Wix3.ColumnType.String:
+                case Wix3.ColumnType.Localized:
+                case Wix3.ColumnType.Preserved:
+                default:
+                    return IntermediateFieldType.String;
+            }
+        }
+
+        private static IntermediateTuple DefaultTupleFromRow(Type tupleType, Wix3.Row row, bool columnZeroIsId)
+        {
+            var tuple = Activator.CreateInstance(tupleType) as IntermediateTuple;
+
+            SetTupleFieldsFromRow(row, tuple, columnZeroIsId);
+
+            tuple.SourceLineNumbers = SourceLineNumber4(row.SourceLineNumbers);
+            return tuple;
+        }
+
+        private static IntermediateTuple GenericTupleFromCustomRow(Wix3.Row row, bool columnZeroIsId)
+        {
+            var columnDefinitions = row.Table.Definition.Columns.Cast<Wix3.ColumnDefinition>();
+            var fieldDefinitions = columnDefinitions.Select(columnDefinition =>
+                new IntermediateFieldDefinition(columnDefinition.Name, ColumnType3ToIntermediateFieldType4(columnDefinition.Type))).ToArray();
+            var tupleDefinition = new IntermediateTupleDefinition(row.Table.Name, fieldDefinitions, null);
+            var tuple = new IntermediateTuple(tupleDefinition, SourceLineNumber4(row.SourceLineNumbers));
+
+            SetTupleFieldsFromRow(row, tuple, columnZeroIsId);
+
+            return tuple;
+        }
+
+        private static void SetTupleFieldsFromRow(Wix3.Row row, IntermediateTuple tuple, bool columnZeroIsId)
+        {
+            int offset = 0;
+            if (columnZeroIsId)
+            {
+                tuple.Id = GetIdentifierForRow(row);
+                offset = 1;
+            }
+
+            for (var i = offset; i < row.Fields.Length; ++i)
+            {
+                var column = row.Fields[i].Column;
+                switch (column.Type)
+                {
+                    case Wix3.ColumnType.String:
+                    case Wix3.ColumnType.Localized:
+                    case Wix3.ColumnType.Object:
+                    case Wix3.ColumnType.Preserved:
+                        tuple.Set(i - offset, FieldAsString(row, i));
+                        break;
+                    case Wix3.ColumnType.Number:
+                        int? nullableValue = FieldAsNullableInt(row, i);
+                        // TODO: Consider whether null values should be coerced to their default value when
+                        // a column is not nullable. For now, just pass through the null.
+                        //int value = FieldAsInt(row, i);
+                        //tuple.Set(i - offset, column.IsNullable ? nullableValue : value);
+                        tuple.Set(i - offset, nullableValue);
+                    break;
+                    case Wix3.ColumnType.Unknown:
+                        break;
+                }
+            }
+        }
+
+        private static Identifier GetIdentifierForRow(Wix3.Row row)
+        {
+            var column = row.Fields[0].Column;
+            switch (column.Type)
+            {
+                case Wix3.ColumnType.String:
+                case Wix3.ColumnType.Localized:
+                case Wix3.ColumnType.Object:
+                case Wix3.ColumnType.Preserved:
+                    return new Identifier(AccessModifier.Public, (string)row.Fields[0].Data);
+                case Wix3.ColumnType.Number:
+                    return new Identifier(AccessModifier.Public, FieldAsInt(row, 0));
+                default:
+                    return null;
+            }
+        }
+
+        private static SectionType OutputType3ToSectionType4(Wix3.OutputType outputType)
+        {
+            switch (outputType)
+            {
+                case Wix3.OutputType.Bundle:
+                    return SectionType.Bundle;
+                case Wix3.OutputType.Module:
+                    return SectionType.Module;
+                case Wix3.OutputType.Patch:
+                    return SectionType.Patch;
+                case Wix3.OutputType.PatchCreation:
+                    return SectionType.PatchCreation;
+                case Wix3.OutputType.Product:
+                    return SectionType.Product;
+                case Wix3.OutputType.Transform:
+                case Wix3.OutputType.Unknown:
+                default:
+                    return SectionType.Unknown;
+            }
+        }
+
+        private static SourceLineNumber SourceLineNumber4(Wix3.SourceLineNumberCollection source)
+        {
+            return String.IsNullOrEmpty(source?.EncodedSourceLineNumbers) ? null : SourceLineNumber.CreateFromEncoded(source.EncodedSourceLineNumbers);
+        }
+
+        private static string FieldAsString(Wix3.Row row, int column)
+        {
+            return (string)row[column];
+        }
+
+        private static int FieldAsInt(Wix3.Row row, int column)
+        {
+            return Convert.ToInt32(row[column]);
+        }
+
+        private static int? FieldAsNullableInt(Wix3.Row row, int column)
+        {
+            var field = row.Fields[column];
+            if (field.Data == null)
+            {
+                return null;
+            }
+            else
+            {
+                return Convert.ToInt32(field.Data);
+            }
+        }
+    }
+}
diff --git a/src/WixToolset.Converters.Tupleizer/WixToolset.Converters.Tupleizer.csproj b/src/WixToolset.Converters.Tupleizer/WixToolset.Converters.Tupleizer.csproj
new file mode 100644
index 00000000..a162807a
--- /dev/null
+++ b/src/WixToolset.Converters.Tupleizer/WixToolset.Converters.Tupleizer.csproj
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <Description>Tupleizer</Description>
+    <Title>WiX Toolset Converters Tuplizer</Title>
+    <DebugType>embedded</DebugType>
+    <PublishRepositoryUrl>true</PublishRepositoryUrl>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <NoWarn>NU1701</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="wix" HintPath="..\deps\wix.dll" />
+    <None Include="..\deps\wix.dll" Pack="true" PackagePath="lib\net461" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="WixToolset.Core" Version="4.0.*" />
+    <PackageReference Include="WixToolset.Core.WindowsInstaller" Version="4.0.*" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
+    <PackageReference Include="Nerdbank.GitVersioning" Version="2.1.65" PrivateAssets="All" />
+  </ItemGroup>
+</Project>
diff --git a/src/WixToolset.Converters/Wix3Converter.cs b/src/WixToolset.Converters/Wix3Converter.cs
new file mode 100644
index 00000000..c23930b6
--- /dev/null
+++ b/src/WixToolset.Converters/Wix3Converter.cs
@@ -0,0 +1,652 @@
+// 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.Converters
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Globalization;
+    using System.IO;
+    using System.Linq;
+    using System.Text;
+    using System.Text.RegularExpressions;
+    using System.Xml;
+    using System.Xml.Linq;
+    using WixToolset.Data;
+    using WixToolset.Extensibility.Services;
+
+    /// <summary>
+    /// WiX source code converter.
+    /// </summary>
+    public class Wix3Converter
+    {
+        private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]", RegexOptions.Compiled);
+        private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}", RegexOptions.Compiled); // non 'words' and assorted valid characters
+
+        private const char XDocumentNewLine = '\n'; // XDocument normalizes "\r\n" to just "\n".
+        private static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs";
+
+        private static readonly XName DirectoryElementName = WixNamespace + "Directory";
+        private static readonly XName FileElementName = WixNamespace + "File";
+        private static readonly XName ExePackageElementName = WixNamespace + "ExePackage";
+        private static readonly XName MsiPackageElementName = WixNamespace + "MsiPackage";
+        private static readonly XName MspPackageElementName = WixNamespace + "MspPackage";
+        private static readonly XName MsuPackageElementName = WixNamespace + "MsuPackage";
+        private static readonly XName PayloadElementName = WixNamespace + "Payload";
+        private static readonly XName CustomActionElementName = WixNamespace + "CustomAction";
+        private static readonly XName PropertyElementName = WixNamespace + "Property";
+        private static readonly XName WixElementWithoutNamespaceName = XNamespace.None + "Wix";
+
+        private static readonly Dictionary<string, XNamespace> OldToNewNamespaceMapping = new Dictionary<string, XNamespace>()
+        {
+            { "http://schemas.microsoft.com/wix/BalExtension", "http://wixtoolset.org/schemas/v4/wxs/bal" },
+            { "http://schemas.microsoft.com/wix/ComPlusExtension", "http://wixtoolset.org/schemas/v4/wxs/complus" },
+            { "http://schemas.microsoft.com/wix/DependencyExtension", "http://wixtoolset.org/schemas/v4/wxs/dependency" },
+            { "http://schemas.microsoft.com/wix/DifxAppExtension", "http://wixtoolset.org/schemas/v4/wxs/difxapp" },
+            { "http://schemas.microsoft.com/wix/FirewallExtension", "http://wixtoolset.org/schemas/v4/wxs/firewall" },
+            { "http://schemas.microsoft.com/wix/GamingExtension", "http://wixtoolset.org/schemas/v4/wxs/gaming" },
+            { "http://schemas.microsoft.com/wix/IIsExtension", "http://wixtoolset.org/schemas/v4/wxs/iis" },
+            { "http://schemas.microsoft.com/wix/MsmqExtension", "http://wixtoolset.org/schemas/v4/wxs/msmq" },
+            { "http://schemas.microsoft.com/wix/NetFxExtension", "http://wixtoolset.org/schemas/v4/wxs/netfx" },
+            { "http://schemas.microsoft.com/wix/PSExtension", "http://wixtoolset.org/schemas/v4/wxs/powershell" },
+            { "http://schemas.microsoft.com/wix/SqlExtension", "http://wixtoolset.org/schemas/v4/wxs/sql" },
+            { "http://schemas.microsoft.com/wix/TagExtension", "http://wixtoolset.org/schemas/v4/wxs/tag" },
+            { "http://schemas.microsoft.com/wix/UtilExtension", "http://wixtoolset.org/schemas/v4/wxs/util" },
+            { "http://schemas.microsoft.com/wix/VSExtension", "http://wixtoolset.org/schemas/v4/wxs/vs" },
+            { "http://wixtoolset.org/schemas/thmutil/2010", "http://wixtoolset.org/schemas/v4/thmutil" },
+            { "http://schemas.microsoft.com/wix/2009/Lux", "http://wixtoolset.org/schemas/v4/lux" },
+            { "http://schemas.microsoft.com/wix/2006/wi", "http://wixtoolset.org/schemas/v4/wxs" },
+            { "http://schemas.microsoft.com/wix/2006/localization", "http://wixtoolset.org/schemas/v4/wxl" },
+            { "http://schemas.microsoft.com/wix/2006/libraries", "http://wixtoolset.org/schemas/v4/wixlib" },
+            { "http://schemas.microsoft.com/wix/2006/objects", "http://wixtoolset.org/schemas/v4/wixobj" },
+            { "http://schemas.microsoft.com/wix/2006/outputs", "http://wixtoolset.org/schemas/v4/wixout" },
+            { "http://schemas.microsoft.com/wix/2007/pdbs", "http://wixtoolset.org/schemas/v4/wixpdb" },
+            { "http://schemas.microsoft.com/wix/2003/04/actions", "http://wixtoolset.org/schemas/v4/wi/actions" },
+            { "http://schemas.microsoft.com/wix/2006/tables", "http://wixtoolset.org/schemas/v4/wi/tables" },
+            { "http://schemas.microsoft.com/wix/2006/WixUnit", "http://wixtoolset.org/schemas/v4/wixunit" },
+        };
+
+        private readonly Dictionary<XName, Action<XElement>> ConvertElementMapping;
+
+        /// <summary>
+        /// Instantiate a new Converter class.
+        /// </summary>
+        /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
+        /// <param name="errorsAsWarnings">Test errors to display as warnings.</param>
+        /// <param name="ignoreErrors">Test errors to ignore.</param>
+        public Wix3Converter(IMessaging messaging, int indentationAmount, IEnumerable<string> errorsAsWarnings = null, IEnumerable<string> ignoreErrors = null)
+        {
+            this.ConvertElementMapping = new Dictionary<XName, Action<XElement>>
+            {
+                { Wix3Converter.DirectoryElementName, this.ConvertDirectoryElement },
+                { Wix3Converter.FileElementName, this.ConvertFileElement },
+                { Wix3Converter.ExePackageElementName, this.ConvertSuppressSignatureValidation },
+                { Wix3Converter.MsiPackageElementName, this.ConvertSuppressSignatureValidation },
+                { Wix3Converter.MspPackageElementName, this.ConvertSuppressSignatureValidation },
+                { Wix3Converter.MsuPackageElementName, this.ConvertSuppressSignatureValidation },
+                { Wix3Converter.PayloadElementName, this.ConvertSuppressSignatureValidation },
+                { Wix3Converter.CustomActionElementName, this.ConvertCustomActionElement },
+                { Wix3Converter.PropertyElementName, this.ConvertPropertyElement },
+                { Wix3Converter.WixElementWithoutNamespaceName, this.ConvertWixElementWithoutNamespace },
+            };
+
+            this.Messaging = messaging;
+
+            this.IndentationAmount = indentationAmount;
+
+            this.ErrorsAsWarnings = new HashSet<ConverterTestType>(this.YieldConverterTypes(errorsAsWarnings));
+
+            this.IgnoreErrors = new HashSet<ConverterTestType>(this.YieldConverterTypes(ignoreErrors));
+        }
+
+        private int Errors { get; set; }
+
+        private HashSet<ConverterTestType> ErrorsAsWarnings { get; set; }
+
+        private HashSet<ConverterTestType> IgnoreErrors { get; set; }
+
+        private IMessaging Messaging { get; }
+
+        private int IndentationAmount { get; set; }
+
+        private string SourceFile { get; set; }
+
+        /// <summary>
+        /// Convert a file.
+        /// </summary>
+        /// <param name="sourceFile">The file to convert.</param>
+        /// <param name="saveConvertedFile">Option to save the converted errors that are found.</param>
+        /// <returns>The number of errors found.</returns>
+        public int ConvertFile(string sourceFile, bool saveConvertedFile)
+        {
+            XDocument document;
+
+            // Set the instance info.
+            this.Errors = 0;
+            this.SourceFile = sourceFile;
+
+            try
+            {
+                document = XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+            }
+            catch (XmlException e)
+            {
+                this.OnError(ConverterTestType.XmlException, null, "The xml is invalid.  Detail: '{0}'", e.Message);
+
+                return this.Errors;
+            }
+
+            this.ConvertDocument(document);
+
+            // Fix errors if requested and necessary.
+            if (saveConvertedFile && 0 < this.Errors)
+            {
+                try
+                {
+                    using (var writer = File.CreateText(this.SourceFile))
+                    {
+                        document.Save(writer, SaveOptions.DisableFormatting | SaveOptions.OmitDuplicateNamespaces);
+                    }
+                }
+                catch (UnauthorizedAccessException)
+                {
+                    this.OnError(ConverterTestType.UnauthorizedAccessException, null, "Could not write to file.");
+                }
+            }
+
+            return this.Errors;
+        }
+
+        /// <summary>
+        /// Convert a document.
+        /// </summary>
+        /// <param name="document">The document to convert.</param>
+        /// <returns>The number of errors found.</returns>
+        public int ConvertDocument(XDocument document)
+        {
+            var declaration = document.Declaration;
+
+            // Convert the declaration.
+            if (null != declaration)
+            {
+                if (!String.Equals("utf-8", declaration.Encoding, StringComparison.OrdinalIgnoreCase))
+                {
+                    if (this.OnError(ConverterTestType.DeclarationEncodingWrong, document.Root, "The XML declaration encoding is not properly set to 'utf-8'."))
+                    {
+                        declaration.Encoding = "utf-8";
+                    }
+                }
+            }
+            else // missing declaration
+            {
+                if (this.OnError(ConverterTestType.DeclarationMissing, null, "This file is missing an XML declaration on the first line."))
+                {
+                    document.Declaration = new XDeclaration("1.0", "utf-8", null);
+                    document.Root.AddBeforeSelf(new XText(XDocumentNewLine.ToString()));
+                }
+            }
+
+            // Start converting the nodes at the top.
+            this.ConvertNodes(document.Nodes(), 0);
+
+            return this.Errors;
+        }
+
+        private void ConvertNodes(IEnumerable<XNode> nodes, int level)
+        {
+            // Note we operate on a copy of the node list since we may
+            // remove some whitespace nodes during this processing.
+            foreach (var node in nodes.ToList())
+            {
+                if (node is XText text)
+                {
+                    if (!String.IsNullOrWhiteSpace(text.Value))
+                    {
+                        text.Value = text.Value.Trim();
+                    }
+                    else if (node.NextNode is XCData cdata)
+                    {
+                        this.EnsurePrecedingWhitespaceRemoved(text, node, ConverterTestType.WhitespacePrecedingNodeWrong);
+                    }
+                    else if (node.NextNode is XElement element)
+                    {
+                        this.EnsurePrecedingWhitespaceCorrect(text, node, level, ConverterTestType.WhitespacePrecedingNodeWrong);
+                    }
+                    else if (node.NextNode is null) // this is the space before the close element
+                    {
+                        if (node.PreviousNode is null || node.PreviousNode is XCData)
+                        {
+                            this.EnsurePrecedingWhitespaceRemoved(text, node.Parent, ConverterTestType.WhitespacePrecedingEndElementWrong);
+                        }
+                        else if (level == 0) // root element's close tag
+                        {
+                            this.EnsurePrecedingWhitespaceCorrect(text, node, 0, ConverterTestType.WhitespacePrecedingEndElementWrong);
+                        }
+                        else
+                        {
+                            this.EnsurePrecedingWhitespaceCorrect(text, node, level - 1, ConverterTestType.WhitespacePrecedingEndElementWrong);
+                        }
+                    }
+                }
+                else if (node is XElement element)
+                {
+                    this.ConvertElement(element);
+
+                    this.ConvertNodes(element.Nodes(), level + 1);
+                }
+            }
+        }
+
+        private void EnsurePrecedingWhitespaceCorrect(XText whitespace, XNode node, int level, ConverterTestType testType)
+        {
+            if (!Wix3Converter.LeadingWhitespaceValid(this.IndentationAmount, level, whitespace.Value))
+            {
+                var message = testType == ConverterTestType.WhitespacePrecedingEndElementWrong ? "The whitespace preceding this end element is incorrect." : "The whitespace preceding this node is incorrect.";
+
+                if (this.OnError(testType, node, message))
+                {
+                    Wix3Converter.FixupWhitespace(this.IndentationAmount, level, whitespace);
+                }
+            }
+        }
+
+        private void EnsurePrecedingWhitespaceRemoved(XText whitespace, XNode node, ConverterTestType testType)
+        {
+            if (!String.IsNullOrEmpty(whitespace.Value))
+            {
+                var message = testType == ConverterTestType.WhitespacePrecedingEndElementWrong ? "The whitespace preceding this end element is incorrect." : "The whitespace preceding this node is incorrect.";
+
+                if (this.OnError(testType, node, message))
+                {
+                    whitespace.Remove();
+                }
+            }
+        }
+
+        private void ConvertElement(XElement element)
+        {
+            // Gather any deprecated namespaces, then update this element tree based on those deprecations.
+            var deprecatedToUpdatedNamespaces = new Dictionary<XNamespace, XNamespace>();
+
+            foreach (var declaration in element.Attributes().Where(a => a.IsNamespaceDeclaration))
+            {
+                if (Wix3Converter.OldToNewNamespaceMapping.TryGetValue(declaration.Value, out var ns))
+                {
+                    if (this.OnError(ConverterTestType.XmlnsValueWrong, declaration, "The namespace '{0}' is out of date.  It must be '{1}'.", declaration.Value, ns.NamespaceName))
+                    {
+                        deprecatedToUpdatedNamespaces.Add(declaration.Value, ns);
+                    }
+                }
+            }
+
+            if (deprecatedToUpdatedNamespaces.Any())
+            {
+                Wix3Converter.UpdateElementsWithDeprecatedNamespaces(element.DescendantsAndSelf(), deprecatedToUpdatedNamespaces);
+            }
+
+            // Apply any specialized conversion actions.
+            if (this.ConvertElementMapping.TryGetValue(element.Name, out var convert))
+            {
+                convert(element);
+            }
+        }
+
+        private void ConvertDirectoryElement(XElement element)
+        {
+            if (null == element.Attribute("Name"))
+            {
+                var attribute = element.Attribute("ShortName");
+                if (null != attribute)
+                {
+                    var shortName = attribute.Value;
+                    if (this.OnError(ConverterTestType.AssignDirectoryNameFromShortName, element, "The directory ShortName attribute is being renamed to Name since Name wasn't specified for value '{0}'", shortName))
+                    {
+                        element.Add(new XAttribute("Name", shortName));
+                        attribute.Remove();
+                    }
+                }
+            }
+        }
+
+        private void ConvertFileElement(XElement element)
+        {
+            if (null == element.Attribute("Id"))
+            {
+                var attribute = element.Attribute("Name");
+
+                if (null == attribute)
+                {
+                    attribute = element.Attribute("Source");
+                }
+
+                if (null != attribute)
+                {
+                    var name = Path.GetFileName(attribute.Value);
+
+                    if (this.OnError(ConverterTestType.AssignAnonymousFileId, element, "The file id is being updated to '{0}' to ensure it remains the same as the default", name))
+                    {
+                        IEnumerable<XAttribute> attributes = element.Attributes().ToList();
+                        element.RemoveAttributes();
+                        element.Add(new XAttribute("Id", GetIdentifierFromName(name)));
+                        element.Add(attributes);
+                    }
+                }
+            }
+        }
+
+        private void ConvertSuppressSignatureValidation(XElement element)
+        {
+            var suppressSignatureValidation = element.Attribute("SuppressSignatureValidation");
+
+            if (null != suppressSignatureValidation)
+            {
+                if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' attribute instead.", suppressSignatureValidation))
+                {
+                    if ("no" == suppressSignatureValidation.Value)
+                    {
+                        element.Add(new XAttribute("EnableSignatureValidation", "yes"));
+                    }
+                }
+
+                suppressSignatureValidation.Remove();
+            }
+        }
+
+        private void ConvertCustomActionElement(XElement xCustomAction)
+        {
+            var xBinaryKey = xCustomAction.Attribute("BinaryKey");
+
+            if (xBinaryKey?.Value == "WixCA")
+            {
+                if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA custom action DLL Binary table id has been renamed. Use the id 'UtilCA' instead."))
+                {
+                    xBinaryKey.Value = "UtilCA";
+                }
+            }
+
+            var xDllEntry = xCustomAction.Attribute("DllEntry");
+
+            if (xDllEntry?.Value == "CAQuietExec" || xDllEntry?.Value == "CAQuietExec64")
+            {
+                if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The CAQuietExec and CAQuietExec64 custom action ids have been renamed. Use the ids 'WixQuietExec' and 'WixQuietExec64' instead."))
+                {
+                    xDllEntry.Value = xDllEntry.Value.Replace("CAQuietExec", "WixQuietExec");
+                }
+            }
+
+            var xProperty = xCustomAction.Attribute("Property");
+
+            if (xProperty?.Value == "QtExecCmdLine" || xProperty?.Value == "QtExec64CmdLine")
+            {
+                if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The QtExecCmdLine and QtExec64CmdLine property ids have been renamed. Use the ids 'WixQuietExecCmdLine' and 'WixQuietExec64CmdLine' instead."))
+                {
+                    xProperty.Value = xProperty.Value.Replace("QtExec", "WixQuietExec");
+                }
+            }
+        }
+
+        private void ConvertPropertyElement(XElement xProperty)
+        {
+            var xId = xProperty.Attribute("Id");
+
+            if (xId.Value == "QtExecCmdTimeout")
+            {
+                this.OnError(ConverterTestType.QtExecCmdTimeoutAmbiguous, xProperty, "QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout.");
+            }
+        }
+
+        /// <summary>
+        /// Converts a Wix element.
+        /// </summary>
+        /// <param name="element">The Wix element to convert.</param>
+        /// <returns>The converted element.</returns>
+        private void ConvertWixElementWithoutNamespace(XElement element)
+        {
+            if (this.OnError(ConverterTestType.XmlnsMissing, element, "The xmlns attribute is missing.  It must be present with a value of '{0}'.", WixNamespace.NamespaceName))
+            {
+                element.Name = WixNamespace.GetName(element.Name.LocalName);
+
+                element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace.
+
+                foreach (var elementWithoutNamespace in element.Elements().Where(e => XNamespace.None == e.Name.Namespace))
+                {
+                    elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName);
+                }
+            }
+        }
+
+        private IEnumerable<ConverterTestType> YieldConverterTypes(IEnumerable<string> types)
+        {
+            if (null != types)
+            {
+                foreach (var type in types)
+                {
+
+                    if (Enum.TryParse<ConverterTestType>(type, true, out var itt))
+                    {
+                        yield return itt;
+                    }
+                    else // not a known ConverterTestType
+                    {
+                        this.OnError(ConverterTestType.ConverterTestTypeUnknown, null, "Unknown error type: '{0}'.", type);
+                    }
+                }
+            }
+        }
+
+        private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable<XElement> elements, Dictionary<XNamespace, XNamespace> deprecatedToUpdatedNamespaces)
+        {
+            foreach (var element in elements)
+            {
+
+                if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out var ns))
+                {
+                    element.Name = ns.GetName(element.Name.LocalName);
+                }
+
+                // Remove all the attributes and add them back to with their namespace updated (as necessary).
+                IEnumerable<XAttribute> attributes = element.Attributes().ToList();
+                element.RemoveAttributes();
+
+                foreach (var attribute in attributes)
+                {
+                    var convertedAttribute = attribute;
+
+                    if (attribute.IsNamespaceDeclaration)
+                    {
+                        if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Value, out ns))
+                        {
+                            convertedAttribute = ("xmlns" == attribute.Name.LocalName) ? new XAttribute(attribute.Name.LocalName, ns.NamespaceName) : new XAttribute(XNamespace.Xmlns + attribute.Name.LocalName, ns.NamespaceName);
+                        }
+                    }
+                    else if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Name.Namespace, out ns))
+                    {
+                        convertedAttribute = new XAttribute(ns.GetName(attribute.Name.LocalName), attribute.Value);
+                    }
+
+                    element.Add(convertedAttribute);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Determine if the whitespace preceding a node is appropriate for its depth level.
+        /// </summary>
+        /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
+        /// <param name="level">The depth level that should match this whitespace.</param>
+        /// <param name="whitespace">The whitespace to validate.</param>
+        /// <returns>true if the whitespace is legal; false otherwise.</returns>
+        private static bool LeadingWhitespaceValid(int indentationAmount, int level, string whitespace)
+        {
+            // Strip off leading newlines; there can be an arbitrary number of these.
+            whitespace = whitespace.TrimStart(XDocumentNewLine);
+
+            var indentation = new string(' ', level * indentationAmount);
+
+            return whitespace == indentation;
+        }
+
+        /// <summary>
+        /// Fix the whitespace in a whitespace node.
+        /// </summary>
+        /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param>
+        /// <param name="level">The depth level of the desired whitespace.</param>
+        /// <param name="whitespace">The whitespace node to fix.</param>
+        private static void FixupWhitespace(int indentationAmount, int level, XText whitespace)
+        {
+            var value = new StringBuilder(whitespace.Value.Length);
+
+            // Keep any previous preceeding new lines.
+            var newlines = whitespace.Value.TakeWhile(c => c == XDocumentNewLine).Count();
+
+            // Ensure there is always at least one new line before the indentation.
+            value.Append(XDocumentNewLine, newlines == 0 ? 1 : newlines);
+
+            whitespace.Value = value.Append(' ', level * indentationAmount).ToString();
+        }
+
+        /// <summary>
+        /// Output an error message to the console.
+        /// </summary>
+        /// <param name="converterTestType">The type of converter test.</param>
+        /// <param name="node">The node that caused the error.</param>
+        /// <param name="message">Detailed error message.</param>
+        /// <param name="args">Additional formatted string arguments.</param>
+        /// <returns>Returns true indicating that action should be taken on this error, and false if it should be ignored.</returns>
+        private bool OnError(ConverterTestType converterTestType, XObject node, string message, params object[] args)
+        {
+            if (this.IgnoreErrors.Contains(converterTestType)) // ignore the error
+            {
+                return false;
+            }
+
+            // Increase the error count.
+            this.Errors++;
+
+            var sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber);
+            var warning = this.ErrorsAsWarnings.Contains(converterTestType);
+            var display = String.Format(CultureInfo.CurrentCulture, message, args);
+
+            var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString());
+
+            this.Messaging.Write(msg);
+
+            return true;
+        }
+
+        /// <summary>
+        /// Return an identifier based on passed file/directory name
+        /// </summary>
+        /// <param name="name">File/directory name to generate identifer from</param>
+        /// <returns>A version of the name that is a legal identifier.</returns>
+        /// <remarks>This is duplicated from WiX's Common class.</remarks>
+        private static string GetIdentifierFromName(string name)
+        {
+            string result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_".
+
+            // MSI identifiers must begin with an alphabetic character or an
+            // underscore. Prefix all other values with an underscore.
+            if (AddPrefix.IsMatch(name))
+            {
+                result = String.Concat("_", result);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Converter test types.  These are used to condition error messages down to warnings.
+        /// </summary>
+        private enum ConverterTestType
+        {
+            /// <summary>
+            /// Internal-only: displayed when a string cannot be converted to an ConverterTestType.
+            /// </summary>
+            ConverterTestTypeUnknown,
+
+            /// <summary>
+            /// Displayed when an XML loading exception has occurred.
+            /// </summary>
+            XmlException,
+
+            /// <summary>
+            /// Displayed when a file cannot be accessed; typically when trying to save back a fixed file.
+            /// </summary>
+            UnauthorizedAccessException,
+
+            /// <summary>
+            /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'.
+            /// </summary>
+            DeclarationEncodingWrong,
+
+            /// <summary>
+            /// Displayed when the XML declaration is missing from the source file.
+            /// </summary>
+            DeclarationMissing,
+
+            /// <summary>
+            /// Displayed when the whitespace preceding a CDATA node is wrong.
+            /// </summary>
+            WhitespacePrecedingCDATAWrong,
+
+            /// <summary>
+            /// Displayed when the whitespace preceding a node is wrong.
+            /// </summary>
+            WhitespacePrecedingNodeWrong,
+
+            /// <summary>
+            /// Displayed when an element is not empty as it should be.
+            /// </summary>
+            NotEmptyElement,
+
+            /// <summary>
+            /// Displayed when the whitespace following a CDATA node is wrong.
+            /// </summary>
+            WhitespaceFollowingCDATAWrong,
+
+            /// <summary>
+            /// Displayed when the whitespace preceding an end element is wrong.
+            /// </summary>
+            WhitespacePrecedingEndElementWrong,
+
+            /// <summary>
+            /// Displayed when the xmlns attribute is missing from the document element.
+            /// </summary>
+            XmlnsMissing,
+
+            /// <summary>
+            /// Displayed when the xmlns attribute on the document element is wrong.
+            /// </summary>
+            XmlnsValueWrong,
+
+            /// <summary>
+            /// Assign an identifier to a File element when on Id attribute is specified.
+            /// </summary>
+            AssignAnonymousFileId,
+
+            /// <summary>
+            /// SuppressSignatureValidation attribute is deprecated and replaced with EnableSignatureValidation.
+            /// </summary>
+            SuppressSignatureValidationDeprecated,
+
+            /// <summary>
+            /// WixCA Binary/@Id has been renamed to UtilCA.
+            /// </summary>
+            WixCABinaryIdRenamed,
+
+            /// <summary>
+            /// QtExec custom actions have been renamed.
+            /// </summary>
+            QuietExecCustomActionsRenamed,
+
+            /// <summary>
+            /// QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout.
+            /// </summary>
+            QtExecCmdTimeoutAmbiguous,
+
+            /// <summary>
+            /// Directory/@ShortName may only be specified with Directory/@Name.
+            /// </summary>
+            AssignDirectoryNameFromShortName,
+        }
+    }
+}
diff --git a/src/WixToolset.Converters/WixToolset.Converters.csproj b/src/WixToolset.Converters/WixToolset.Converters.csproj
new file mode 100644
index 00000000..94a956d5
--- /dev/null
+++ b/src/WixToolset.Converters/WixToolset.Converters.csproj
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <Description>Converter</Description>
+    <Title>WiX Toolset Converters</Title>
+    <DebugType>embedded</DebugType>
+    <PublishRepositoryUrl>true</PublishRepositoryUrl>
+  </PropertyGroup>
+
+  <PropertyGroup>
+    <NoWarn>NU1701</NoWarn>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="WixToolset.Core" Version="4.0.*" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0-beta2-18618-05" PrivateAssets="All"/>
+    <PackageReference Include="Nerdbank.GitVersioning" Version="2.1.65" PrivateAssets="All" />
+  </ItemGroup>
+</Project>
diff --git a/src/deps/wix.dll b/src/deps/wix.dll
new file mode 100644
index 00000000..64f70f75
Binary files /dev/null and b/src/deps/wix.dll differ
diff --git a/src/test/WixToolsetTest.Converters.Tupleizer/ConvertTuplesFixture.cs b/src/test/WixToolsetTest.Converters.Tupleizer/ConvertTuplesFixture.cs
new file mode 100644
index 00000000..ae33d6b1
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters.Tupleizer/ConvertTuplesFixture.cs
@@ -0,0 +1,391 @@
+// 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 WixToolsetTest.Converters.Tupleizer
+{
+    using System;
+    using System.IO;
+    using System.Linq;
+    using WixBuildTools.TestSupport;
+    using Wix3 = Microsoft.Tools.WindowsInstallerXml;
+    using WixToolset.Converters.Tupleizer;
+    using WixToolset.Data;
+    using WixToolset.Data.WindowsInstaller;
+    using WixToolset.Data.Tuples;
+    using Xunit;
+
+    public class ConvertTuplesFixture
+    {
+        [Fact]
+        public void CanLoadWixoutAndConvertToIntermediate()
+        {
+            var rootFolder = TestData.Get();
+            var dataFolder = TestData.Get(@"TestData\Integration");
+
+            using (var fs = new DisposableFileSystem())
+            {
+                var intermediateFolder = fs.GetFolder();
+
+                var path = Path.Combine(dataFolder, "test.wixout");
+                var output = Wix3.Output.Load(path, suppressVersionCheck: true, suppressSchema: true);
+
+                var command = new ConvertTuplesCommand();
+                var intermediate = command.Execute(output);
+
+                Assert.NotNull(intermediate);
+                Assert.Single(intermediate.Sections);
+                Assert.Equal(String.Empty, intermediate.Id);
+
+                // Save and load to guarantee round-tripping support.
+                //
+                var wixiplFile = Path.Combine(intermediateFolder, "test.wixipl");
+                intermediate.Save(wixiplFile);
+
+                intermediate = Intermediate.Load(wixiplFile);
+
+                // Dump to text for easy diffing, with some massaging to keep v3 and v4 diffable.
+                //
+                var tables = output.Tables.Cast<Wix3.Table>();
+                var wix3Dump = tables
+                    .SelectMany(table => table.Rows.Cast<Wix3.Row>()
+                    .Select(row => RowToString(row)))
+                    .ToArray();
+
+                var tuples = intermediate.Sections.SelectMany(s => s.Tuples);
+                var wix4Dump = tuples.Select(tuple => TupleToString(tuple)).ToArray();
+
+                Assert.Equal(wix3Dump, wix4Dump);
+
+                // Useful when you want to diff the outputs with another diff tool...
+                // 
+                //var wix3TextDump = String.Join(Environment.NewLine, wix3Dump.OrderBy(val => val));
+                //var wix4TextDump = String.Join(Environment.NewLine, wix4Dump.OrderBy(val => val));
+                //Assert.Equal(wix3TextDump, wix4TextDump);
+            }
+        }
+
+        private static string RowToString(Wix3.Row row)
+        {
+            var fields = String.Join(",", row.Fields.Select(field => field.Data?.ToString()));
+
+            // Massage output to match WiX v3 rows and v4 tuples.
+            //
+            switch (row.Table.Name)
+            {
+                case "File":
+                    var fieldValues = row.Fields.Take(7).Select(field => field.Data?.ToString()).ToArray();
+                    if (fieldValues[3] == null)
+                    {
+                        // "Somebody" sometimes writes out a null field even when the column definition says
+                        // it's non-nullable. Not naming names or anything. (SWID tags.)
+                        fieldValues[3] = "0";
+                    }
+                    fields = String.Join(",", fieldValues);
+                    break;
+                case "WixFile":
+                    fields = String.Join(",", row.Fields.Take(8).Select(field => field.Data?.ToString()));
+                    break;
+            }
+
+            return $"{row.Table.Name},{fields}";
+        }
+
+        private static string TupleToString(WixToolset.Data.IntermediateTuple tuple)
+        {
+            var fields = String.Join(",", tuple.Fields.Select(field => field?.AsString()));
+
+            switch (tuple.Definition.Name)
+            {
+                // Massage output to match WiX v3 rows and v4 tuples.
+                //
+                case "Component":
+                {
+                    var componentTuple = (ComponentTuple)tuple;
+                    var attributes = ComponentLocation.Either == componentTuple.Location ? WindowsInstallerConstants.MsidbComponentAttributesOptional : 0;
+                    attributes |= ComponentLocation.SourceOnly == componentTuple.Location ? WindowsInstallerConstants.MsidbComponentAttributesSourceOnly : 0;
+                    attributes |= ComponentKeyPathType.Registry == componentTuple.KeyPathType ? WindowsInstallerConstants.MsidbComponentAttributesRegistryKeyPath : 0;
+                    attributes |= ComponentKeyPathType.OdbcDataSource == componentTuple.KeyPathType ? WindowsInstallerConstants.MsidbComponentAttributesODBCDataSource : 0;
+                    attributes |= componentTuple.DisableRegistryReflection ? WindowsInstallerConstants.MsidbComponentAttributesDisableRegistryReflection : 0;
+                    attributes |= componentTuple.NeverOverwrite ? WindowsInstallerConstants.MsidbComponentAttributesNeverOverwrite : 0;
+                    attributes |= componentTuple.Permanent ? WindowsInstallerConstants.MsidbComponentAttributesPermanent : 0;
+                    attributes |= componentTuple.SharedDllRefCount ? WindowsInstallerConstants.MsidbComponentAttributesSharedDllRefCount : 0;
+                    attributes |= componentTuple.Shared ? WindowsInstallerConstants.MsidbComponentAttributesShared : 0;
+                    attributes |= componentTuple.Transitive ? WindowsInstallerConstants.MsidbComponentAttributesTransitive : 0;
+                    attributes |= componentTuple.UninstallWhenSuperseded ? WindowsInstallerConstants.MsidbComponentAttributes64bit : 0;
+                    attributes |= componentTuple.Win64 ? WindowsInstallerConstants.MsidbComponentAttributes64bit : 0;
+
+                    fields = String.Join(",",
+                        componentTuple.ComponentId,
+                        componentTuple.Directory_,
+                        attributes.ToString(),
+                        componentTuple.Condition,
+                        componentTuple.KeyPath
+                        );
+                    break;
+                }
+                case "CustomAction":
+                {
+                    var customActionTuple = (CustomActionTuple)tuple;
+                    var type = customActionTuple.Win64 ? WindowsInstallerConstants.MsidbCustomActionType64BitScript : 0;
+                    type |= customActionTuple.TSAware ? WindowsInstallerConstants.MsidbCustomActionTypeTSAware : 0;
+                    type |= customActionTuple.Impersonate ? 0 : WindowsInstallerConstants.MsidbCustomActionTypeNoImpersonate;
+                    type |= customActionTuple.IgnoreResult ? WindowsInstallerConstants.MsidbCustomActionTypeContinue : 0;
+                    type |= customActionTuple.Hidden ? WindowsInstallerConstants.MsidbCustomActionTypeHideTarget : 0;
+                    type |= customActionTuple.Async ? WindowsInstallerConstants.MsidbCustomActionTypeAsync : 0;
+                    type |= CustomActionExecutionType.FirstSequence == customActionTuple.ExecutionType ? WindowsInstallerConstants.MsidbCustomActionTypeFirstSequence : 0;
+                    type |= CustomActionExecutionType.OncePerProcess == customActionTuple.ExecutionType ? WindowsInstallerConstants.MsidbCustomActionTypeOncePerProcess : 0;
+                    type |= CustomActionExecutionType.ClientRepeat == customActionTuple.ExecutionType ? WindowsInstallerConstants.MsidbCustomActionTypeClientRepeat : 0;
+                    type |= CustomActionExecutionType.Deferred == customActionTuple.ExecutionType ? WindowsInstallerConstants.MsidbCustomActionTypeInScript : 0;
+                    type |= CustomActionExecutionType.Rollback == customActionTuple.ExecutionType ? WindowsInstallerConstants.MsidbCustomActionTypeInScript | WindowsInstallerConstants.MsidbCustomActionTypeRollback : 0;
+                    type |= CustomActionExecutionType.Commit == customActionTuple.ExecutionType ? WindowsInstallerConstants.MsidbCustomActionTypeInScript | WindowsInstallerConstants.MsidbCustomActionTypeCommit : 0;
+                    type |= CustomActionSourceType.File == customActionTuple.SourceType ? WindowsInstallerConstants.MsidbCustomActionTypeSourceFile : 0;
+                    type |= CustomActionSourceType.Directory == customActionTuple.SourceType ? WindowsInstallerConstants.MsidbCustomActionTypeDirectory : 0;
+                    type |= CustomActionSourceType.Property == customActionTuple.SourceType ? WindowsInstallerConstants.MsidbCustomActionTypeProperty : 0;
+                    type |= CustomActionTargetType.Dll == customActionTuple.TargetType ? WindowsInstallerConstants.MsidbCustomActionTypeDll : 0;
+                    type |= CustomActionTargetType.Exe == customActionTuple.TargetType ? WindowsInstallerConstants.MsidbCustomActionTypeExe : 0;
+                    type |= CustomActionTargetType.TextData == customActionTuple.TargetType ? WindowsInstallerConstants.MsidbCustomActionTypeTextData : 0;
+                    type |= CustomActionTargetType.JScript == customActionTuple.TargetType ? WindowsInstallerConstants.MsidbCustomActionTypeJScript : 0;
+                    type |= CustomActionTargetType.VBScript == customActionTuple.TargetType ? WindowsInstallerConstants.MsidbCustomActionTypeVBScript : 0;
+
+                    fields = String.Join(",",
+                        type.ToString(),
+                        customActionTuple.Source,
+                        customActionTuple.Target,
+                        customActionTuple.PatchUninstall ? WindowsInstallerConstants.MsidbCustomActionTypePatchUninstall.ToString() : null
+                        );
+                    break;
+                }
+                case "Feature":
+                {
+                    var featureTuple = (FeatureTuple)tuple;
+                    var attributes = featureTuple.DisallowAbsent ? WindowsInstallerConstants.MsidbFeatureAttributesUIDisallowAbsent : 0;
+                    attributes |= featureTuple.DisallowAdvertise ? WindowsInstallerConstants.MsidbFeatureAttributesDisallowAdvertise : 0;
+                    attributes |= FeatureInstallDefault.FollowParent == featureTuple.InstallDefault ? WindowsInstallerConstants.MsidbFeatureAttributesFollowParent : 0;
+                    attributes |= FeatureInstallDefault.Source == featureTuple.InstallDefault ? WindowsInstallerConstants.MsidbFeatureAttributesFavorSource : 0;
+                    attributes |= FeatureTypicalDefault.Advertise == featureTuple.TypicalDefault ? WindowsInstallerConstants.MsidbFeatureAttributesFavorAdvertise : 0;
+
+                    fields = String.Join(",",
+                        featureTuple.Feature_Parent,
+                        featureTuple.Title,
+                        featureTuple.Description,
+                        featureTuple.Display.ToString(),
+                        featureTuple.Level.ToString(),
+                        featureTuple.Directory_,
+                        attributes.ToString());
+                    break;
+                }
+                case "File":
+                {
+                    var fileTuple = (FileTuple)tuple;
+                    fields = String.Join(",",
+                    fileTuple.Component_,
+                    fileTuple.LongFileName,
+                    fileTuple.FileSize.ToString(),
+                    fileTuple.Version,
+                    fileTuple.Language,
+                    ((fileTuple.ReadOnly ? WindowsInstallerConstants.MsidbFileAttributesReadOnly : 0)
+                        | (fileTuple.Hidden ? WindowsInstallerConstants.MsidbFileAttributesHidden : 0)
+                        | (fileTuple.System ? WindowsInstallerConstants.MsidbFileAttributesSystem : 0)
+                        | (fileTuple.Vital ? WindowsInstallerConstants.MsidbFileAttributesVital : 0)
+                        | (fileTuple.Checksum ? WindowsInstallerConstants.MsidbFileAttributesChecksum : 0)
+                        | ((fileTuple.Compressed.HasValue && fileTuple.Compressed.Value) ? WindowsInstallerConstants.MsidbFileAttributesCompressed : 0)
+                        | ((fileTuple.Compressed.HasValue && !fileTuple.Compressed.Value) ? WindowsInstallerConstants.MsidbFileAttributesNoncompressed : 0))
+                        .ToString());
+                    break;
+                }
+
+                case "Registry":
+                {
+                    var registryTuple = (RegistryTuple)tuple;
+                    var value = registryTuple.Value;
+
+                    switch (registryTuple.ValueType)
+                    {
+                        case RegistryValueType.Binary:
+                            value = String.Concat("#x", value);
+                            break;
+                        case RegistryValueType.Expandable:
+                            value = String.Concat("#%", value);
+                            break;
+                        case RegistryValueType.Integer:
+                            value = String.Concat("#", value);
+                            break;
+                        case RegistryValueType.MultiString:
+                            switch (registryTuple.ValueAction)
+                            {
+                                case RegistryValueActionType.Append:
+                                    value = String.Concat("[~]", value);
+                                    break;
+                                case RegistryValueActionType.Prepend:
+                                    value = String.Concat(value, "[~]");
+                                    break;
+                                case RegistryValueActionType.Write:
+                                default:
+                                    if (null != value && -1 == value.IndexOf("[~]", StringComparison.Ordinal))
+                                    {
+                                        value = String.Concat("[~]", value, "[~]");
+                                    }
+                                    break;
+                            }
+                            break;
+                        case RegistryValueType.String:
+                            // escape the leading '#' character for string registry keys
+                            if (null != value && value.StartsWith("#", StringComparison.Ordinal))
+                            {
+                                value = String.Concat("#", value);
+                            }
+                            break;
+                    }
+
+                    fields = String.Join(",",
+                        ((int)registryTuple.Root).ToString(),
+                        registryTuple.Key,
+                        registryTuple.Name,
+                        value,
+                        registryTuple.Component_
+                        );
+                    break;
+                }
+
+                case "RemoveRegistry":
+                {
+                    var removeRegistryTuple = (RemoveRegistryTuple)tuple;
+                    fields = String.Join(",",
+                        ((int)removeRegistryTuple.Root).ToString(),
+                        removeRegistryTuple.Key,
+                        removeRegistryTuple.Name,
+                        removeRegistryTuple.Component_
+                        );
+                    break;
+                }
+
+                case "ServiceControl":
+                {
+                    var serviceControlTuple = (ServiceControlTuple)tuple;
+
+                    var events = serviceControlTuple.InstallRemove ? WindowsInstallerConstants.MsidbServiceControlEventDelete : 0;
+                    events |= serviceControlTuple.UninstallRemove ? WindowsInstallerConstants.MsidbServiceControlEventUninstallDelete : 0;
+                    events |= serviceControlTuple.InstallStart ? WindowsInstallerConstants.MsidbServiceControlEventStart : 0;
+                    events |= serviceControlTuple.UninstallStart ? WindowsInstallerConstants.MsidbServiceControlEventUninstallStart : 0;
+                    events |= serviceControlTuple.InstallStop ? WindowsInstallerConstants.MsidbServiceControlEventStop : 0;
+                    events |= serviceControlTuple.UninstallStop ? WindowsInstallerConstants.MsidbServiceControlEventUninstallStop : 0;
+
+                    fields = String.Join(",",
+                        serviceControlTuple.Name,
+                        events.ToString(),
+                        serviceControlTuple.Arguments,
+                        serviceControlTuple.Wait == true ? "1" : "0",
+                        serviceControlTuple.Component_
+                        );
+                    break;
+                }
+
+                case "ServiceInstall":
+                {
+                    var serviceInstallTuple = (ServiceInstallTuple)tuple;
+
+                    var errorControl = (int)serviceInstallTuple.ErrorControl;
+                    errorControl |= serviceInstallTuple.Vital ? WindowsInstallerConstants.MsidbServiceInstallErrorControlVital : 0;
+
+                    var serviceType = (int)serviceInstallTuple.ServiceType;
+                    serviceType |= serviceInstallTuple.Interactive ? WindowsInstallerConstants.MsidbServiceInstallInteractive : 0;
+
+                    fields = String.Join(",",
+                        serviceInstallTuple.Name,
+                        serviceInstallTuple.DisplayName,
+                        serviceType.ToString(),
+                        ((int)serviceInstallTuple.StartType).ToString(),
+                        errorControl.ToString(),
+                        serviceInstallTuple.LoadOrderGroup,
+                        serviceInstallTuple.Dependencies,
+                        serviceInstallTuple.StartName,
+                        serviceInstallTuple.Password,
+                        serviceInstallTuple.Arguments,
+                        serviceInstallTuple.Component_,
+                        serviceInstallTuple.Description
+                        );
+                    break;
+                }
+
+                case "Upgrade":
+                {
+                    var upgradeTuple = (UpgradeTuple)tuple;
+
+                    var attributes = upgradeTuple.MigrateFeatures ? WindowsInstallerConstants.MsidbUpgradeAttributesMigrateFeatures : 0;
+                    attributes |= upgradeTuple.OnlyDetect ? WindowsInstallerConstants.MsidbUpgradeAttributesOnlyDetect : 0;
+                    attributes |= upgradeTuple.IgnoreRemoveFailures ? WindowsInstallerConstants.MsidbUpgradeAttributesIgnoreRemoveFailure : 0;
+                    attributes |= upgradeTuple.VersionMinInclusive ? WindowsInstallerConstants.MsidbUpgradeAttributesVersionMinInclusive : 0;
+                    attributes |= upgradeTuple.VersionMaxInclusive ? WindowsInstallerConstants.MsidbUpgradeAttributesVersionMaxInclusive : 0;
+                    attributes |= upgradeTuple.ExcludeLanguages ? WindowsInstallerConstants.MsidbUpgradeAttributesLanguagesExclusive : 0;
+
+                    fields = String.Join(",",
+                        upgradeTuple.VersionMin,
+                        upgradeTuple.VersionMax,
+                        upgradeTuple.Language,
+                        attributes.ToString(),
+                        upgradeTuple.Remove,
+                        upgradeTuple.ActionProperty
+                        );
+                    break;
+                }
+
+                case "WixAction":
+                {
+                    var wixActionTuple = (WixActionTuple)tuple;
+                    fields = String.Join(",",
+                        wixActionTuple.SequenceTable,
+                        wixActionTuple.Action,
+                        wixActionTuple.Condition,
+                        // BUGBUGBUG: AB#2626
+                        wixActionTuple.Sequence == 0 ? String.Empty : wixActionTuple.Sequence.ToString(),
+                        wixActionTuple.Before,
+                        wixActionTuple.After,
+                        wixActionTuple.Overridable == true ? "1" : "0"
+                        );
+                    break;
+                }
+
+                case "WixComplexReference":
+                {
+                    var wixComplexReferenceTuple = (WixComplexReferenceTuple)tuple;
+                    fields = String.Join(",",
+                        wixComplexReferenceTuple.Parent,
+                        ((int)wixComplexReferenceTuple.ParentType).ToString(),
+                        wixComplexReferenceTuple.ParentLanguage,
+                        wixComplexReferenceTuple.Child,
+                        ((int)wixComplexReferenceTuple.ChildType).ToString(),
+                        wixComplexReferenceTuple.IsPrimary ? "1" : "0"
+                        );
+                    break;
+                }
+
+                case "WixFile":
+                {
+                    var wixFileTuple = (WixFileTuple)tuple;
+                    fields = String.Concat(
+                        wixFileTuple.AssemblyType == FileAssemblyType.DotNetAssembly ? "0" : wixFileTuple.AssemblyType == FileAssemblyType.Win32Assembly ? "1" : String.Empty, ",",
+                        String.Join(",", tuple.Fields.Skip(2).Take(6).Select(field => (string)field).ToArray()));
+                    break;
+                }
+
+                case "WixProperty":
+                {
+                    var wixPropertyTuple = (WixPropertyTuple)tuple;
+                    var attributes = 0;
+                    attributes |= wixPropertyTuple.Admin ? 0x1 : 0;
+                    attributes |= wixPropertyTuple.Hidden ? 0x2 : 0;
+                    attributes |= wixPropertyTuple.Secure ? 0x4 : 0;
+
+                    fields = String.Join(",",
+                        wixPropertyTuple.Property_,
+                        attributes.ToString()
+                        );
+                    break;
+                }
+
+            }
+
+            var id = tuple.Id == null ? String.Empty : String.Concat(",", tuple.Id.Id);
+            return $"{tuple.Definition.Name}{id},{fields}";
+        }
+    }
+}
diff --git a/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wixout b/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wixout
new file mode 100644
index 00000000..da64b8af
Binary files /dev/null and b/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wixout differ
diff --git a/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wixproj b/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wixproj
new file mode 100644
index 00000000..8af13dc8
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wixproj
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" DefaultTargets="Build" InitialTargets="EnsureWixToolsetInstalled" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">x86</Platform>
+    <ProductVersion>3.10</ProductVersion>
+    <ProjectGuid>d59f1c1e-9238-49fa-bfa2-ec1d9c2dda1d</ProjectGuid>
+    <SchemaVersion>2.0</SchemaVersion>
+    <OutputName>TupleizerWixout</OutputName>
+    <OutputType>Package</OutputType>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
+    <OutputPath>bin\$(Configuration)\</OutputPath>
+    <IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath>
+    <DefineConstants>Debug</DefineConstants>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
+    <OutputPath>bin\$(Configuration)\</OutputPath>
+    <IntermediateOutputPath>obj\$(Configuration)\</IntermediateOutputPath>
+  </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="Product.wxs" />
+  </ItemGroup>
+  <ItemGroup>
+    <WixExtension Include="WixUtilExtension">
+      <HintPath>$(WixExtDir)\WixUtilExtension.dll</HintPath>
+      <Name>WixUtilExtension</Name>
+    </WixExtension>
+    <WixExtension Include="WixNetFxExtension">
+      <HintPath>$(WixExtDir)\WixNetFxExtension.dll</HintPath>
+      <Name>WixNetFxExtension</Name>
+    </WixExtension>
+  </ItemGroup>
+  <Import Project="$(WixTargetsPath)" Condition=" '$(WixTargetsPath)' != '' " />
+  <Import Project="$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets" Condition=" '$(WixTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets') " />
+  <Target Name="EnsureWixToolsetInstalled" Condition=" '$(WixTargetsImported)' != 'true' ">
+    <Error Text="The WiX Toolset v3.11 (or newer) build tools must be installed to build this project. To download the WiX Toolset, see http://wixtoolset.org/releases/" />
+  </Target>
+  <!--
+	To modify your build process, add your task inside one of the targets below and uncomment it.
+	Other similar extension points exist, see Wix.targets.
+	<Target Name="BeforeBuild">
+	</Target>
+	<Target Name="AfterBuild">
+	</Target>
+	-->
+</Project>
\ No newline at end of file
diff --git a/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wxs b/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wxs
new file mode 100644
index 00000000..1006a254
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters.Tupleizer/TestData/Integration/test.wxs
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
+  <Product Id="*" Name="TupleizerWixout" Language="1033" Version="1.0.0.0" Manufacturer="FireGiant" UpgradeCode="14a02d7f-9b32-4c92-b1f1-da518bf4e32a">
+    <Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
+
+    <MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." AllowSameVersionUpgrades="yes" IgnoreRemoveFailure="yes" />
+    <MediaTemplate />
+
+    <Feature Id="ProductFeature" Title="TupleizerWixout" Level="1">
+      <Feature Id="ChildFeature">
+        <ComponentGroupRef Id="ProductComponents" />
+      </Feature>
+    </Feature>
+
+    <PropertyRef Id="WIX_IS_NETFRAMEWORK_462_OR_LATER_INSTALLED" />
+    <CustomActionRef Id="WixFailWhenDeferred" />
+    <Property Id="WIXFAILWHENDEFERRED" Value="1" Secure="yes" />
+  </Product>
+
+  <Fragment>
+    <Directory Id="TARGETDIR" Name="SourceDir">
+      <Directory Id="ProgramFilesFolder">
+        <Directory Id="INSTALLFOLDER" Name="TupleizerWixout" />
+      </Directory>
+    </Directory>
+  </Fragment>
+
+  <Fragment>
+    <ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
+      <Component Id="ProductComponent">
+        <File Checksum="yes" ReadOnly="yes" Source="$(env.WIX)\bin\candle.exe" Assembly=".net" AssemblyApplication="candle.exe" />
+        <RegistryValue Root="HKLM" Key="SOFTWARE\WiX Toolset" Name="[ProductName]Installed" Value="1" Type="integer" />
+      </Component>
+    </ComponentGroup>
+  </Fragment>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters.Tupleizer/WixToolsetTest.Converters.Tupleizer.csproj b/src/test/WixToolsetTest.Converters.Tupleizer/WixToolsetTest.Converters.Tupleizer.csproj
new file mode 100644
index 00000000..fa6a6bcf
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters.Tupleizer/WixToolsetTest.Converters.Tupleizer.csproj
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net461</TargetFramework>
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Content Include="TestData\Integration\test.wixout" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\WixToolset.Converters.Tupleizer\WixToolset.Converters.Tupleizer.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="WixToolset.Data" Version="4.0.*" />
+    <PackageReference Include="WixBuildTools.TestSupport" Version="4.0.*" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="wix" HintPath="..\..\deps\wix.dll" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.1.0" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" PrivateAssets="All" />
+  </ItemGroup>
+</Project>
diff --git a/src/test/WixToolsetTest.Converters/ConverterFixture.cs b/src/test/WixToolsetTest.Converters/ConverterFixture.cs
new file mode 100644
index 00000000..97769cd6
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/ConverterFixture.cs
@@ -0,0 +1,554 @@
+// 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 WixToolsetTest.Converters
+{
+    using System;
+    using System.IO;
+    using System.Text;
+    using System.Xml.Linq;
+    using WixToolset.Converters;
+    using WixToolset.Data;
+    using WixToolset.Extensibility;
+    using WixToolset.Extensibility.Services;
+    using Xunit;
+
+    public class ConverterFixture
+    {
+        private static readonly XNamespace Wix4Namespace = "http://wixtoolset.org/schemas/v4/wxs";
+
+        [Fact]
+        public void EnsuresDeclaration()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <Fragment />",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <Fragment />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(1, errors);
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void EnsuresUtf8Declaration()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "    <Fragment />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 4, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            Assert.Equal(1, errors);
+            Assert.Equal("1.0", document.Declaration.Version);
+            Assert.Equal("utf-8", document.Declaration.Encoding);
+        }
+
+        [Fact]
+        public void CanFixWhitespace()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <Fragment>",
+                "    <Property Id='Prop'",
+                "              Value='Val'>",
+                "    </Property>",
+                "  </Fragment>",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "    <Fragment>",
+                "        <Property Id=\"Prop\" Value=\"Val\" />",
+                "    </Fragment>",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 4, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(expected, actual);
+            Assert.Equal(4, errors);
+        }
+
+        [Fact]
+        public void CanPreserveNewLines()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <Fragment>",
+                "",
+                "    <Property Id='Prop' Value='Val' />",
+                "",
+                "  </Fragment>",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "    <Fragment>",
+                "",
+                "        <Property Id=\"Prop\" Value=\"Val\" />",
+                "",
+                "    </Fragment>",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 4, null, null);
+
+            var conversions = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(expected, actual);
+            Assert.Equal(3, conversions);
+        }
+
+        [Fact]
+        public void CanConvertWithNewLineAtEndOfFile()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <Fragment>",
+                "",
+                "    <Property Id='Prop' Value='Val' />",
+                "",
+                "  </Fragment>",
+                "</Wix>",
+                "");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "    <Fragment>",
+                "",
+                "        <Property Id=\"Prop\" Value=\"Val\" />",
+                "",
+                "    </Fragment>",
+                "</Wix>",
+                "");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 4, null, null);
+
+            var conversions = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(expected, actual);
+            Assert.Equal(3, conversions);
+        }
+
+        [Fact]
+        public void CanFixCdataWhitespace()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <Fragment>",
+                "    <Property Id='Prop'>",
+                "       <![CDATA[1<2]]>",
+                "    </Property>",
+                "  </Fragment>",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <Fragment>",
+                "    <Property Id=\"Prop\"><![CDATA[1<2]]></Property>",
+                "  </Fragment>",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(expected, actual);
+            Assert.Equal(2, errors);
+        }
+
+        [Fact]
+        public void CanFixCdataWithWhitespace()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <Fragment>",
+                "    <Property Id='Prop'>",
+                "       <![CDATA[",
+                "           1<2",
+                "       ]]>",
+                "    </Property>",
+                "  </Fragment>",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <Fragment>",
+                "    <Property Id=\"Prop\"><![CDATA[1<2]]></Property>",
+                "  </Fragment>",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(expected, actual);
+            Assert.Equal(2, errors);
+        }
+
+        [Fact]
+        public void CanConvertMainNamespace()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>",
+                "  <Fragment />",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <Fragment />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(1, errors);
+            //Assert.Equal(Wix4Namespace, document.Root.GetDefaultNamespace());
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void CanConvertNamedMainNamespace()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<w:Wix xmlns:w='http://schemas.microsoft.com/wix/2006/wi'>",
+                "  <w:Fragment />",
+                "</w:Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<w:Wix xmlns:w=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <w:Fragment />",
+                "</w:Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(1, errors);
+            Assert.Equal(expected, actual);
+            Assert.Equal(Wix4Namespace, document.Root.GetNamespaceOfPrefix("w"));
+        }
+
+        [Fact]
+        public void CanConvertNonWixDefaultNamespace()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<w:Wix xmlns:w='http://schemas.microsoft.com/wix/2006/wi' xmlns='http://schemas.microsoft.com/wix/UtilExtension'>",
+                "  <w:Fragment>",
+                "    <Test />",
+                "  </w:Fragment>",
+                "</w:Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<w:Wix xmlns:w=\"http://wixtoolset.org/schemas/v4/wxs\" xmlns=\"http://wixtoolset.org/schemas/v4/wxs/util\">",
+                "  <w:Fragment>",
+                "    <Test />",
+                "  </w:Fragment>",
+                "</w:Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(expected, actual);
+            Assert.Equal(2, errors);
+            Assert.Equal(Wix4Namespace, document.Root.GetNamespaceOfPrefix("w"));
+            Assert.Equal("http://wixtoolset.org/schemas/v4/wxs/util", document.Root.GetDefaultNamespace());
+        }
+
+        [Fact]
+        public void CanConvertExtensionNamespace()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi' xmlns:util='http://schemas.microsoft.com/wix/UtilExtension'>",
+                "  <Fragment />",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\" xmlns:util=\"http://wixtoolset.org/schemas/v4/wxs/util\">",
+                "  <Fragment />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(2, errors);
+            Assert.Equal(expected, actual);
+            Assert.Equal(Wix4Namespace, document.Root.GetDefaultNamespace());
+        }
+
+        [Fact]
+        public void CanConvertMissingNamespace()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix>",
+                "  <Fragment />",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <Fragment />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(1, errors);
+            Assert.Equal(expected, actual);
+            Assert.Equal(Wix4Namespace, document.Root.GetDefaultNamespace());
+        }
+
+        [Fact]
+        public void CanConvertAnonymousFile()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <File Source='path\\to\\foo.txt' />",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <File Id=\"foo.txt\" Source=\"path\\to\\foo.txt\" />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(1, errors);
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void CanConvertShortNameDirectoryWithoutName()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <Directory ShortName='iamshort' />",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <Directory Name=\"iamshort\" />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(1, errors);
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void CanConvertSuppressSignatureValidationNo()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <MsiPackage SuppressSignatureValidation='no' />",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <MsiPackage EnableSignatureValidation=\"yes\" />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(1, errors);
+            Assert.Equal(expected, actual);
+        }
+
+        [Fact]
+        public void CanConvertSuppressSignatureValidationYes()
+        {
+            var parse = String.Join(Environment.NewLine,
+                "<?xml version='1.0' encoding='utf-8'?>",
+                "<Wix xmlns='http://wixtoolset.org/schemas/v4/wxs'>",
+                "  <Payload SuppressSignatureValidation='yes' />",
+                "</Wix>");
+
+            var expected = String.Join(Environment.NewLine,
+                "<?xml version=\"1.0\" encoding=\"utf-16\"?>",
+                "<Wix xmlns=\"http://wixtoolset.org/schemas/v4/wxs\">",
+                "  <Payload />",
+                "</Wix>");
+
+            var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo);
+
+            var messaging = new DummyMessaging();
+            var converter = new Wix3Converter(messaging, 2, null, null);
+
+            var errors = converter.ConvertDocument(document);
+
+            var actual = UnformattedDocumentString(document);
+
+            Assert.Equal(1, errors);
+            Assert.Equal(expected, actual);
+        }
+
+        private static string UnformattedDocumentString(XDocument document)
+        {
+            var sb = new StringBuilder();
+
+            using (var writer = new StringWriter(sb))
+            {
+                document.Save(writer, SaveOptions.DisableFormatting);
+            }
+
+            return sb.ToString();
+        }
+
+        private class DummyMessaging : IMessaging
+        {
+            public bool EncounteredError { get; set; }
+
+            public int LastErrorNumber { get; set; }
+
+            public bool ShowVerboseMessages { get; set; }
+
+            public bool SuppressAllWarnings { get; set; }
+
+            public bool WarningsAsError { get; set; }
+
+            public void ElevateWarningMessage(int warningNumber)
+            {
+            }
+
+            public string FormatMessage(Message message) => String.Empty;
+
+            public void SetListener(IMessageListener listener)
+            {
+            }
+
+            public void SuppressWarningMessage(int warningNumber)
+            {
+            }
+
+            public void Write(Message message)
+            {
+            }
+
+            public void Write(string message, bool verbose = false)
+            {
+            }
+        }
+    }
+}
diff --git a/src/test/WixToolsetTest.Converters/TestData/Preprocessor/ConvertedPreprocessor.wxs b/src/test/WixToolsetTest.Converters/TestData/Preprocessor/ConvertedPreprocessor.wxs
new file mode 100644
index 00000000..dcd43e35
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/Preprocessor/ConvertedPreprocessor.wxs
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
+
+
+<?include WixVer.wxi ?>
+
+<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:swid="http://wixtoolset.org/schemas/v4/wxs/tag" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
+    <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)" Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
+        <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
+        <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
+
+        <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
+
+        <MediaTemplate CabinetTemplate="core{0}.cab" />
+
+        <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
+            <Component Id="Licensing" Directory="INSTALLFOLDER">
+                <File Id="LICENSE.TXT" Source="LICENSE.TXT" />
+            </Component>
+
+            <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
+                    <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductInformation" Directory="BinFolder">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string" />
+                    <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
+                </RegistryKey>
+
+                <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
+            </Component>
+
+            <Component Directory="BinFolder">
+                <File Id="wixtoolset.org.ico" Source="common\wixtoolset.org.ico">
+                  <?include ComRegistration.wxi ?>
+                </File>
+                <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
+            </Component>
+
+            <ComponentGroupRef Id="ToolsetComponents" />
+            <ComponentGroupRef Id="ExtensionComponents" />
+            <ComponentGroupRef Id="LuxComponents" />
+            <ComponentGroupRef Id="DocComponents" />
+        </Feature>
+
+        <FeatureRef Id="Feature_MSBuild" />
+        <FeatureRef Id="Feature_Intellisense2010" />
+        <FeatureRef Id="Feature_Intellisense2012" />
+        <FeatureRef Id="Feature_Intellisense2013" />
+        <FeatureRef Id="Feature_Intellisense2015" />
+    </Product>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters/TestData/Preprocessor/Preprocessor.wxs b/src/test/WixToolsetTest.Converters/TestData/Preprocessor/Preprocessor.wxs
new file mode 100644
index 00000000..2eb908c2
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/Preprocessor/Preprocessor.wxs
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+
+
+
+<?include WixVer.wxi ?>
+
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:swid="http://schemas.microsoft.com/wix/TagExtension" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
+    <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)"
+             Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
+        <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
+        <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
+
+        <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
+
+        <MediaTemplate CabinetTemplate="core{0}.cab" />
+
+        <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
+            <Component Id="Licensing" Directory="INSTALLFOLDER">
+                <File Source="LICENSE.TXT" />
+            </Component>
+
+            <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
+                    <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductInformation" Directory="BinFolder">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string"/>
+                    <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
+                </RegistryKey>
+
+                <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
+            </Component>
+
+            <Component Directory="BinFolder">
+                <File Source="common\wixtoolset.org.ico">
+                  <?include ComRegistration.wxi ?>
+                </File>
+                <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
+            </Component>
+
+            <ComponentGroupRef Id="ToolsetComponents" />
+            <ComponentGroupRef Id="ExtensionComponents" />
+            <ComponentGroupRef Id="LuxComponents" />
+            <ComponentGroupRef Id="DocComponents" />
+        </Feature>
+
+        <FeatureRef Id="Feature_MSBuild" />
+        <FeatureRef Id="Feature_Intellisense2010" />
+        <FeatureRef Id="Feature_Intellisense2012" />
+        <FeatureRef Id="Feature_Intellisense2013" />
+        <FeatureRef Id="Feature_Intellisense2015" />
+    </Product>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters/TestData/Preprocessor/wixcop.settings.xml b/src/test/WixToolsetTest.Converters/TestData/Preprocessor/wixcop.settings.xml
new file mode 100644
index 00000000..9d3ad496
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/Preprocessor/wixcop.settings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<Settings>
+  <IgnoreErrors>
+    <Test Id="WhitespacePrecedingNodeWrong"/>
+    <Test Id="WhitespacePrecedingEndElementWrong"/>
+  </IgnoreErrors>
+  <ErrorsAsWarnings/>
+  <ExemptFiles/>
+</Settings>
\ No newline at end of file
diff --git a/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v3.wxs b/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v3.wxs
new file mode 100644
index 00000000..b0630f65
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v3.wxs
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+
+
+
+<?include WixVer.wxi ?>
+
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:swid="http://schemas.microsoft.com/wix/TagExtension" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
+    <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)"
+             Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
+        <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
+        <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
+
+        <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
+
+        <MediaTemplate CabinetTemplate="core{0}.cab" />
+
+        <Property Id="QtExecCmdTimeout" Value="600000" />
+        <CustomAction Id="InstallVSTemplateCommand" Property="QtExecCmdLine" Value="&quot;[VSENVPRODUCT80]\devenv.exe&quot; /setup" />
+        <CustomAction Id="InstallVSTemplate" BinaryKey="WixCA" DllEntry="CAQuietExec" Return="asyncWait" />
+
+        <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
+            <Component Id="Licensing" Directory="INSTALLFOLDER">
+                <File Source="LICENSE.TXT" />
+            </Component>
+
+            <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
+                    <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductInformation" Directory="BinFolder">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string"/>
+                    <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
+                </RegistryKey>
+
+                <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
+            </Component>
+
+            <Component Directory="BinFolder">
+                <File Source="common\wixtoolset.org.ico" />
+                <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
+            </Component>
+
+            <ComponentGroupRef Id="ToolsetComponents" />
+            <ComponentGroupRef Id="ExtensionComponents" />
+            <ComponentGroupRef Id="LuxComponents" />
+            <ComponentGroupRef Id="DocComponents" />
+        </Feature>
+
+        <FeatureRef Id="Feature_MSBuild" />
+        <FeatureRef Id="Feature_Intellisense2010" />
+        <FeatureRef Id="Feature_Intellisense2012" />
+        <FeatureRef Id="Feature_Intellisense2013" />
+        <FeatureRef Id="Feature_Intellisense2015" />
+    </Product>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v4_expected.wxs b/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v4_expected.wxs
new file mode 100644
index 00000000..be487147
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/QtExec.bad/v4_expected.wxs
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
+
+
+<?include WixVer.wxi ?>
+
+<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:swid="http://wixtoolset.org/schemas/v4/wxs/tag" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
+    <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)" Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
+        <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
+        <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
+
+        <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
+
+        <MediaTemplate CabinetTemplate="core{0}.cab" />
+
+        <Property Id="QtExecCmdTimeout" Value="600000" />
+        <CustomAction Id="InstallVSTemplateCommand" Property="WixQuietExecCmdLine" Value="&quot;[VSENVPRODUCT80]\devenv.exe&quot; /setup" />
+        <CustomAction Id="InstallVSTemplate" BinaryKey="UtilCA" DllEntry="WixQuietExec" Return="asyncWait" />
+
+        <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
+            <Component Id="Licensing" Directory="INSTALLFOLDER">
+                <File Id="LICENSE.TXT" Source="LICENSE.TXT" />
+            </Component>
+
+            <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
+                    <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductInformation" Directory="BinFolder">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string" />
+                    <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
+                </RegistryKey>
+
+                <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
+            </Component>
+
+            <Component Directory="BinFolder">
+                <File Id="wixtoolset.org.ico" Source="common\wixtoolset.org.ico" />
+                <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
+            </Component>
+
+            <ComponentGroupRef Id="ToolsetComponents" />
+            <ComponentGroupRef Id="ExtensionComponents" />
+            <ComponentGroupRef Id="LuxComponents" />
+            <ComponentGroupRef Id="DocComponents" />
+        </Feature>
+
+        <FeatureRef Id="Feature_MSBuild" />
+        <FeatureRef Id="Feature_Intellisense2010" />
+        <FeatureRef Id="Feature_Intellisense2012" />
+        <FeatureRef Id="Feature_Intellisense2013" />
+        <FeatureRef Id="Feature_Intellisense2015" />
+    </Product>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters/TestData/QtExec/v3.wxs b/src/test/WixToolsetTest.Converters/TestData/QtExec/v3.wxs
new file mode 100644
index 00000000..8d81a758
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/QtExec/v3.wxs
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+
+
+
+<?include WixVer.wxi ?>
+
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:swid="http://schemas.microsoft.com/wix/TagExtension" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
+    <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)"
+             Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
+        <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
+        <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
+
+        <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
+
+        <MediaTemplate CabinetTemplate="core{0}.cab" />
+
+        <CustomAction Id="InstallVSTemplateCommand" Property="QtExecCmdLine" Value="&quot;[VSENVPRODUCT80]\devenv.exe&quot; /setup" />
+        <CustomAction Id="InstallVSTemplate" BinaryKey="WixCA" DllEntry="CAQuietExec" Return="asyncWait" />
+
+        <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
+            <Component Id="Licensing" Directory="INSTALLFOLDER">
+                <File Source="LICENSE.TXT" />
+            </Component>
+
+            <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
+                    <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductInformation" Directory="BinFolder">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string"/>
+                    <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
+                </RegistryKey>
+
+                <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
+            </Component>
+
+            <Component Directory="BinFolder">
+                <File Source="common\wixtoolset.org.ico" />
+                <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
+            </Component>
+
+            <ComponentGroupRef Id="ToolsetComponents" />
+            <ComponentGroupRef Id="ExtensionComponents" />
+            <ComponentGroupRef Id="LuxComponents" />
+            <ComponentGroupRef Id="DocComponents" />
+        </Feature>
+
+        <FeatureRef Id="Feature_MSBuild" />
+        <FeatureRef Id="Feature_Intellisense2010" />
+        <FeatureRef Id="Feature_Intellisense2012" />
+        <FeatureRef Id="Feature_Intellisense2013" />
+        <FeatureRef Id="Feature_Intellisense2015" />
+    </Product>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters/TestData/QtExec/v4_expected.wxs b/src/test/WixToolsetTest.Converters/TestData/QtExec/v4_expected.wxs
new file mode 100644
index 00000000..22a961b2
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/QtExec/v4_expected.wxs
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
+
+
+<?include WixVer.wxi ?>
+
+<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:swid="http://wixtoolset.org/schemas/v4/wxs/tag" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
+    <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)" Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
+        <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
+        <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
+
+        <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
+
+        <MediaTemplate CabinetTemplate="core{0}.cab" />
+
+        <CustomAction Id="InstallVSTemplateCommand" Property="WixQuietExecCmdLine" Value="&quot;[VSENVPRODUCT80]\devenv.exe&quot; /setup" />
+        <CustomAction Id="InstallVSTemplate" BinaryKey="UtilCA" DllEntry="WixQuietExec" Return="asyncWait" />
+
+        <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
+            <Component Id="Licensing" Directory="INSTALLFOLDER">
+                <File Id="LICENSE.TXT" Source="LICENSE.TXT" />
+            </Component>
+
+            <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
+                    <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductInformation" Directory="BinFolder">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string" />
+                    <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
+                </RegistryKey>
+
+                <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
+            </Component>
+
+            <Component Directory="BinFolder">
+                <File Id="wixtoolset.org.ico" Source="common\wixtoolset.org.ico" />
+                <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
+            </Component>
+
+            <ComponentGroupRef Id="ToolsetComponents" />
+            <ComponentGroupRef Id="ExtensionComponents" />
+            <ComponentGroupRef Id="LuxComponents" />
+            <ComponentGroupRef Id="DocComponents" />
+        </Feature>
+
+        <FeatureRef Id="Feature_MSBuild" />
+        <FeatureRef Id="Feature_Intellisense2010" />
+        <FeatureRef Id="Feature_Intellisense2012" />
+        <FeatureRef Id="Feature_Intellisense2013" />
+        <FeatureRef Id="Feature_Intellisense2015" />
+    </Product>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters/TestData/SingleFile/ConvertedSingleFile.wxs b/src/test/WixToolsetTest.Converters/TestData/SingleFile/ConvertedSingleFile.wxs
new file mode 100644
index 00000000..aacb68fa
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/SingleFile/ConvertedSingleFile.wxs
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
+
+
+<?include WixVer.wxi ?>
+
+<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:swid="http://wixtoolset.org/schemas/v4/wxs/tag" xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
+    <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)" Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
+        <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
+        <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
+
+        <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
+
+        <MediaTemplate CabinetTemplate="core{0}.cab" />
+
+        <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
+            <Component Id="Licensing" Directory="INSTALLFOLDER">
+                <File Id="LICENSE.TXT" Source="LICENSE.TXT" />
+            </Component>
+
+            <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
+                    <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductInformation" Directory="BinFolder">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string" />
+                    <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
+                </RegistryKey>
+
+                <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
+            </Component>
+
+            <Component Directory="BinFolder">
+                <File Id="wixtoolset.org.ico" Source="common\wixtoolset.org.ico" />
+                <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
+            </Component>
+
+            <ComponentGroupRef Id="ToolsetComponents" />
+            <ComponentGroupRef Id="ExtensionComponents" />
+            <ComponentGroupRef Id="LuxComponents" />
+            <ComponentGroupRef Id="DocComponents" />
+        </Feature>
+
+        <FeatureRef Id="Feature_MSBuild" />
+        <FeatureRef Id="Feature_Intellisense2010" />
+        <FeatureRef Id="Feature_Intellisense2012" />
+        <FeatureRef Id="Feature_Intellisense2013" />
+        <FeatureRef Id="Feature_Intellisense2015" />
+    </Product>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters/TestData/SingleFile/SingleFile.wxs b/src/test/WixToolsetTest.Converters/TestData/SingleFile/SingleFile.wxs
new file mode 100644
index 00000000..310ae811
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/TestData/SingleFile/SingleFile.wxs
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+
+
+
+<?include WixVer.wxi ?>
+
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi" xmlns:swid="http://schemas.microsoft.com/wix/TagExtension" xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
+    <Product Id="*" Name="!(loc.ShortProduct) v$(var.WixMajorMinor) Core" Language="1033" Manufacturer="!(loc.Company)"
+             Version="$(var.WixMsiProductVersion)" UpgradeCode="3618724B-2523-44F9-A908-866AA619504D">
+        <Package Compressed="yes" InstallerVersion="200" SummaryCodepage="1252" InstallScope="perMachine" />
+        <swid:Tag Regid="!(loc.Regid)" InstallDirectory="INSTALLFOLDER" />
+
+        <MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed." />
+
+        <MediaTemplate CabinetTemplate="core{0}.cab" />
+
+        <Feature Id="Feature_WiX" Title="WiX Toolset" Level="1">
+            <Component Id="Licensing" Directory="INSTALLFOLDER">
+                <File Source="LICENSE.TXT" />
+            </Component>
+
+            <Component Id="ProductRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductFamilyRegistration" Directory="INSTALLFOLDER">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajor).x">
+                    <RegistryValue Name="v$(var.WixMajorMinor)" Value="[INSTALLFOLDER]" Type="string" />
+                </RegistryKey>
+            </Component>
+
+            <Component Id="ProductInformation" Directory="BinFolder">
+                <RegistryKey Root="HKLM" Key="SOFTWARE\Microsoft\Windows Installer XML\$(var.WixMajorMinor)">
+                    <RegistryValue Name="InstallRoot" Value="[BinFolder]" Type="string"/>
+                    <RegistryValue Name="ProductVersion" Value="[ProductVersion]" Type="string" />
+                </RegistryKey>
+
+                <RemoveFolder Id="CleanupShortcutFolder" Directory="ShortcutFolder" On="uninstall" />
+            </Component>
+
+            <Component Directory="BinFolder">
+                <File Source="common\wixtoolset.org.ico" />
+                <util:InternetShortcut Id="wixtoolset.org" Directory="ShortcutFolder" Name="WiX Home Page" Target="http://wixtoolset.org/" IconFile="file://[#wixtoolset.org.ico]" />
+            </Component>
+
+            <ComponentGroupRef Id="ToolsetComponents" />
+            <ComponentGroupRef Id="ExtensionComponents" />
+            <ComponentGroupRef Id="LuxComponents" />
+            <ComponentGroupRef Id="DocComponents" />
+        </Feature>
+
+        <FeatureRef Id="Feature_MSBuild" />
+        <FeatureRef Id="Feature_Intellisense2010" />
+        <FeatureRef Id="Feature_Intellisense2012" />
+        <FeatureRef Id="Feature_Intellisense2013" />
+        <FeatureRef Id="Feature_Intellisense2015" />
+    </Product>
+</Wix>
diff --git a/src/test/WixToolsetTest.Converters/WixToolsetTest.Converters.csproj b/src/test/WixToolsetTest.Converters/WixToolsetTest.Converters.csproj
new file mode 100644
index 00000000..d16c3d16
--- /dev/null
+++ b/src/test/WixToolsetTest.Converters/WixToolsetTest.Converters.csproj
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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. -->
+
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <None Remove="TestData\SingleFile\ConvertedSingleFile.wxs" />
+    <None Remove="TestData\SingleFile\SingleFile.wxs" />
+  </ItemGroup>
+  <ItemGroup>
+    <Content Include="TestData\SingleFile\ConvertedSingleFile.wxs" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="TestData\SingleFile\SingleFile.wxs" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="TestData\Preprocessor\ConvertedPreprocessor.wxs" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="TestData\Preprocessor\Preprocessor.wxs" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="TestData\Preprocessor\wixcop.settings.xml" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="TestData\QtExec\v3.wxs" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="TestData\QtExec\v4_expected.wxs" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="TestData\QtExec.bad\v3.wxs" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="TestData\QtExec.bad\v4_expected.wxs" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\WixToolset.Converters\WixToolset.Converters.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="WixBuildTools.TestSupport" Version="4.0.*" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.1.0" />
+    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" PrivateAssets="All" />
+  </ItemGroup>
+</Project>
-- 
cgit v1.2.3-55-g6feb