main
cs 236 lines 11.2 KB
Raw
1 // 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.
2
3 namespace WixToolset.Core.WindowsInstaller.Bind
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Globalization;
9 using System.Linq;
10 using System.Text;
11 using System.Text.RegularExpressions;
12 using WixToolset.Data;
13 using WixToolset.Data.Symbols;
14 using WixToolset.Data.WindowsInstaller;
15 using WixToolset.Extensibility.Services;
16
17 internal class ModularizeCommand
18 {
19 public ModularizeCommand(IBackendHelper backendHelper, WindowsInstallerData output, string modularizationSuffix, IEnumerable<WixSuppressModularizationSymbol> suppressSymbols)
20 {
21 this.BackendHelper = backendHelper;
22 this.Output = output;
23 this.ModularizationSuffix = modularizationSuffix;
24
25 // Gather all the unique suppress modularization identifiers.
26 this.SuppressModularizationIdentifiers = new HashSet<string>(suppressSymbols.Select(s => s.SuppressIdentifier));
27 }
28
29 private IBackendHelper BackendHelper { get; }
30
31 private WindowsInstallerData Output { get; }
32
33 private string ModularizationSuffix { get; }
34
35 private HashSet<string> SuppressModularizationIdentifiers { get; }
36
37 public void Execute()
38 {
39 foreach (var table in this.Output.Tables)
40 {
41 this.ModularizeTable(table);
42 }
43 }
44
45 private void ModularizeTable(Table table)
46 {
47 var modularizedColumns = new List<int>();
48
49 // find the modularized columns
50 for (var i = 0; i < table.Definition.Columns.Length; ++i)
51 {
52 if (ColumnModularizeType.None != table.Definition.Columns[i].ModularizeType)
53 {
54 modularizedColumns.Add(i);
55 }
56 }
57
58 if (0 < modularizedColumns.Count)
59 {
60 foreach (var row in table.Rows)
61 {
62 foreach (var modularizedColumn in modularizedColumns)
63 {
64 var field = row.Fields[modularizedColumn];
65
66 if (field.Data != null)
67 {
68 field.Data = this.ModularizedRowFieldValue(row, field);
69 }
70 }
71 }
72 }
73 }
74
75 private string ModularizedRowFieldValue(Row row, Field field)
76 {
77 var fieldData = field.AsString();
78
79 if (!(WindowsInstallerStandard.IsStandardAction(fieldData) || WindowsInstallerStandard.IsStandardProperty(fieldData)))
80 {
81 var modularizeType = field.Column.ModularizeType;
82
83 // special logic for the ControlEvent table's Argument column
84 // this column requires different modularization methods depending upon the value of the Event column
85 if (ColumnModularizeType.ControlEventArgument == field.Column.ModularizeType)
86 {
87 switch (row[2].ToString())
88 {
89 case "CheckExistingTargetPath": // redirectable property name
90 case "CheckTargetPath":
91 case "DoAction": // custom action name
92 case "NewDialog": // dialog name
93 case "SelectionBrowse":
94 case "SetTargetPath":
95 case "SpawnDialog":
96 case "SpawnWaitDialog":
97 if (this.BackendHelper.IsValidIdentifier(fieldData))
98 {
99 modularizeType = ColumnModularizeType.Column;
100 }
101 else
102 {
103 modularizeType = ColumnModularizeType.Property;
104 }
105 break;
106 default: // formatted
107 modularizeType = ColumnModularizeType.Property;
108 break;
109 }
110 }
111 else if (ColumnModularizeType.ControlText == field.Column.ModularizeType)
112 {
113 // icons are stored in the Binary table, so they get column-type modularization
114 if (("Bitmap" == row[2].ToString() || "Icon" == row[2].ToString()) && this.BackendHelper.IsValidIdentifier(fieldData))
115 {
116 modularizeType = ColumnModularizeType.Column;
117 }
118 else
119 {
120 modularizeType = ColumnModularizeType.Property;
121 }
122 }
123
124 switch (modularizeType)
125 {
126 case ColumnModularizeType.Column:
127 // ensure the value is an identifier (otherwise it shouldn't be modularized this way)
128 if (!this.BackendHelper.IsValidIdentifier(fieldData))
129 {
130 throw new InvalidOperationException($"The value '{fieldData}' is not a legal identifier and therefore cannot be modularized.");
131 }
132
133 // if we're not supposed to suppress modularization of this identifier
134 if (!this.SuppressModularizationIdentifiers.Contains(fieldData))
135 {
136 fieldData = String.Concat(fieldData, this.ModularizationSuffix);
137 }
138 break;
139
140 case ColumnModularizeType.Property:
141 case ColumnModularizeType.Condition:
142 Regex regex;
143 if (ColumnModularizeType.Property == modularizeType)
144 {
145 regex = new Regex(@"\[(?<identifier>[#$!]?[a-zA-Z_][a-zA-Z0-9_\.]*)]", RegexOptions.Singleline | RegexOptions.ExplicitCapture);
146 }
147 else
148 {
149 Debug.Assert(ColumnModularizeType.Condition == modularizeType);
150
151 // This heinous looking regular expression is actually quite an elegant way
152 // to shred the entire condition into the identifiers that need to be
153 // modularized. Let's break it down piece by piece:
154 //
155 // 1. Look for the operators: NOT, EQV, XOR, OR, AND, IMP (plus a space). Note that the
156 // regular expression is case insensitive so we don't have to worry about
157 // all the permutations of these strings.
158 // 2. Look for quoted strings. Quoted strings are just text and are ignored
159 // outright.
160 // 3. Look for environment variables. These look like identifiers we might
161 // otherwise be interested in but start with a percent sign. Like quoted
162 // strings these enviroment variable references are ignored outright.
163 // 4. Match all identifiers that are things that need to be modularized. Note
164 // the special characters (!, $, ?, &) that denote Component and Feature states.
165 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);
166
167 // less performant version of the above with captures showing where everything lives
168 // 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);
169 }
170
171 var matches = regex.Matches(fieldData);
172
173 var sb = new StringBuilder(fieldData);
174
175 // Notice how this code walks backward through the list
176 // because it modifies the string as we through it.
177 for (var i = matches.Count - 1; 0 <= i; i--)
178 {
179 var group = matches[i].Groups["identifier"];
180 if (group.Success)
181 {
182 var identifier = group.Value;
183 if (!WindowsInstallerStandard.IsStandardProperty(identifier) && !this.SuppressModularizationIdentifiers.Contains(identifier))
184 {
185 sb.Insert(group.Index + group.Length, this.ModularizationSuffix);
186 }
187 }
188 }
189
190 fieldData = sb.ToString();
191 break;
192
193 case ColumnModularizeType.CompanionFile:
194 // if we're not supposed to ignore this identifier and the value does not start with
195 // a digit, we must have a companion file so modularize it
196 if (!this.SuppressModularizationIdentifiers.Contains(fieldData) &&
197 0 < fieldData.Length && !Char.IsDigit(fieldData, 0))
198 {
199 fieldData = String.Concat(fieldData, this.ModularizationSuffix);
200 }
201 break;
202
203 case ColumnModularizeType.Icon:
204 if (!this.SuppressModularizationIdentifiers.Contains(fieldData))
205 {
206 var start = fieldData.LastIndexOf(".", StringComparison.Ordinal);
207 if (-1 == start)
208 {
209 fieldData = String.Concat(fieldData, this.ModularizationSuffix);
210 }
211 else
212 {
213 fieldData = String.Concat(fieldData.Substring(0, start), this.ModularizationSuffix, fieldData.Substring(start));
214 }
215 }
216 break;
217
218 case ColumnModularizeType.SemicolonDelimited:
219 var keys = fieldData.Split(';');
220 for (var i = 0; i < keys.Length; ++i)
221 {
222 if (!String.IsNullOrEmpty(keys[i]))
223 {
224 keys[i] = String.Concat(keys[i], this.ModularizationSuffix);
225 }
226 }
227
228 fieldData = String.Join(";", keys);
229 break;
230 }
231 }
232
233 return fieldData;
234 }
235 }
236 }