aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Data.WindowsInstaller/Row.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Data.WindowsInstaller/Row.cs')
-rw-r--r--src/WixToolset.Data.WindowsInstaller/Row.cs620
1 files changed, 620 insertions, 0 deletions
diff --git a/src/WixToolset.Data.WindowsInstaller/Row.cs b/src/WixToolset.Data.WindowsInstaller/Row.cs
new file mode 100644
index 00000000..962ed0f4
--- /dev/null
+++ b/src/WixToolset.Data.WindowsInstaller/Row.cs
@@ -0,0 +1,620 @@
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
3namespace WixToolset.Data
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.Diagnostics.CodeAnalysis;
9 using System.Globalization;
10 using System.Text;
11 using System.Text.RegularExpressions;
12 using System.Xml;
13
14 /// <summary>
15 /// Row containing data for a table.
16 /// </summary>
17 public class Row
18 {
19 private static long rowCount;
20
21 private Field[] fields;
22
23 /// <summary>
24 /// Creates a row that belongs to a table.
25 /// </summary>
26 /// <param name="sourceLineNumbers">Original source lines for this row.</param>
27 /// <param name="table">Table this row belongs to and should get its column definitions from.</param>
28 /// <remarks>The compiler should use this constructor exclusively.</remarks>
29 public Row(SourceLineNumber sourceLineNumbers, Table table)
30 : this(sourceLineNumbers, table.Definition)
31 {
32 this.Table = table;
33 }
34
35 /// <summary>
36 /// Creates a row that does not belong to a table.
37 /// </summary>
38 /// <param name="sourceLineNumbers">Original source lines for this row.</param>
39 /// <param name="tableDefinition">TableDefinition this row should get its column definitions from.</param>
40 /// <remarks>This constructor is used in cases where there isn't a clear owner of the row. The linker uses this constructor for the rows it generates.</remarks>
41 public Row(SourceLineNumber sourceLineNumbers, TableDefinition tableDefinition)
42 {
43 this.Number = rowCount++;
44 this.SourceLineNumbers = sourceLineNumbers;
45 this.fields = new Field[tableDefinition.Columns.Count];
46 this.TableDefinition = tableDefinition;
47
48 for (int i = 0; i < this.fields.Length; ++i)
49 {
50 this.fields[i] = Field.Create(this.TableDefinition.Columns[i]);
51 }
52 }
53
54 /// <summary>
55 /// Creates a shallow copy of a row from another row.
56 /// </summary>
57 /// <param name="source">The row the data is copied from.</param>
58 protected Row(Row source)
59 {
60 this.Table = source.Table;
61 this.TableDefinition = source.TableDefinition;
62 this.Number = source.Number;
63 this.Access = source.Access;
64 this.Operation = source.Operation;
65 this.Redundant = source.Redundant;
66 this.SectionId = source.SectionId;
67 this.SourceLineNumbers = source.SourceLineNumbers;
68 this.fields = source.fields;
69 }
70
71 /// <summary>
72 /// Gets or sets the access to the row's primary key.
73 /// </summary>
74 /// <value>The row access modifier.</value>
75 public AccessModifier Access { get; set; }
76
77 /// <summary>
78 /// Gets or sets the row transform operation.
79 /// </summary>
80 /// <value>The row transform operation.</value>
81 public RowOperation Operation { get; set; }
82
83 /// <summary>
84 /// Gets or sets wether the row is a duplicate of another row thus redundant.
85 /// </summary>
86 public bool Redundant { get; set; }
87
88 /// <summary>
89 /// Gets or sets the SectionId property on the row.
90 /// </summary>
91 /// <value>The SectionId property on the row.</value>
92 public string SectionId { get; set; }
93
94 /// <summary>
95 /// Gets the source file and line number for the row.
96 /// </summary>
97 /// <value>Source file and line number.</value>
98 public SourceLineNumber SourceLineNumbers { get; private set; }
99
100 /// <summary>
101 /// Gets the table this row belongs to.
102 /// </summary>
103 /// <value>null if Row does not belong to a Table, or owner Table otherwise.</value>
104 public Table Table { get; private set; }
105
106 /// <summary>
107 /// Gets the table definition for this row.
108 /// </summary>
109 /// <remarks>A Row always has a TableDefinition, even if the Row does not belong to a Table.</remarks>
110 /// <value>TableDefinition for Row.</value>
111 public TableDefinition TableDefinition { get; private set; }
112
113 /// <summary>
114 /// Gets the fields contained by this row.
115 /// </summary>
116 /// <value>Array of field objects</value>
117 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")]
118 public Field[] Fields
119 {
120 get { return this.fields; }
121 }
122
123 /// <summary>
124 /// Gets the unique number for the row.
125 /// </summary>
126 /// <value>Number for row.</value>
127 public long Number { get; private set; }
128
129 /// <summary>
130 /// Gets or sets the value of a particular field in the row.
131 /// </summary>
132 /// <param name="field">field index.</param>
133 /// <value>Value of a field in the row.</value>
134 public object this[int field]
135 {
136 get { return this.fields[field].Data; }
137 set { this.fields[field].Data = value; }
138 }
139
140 /// <summary>
141 /// Gets the field as an integer.
142 /// </summary>
143 /// <returns>Field's data as an integer.</returns>
144 public int FieldAsInteger(int field)
145 {
146 return this.fields[field].AsInteger();
147 }
148
149 /// <summary>
150 /// Gets the field as an integer that could be null.
151 /// </summary>
152 /// <returns>Field's data as an integer that could be null.</returns>
153 public int? FieldAsNullableInteger(int field)
154 {
155 return this.fields[field].AsNullableInteger();
156 }
157
158 /// <summary>
159 /// Gets the field as a string.
160 /// </summary>
161 /// <returns>Field's data as a string.</returns>
162 public string FieldAsString(int field)
163 {
164 return this.fields[field].AsString();
165 }
166
167 /// <summary>
168 /// Sets the value of a particular field in the row without validating.
169 /// </summary>
170 /// <param name="field">field index.</param>
171 /// <param name="value">Value of a field in the row.</param>
172 /// <returns>True if successful, false if validation failed.</returns>
173 public bool BestEffortSetField(int field, object value)
174 {
175 return this.fields[field].BestEffortSet(value);
176 }
177
178 /// <summary>
179 /// Get the value used to represent the row in a keyed row collection.
180 /// </summary>
181 /// <returns>Primary key or row number if no primary key is available.</returns>
182 public string GetKey()
183 {
184 return this.GetPrimaryKey() ?? Convert.ToString(this.Number, CultureInfo.InvariantCulture);
185 }
186
187 /// <summary>
188 /// Get the primary key of this row.
189 /// </summary>
190 /// <param name="delimiter">Delimiter character for multiple column primary keys.</param>
191 /// <returns>The primary key or null if the row's table has no primary key columns.</returns>
192 public string GetPrimaryKey(char delimiter = '/')
193 {
194 return this.GetPrimaryKey(delimiter, String.Empty);
195 }
196
197 /// <summary>
198 /// Get the primary key of this row.
199 /// </summary>
200 /// <param name="delimiter">Delimiter character for multiple column primary keys.</param>
201 /// <param name="nullReplacement">String to represent null values in the primary key.</param>
202 /// <returns>The primary key or null if the row's table has no primary key columns.</returns>
203 public string GetPrimaryKey(char delimiter, string nullReplacement)
204 {
205 bool foundPrimaryKey = false;
206 StringBuilder primaryKey = new StringBuilder();
207
208 foreach (Field field in this.fields)
209 {
210 if (field.Column.PrimaryKey)
211 {
212 if (foundPrimaryKey)
213 {
214 primaryKey.Append(delimiter);
215 }
216
217 primaryKey.Append((null == field.Data) ? nullReplacement : Convert.ToString(field.Data, CultureInfo.InvariantCulture));
218
219 foundPrimaryKey = true;
220 }
221 else // primary keys must be the first columns of a row so the first non-primary key means we can stop looking.
222 {
223 break;
224 }
225 }
226
227 return foundPrimaryKey ? primaryKey.ToString() : null;
228 }
229
230 /// <summary>
231 /// Returns true if the specified field is null or an empty string.
232 /// </summary>
233 /// <param name="field">Index of the field to check.</param>
234 /// <returns>true if the specified field is null or an empty string, false otherwise.</returns>
235 public bool IsColumnEmpty(int field)
236 {
237 if (null == this.fields[field].Data)
238 {
239 return true;
240 }
241
242 string dataString = this.fields[field].Data as string;
243 if (null != dataString && 0 == dataString.Length)
244 {
245 return true;
246 }
247
248 return false;
249 }
250
251 /// <summary>
252 /// Tests if the passed in row is identical.
253 /// </summary>
254 /// <param name="row">Row to compare against.</param>
255 /// <returns>True if two rows are identical.</returns>
256 public bool IsIdentical(Row row)
257 {
258 bool identical = (this.TableDefinition.Name == row.TableDefinition.Name && this.fields.Length == row.fields.Length);
259
260 for (int i = 0; identical && i < this.fields.Length; ++i)
261 {
262 if (!(this.fields[i].IsIdentical(row.fields[i])))
263 {
264 identical = false;
265 }
266 }
267
268 return identical;
269 }
270
271 /// <summary>
272 /// Returns a string representation of the Row.
273 /// </summary>
274 /// <returns>A string representation of the Row.</returns>
275 public override string ToString()
276 {
277 return String.Join("/", (object[])this.fields);
278 }
279
280 /// <summary>
281 /// Creates a Row from the XmlReader.
282 /// </summary>
283 /// <param name="reader">Reader to get data from.</param>
284 /// <param name="table">Table for this row.</param>
285 /// <returns>New row object.</returns>
286 internal static Row Read(XmlReader reader, Table table)
287 {
288 Debug.Assert("row" == reader.LocalName);
289
290 bool empty = reader.IsEmptyElement;
291 AccessModifier access = AccessModifier.Public;
292 RowOperation operation = RowOperation.None;
293 bool redundant = false;
294 string sectionId = null;
295 SourceLineNumber sourceLineNumbers = null;
296
297 while (reader.MoveToNextAttribute())
298 {
299 switch (reader.LocalName)
300 {
301 case "access":
302 access = (AccessModifier)Enum.Parse(typeof(AccessModifier), reader.Value, true);
303 break;
304 case "op":
305 operation = (RowOperation)Enum.Parse(typeof(RowOperation), reader.Value, true);
306 break;
307 case "redundant":
308 redundant = reader.Value.Equals("yes");
309 break;
310 case "sectionId":
311 sectionId = reader.Value;
312 break;
313 case "sourceLineNumber":
314 sourceLineNumbers = SourceLineNumber.CreateFromEncoded(reader.Value);
315 break;
316 }
317 }
318
319 Row row = table.CreateRow(sourceLineNumbers);
320 row.Access = access;
321 row.Operation = operation;
322 row.Redundant = redundant;
323 row.SectionId = sectionId;
324
325 // loop through all the fields in a row
326 if (!empty)
327 {
328 bool done = false;
329 int field = 0;
330
331 // loop through all the fields in a row
332 while (!done && reader.Read())
333 {
334 switch (reader.NodeType)
335 {
336 case XmlNodeType.Element:
337 switch (reader.LocalName)
338 {
339 case "field":
340 if (row.Fields.Length <= field)
341 {
342 if (!reader.IsEmptyElement)
343 {
344 throw new XmlException();
345 }
346 }
347 else
348 {
349 row.fields[field].Read(reader);
350 }
351 ++field;
352 break;
353 default:
354 throw new XmlException();
355 }
356 break;
357 case XmlNodeType.EndElement:
358 done = true;
359 break;
360 }
361 }
362
363 if (!done)
364 {
365 throw new XmlException();
366 }
367 }
368
369 return row;
370 }
371
372 /// <summary>
373 /// Returns the row in a format usable in IDT files.
374 /// </summary>
375 /// <param name="keepAddedColumns">Whether to keep columns added in a transform.</param>
376 /// <returns>String with tab delimited field values.</returns>
377 internal string ToIdtDefinition(bool keepAddedColumns)
378 {
379 bool first = true;
380 StringBuilder sb = new StringBuilder();
381
382 foreach (Field field in this.fields)
383 {
384 // Conditionally keep columns added in a transform; otherwise,
385 // break because columns can only be added at the end.
386 if (field.Column.Added && !keepAddedColumns)
387 {
388 break;
389 }
390
391 if (first)
392 {
393 first = false;
394 }
395 else
396 {
397 sb.Append('\t');
398 }
399
400 sb.Append(field.ToIdtValue());
401 }
402 sb.Append("\r\n");
403
404 return sb.ToString();
405 }
406
407 /// <summary>
408 /// Gets the modularized version of the field data.
409 /// </summary>
410 /// <param name="field">The field to modularize.</param>
411 /// <param name="modularizationGuid">String containing the GUID of the Merge Module to append the the field value, if appropriate.</param>
412 /// <param name="suppressModularizationIdentifiers">Optional collection of identifiers that should not be modularized.</param>
413 /// <remarks>moduleGuid is expected to be null when not being used to compile a Merge Module.</remarks>
414 /// <returns>The modularized version of the field data.</returns>
415 internal string GetModularizedValue(Field field, string modularizationGuid, ISet<string> suppressModularizationIdentifiers)
416 {
417 Debug.Assert(null != field.Data && 0 < ((string)field.Data).Length);
418 string fieldData = Convert.ToString(field.Data, CultureInfo.InvariantCulture);
419
420 if (null != modularizationGuid && ColumnModularizeType.None != field.Column.ModularizeType && !(WindowsInstallerStandard.IsStandardAction(fieldData) || WindowsInstallerStandard.IsStandardProperty(fieldData)))
421 {
422 StringBuilder sb;
423 int start;
424 ColumnModularizeType modularizeType = field.Column.ModularizeType;
425
426 // special logic for the ControlEvent table's Argument column
427 // this column requires different modularization methods depending upon the value of the Event column
428 if (ColumnModularizeType.ControlEventArgument == field.Column.ModularizeType)
429 {
430 switch (this[2].ToString())
431 {
432 case "CheckExistingTargetPath": // redirectable property name
433 case "CheckTargetPath":
434 case "DoAction": // custom action name
435 case "NewDialog": // dialog name
436 case "SelectionBrowse":
437 case "SetTargetPath":
438 case "SpawnDialog":
439 case "SpawnWaitDialog":
440 if (Common.IsIdentifier(fieldData))
441 {
442 modularizeType = ColumnModularizeType.Column;
443 }
444 else
445 {
446 modularizeType = ColumnModularizeType.Property;
447 }
448 break;
449 default: // formatted
450 modularizeType = ColumnModularizeType.Property;
451 break;
452 }
453 }
454 else if (ColumnModularizeType.ControlText == field.Column.ModularizeType)
455 {
456 // icons are stored in the Binary table, so they get column-type modularization
457 if (("Bitmap" == this[2].ToString() || "Icon" == this[2].ToString()) && Common.IsIdentifier(fieldData))
458 {
459 modularizeType = ColumnModularizeType.Column;
460 }
461 else
462 {
463 modularizeType = ColumnModularizeType.Property;
464 }
465 }
466
467 switch (modularizeType)
468 {
469 case ColumnModularizeType.Column:
470 // ensure the value is an identifier (otherwise it shouldn't be modularized this way)
471 if (!Common.IsIdentifier(fieldData))
472 {
473 throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, WixDataStrings.EXP_CannotModularizeIllegalID, fieldData));
474 }
475
476 // if we're not supposed to suppress modularization of this identifier
477 if (null == suppressModularizationIdentifiers || !suppressModularizationIdentifiers.Contains(fieldData))
478 {
479 fieldData = String.Concat(fieldData, ".", modularizationGuid);
480 }
481 break;
482
483 case ColumnModularizeType.Property:
484 case ColumnModularizeType.Condition:
485 Regex regex;
486 if (ColumnModularizeType.Property == modularizeType)
487 {
488 regex = new Regex(@"\[(?<identifier>[#$!]?[a-zA-Z_][a-zA-Z0-9_\.]*)]", RegexOptions.Singleline | RegexOptions.ExplicitCapture);
489 }
490 else
491 {
492 Debug.Assert(ColumnModularizeType.Condition == modularizeType);
493
494 // This heinous looking regular expression is actually quite an elegant way
495 // to shred the entire condition into the identifiers that need to be
496 // modularized. Let's break it down piece by piece:
497 //
498 // 1. Look for the operators: NOT, EQV, XOR, OR, AND, IMP (plus a space). Note that the
499 // regular expression is case insensitive so we don't have to worry about
500 // all the permutations of these strings.
501 // 2. Look for quoted strings. Quoted strings are just text and are ignored
502 // outright.
503 // 3. Look for environment variables. These look like identifiers we might
504 // otherwise be interested in but start with a percent sign. Like quoted
505 // strings these enviroment variable references are ignored outright.
506 // 4. Match all identifiers that are things that need to be modularized. Note
507 // the special characters (!, $, ?, &) that denote Component and Feature states.
508 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);
509
510 // less performant version of the above with captures showing where everything lives
511 // 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);
512 }
513
514 MatchCollection matches = regex.Matches(fieldData);
515
516 sb = new StringBuilder(fieldData);
517
518 // notice how this code walks backward through the list
519 // because it modifies the string as we through it
520 for (int i = matches.Count - 1; 0 <= i; i--)
521 {
522 Group group = matches[i].Groups["identifier"];
523 if (group.Success)
524 {
525 string identifier = group.Value;
526 if (!WindowsInstallerStandard.IsStandardProperty(identifier) && (null == suppressModularizationIdentifiers || !suppressModularizationIdentifiers.Contains(identifier)))
527 {
528 sb.Insert(group.Index + group.Length, '.');
529 sb.Insert(group.Index + group.Length + 1, modularizationGuid);
530 }
531 }
532 }
533
534 fieldData = sb.ToString();
535 break;
536
537 case ColumnModularizeType.CompanionFile:
538 // if we're not supposed to ignore this identifier and the value does not start with
539 // a digit, we must have a companion file so modularize it
540 if ((null == suppressModularizationIdentifiers || !suppressModularizationIdentifiers.Contains(fieldData)) &&
541 0 < fieldData.Length && !Char.IsDigit(fieldData, 0))
542 {
543 fieldData = String.Concat(fieldData, ".", modularizationGuid);
544 }
545 break;
546
547 case ColumnModularizeType.Icon:
548 if (null == suppressModularizationIdentifiers || !suppressModularizationIdentifiers.Contains(fieldData))
549 {
550 start = fieldData.LastIndexOf(".", StringComparison.Ordinal);
551 if (-1 == start)
552 {
553 fieldData = String.Concat(fieldData, ".", modularizationGuid);
554 }
555 else
556 {
557 fieldData = String.Concat(fieldData.Substring(0, start), ".", modularizationGuid, fieldData.Substring(start));
558 }
559 }
560 break;
561
562 case ColumnModularizeType.SemicolonDelimited:
563 string[] keys = fieldData.Split(';');
564 for (int i = 0; i < keys.Length; ++i)
565 {
566 keys[i] = String.Concat(keys[i], ".", modularizationGuid);
567 }
568 fieldData = String.Join(";", keys);
569 break;
570 }
571 }
572
573 return fieldData;
574 }
575
576 /// <summary>
577 /// Persists a row in an XML format.
578 /// </summary>
579 /// <param name="writer">XmlWriter where the Row should persist itself as XML.</param>
580 [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Changing the way this string normalizes would result " +
581 "in a change to the way intermediate files are generated, potentially causing extra churn in patches on an MSI built from an older version of WiX. " +
582 "Furthermore, there is no security hole here, as the strings won't need to make a round trip")]
583 internal void Write(XmlWriter writer)
584 {
585 writer.WriteStartElement("row", Intermediate.XmlNamespaceUri);
586
587 if (AccessModifier.Public != this.Access)
588 {
589 writer.WriteAttributeString("access", this.Access.ToString().ToLowerInvariant());
590 }
591
592 if (RowOperation.None != this.Operation)
593 {
594 writer.WriteAttributeString("op", this.Operation.ToString().ToLowerInvariant());
595 }
596
597 if (this.Redundant)
598 {
599 writer.WriteAttributeString("redundant", "yes");
600 }
601
602 if (null != this.SectionId)
603 {
604 writer.WriteAttributeString("sectionId", this.SectionId);
605 }
606
607 if (null != this.SourceLineNumbers)
608 {
609 writer.WriteAttributeString("sourceLineNumber", this.SourceLineNumbers.GetEncoded());
610 }
611
612 for (int i = 0; i < this.fields.Length; ++i)
613 {
614 this.fields[i].Write(writer);
615 }
616
617 writer.WriteEndElement();
618 }
619 }
620}