aboutsummaryrefslogtreecommitdiff
path: root/src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs')
-rw-r--r--src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs1048
1 files changed, 1048 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs
new file mode 100644
index 00000000..2d31fa75
--- /dev/null
+++ b/src/dtf/WixToolset.Dtf.WindowsInstaller/Record.cs
@@ -0,0 +1,1048 @@
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.Text;
8 using System.Collections.Generic;
9 using System.Runtime.InteropServices;
10 using System.Diagnostics.CodeAnalysis;
11
12 /// <summary>
13 /// The Record object is a container for holding and transferring a variable number of values.
14 /// Fields within the record are numerically indexed and can contain strings, integers, streams,
15 /// and null values. Record fields are indexed starting with 1. Field 0 is a special format field.
16 /// </summary>
17 /// <remarks><p>
18 /// Most methods on the Record class have overloads that allow using either a number
19 /// or a name to designate a field. However note that field names only exist when the
20 /// Record is directly returned from a query on a database. For other records, attempting
21 /// to access a field by name will result in an InvalidOperationException.
22 /// </p></remarks>
23 public class Record : InstallerHandle
24 {
25 private View view;
26 private bool isFormatStringInvalid;
27
28 /// <summary>
29 /// IsFormatStringInvalid is set from several View methods that invalidate the FormatString
30 /// and used to determine behavior during Record.ToString().
31 /// </summary>
32 internal protected bool IsFormatStringInvalid
33 {
34 set { this.isFormatStringInvalid = value; }
35
36 get { return this.isFormatStringInvalid; }
37 }
38
39 /// <summary>
40 /// Creates a new record object with the requested number of fields.
41 /// </summary>
42 /// <param name="fieldCount">Required number of fields, which may be 0.
43 /// The maximum number of fields in a record is limited to 65535.</param>
44 /// <remarks><p>
45 /// The Record object should be <see cref="InstallerHandle.Close"/>d after use.
46 /// It is best that the handle be closed manually as soon as it is no longer
47 /// needed, as leaving lots of unused handles open can degrade performance.
48 /// </p><p>
49 /// Win32 MSI API:
50 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicreaterecord.asp">MsiCreateRecord</a>
51 /// </p></remarks>
52 public Record(int fieldCount)
53 : this((IntPtr) RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, 0), true, (View) null)
54 {
55 }
56
57 /// <summary>
58 /// Creates a new record object, providing values for an arbitrary number of fields.
59 /// </summary>
60 /// <param name="fields">The values of the record fields. The parameters should be of type Int16, Int32 or String</param>
61 /// <remarks><p>
62 /// The Record object should be <see cref="InstallerHandle.Close"/>d after use.
63 /// It is best that the handle be closed manually as soon as it is no longer
64 /// needed, as leaving lots of unused handles open can degrade performance.
65 /// </p><p>
66 /// Win32 MSI API:
67 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msicreaterecord.asp">MsiCreateRecord</a>
68 /// </p></remarks>
69 public Record(params object[] fields)
70 : this(fields.Length)
71 {
72 for (int i = 0; i < fields.Length; i++)
73 {
74 this[i + 1] = fields[i];
75 }
76 }
77
78 internal Record(IntPtr handle, bool ownsHandle, View view)
79 : base(handle, ownsHandle)
80 {
81 this.view = view;
82 }
83
84 /// <summary>
85 /// Gets the number of fields in a record.
86 /// </summary>
87 /// <remarks><p>
88 /// Win32 MSI API:
89 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetfieldcount.asp">MsiRecordGetFieldCount</a>
90 /// </p></remarks>
91 public int FieldCount
92 {
93 get
94 {
95 return (int) RemotableNativeMethods.MsiRecordGetFieldCount((int) this.Handle);
96 }
97 }
98
99 /// <summary>
100 /// Gets or sets field 0 of the Record, which is the format string.
101 /// </summary>
102 public string FormatString
103 {
104 get { return this.GetString(0); }
105 set { this.SetString(0, value); }
106 }
107
108 /// <summary>
109 /// Gets or sets a record field value.
110 /// </summary>
111 /// <param name="fieldName">Specifies the name of the field of the Record to get or set.</param>
112 /// <exception cref="ArgumentOutOfRangeException">The name does not match any known field of the Record.</exception>
113 /// <remarks><p>
114 /// When getting a field, the type of the object returned depends on the type of the Record field.
115 /// The object will be one of: Int16, Int32, String, Stream, or null.
116 /// </p><p>
117 /// When setting a field, the type of the object provided will be converted to match the View
118 /// query that returned the record, or if Record was not returned from a view then the type of
119 /// the object provided will determine the type of the Record field. The object should be one of:
120 /// Int16, Int32, String, Stream, or null.
121 /// </p></remarks>
122 public object this[string fieldName]
123 {
124 get
125 {
126 int field = this.FindColumn(fieldName);
127 return this[field];
128 }
129
130 set
131 {
132 int field = this.FindColumn(fieldName);
133 this[field] = value;
134 }
135 }
136
137 /// <summary>
138 /// Gets or sets a record field value.
139 /// </summary>
140 /// <param name="field">Specifies the field of the Record to get or set.</param>
141 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
142 /// number of fields in the Record.</exception>
143 /// <remarks><p>
144 /// Record fields are indexed starting with 1. Field 0 is a special format field.
145 /// </p><p>
146 /// When getting a field, the type of the object returned depends on the type of the Record field.
147 /// The object will be one of: Int16, Int32, String, Stream, or null. If the Record was returned
148 /// from a View, the type will match that of the field from the View query. Otherwise, the type
149 /// will match the type of the last value set for the field.
150 /// </p><p>
151 /// When setting a field, the type of the object provided will be converted to match the View
152 /// query that returned the Record, or if Record was not returned from a View then the type of
153 /// the object provided will determine the type of the Record field. The object should be one of:
154 /// Int16, Int32, String, Stream, or null.
155 /// </p><p>
156 /// The type-specific getters and setters are slightly more efficient than this property, since
157 /// they don't have to do the extra work to infer the value's type every time.
158 /// </p><p>
159 /// Win32 MSI APIs:
160 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetinteger.asp">MsiRecordGetInteger</a>,
161 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetstring.asp">MsiRecordGetString</a>,
162 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetinteger.asp">MsiRecordSetInteger</a>,
163 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetstring.asp">MsiRecordSetString</a>
164 /// </p></remarks>
165 public object this[int field]
166 {
167 get
168 {
169 if (field == 0)
170 {
171 return this.GetString(0);
172 }
173 else
174 {
175 Type valueType = null;
176 if (this.view != null)
177 {
178 this.CheckRange(field);
179
180 valueType = this.view.Columns[field - 1].Type;
181 }
182
183 if (valueType == null || valueType == typeof(String))
184 {
185 return this.GetString(field);
186 }
187 else if (valueType == typeof(Stream))
188 {
189 return this.IsNull(field) ? null : new RecordStream(this, field);
190 }
191 else
192 {
193 int? value = this.GetNullableInteger(field);
194 return value.HasValue ? (object) value.Value : null;
195 }
196 }
197 }
198
199 set
200 {
201 if (field == 0)
202 {
203 if (value == null)
204 {
205 value = String.Empty;
206 }
207
208 this.SetString(0, value.ToString());
209 }
210 else if (value == null)
211 {
212 this.SetNullableInteger(field, null);
213 }
214 else
215 {
216 Type valueType = value.GetType();
217 if (valueType == typeof(Int32) || valueType == typeof(Int16))
218 {
219 this.SetInteger(field, (int) value);
220 }
221 else if (valueType.IsSubclassOf(typeof(Stream)))
222 {
223 this.SetStream(field, (Stream) value);
224 }
225 else
226 {
227 this.SetString(field, value.ToString());
228 }
229 }
230 }
231 }
232
233 /// <summary>
234 /// Creates a new Record object from an integer record handle.
235 /// </summary>
236 /// <remarks><p>
237 /// This method is only provided for interop purposes. A Record object
238 /// should normally be obtained by calling <see cref="WixToolset.Dtf.WindowsInstaller.View.Fetch"/>
239 /// other methods.
240 /// <p>The handle will be closed when this object is disposed or finalized.</p>
241 /// </p></remarks>
242 /// <param name="handle">Integer record handle</param>
243 /// <param name="ownsHandle">true to close the handle when this object is disposed or finalized</param>
244 public static Record FromHandle(IntPtr handle, bool ownsHandle)
245 {
246 return new Record(handle, ownsHandle, (View) null);
247 }
248
249 /// <summary>
250 /// Sets all fields in a record to null.
251 /// </summary>
252 /// <remarks><p>
253 /// Win32 MSI API:
254 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordcleardata.asp">MsiRecordClearData</a>
255 /// </p></remarks>
256 public void Clear()
257 {
258 uint ret = RemotableNativeMethods.MsiRecordClearData((int) this.Handle);
259 if (ret != 0)
260 {
261 throw InstallerException.ExceptionFromReturnCode(ret);
262 }
263 }
264
265 /// <summary>
266 /// Reports whether a record field is null.
267 /// </summary>
268 /// <param name="field">Specifies the field to check.</param>
269 /// <returns>True if the field is null, false otherwise.</returns>
270 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
271 /// number of fields in the Record.</exception>
272 /// <remarks><p>
273 /// Win32 MSI API:
274 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordisnull.asp">MsiRecordIsNull</a>
275 /// </p></remarks>
276 public bool IsNull(int field)
277 {
278 this.CheckRange(field);
279 return RemotableNativeMethods.MsiRecordIsNull((int) this.Handle, (uint) field);
280 }
281
282 /// <summary>
283 /// Reports whether a record field is null.
284 /// </summary>
285 /// <param name="fieldName">Specifies the field to check.</param>
286 /// <returns>True if the field is null, false otherwise.</returns>
287 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
288 /// of the named fields in the Record.</exception>
289 public bool IsNull(string fieldName)
290 {
291 int field = this.FindColumn(fieldName);
292 return this.IsNull(field);
293 }
294
295 /// <summary>
296 /// Gets the length of a record field. The count does not include the terminating null.
297 /// </summary>
298 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
299 /// number of fields in the Record.</exception>
300 /// <remarks><p>
301 /// The returned data size is 0 if the field is null, non-existent,
302 /// or an internal object pointer. The method also returns 0 if the handle is not a valid
303 /// Record handle.
304 /// </p><p>
305 /// If the data is in integer format, the property returns 2 or 4.
306 /// </p><p>
307 /// If the data is in string format, the property returns the character count
308 /// (not including the NULL terminator).
309 /// </p><p>
310 /// If the data is in stream format, the property returns the byte count.
311 /// </p><p>
312 /// Win32 MSI API:
313 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecorddatasize.asp">MsiRecordDataSize</a>
314 /// </p></remarks>
315 public int GetDataSize(int field)
316 {
317 this.CheckRange(field);
318 return (int) RemotableNativeMethods.MsiRecordDataSize((int) this.Handle, (uint) field);
319 }
320
321 /// <summary>
322 /// Gets the length of a record field. The count does not include the terminating null.
323 /// </summary>
324 /// <param name="fieldName">Specifies the field to check.</param>
325 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
326 /// of the named fields in the Record.</exception>
327 /// <remarks><p>The returned data size is 0 if the field is null, non-existent,
328 /// or an internal object pointer. The method also returns 0 if the handle is not a valid
329 /// Record handle.
330 /// </p><p>
331 /// If the data is in integer format, the property returns 2 or 4.
332 /// </p><p>
333 /// If the data is in string format, the property returns the character count
334 /// (not including the NULL terminator).
335 /// </p><p>
336 /// If the data is in stream format, the property returns the byte count.
337 /// </p></remarks>
338 public int GetDataSize(string fieldName)
339 {
340 int field = this.FindColumn(fieldName);
341 return this.GetDataSize(field);
342 }
343
344 /// <summary>
345 /// Gets a field value as an integer.
346 /// </summary>
347 /// <param name="field">Specifies the field to retrieve.</param>
348 /// <returns>Integer value of the field, or 0 if the field is null.</returns>
349 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
350 /// number of fields in the Record.</exception>
351 /// <remarks><p>
352 /// Win32 MSI API:
353 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetinteger.asp">MsiRecordGetInteger</a>
354 /// </p></remarks>
355 /// <seealso cref="GetNullableInteger(int)"/>
356 [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")]
357 public int GetInteger(int field)
358 {
359 this.CheckRange(field);
360
361 int value = RemotableNativeMethods.MsiRecordGetInteger((int) this.Handle, (uint) field);
362 if (value == Int32.MinValue) // MSI_NULL_INTEGER
363 {
364 return 0;
365 }
366 return value;
367 }
368
369 /// <summary>
370 /// Gets a field value as an integer.
371 /// </summary>
372 /// <param name="fieldName">Specifies the field to retrieve.</param>
373 /// <returns>Integer value of the field, or 0 if the field is null.</returns>
374 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
375 /// of the named fields in the Record.</exception>
376 /// <seealso cref="GetNullableInteger(string)"/>
377 [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")]
378 public int GetInteger(string fieldName)
379 {
380 int field = this.FindColumn(fieldName);
381 return this.GetInteger(field);
382 }
383
384 /// <summary>
385 /// Gets a field value as an integer.
386 /// </summary>
387 /// <param name="field">Specifies the field to retrieve.</param>
388 /// <returns>Integer value of the field, or null if the field is null.</returns>
389 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
390 /// number of fields in the Record.</exception>
391 /// <remarks><p>
392 /// Win32 MSI API:
393 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetinteger.asp">MsiRecordGetInteger</a>
394 /// </p></remarks>
395 /// <seealso cref="GetInteger(int)"/>
396 public int? GetNullableInteger(int field)
397 {
398 this.CheckRange(field);
399
400 int value = RemotableNativeMethods.MsiRecordGetInteger((int) this.Handle, (uint) field);
401 if (value == Int32.MinValue) // MSI_NULL_INTEGER
402 {
403 return null;
404 }
405 return value;
406 }
407
408 /// <summary>
409 /// Gets a field value as an integer.
410 /// </summary>
411 /// <param name="fieldName">Specifies the field to retrieve.</param>
412 /// <returns>Integer value of the field, or null if the field is null.</returns>
413 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
414 /// of the named fields in the Record.</exception>
415 /// <seealso cref="GetInteger(string)"/>
416 public int? GetNullableInteger(string fieldName)
417 {
418 int field = this.FindColumn(fieldName);
419 return this.GetInteger(field);
420 }
421
422 /// <summary>
423 /// Sets the value of a field to an integer.
424 /// </summary>
425 /// <param name="field">Specifies the field to set.</param>
426 /// <param name="value">new value of the field</param>
427 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
428 /// number of fields in the Record.</exception>
429 /// <remarks><p>
430 /// Win32 MSI API:
431 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetinteger.asp">MsiRecordSetInteger</a>
432 /// </p></remarks>
433 /// <seealso cref="SetNullableInteger(int,int?)"/>
434 public void SetInteger(int field, int value)
435 {
436 this.CheckRange(field);
437
438 uint ret = RemotableNativeMethods.MsiRecordSetInteger((int) this.Handle, (uint) field, value);
439 if (ret != 0)
440 {
441 throw InstallerException.ExceptionFromReturnCode(ret);
442 }
443 }
444
445 /// <summary>
446 /// Sets the value of a field to an integer.
447 /// </summary>
448 /// <param name="fieldName">Specifies the field to set.</param>
449 /// <param name="value">new value of the field</param>
450 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
451 /// of the named fields in the Record.</exception>
452 /// <seealso cref="SetNullableInteger(string,int?)"/>
453 public void SetInteger(string fieldName, int value)
454 {
455 int field = this.FindColumn(fieldName);
456 this.SetInteger(field, value);
457 }
458
459 /// <summary>
460 /// Sets the value of a field to a nullable integer.
461 /// </summary>
462 /// <param name="field">Specifies the field to set.</param>
463 /// <param name="value">new value of the field</param>
464 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
465 /// number of fields in the Record.</exception>
466 /// <remarks><p>
467 /// Win32 MSI API:
468 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetinteger.asp">MsiRecordSetInteger</a>
469 /// </p></remarks>
470 /// <seealso cref="SetInteger(int,int)"/>
471 public void SetNullableInteger(int field, int? value)
472 {
473 this.CheckRange(field);
474
475 uint ret = RemotableNativeMethods.MsiRecordSetInteger(
476 (int) this.Handle,
477 (uint) field,
478 value.HasValue ? (int) value : Int32.MinValue);
479 if (ret != 0)
480 {
481 throw InstallerException.ExceptionFromReturnCode(ret);
482 }
483 }
484
485 /// <summary>
486 /// Sets the value of a field to a nullable integer.
487 /// </summary>
488 /// <param name="fieldName">Specifies the field to set.</param>
489 /// <param name="value">new value of the field</param>
490 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
491 /// of the named fields in the Record.</exception>
492 /// <seealso cref="SetInteger(string,int)"/>
493 public void SetNullableInteger(string fieldName, int? value)
494 {
495 int field = this.FindColumn(fieldName);
496 this.SetNullableInteger(field, value);
497 }
498
499 /// <summary>
500 /// Gets a field value as a string.
501 /// </summary>
502 /// <param name="field">Specifies the field to retrieve.</param>
503 /// <returns>String value of the field, or an empty string if the field is null.</returns>
504 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
505 /// number of fields in the Record.</exception>
506 /// <remarks><p>
507 /// Win32 MSI API:
508 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordgetstring.asp">MsiRecordGetString</a>
509 /// </p></remarks>
510 public string GetString(int field)
511 {
512 this.CheckRange(field);
513
514 StringBuilder buf = new StringBuilder(String.Empty);
515 uint bufSize = 0;
516 uint ret = RemotableNativeMethods.MsiRecordGetString((int) this.Handle, (uint) field, buf, ref bufSize);
517 if (ret == (uint) NativeMethods.Error.MORE_DATA)
518 {
519 buf.Capacity = (int) ++bufSize;
520 ret = RemotableNativeMethods.MsiRecordGetString((int) this.Handle, (uint) field, buf, ref bufSize);
521 }
522 if (ret != 0)
523 {
524 throw InstallerException.ExceptionFromReturnCode(ret);
525 }
526 return buf.ToString();
527 }
528
529 /// <summary>
530 /// Gets a field value as a string.
531 /// </summary>
532 /// <param name="fieldName">Specifies the field to retrieve.</param>
533 /// <returns>String value of the field, or an empty string if the field is null.</returns>
534 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
535 /// of the named fields in the Record.</exception>
536 public string GetString(string fieldName)
537 {
538 int field = this.FindColumn(fieldName);
539 return this.GetString(field);
540 }
541
542 /// <summary>
543 /// Sets the value of a field to a string.
544 /// </summary>
545 /// <param name="field">Specifies the field to set.</param>
546 /// <param name="value">new value of the field</param>
547 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
548 /// number of fields in the Record.</exception>
549 /// <remarks><p>
550 /// Win32 MSI API:
551 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetstring.asp">MsiRecordSetString</a>
552 /// </p></remarks>
553 public void SetString(int field, string value)
554 {
555 this.CheckRange(field);
556
557 if (value == null)
558 {
559 value = String.Empty;
560 }
561
562 uint ret = RemotableNativeMethods.MsiRecordSetString((int) this.Handle, (uint) field, value);
563 if (ret != 0)
564 {
565 throw InstallerException.ExceptionFromReturnCode(ret);
566 }
567
568 // If we set the FormatString manually, then it should be valid again
569 if (field == 0)
570 {
571 this.IsFormatStringInvalid = false;
572 }
573 }
574
575 /// <summary>
576 /// Sets the value of a field to a string.
577 /// </summary>
578 /// <param name="fieldName">Specifies the field to set.</param>
579 /// <param name="value">new value of the field</param>
580 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
581 /// of the named fields in the Record.</exception>
582 public void SetString(string fieldName, string value)
583 {
584 int field = this.FindColumn(fieldName);
585 this.SetString(field, value);
586 }
587
588 /// <summary>
589 /// Reads a record stream field into a file.
590 /// </summary>
591 /// <param name="field">Specifies the field of the Record to get.</param>
592 /// <param name="filePath">Specifies the path to the file to contain the stream.</param>
593 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
594 /// number of fields in the Record.</exception>
595 /// <exception cref="NotSupportedException">Attempt to extract a storage from a database open
596 /// in read-write mode, or from a database without an associated file path</exception>
597 /// <remarks><p>
598 /// This method is capable of directly extracting substorages. To do so, first select both the
599 /// `Name` and `Data` column of the `_Storages` table, then get the stream of the `Data` field.
600 /// However, substorages may only be extracted from a database that is open in read-only mode.
601 /// </p><p>
602 /// Win32 MSI API:
603 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordreadstream.asp">MsiRecordReadStream</a>
604 /// </p></remarks>
605 public void GetStream(int field, string filePath)
606 {
607 if (String.IsNullOrEmpty(filePath))
608 {
609 throw new ArgumentNullException("filePath");
610 }
611
612 IList<TableInfo> tables = (this.view != null ? this.view.Tables : null);
613 if (tables != null && tables.Count == 1 && tables[0].Name == "_Storages" && field == this.FindColumn("Data"))
614 {
615 if (!this.view.Database.IsReadOnly)
616 {
617 throw new NotSupportedException("Database must be opened read-only to support substorage extraction.");
618 }
619 else if (this.view.Database.FilePath == null)
620 {
621 throw new NotSupportedException("Database must have an associated file path to support substorage extraction.");
622 }
623 else if (this.FindColumn("Name") <= 0)
624 {
625 throw new NotSupportedException("Name column must be part of the Record in order to extract substorage.");
626 }
627 else
628 {
629 Record.ExtractSubStorage(this.view.Database.FilePath, this.GetString("Name"), filePath);
630 }
631 }
632 else
633 {
634 if (!this.IsNull(field))
635 {
636 Stream readStream = null, writeStream = null;
637 try
638 {
639 readStream = new RecordStream(this, field);
640 writeStream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
641 int count = 512;
642 byte[] buf = new byte[count];
643 while (count == buf.Length)
644 {
645 if ((count = readStream.Read(buf, 0, buf.Length)) > 0)
646 {
647 writeStream.Write(buf, 0, count);
648 }
649 }
650 }
651 finally
652 {
653 if (readStream != null) readStream.Close();
654 if (writeStream != null) writeStream.Close();
655 }
656 }
657 }
658 }
659
660 /// <summary>
661 /// Reads a record stream field into a file.
662 /// </summary>
663 /// <param name="fieldName">Specifies the field of the Record to get.</param>
664 /// <param name="filePath">Specifies the path to the file to contain the stream.</param>
665 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
666 /// of the named fields in the Record.</exception>
667 /// <exception cref="NotSupportedException">Attempt to extract a storage from a database open
668 /// in read-write mode, or from a database without an associated file path</exception>
669 /// <remarks><p>
670 /// This method is capable of directly extracting substorages. To do so, first select both the
671 /// `Name` and `Data` column of the `_Storages` table, then get the stream of the `Data` field.
672 /// However, substorages may only be extracted from a database that is open in read-only mode.
673 /// </p></remarks>
674 public void GetStream(string fieldName, string filePath)
675 {
676 int field = this.FindColumn(fieldName);
677 this.GetStream(field, filePath);
678 }
679
680 /// <summary>
681 /// Gets a record stream field.
682 /// </summary>
683 /// <param name="field">Specifies the field of the Record to get.</param>
684 /// <returns>A Stream that reads the field data.</returns>
685 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
686 /// number of fields in the Record.</exception>
687 /// <remarks><p>
688 /// This method is not capable of reading substorages. To extract a substorage,
689 /// use <see cref="GetStream(int,string)"/>.
690 /// </p><p>
691 /// Win32 MSI API:
692 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordreadstream.asp">MsiRecordReadStream</a>
693 /// </p></remarks>
694 public Stream GetStream(int field)
695 {
696 this.CheckRange(field);
697
698 return this.IsNull(field) ? null : new RecordStream(this, field);
699 }
700
701 /// <summary>
702 /// Gets a record stream field.
703 /// </summary>
704 /// <param name="fieldName">Specifies the field of the Record to get.</param>
705 /// <returns>A Stream that reads the field data.</returns>
706 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
707 /// of the named fields in the Record.</exception>
708 /// <remarks><p>
709 /// This method is not capable of reading substorages. To extract a substorage,
710 /// use <see cref="GetStream(string,string)"/>.
711 /// </p></remarks>
712 public Stream GetStream(string fieldName)
713 {
714 int field = this.FindColumn(fieldName);
715 return this.GetStream(field);
716 }
717
718 /// <summary>
719 /// Sets a record stream field from a file. Stream data cannot be inserted into temporary fields.
720 /// </summary>
721 /// <param name="field">Specifies the field of the Record to set.</param>
722 /// <param name="filePath">Specifies the path to the file containing the stream.</param>
723 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
724 /// number of fields in the Record.</exception>
725 /// <remarks><p>
726 /// The contents of the specified file are read into a stream object. The stream persists if
727 /// the Record is inserted into the Database and the Database is committed.
728 /// </p><p>
729 /// To reset the stream to its beginning you must pass in null for filePath.
730 /// Do not pass an empty string, "", to reset the stream.
731 /// </p><p>
732 /// Setting a stream with this method is more efficient than setting a field to a
733 /// FileStream object.
734 /// </p><p>
735 /// Win32 MSI API:
736 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetstream.asp">MsiRecordsetStream</a>
737 /// </p></remarks>
738 public void SetStream(int field, string filePath)
739 {
740 this.CheckRange(field);
741
742 if (String.IsNullOrEmpty(filePath))
743 {
744 throw new ArgumentNullException("filePath");
745 }
746
747 uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, filePath);
748 if (ret != 0)
749 {
750 throw InstallerException.ExceptionFromReturnCode(ret);
751 }
752 }
753
754 /// <summary>
755 /// Sets a record stream field from a file. Stream data cannot be inserted into temporary fields.
756 /// </summary>
757 /// <param name="fieldName">Specifies the field name of the Record to set.</param>
758 /// <param name="filePath">Specifies the path to the file containing the stream.</param>
759 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
760 /// of the named fields in the Record.</exception>
761 /// <remarks><p>
762 /// The contents of the specified file are read into a stream object. The stream persists if
763 /// the Record is inserted into the Database and the Database is committed.
764 /// To reset the stream to its beginning you must pass in null for filePath.
765 /// Do not pass an empty string, "", to reset the stream.
766 /// </p><p>
767 /// Setting a stream with this method is more efficient than setting a field to a
768 /// FileStream object.
769 /// </p></remarks>
770 public void SetStream(string fieldName, string filePath)
771 {
772 int field = this.FindColumn(fieldName);
773 this.SetStream(field, filePath);
774 }
775
776 /// <summary>
777 /// Sets a record stream field from a Stream object. Stream data cannot be inserted into temporary fields.
778 /// </summary>
779 /// <param name="field">Specifies the field of the Record to set.</param>
780 /// <param name="stream">Specifies the stream data.</param>
781 /// <exception cref="ArgumentOutOfRangeException">The field is less than 0 or greater than the
782 /// number of fields in the Record.</exception>
783 /// <remarks><p>
784 /// The stream persists if the Record is inserted into the Database and the Database is committed.
785 /// </p><p>
786 /// Win32 MSI API:
787 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msirecordsetstream.asp">MsiRecordsetStream</a>
788 /// </p></remarks>
789 public void SetStream(int field, Stream stream)
790 {
791 this.CheckRange(field);
792
793 if (stream == null)
794 {
795 uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, null);
796 if (ret != 0)
797 {
798 throw InstallerException.ExceptionFromReturnCode(ret);
799 }
800 }
801 else
802 {
803 Stream writeStream = null;
804 string tempPath = Path.GetTempFileName();
805 try
806 {
807 writeStream = new FileStream(tempPath, FileMode.Truncate, FileAccess.Write);
808 byte[] buf = new byte[512];
809 int count;
810 while ((count = stream.Read(buf, 0, buf.Length)) > 0)
811 {
812 writeStream.Write(buf, 0, count);
813 }
814 writeStream.Close();
815 writeStream = null;
816
817 uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, tempPath);
818 if (ret != 0)
819 {
820 throw InstallerException.ExceptionFromReturnCode(ret);
821 }
822 }
823 finally
824 {
825 if (writeStream != null) writeStream.Close();
826 if (File.Exists(tempPath))
827 {
828 try
829 {
830 File.Delete(tempPath);
831 }
832 catch (IOException)
833 {
834 if (this.view != null)
835 {
836 this.view.Database.DeleteOnClose(tempPath);
837 }
838 }
839 }
840 }
841 }
842 }
843
844 /// <summary>
845 /// Sets a record stream field from a Stream object. Stream data cannot be inserted into temporary fields.
846 /// </summary>
847 /// <param name="fieldName">Specifies the field name of the Record to set.</param>
848 /// <param name="stream">Specifies the stream data.</param>
849 /// <exception cref="ArgumentOutOfRangeException">The field name does not match any
850 /// of the named fields in the Record.</exception>
851 /// <remarks><p>
852 /// The stream persists if the Record is inserted into the Database and the Database is committed.
853 /// </p></remarks>
854 public void SetStream(string fieldName, Stream stream)
855 {
856 int field = this.FindColumn(fieldName);
857 this.SetStream(field, stream);
858 }
859
860 /// <summary>
861 /// Gets a formatted string representation of the Record.
862 /// </summary>
863 /// <returns>A formatted string representation of the Record.</returns>
864 /// <remarks><p>
865 /// If field 0 of the Record is set to a nonempty string, it is used to format the data in the Record.
866 /// </p><p>
867 /// Win32 MSI API:
868 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
869 /// </p></remarks>
870 /// <seealso cref="FormatString"/>
871 /// <seealso cref="Session.FormatRecord(Record)"/>
872 public override string ToString()
873 {
874 return this.ToString((IFormatProvider) null);
875 }
876
877 /// <summary>
878 /// Gets a formatted string representation of the Record, optionally using a Session to format properties.
879 /// </summary>
880 /// <param name="provider">an optional Session instance that will be used to lookup any
881 /// properties in the Record's format string</param>
882 /// <returns>A formatted string representation of the Record.</returns>
883 /// <remarks><p>
884 /// If field 0 of the Record is set to a nonempty string, it is used to format the data in the Record.
885 /// </p><p>
886 /// Win32 MSI API:
887 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
888 /// </p></remarks>
889 /// <seealso cref="FormatString"/>
890 /// <seealso cref="Session.FormatRecord(Record)"/>
891 public string ToString(IFormatProvider provider)
892 {
893 if (this.IsFormatStringInvalid) // Format string is invalid
894 {
895 // TODO: return all values by default?
896 return String.Empty;
897 }
898
899 InstallerHandle session = provider as InstallerHandle;
900 int sessionHandle = session != null ? (int) session.Handle : 0;
901 StringBuilder buf = new StringBuilder(String.Empty);
902 uint bufSize = 1;
903 uint ret = RemotableNativeMethods.MsiFormatRecord(sessionHandle, (int) this.Handle, buf, ref bufSize);
904 if (ret == (uint) NativeMethods.Error.MORE_DATA)
905 {
906 bufSize++;
907 buf = new StringBuilder((int) bufSize);
908 ret = RemotableNativeMethods.MsiFormatRecord(sessionHandle, (int) this.Handle, buf, ref bufSize);
909 }
910 if (ret != 0)
911 {
912 throw InstallerException.ExceptionFromReturnCode(ret);
913 }
914 return buf.ToString();
915 }
916
917 /// <summary>
918 /// Gets a formatted string representation of the Record.
919 /// </summary>
920 /// <param name="format">String to be used to format the data in the Record,
921 /// instead of the Record's format string.</param>
922 /// <returns>A formatted string representation of the Record.</returns>
923 /// <remarks><p>
924 /// Win32 MSI API:
925 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
926 /// </p></remarks>
927 [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the FormatString " +
928 "property separately before calling the ToString() override that takes no parameters.")]
929 public string ToString(string format)
930 {
931 return this.ToString(format, null);
932 }
933
934 /// <summary>
935 /// Gets a formatted string representation of the Record, optionally using a Session to format properties.
936 /// </summary>
937 /// <param name="format">String to be used to format the data in the Record,
938 /// instead of the Record's format string.</param>
939 /// <param name="provider">an optional Session instance that will be used to lookup any
940 /// properties in the Record's format string</param>
941 /// <returns>A formatted string representation of the Record.</returns>
942 /// <remarks><p>
943 /// Win32 MSI API:
944 /// <a href="http://msdn.microsoft.com/library/en-us/msi/setup/msiformatrecord.asp">MsiFormatRecord</a>
945 /// </p></remarks>
946 /// <seealso cref="FormatString"/>
947 /// <seealso cref="Session.FormatRecord(Record)"/>
948 [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the FormatString " +
949 "property separately before calling the ToString() override that takes just a format provider.")]
950 public string ToString(string format, IFormatProvider provider)
951 {
952 if (format == null)
953 {
954 return this.ToString(provider);
955 }
956 else if (format.Length == 0)
957 {
958 return String.Empty;
959 }
960 else
961 {
962 string savedFormatString = (string) this[0];
963 try
964 {
965 this.FormatString = format;
966 return this.ToString(provider);
967 }
968 finally
969 {
970 this.FormatString = savedFormatString;
971 }
972 }
973 }
974
975 [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
976 private static void ExtractSubStorage(string databaseFile, string storageName, string extractFile)
977 {
978 IStorage storage;
979 NativeMethods.STGM openMode = NativeMethods.STGM.READ | NativeMethods.STGM.SHARE_DENY_WRITE;
980 int hr = NativeMethods.StgOpenStorage(databaseFile, IntPtr.Zero, (uint) openMode, IntPtr.Zero, 0, out storage);
981 if (hr != 0)
982 {
983 Marshal.ThrowExceptionForHR(hr);
984 }
985
986 try
987 {
988 openMode = NativeMethods.STGM.READ | NativeMethods.STGM.SHARE_EXCLUSIVE;
989 IStorage subStorage = storage.OpenStorage(storageName, IntPtr.Zero, (uint) openMode, IntPtr.Zero, 0);
990
991 try
992 {
993 IStorage newStorage;
994 openMode = NativeMethods.STGM.CREATE | NativeMethods.STGM.READWRITE | NativeMethods.STGM.SHARE_EXCLUSIVE;
995 hr = NativeMethods.StgCreateDocfile(extractFile, (uint) openMode, 0, out newStorage);
996 if (hr != 0)
997 {
998 Marshal.ThrowExceptionForHR(hr);
999 }
1000
1001 try
1002 {
1003 subStorage.CopyTo(0, IntPtr.Zero, IntPtr.Zero, newStorage);
1004
1005 newStorage.Commit(0);
1006 }
1007 finally
1008 {
1009 Marshal.ReleaseComObject(newStorage);
1010 }
1011 }
1012 finally
1013 {
1014 Marshal.ReleaseComObject(subStorage);
1015 }
1016 }
1017 finally
1018 {
1019 Marshal.ReleaseComObject(storage);
1020 }
1021 }
1022
1023 private int FindColumn(string fieldName)
1024 {
1025 if (this.view == null)
1026 {
1027 throw new InvalidOperationException();
1028 }
1029 ColumnCollection columns = this.view.Columns;
1030 for (int i = 0; i < columns.Count; i++)
1031 {
1032 if (columns[i].Name == fieldName)
1033 {
1034 return i + 1;
1035 }
1036 }
1037 throw new ArgumentOutOfRangeException("fieldName");
1038 }
1039
1040 private void CheckRange(int field)
1041 {
1042 if (field < 0 || field > this.FieldCount)
1043 {
1044 throw new ArgumentOutOfRangeException("field");
1045 }
1046 }
1047 }
1048}