aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/ModularizeCommand.cs
blob: ba6af9863638e60619b9d323b32244313263d68d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
// 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.Core.WindowsInstaller.Bind
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.Linq;
    using System.Text;
    using System.Text.RegularExpressions;
    using WixToolset.Data;
    using WixToolset.Data.Tuples;
    using WixToolset.Data.WindowsInstaller;

    internal class ModularizeCommand
    {
        public ModularizeCommand(Output output, string modularizationGuid, IEnumerable<WixSuppressModularizationTuple> suppressTuples)
        {
            this.Output = output;
            this.ModularizationGuid = modularizationGuid;

            // Gather all the unique suppress modularization identifiers.
            this.SuppressModularizationIdentifiers = new HashSet<string>(suppressTuples.Select(s => s.WixSuppressModularization));
        }

        private Output Output { get; }

        private string ModularizationGuid { get; }

        private HashSet<string> SuppressModularizationIdentifiers { get; }

        public void Execute()
        {
            foreach (var table in this.Output.Tables)
            {
                this.ModularizeTable(table);
            }
        }

        private void ModularizeTable(Table table)
        {
            var modularizedColumns = new List<int>();

            // find the modularized columns
            for (var i = 0; i < table.Definition.Columns.Length; ++i)
            {
                if (ColumnModularizeType.None != table.Definition.Columns[i].ModularizeType)
                {
                    modularizedColumns.Add(i);
                }
            }

            if (0 < modularizedColumns.Count)
            {
                foreach (var row in table.Rows)
                {
                    foreach (var modularizedColumn in modularizedColumns)
                    {
                        var field = row.Fields[modularizedColumn];

                        if (field.Data != null)
                        {
                            field.Data = this.ModularizedRowFieldValue(row, field);
                        }
                    }
                }
            }
        }

        private string ModularizedRowFieldValue(Row row, Field field)
        {
            var fieldData = field.AsString();

            if (!(WindowsInstallerStandard.IsStandardAction(fieldData) || WindowsInstallerStandard.IsStandardProperty(fieldData)))
            {
                var modularizeType = field.Column.ModularizeType;

                // special logic for the ControlEvent table's Argument column
                // this column requires different modularization methods depending upon the value of the Event column
                if (ColumnModularizeType.ControlEventArgument == field.Column.ModularizeType)
                {
                    switch (row[2].ToString())
                    {
                        case "CheckExistingTargetPath": // redirectable property name
                        case "CheckTargetPath":
                        case "DoAction": // custom action name
                        case "NewDialog": // dialog name
                        case "SelectionBrowse":
                        case "SetTargetPath":
                        case "SpawnDialog":
                        case "SpawnWaitDialog":
                            if (Common.IsIdentifier(fieldData))
                            {
                                modularizeType = ColumnModularizeType.Column;
                            }
                            else
                            {
                                modularizeType = ColumnModularizeType.Property;
                            }
                            break;
                        default: // formatted
                            modularizeType = ColumnModularizeType.Property;
                            break;
                    }
                }
                else if (ColumnModularizeType.ControlText == field.Column.ModularizeType)
                {
                    // icons are stored in the Binary table, so they get column-type modularization
                    if (("Bitmap" == row[2].ToString() || "Icon" == row[2].ToString()) && Common.IsIdentifier(fieldData))
                    {
                        modularizeType = ColumnModularizeType.Column;
                    }
                    else
                    {
                        modularizeType = ColumnModularizeType.Property;
                    }
                }

                switch (modularizeType)
                {
                    case ColumnModularizeType.Column:
                        // ensure the value is an identifier (otherwise it shouldn't be modularized this way)
                        if (!Common.IsIdentifier(fieldData))
                        {
                            throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, WixDataStrings.EXP_CannotModularizeIllegalID, fieldData));
                        }

                        // if we're not supposed to suppress modularization of this identifier
                        if (!this.SuppressModularizationIdentifiers.Contains(fieldData))
                        {
                            fieldData = String.Concat(fieldData, ".", this.ModularizationGuid);
                        }
                        break;

                    case ColumnModularizeType.Property:
                    case ColumnModularizeType.Condition:
                        Regex regex;
                        if (ColumnModularizeType.Property == modularizeType)
                        {
                            regex = new Regex(@"\[(?<identifier>[#$!]?[a-zA-Z_][a-zA-Z0-9_\.]*)]", RegexOptions.Singleline | RegexOptions.ExplicitCapture);
                        }
                        else
                        {
                            Debug.Assert(ColumnModularizeType.Condition == modularizeType);

                            // This heinous looking regular expression is actually quite an elegant way 
                            // to shred the entire condition into the identifiers that need to be 
                            // modularized.  Let's break it down piece by piece:
                            //
                            // 1. Look for the operators: NOT, EQV, XOR, OR, AND, IMP (plus a space).  Note that the
                            //    regular expression is case insensitive so we don't have to worry about
                            //    all the permutations of these strings.
                            // 2. Look for quoted strings.  Quoted strings are just text and are ignored 
                            //    outright.
                            // 3. Look for environment variables.  These look like identifiers we might 
                            //    otherwise be interested in but start with a percent sign.  Like quoted 
                            //    strings these enviroment variable references are ignored outright.
                            // 4. Match all identifiers that are things that need to be modularized.  Note
                            //    the special characters (!, $, ?, &) that denote Component and Feature states.
                            regex = new Regex(@"NOT\s|EQV\s|XOR\s|OR\s|AND\s|IMP\s|"".*?""|%[a-zA-Z_][a-zA-Z0-9_\.]*|(?<identifier>[!$\?&]?[a-zA-Z_][a-zA-Z0-9_\.]*)", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

                            // less performant version of the above with captures showing where everything lives
                            // regex = new Regex(@"(?<operator>NOT|EQV|XOR|OR|AND|IMP)|(?<string>"".*?"")|(?<environment>%[a-zA-Z_][a-zA-Z0-9_\.]*)|(?<identifier>[!$\?&]?[a-zA-Z_][a-zA-Z0-9_\.]*)",RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);
                        }

                        var matches = regex.Matches(fieldData);

                        var sb = new StringBuilder(fieldData);

                        // Notice how this code walks backward through the list
                        // because it modifies the string as we through it.
                        for (var i = matches.Count - 1; 0 <= i; i--)
                        {
                            var group = matches[i].Groups["identifier"];
                            if (group.Success)
                            {
                                var identifier = group.Value;
                                if (!WindowsInstallerStandard.IsStandardProperty(identifier) && !this.SuppressModularizationIdentifiers.Contains(identifier))
                                {
                                    sb.Insert(group.Index + group.Length, '.');
                                    sb.Insert(group.Index + group.Length + 1, this.ModularizationGuid);
                                }
                            }
                        }

                        fieldData = sb.ToString();
                        break;

                    case ColumnModularizeType.CompanionFile:
                        // if we're not supposed to ignore this identifier and the value does not start with
                        // a digit, we must have a companion file so modularize it
                        if (!this.SuppressModularizationIdentifiers.Contains(fieldData) &&
                            0 < fieldData.Length && !Char.IsDigit(fieldData, 0))
                        {
                            fieldData = String.Concat(fieldData, ".", this.ModularizationGuid);
                        }
                        break;

                    case ColumnModularizeType.Icon:
                        if (!this.SuppressModularizationIdentifiers.Contains(fieldData))
                        {
                            var start = fieldData.LastIndexOf(".", StringComparison.Ordinal);
                            if (-1 == start)
                            {
                                fieldData = String.Concat(fieldData, ".", this.ModularizationGuid);
                            }
                            else
                            {
                                fieldData = String.Concat(fieldData.Substring(0, start), ".", this.ModularizationGuid, fieldData.Substring(start));
                            }
                        }
                        break;

                    case ColumnModularizeType.SemicolonDelimited:
                        var keys = fieldData.Split(';');
                        for (var i = 0; i < keys.Length; ++i)
                        {
                            if (!String.IsNullOrEmpty(keys[i]))
                            {
                                keys[i] = String.Concat(keys[i], ".", this.ModularizationGuid);
                            }
                        }

                        fieldData = String.Join(";", keys);
                        break;
                }
            }

            return fieldData;
        }
    }
}