aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs469
1 files changed, 469 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs
new file mode 100644
index 00000000..88a0295d
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/customactiondata.cs
@@ -0,0 +1,469 @@
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.Dtf.WindowsInstaller
4{
5 using System;
6 using System.IO;
7 using System.Xml;
8 using System.Xml.Serialization;
9 using System.Text;
10 using System.Collections.Generic;
11 using System.Globalization;
12 using System.Diagnostics.CodeAnalysis;
13
14 /// <summary>
15 /// Contains a collection of key-value pairs suitable for passing between
16 /// immediate and deferred/rollback/commit custom actions.
17 /// </summary>
18 /// <remarks>
19 /// Call the <see cref="CustomActionData.ToString" /> method to get a string
20 /// suitable for storing in a property and reconstructing the custom action data later.
21 /// </remarks>
22 /// <seealso cref="Session.CustomActionData"/>
23 /// <seealso cref="Session.DoAction(string,CustomActionData)"/>
24 [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
25 public sealed class CustomActionData : IDictionary<string, string>
26 {
27 /// <summary>
28 /// "CustomActionData" literal property name.
29 /// </summary>
30 public const string PropertyName = "CustomActionData";
31
32 private const char DataSeparator = ';';
33 private const char KeyValueSeparator = '=';
34
35 private IDictionary<string, string> data;
36
37 /// <summary>
38 /// Creates a new empty custom action data object.
39 /// </summary>
40 public CustomActionData() : this(null)
41 {
42 }
43
44 /// <summary>
45 /// Reconstructs a custom action data object from data that was previously
46 /// persisted in a string.
47 /// </summary>
48 /// <param name="keyValueList">Previous output from <see cref="CustomActionData.ToString" />.</param>
49 public CustomActionData(string keyValueList)
50 {
51 this.data = new Dictionary<string, string>();
52
53 if (keyValueList != null)
54 {
55 this.Parse(keyValueList);
56 }
57 }
58
59 /// <summary>
60 /// Adds a key and value to the data collection.
61 /// </summary>
62 /// <param name="key">Case-sensitive data key.</param>
63 /// <param name="value">Data value (may be null).</param>
64 /// <exception cref="ArgumentException">the key does not consist solely of letters,
65 /// numbers, and the period, underscore, and space characters.</exception>
66 public void Add(string key, string value)
67 {
68 CustomActionData.ValidateKey(key);
69 this.data.Add(key, value);
70 }
71
72 /// <summary>
73 /// Adds a value to the data collection, using XML serialization to persist the object as a string.
74 /// </summary>
75 /// <param name="key">Case-sensitive data key.</param>
76 /// <param name="value">Data value (may be null).</param>
77 /// <exception cref="ArgumentException">the key does not consist solely of letters,
78 /// numbers, and the period, underscore, and space characters.</exception>
79 /// <exception cref="NotSupportedException">The value type does not support XML serialization.</exception>
80 /// <exception cref="InvalidOperationException">The value could not be serialized.</exception>
81 public void AddObject<T>(string key, T value)
82 {
83 if (value == null)
84 {
85 this.Add(key, null);
86 }
87 else if (typeof(T) == typeof(string) ||
88 typeof(T) == typeof(CustomActionData)) // Serialize nested CustomActionData
89 {
90 this.Add(key, value.ToString());
91 }
92 else
93 {
94 string valueString = CustomActionData.Serialize<T>(value);
95 this.Add(key, valueString);
96 }
97 }
98
99 /// <summary>
100 /// Gets a value from the data collection, using XML serialization to load the object from a string.
101 /// </summary>
102 /// <param name="key">Case-sensitive data key.</param>
103 /// <exception cref="InvalidOperationException">The value could not be deserialized.</exception>
104 [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
105 public T GetObject<T>(string key)
106 {
107 string value = this[key];
108 if (value == null)
109 {
110 return default(T);
111 }
112 else if (typeof(T) == typeof(string))
113 {
114 // Funny casting because the compiler doesn't know T is string here.
115 return (T) (object) value;
116 }
117 else if (typeof(T) == typeof(CustomActionData))
118 {
119 // Deserialize nested CustomActionData.
120 return (T) (object) new CustomActionData(value);
121 }
122 else if (value.Length == 0)
123 {
124 return default(T);
125 }
126 else
127 {
128 return CustomActionData.Deserialize<T>(value);
129 }
130 }
131
132 /// <summary>
133 /// Determines whether the data contains an item with the specified key.
134 /// </summary>
135 /// <param name="key">Case-sensitive data key.</param>
136 /// <returns>true if the data contains an item with the key; otherwise, false</returns>
137 public bool ContainsKey(string key)
138 {
139 return this.data.ContainsKey(key);
140 }
141
142 /// <summary>
143 /// Gets a collection object containing all the keys of the data.
144 /// </summary>
145 public ICollection<string> Keys
146 {
147 get
148 {
149 return this.data.Keys;
150 }
151 }
152
153 /// <summary>
154 /// Removes the item with the specified key from the data.
155 /// </summary>
156 /// <param name="key">Case-sensitive data key.</param>
157 /// <returns>true if the item was successfully removed from the data;
158 /// false if an item with the specified key was not found</returns>
159 public bool Remove(string key)
160 {
161 return this.data.Remove(key);
162 }
163
164 /// <summary>
165 /// Gets the value with the specified key.
166 /// </summary>
167 /// <param name="key">Case-sensitive data key.</param>
168 /// <param name="value">Value associated with the specified key, or
169 /// null if an item with the specified key was not found</param>
170 /// <returns>true if the data contains an item with the specified key; otherwise, false.</returns>
171 public bool TryGetValue(string key, out string value)
172 {
173 return this.data.TryGetValue(key, out value);
174 }
175
176 /// <summary>
177 /// Gets a collection containing all the values of the data.
178 /// </summary>
179 public ICollection<string> Values
180 {
181 get
182 {
183 return this.data.Values;
184 }
185 }
186
187 /// <summary>
188 /// Gets or sets a data value with a specified key.
189 /// </summary>
190 /// <param name="key">Case-sensitive data key.</param>
191 /// <exception cref="ArgumentException">the key does not consist solely of letters,
192 /// numbers, and the period, underscore, and space characters.</exception>
193 public string this[string key]
194 {
195 get
196 {
197 return this.data[key];
198 }
199 set
200 {
201 CustomActionData.ValidateKey(key);
202 this.data[key] = value;
203 }
204 }
205
206 /// <summary>
207 /// Adds an item with key and value to the data collection.
208 /// </summary>
209 /// <param name="item">Case-sensitive data key, with a data value that may be null.</param>
210 /// <exception cref="ArgumentException">the key does not consist solely of letters,
211 /// numbers, and the period, underscore, and space characters.</exception>
212 public void Add(KeyValuePair<string, string> item)
213 {
214 CustomActionData.ValidateKey(item.Key);
215 this.data.Add(item);
216 }
217
218 /// <summary>
219 /// Removes all items from the data.
220 /// </summary>
221 public void Clear()
222 {
223 if (this.data.Count > 0)
224 {
225 this.data.Clear();
226 }
227 }
228
229 /// <summary>
230 /// Determines whether the data contains a specified item.
231 /// </summary>
232 /// <param name="item">The data item to locate.</param>
233 /// <returns>true if the data contains the item; otherwise, false</returns>
234 public bool Contains(KeyValuePair<string, string> item)
235 {
236 return this.data.Contains(item);
237 }
238
239 /// <summary>
240 /// Copies the data to an array, starting at a particular array index.
241 /// </summary>
242 /// <param name="array">Destination array.</param>
243 /// <param name="arrayIndex">Index in the array at which copying begins.</param>
244 public void CopyTo(KeyValuePair<string, string>[] array, int arrayIndex)
245 {
246 this.data.CopyTo(array, arrayIndex);
247 }
248
249 /// <summary>
250 /// Gets the number of items in the data.
251 /// </summary>
252 public int Count
253 {
254 get
255 {
256 return this.data.Count;
257 }
258 }
259
260 /// <summary>
261 /// Gets a value indicating whether the data is read-only.
262 /// </summary>
263 public bool IsReadOnly
264 {
265 get
266 {
267 return false;
268 }
269 }
270
271 /// <summary>
272 /// Removes an item from the data.
273 /// </summary>
274 /// <param name="item">The item to remove.</param>
275 /// <returns>true if the item was successfully removed from the data;
276 /// false if the item was not found</returns>
277 public bool Remove(KeyValuePair<string, string> item)
278 {
279 return this.data.Remove(item);
280 }
281
282 /// <summary>
283 /// Returns an enumerator that iterates through the collection.
284 /// </summary>
285 /// <returns>An enumerator that can be used to iterate through the collection.</returns>
286 public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
287 {
288 return this.data.GetEnumerator();
289 }
290
291 /// <summary>
292 /// Returns an enumerator that iterates through the collection.
293 /// </summary>
294 /// <returns>An enumerator that can be used to iterate through the collection.</returns>
295 System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
296 {
297 return ((System.Collections.IEnumerable) this.data).GetEnumerator();
298 }
299
300 /// <summary>
301 /// Gets a string representation of the data suitable for persisting in a property.
302 /// </summary>
303 /// <returns>Data string in the form "Key1=Value1;Key2=Value2"</returns>
304 public override string ToString()
305 {
306 StringBuilder buf = new StringBuilder();
307
308 foreach (KeyValuePair<string, string> item in this.data)
309 {
310 if (buf.Length > 0)
311 {
312 buf.Append(CustomActionData.DataSeparator);
313 }
314
315 buf.Append(item.Key);
316
317 if (item.Value != null)
318 {
319 buf.Append(CustomActionData.KeyValueSeparator);
320 buf.Append(CustomActionData.Escape(item.Value));
321 }
322 }
323
324 return buf.ToString();
325 }
326
327 /// <summary>
328 /// Ensures that a key contains valid characters.
329 /// </summary>
330 /// <param name="key">key to be validated</param>
331 /// <exception cref="ArgumentException">the key does not consist solely of letters,
332 /// numbers, and the period, underscore, and space characters.</exception>
333 private static void ValidateKey(string key)
334 {
335 if (String.IsNullOrEmpty(key))
336 {
337 throw new ArgumentNullException("key");
338 }
339
340 for (int i = 0; i < key.Length; i++)
341 {
342 char c = key[i];
343 if (!Char.IsLetterOrDigit(c) && c != '_' && c != '.' &&
344 !(i > 0 && i < key.Length - 1 && c == ' '))
345 {
346 throw new ArgumentOutOfRangeException("key");
347 }
348 }
349 }
350
351 /// <summary>
352 /// Serializes a value into an XML string.
353 /// </summary>
354 /// <typeparam name="T">Type of the value.</typeparam>
355 /// <param name="value">Value to be serialized.</param>
356 /// <returns>Serialized value data as a string.</returns>
357 private static string Serialize<T>(T value)
358 {
359 XmlWriterSettings xws = new XmlWriterSettings();
360 xws.OmitXmlDeclaration = true;
361
362 StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
363 using (XmlWriter xw = XmlWriter.Create(sw, xws))
364 {
365 XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
366 ns.Add(string.Empty, String.Empty); // Prevent output of any namespaces
367
368 XmlSerializer ser = new XmlSerializer(typeof(T));
369 ser.Serialize(xw, value, ns);
370
371 return sw.ToString();
372 }
373 }
374
375 /// <summary>
376 /// Deserializes a value from an XML string.
377 /// </summary>
378 /// <typeparam name="T">Expected type of the value.</typeparam>
379 /// <param name="value">Serialized value data.</param>
380 /// <returns>Deserialized value object.</returns>
381 private static T Deserialize<T>(string value)
382 {
383 StringReader sr = new StringReader(value);
384 using (XmlReader xr = XmlReader.Create(sr))
385 {
386 XmlSerializer ser = new XmlSerializer(typeof(T));
387 return (T) ser.Deserialize(xr);
388 }
389 }
390
391 /// <summary>
392 /// Escapes a value string by doubling any data-separator (semicolon) characters.
393 /// </summary>
394 /// <param name="value"></param>
395 /// <returns>Escaped value string</returns>
396 private static string Escape(string value)
397 {
398 value = value.Replace(String.Empty + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator);
399 return value;
400 }
401
402 /// <summary>
403 /// Unescapes a value string by undoubling any doubled data-separator (semicolon) characters.
404 /// </summary>
405 /// <param name="value"></param>
406 /// <returns>Unescaped value string</returns>
407 private static string Unescape(string value)
408 {
409 value = value.Replace(String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator);
410 return value;
411 }
412
413 /// <summary>
414 /// Loads key-value pairs from a string into the data collection.
415 /// </summary>
416 /// <param name="keyValueList">key-value pair list of the form returned by <see cref="ToString"/></param>
417 private void Parse(string keyValueList)
418 {
419 int itemStart = 0;
420 while (itemStart < keyValueList.Length)
421 {
422 // Find the next non-escaped data separator.
423 int semi = itemStart - 2;
424 do
425 {
426 semi = keyValueList.IndexOf(CustomActionData.DataSeparator, semi + 2);
427 }
428 while (semi >= 0 && semi < keyValueList.Length - 1 && keyValueList[semi + 1] == CustomActionData.DataSeparator);
429
430 if (semi < 0)
431 {
432 semi = keyValueList.Length;
433 }
434
435 // Find the next non-escaped key-value separator.
436 int equals = itemStart - 2;
437 do
438 {
439 equals = keyValueList.IndexOf(CustomActionData.KeyValueSeparator, equals + 2);
440 }
441 while (equals >= 0 && equals < keyValueList.Length - 1 && keyValueList[equals + 1] == CustomActionData.KeyValueSeparator);
442
443 if (equals < 0 || equals > semi)
444 {
445 equals = semi;
446 }
447
448 string key = keyValueList.Substring(itemStart, equals - itemStart);
449 string value = null;
450
451 // If there's a key-value separator before the next data separator, then the item has a value.
452 if (equals < semi)
453 {
454 value = keyValueList.Substring(equals + 1, semi - (equals + 1));
455 value = CustomActionData.Unescape(value);
456 }
457
458 // Add non-duplicate items to the collection.
459 if (key.Length > 0 && !this.data.ContainsKey(key))
460 {
461 this.data.Add(key, value);
462 }
463
464 // Move past the data separator to the next item.
465 itemStart = semi + 1;
466 }
467 }
468 }
469}