3 // Copyright (c) Microsoft Corporation. All rights reserved.
6 /*============================================================
8 ** Class: ResourceWriter
10 ** <OWNER>[....]</OWNER>
13 ** Purpose: Default way to write strings to a CLR resource
17 ===========================================================*/
18 namespace System.Resources {
22 using System.Collections;
23 using System.Collections.Generic;
24 #if FEATURE_SERIALIZATION
25 using System.Runtime.Serialization;
26 using System.Runtime.Serialization.Formatters.Binary;
27 #endif // FEATURE_SERIALIZATION
28 using System.Globalization;
29 using System.Runtime.Versioning;
30 using System.Diagnostics.Contracts;
31 using System.Security;
32 using System.Security.Permissions;
34 // Generates a binary .resources file in the system default format
35 // from name and value pairs. Create one with a unique file name,
36 // call AddResource() at least once, then call Generate() to write
37 // the .resources file to disk, then call Close() to close the file.
39 // The resources generally aren't written out in the same order
42 // See the RuntimeResourceSet overview for details on the system
43 // default file format.
45 [System.Runtime.InteropServices.ComVisible(true)]
46 public sealed class ResourceWriter : IResourceWriter
49 private Func<Type, String> typeConverter;
51 // Set this delegate to allow multi-targeting for .resources files.
52 public Func<Type, String> TypeNameConverter
60 typeConverter = value;
64 // For cases where users can't create an instance of the deserialized
65 // type in memory, and need to pass us serialized blobs instead.
66 // LocStudio's managed code parser will do this in some cases.
67 private class PrecannedResource
69 internal String TypeName;
72 internal PrecannedResource(String typeName, byte[] data)
79 private class StreamWrapper
81 internal Stream m_stream;
82 internal bool m_closeAfterWrite;
84 internal StreamWrapper(Stream s, bool closeAfterWrite)
87 m_closeAfterWrite = closeAfterWrite;
91 // An initial size for our internal sorted list, to avoid extra resizes.
92 private const int _ExpectedNumberOfResources = 1000;
93 private const int AverageNameSize = 20 * 2; // chars in little endian Unicode
94 private const int AverageValueSize = 40;
96 private Dictionary<String, Object> _resourceList;
97 internal Stream _output;
98 private Dictionary<String, Object> _caseInsensitiveDups;
99 private Dictionary<String, PrecannedResource> _preserializedData;
100 private const int _DefaultBufferSize = 4096;
102 [ResourceExposure(ResourceScope.Machine)]
103 [ResourceConsumption(ResourceScope.Machine)]
104 public ResourceWriter(String fileName)
107 throw new ArgumentNullException("fileName");
108 Contract.EndContractBlock();
109 _output = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
110 _resourceList = new Dictionary<String, Object>(_ExpectedNumberOfResources, FastResourceComparer.Default);
111 _caseInsensitiveDups = new Dictionary<String, Object>(StringComparer.OrdinalIgnoreCase);
114 public ResourceWriter(Stream stream)
117 throw new ArgumentNullException("stream");
118 if (!stream.CanWrite)
119 throw new ArgumentException(Environment.GetResourceString("Argument_StreamNotWritable"));
120 Contract.EndContractBlock();
122 _resourceList = new Dictionary<String, Object>(_ExpectedNumberOfResources, FastResourceComparer.Default);
123 _caseInsensitiveDups = new Dictionary<String, Object>(StringComparer.OrdinalIgnoreCase);
126 // Adds a string resource to the list of resources to be written to a file.
127 // They aren't written until Generate() is called.
129 public void AddResource(String name, String value)
132 throw new ArgumentNullException("name");
133 Contract.EndContractBlock();
134 if (_resourceList == null)
135 throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ResourceWriterSaved"));
137 // Check for duplicate resources whose names vary only by case.
138 _caseInsensitiveDups.Add(name, null);
139 _resourceList.Add(name, value);
142 // Adds a resource of type Object to the list of resources to be
143 // written to a file. They aren't written until Generate() is called.
145 public void AddResource(String name, Object value)
148 throw new ArgumentNullException("name");
149 Contract.EndContractBlock();
150 if (_resourceList == null)
151 throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ResourceWriterSaved"));
153 // needed for binary compat
154 if (value != null && value is Stream)
156 AddResourceInternal(name, (Stream)value, false);
160 // Check for duplicate resources whose names vary only by case.
161 _caseInsensitiveDups.Add(name, null);
162 _resourceList.Add(name, value);
166 // Adds a resource of type Stream to the list of resources to be
167 // written to a file. They aren't written until Generate() is called.
168 // Doesn't close the Stream when done.
170 public void AddResource(String name, Stream value)
173 throw new ArgumentNullException("name");
174 Contract.EndContractBlock();
175 if (_resourceList == null)
176 throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ResourceWriterSaved"));
178 AddResourceInternal(name, value, false);
181 // Adds a resource of type Stream to the list of resources to be
182 // written to a file. They aren't written until Generate() is called.
183 // closeAfterWrite parameter indicates whether to close the stream when done.
185 public void AddResource(String name, Stream value, bool closeAfterWrite)
188 throw new ArgumentNullException("name");
189 Contract.EndContractBlock();
190 if (_resourceList == null)
191 throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ResourceWriterSaved"));
193 AddResourceInternal(name, value, closeAfterWrite);
196 private void AddResourceInternal(String name, Stream value, bool closeAfterWrite)
200 // Check for duplicate resources whose names vary only by case.
201 _caseInsensitiveDups.Add(name, null);
202 _resourceList.Add(name, value);
206 // make sure the Stream is seekable
208 throw new ArgumentException(Environment.GetResourceString("NotSupported_UnseekableStream"));
210 // Check for duplicate resources whose names vary only by case.
211 _caseInsensitiveDups.Add(name, null);
212 _resourceList.Add(name, new StreamWrapper(value, closeAfterWrite));
216 // Adds a named byte array as a resource to the list of resources to
217 // be written to a file. They aren't written until Generate() is called.
219 public void AddResource(String name, byte[] value)
222 throw new ArgumentNullException("name");
223 Contract.EndContractBlock();
224 if (_resourceList == null)
225 throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ResourceWriterSaved"));
227 // Check for duplicate resources whose names vary only by case.
228 _caseInsensitiveDups.Add(name, null);
229 _resourceList.Add(name, value);
232 public void AddResourceData(String name, String typeName, byte[] serializedData)
235 throw new ArgumentNullException("name");
236 if (typeName == null)
237 throw new ArgumentNullException("typeName");
238 if (serializedData == null)
239 throw new ArgumentNullException("serializedData");
240 Contract.EndContractBlock();
241 if (_resourceList == null)
242 throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ResourceWriterSaved"));
244 // Check for duplicate resources whose names vary only by case.
245 _caseInsensitiveDups.Add(name, null);
246 if (_preserializedData == null)
247 _preserializedData = new Dictionary<String, PrecannedResource>(FastResourceComparer.Default);
249 _preserializedData.Add(name, new PrecannedResource(typeName, serializedData));
253 // Closes the output stream.
259 private void Dispose(bool disposing)
262 if (_resourceList != null) {
265 if (_output != null) {
270 _caseInsensitiveDups = null;
271 // _resourceList is set to null by Generate.
274 public void Dispose()
279 // After calling AddResource, Generate() writes out all resources to the
280 // output stream in the system default format.
281 // If an exception occurs during object serialization or during IO,
282 // the .resources file is closed and deleted, since it is most likely
284 [SecuritySafeCritical] // Asserts permission to create & delete a temp file.
285 public void Generate()
287 if (_resourceList == null)
288 throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ResourceWriterSaved"));
290 BinaryWriter bw = new BinaryWriter(_output, Encoding.UTF8);
291 List<String> typeNames = new List<String>();
293 // Write out the ResourceManager header
294 // Write out magic number
295 bw.Write(ResourceManager.MagicNumber);
297 // Write out ResourceManager header version number
298 bw.Write(ResourceManager.HeaderVersionNumber);
300 MemoryStream resMgrHeaderBlob = new MemoryStream(240);
301 BinaryWriter resMgrHeaderPart = new BinaryWriter(resMgrHeaderBlob);
303 // Write out class name of IResourceReader capable of handling
305 resMgrHeaderPart.Write(MultitargetingHelpers.GetAssemblyQualifiedName(typeof(ResourceReader),typeConverter));
307 // Write out class name of the ResourceSet class best suited to
308 // handling this file.
309 // This needs to be the same even with multi-targeting. It's the
310 // full name -- not the ----sembly qualified name.
311 resMgrHeaderPart.Write(ResourceManager.ResSetTypeName);
312 resMgrHeaderPart.Flush();
314 // Write number of bytes to skip over to get past ResMgr header
315 bw.Write((int)resMgrHeaderBlob.Length);
317 // Write the rest of the ResMgr header
318 bw.Write(resMgrHeaderBlob.GetBuffer(), 0, (int)resMgrHeaderBlob.Length);
319 // End ResourceManager header
322 // Write out the RuntimeResourceSet header
324 bw.Write(RuntimeResourceSet.Version);
325 #if RESOURCE_FILE_FORMAT_DEBUG
326 // Write out a tag so we know whether to enable or disable
327 // debugging support when reading the file.
328 bw.Write("***DEBUG***");
331 // number of resources
332 int numResources = _resourceList.Count;
333 if (_preserializedData != null)
334 numResources += _preserializedData.Count;
335 bw.Write(numResources);
337 // Store values in temporary streams to write at end of file.
338 int[] nameHashes = new int[numResources];
339 int[] namePositions = new int[numResources];
340 int curNameNumber = 0;
341 MemoryStream nameSection = new MemoryStream(numResources * AverageNameSize);
342 BinaryWriter names = new BinaryWriter(nameSection, Encoding.Unicode);
344 // The data section can be very large, and worse yet, we can grow the byte[] used
345 // for the data section repeatedly. When using large resources like ~100 images,
346 // this can lead to both a fragmented large object heap as well as allocating about
347 // 2-3x of our storage needs in extra overhead. Using a temp file can avoid this.
348 // Assert permission to get a temp file name, which requires two permissions.
349 // Additionally, we may be running under an account that doesn't have permission to
350 // write to the temp directory (enforced via a Windows ACL). Fall back to a MemoryStream.
351 Stream dataSection = null; // Either a FileStream or a MemoryStream
352 String tempFile = null;
354 PermissionSet permSet = new PermissionSet(PermissionState.None);
355 permSet.AddPermission(new EnvironmentPermission(PermissionState.Unrestricted));
356 permSet.AddPermission(new FileIOPermission(PermissionState.Unrestricted));
362 tempFile = Path.GetTempFileName();
363 File.SetAttributes(tempFile, FileAttributes.Temporary | FileAttributes.NotContentIndexed);
364 // Explicitly opening with FileOptions.DeleteOnClose to avoid complicated File.Delete
365 // (safe from ----s w/ antivirus software, etc)
366 dataSection = new FileStream(tempFile, FileMode.Open, FileAccess.ReadWrite, FileShare.Read,
367 4096, FileOptions.DeleteOnClose | FileOptions.SequentialScan);
369 catch (UnauthorizedAccessException) {
370 // In case we're running under an account that can't access a temp directory.
371 dataSection = new MemoryStream();
373 catch (IOException) {
374 // In case Path.GetTempFileName fails because no unique file names are available
375 dataSection = new MemoryStream();
379 PermissionSet.RevertAssert();
384 BinaryWriter data = new BinaryWriter(dataSection, Encoding.UTF8);
385 #if FEATURE_SERIALIZATION
386 IFormatter objFormatter = new BinaryFormatter(null, new StreamingContext(StreamingContextStates.File | StreamingContextStates.Persistence));
387 #endif // FEATURE_SERIALIZATION
389 #if RESOURCE_FILE_FORMAT_DEBUG
390 // Write NAMES right before the names section.
391 names.Write(new byte[] { (byte) 'N', (byte) 'A', (byte) 'M', (byte) 'E', (byte) 'S', (byte) '-', (byte) '-', (byte) '>'});
393 // Write DATA at the end of the name table section.
394 data.Write(new byte[] { (byte) 'D', (byte) 'A', (byte) 'T', (byte) 'A', (byte) '-', (byte) '-', (byte)'-', (byte)'>'});
397 // We've stored our resources internally in a Hashtable, which
398 // makes no guarantees about the ordering while enumerating.
399 // While we do our own sorting of the resource names based on their
400 // hash values, that's only sorting the nameHashes and namePositions
401 // arrays. That's all that is strictly required for correctness,
402 // but for ease of generating a patch in the future that
403 // modifies just .resources files, we should re-sort them.
405 SortedList sortedResources = new SortedList(_resourceList, FastResourceComparer.Default);
406 if (_preserializedData != null) {
407 foreach (KeyValuePair<String, PrecannedResource> entry in _preserializedData)
408 sortedResources.Add(entry.Key, entry.Value);
411 IDictionaryEnumerator items = sortedResources.GetEnumerator();
412 // Write resource name and position to the file, and the value
413 // to our temporary buffer. Save Type as well.
414 while (items.MoveNext()) {
415 nameHashes[curNameNumber] = FastResourceComparer.HashFunction((String)items.Key);
416 namePositions[curNameNumber++] = (int)names.Seek(0, SeekOrigin.Current);
417 names.Write((String)items.Key); // key
418 names.Write((int)data.Seek(0, SeekOrigin.Current)); // virtual offset of value.
419 #if RESOURCE_FILE_FORMAT_DEBUG
420 names.Write((byte) '*');
422 Object value = items.Value;
423 ResourceTypeCode typeCode = FindTypeCode(value, typeNames);
425 // Write out type code
426 Write7BitEncodedInt(data, (int)typeCode);
429 PrecannedResource userProvidedResource = value as PrecannedResource;
430 if (userProvidedResource != null) {
431 data.Write(userProvidedResource.Data);
434 #if FEATURE_SERIALIZATION
435 WriteValue(typeCode, value, data, objFormatter);
437 WriteValue(typeCode, value, data);
441 #if RESOURCE_FILE_FORMAT_DEBUG
442 data.Write(new byte[] { (byte) 'S', (byte) 'T', (byte) 'O', (byte) 'P'});
446 // At this point, the ResourceManager header has been written.
447 // Finish RuntimeResourceSet header
448 // Write size & contents of class table
449 bw.Write(typeNames.Count);
450 for (int i = 0; i < typeNames.Count; i++)
451 bw.Write(typeNames[i]);
453 // Write out the name-related items for lookup.
454 // Note that the hash array and the namePositions array must
455 // be sorted in parallel.
456 Array.Sort(nameHashes, namePositions);
458 // Prepare to write sorted name hashes (alignment fixup)
459 // Note: For 64-bit machines, these MUST be aligned on 8 byte
460 // boundaries! Pointers on IA64 must be aligned! And we'll
461 // run faster on X86 machines too.
463 int alignBytes = ((int)bw.BaseStream.Position) & 7;
464 if (alignBytes > 0) {
465 for (int i = 0; i < 8 - alignBytes; i++)
466 bw.Write("PAD"[i % 3]);
469 // Write out sorted name hashes.
471 Contract.Assert((bw.BaseStream.Position & 7) == 0, "ResourceWriter: Name hashes array won't be 8 byte aligned! Ack!");
472 #if RESOURCE_FILE_FORMAT_DEBUG
473 bw.Write(new byte[] { (byte) 'H', (byte) 'A', (byte) 'S', (byte) 'H', (byte) 'E', (byte) 'S', (byte) '-', (byte) '>'} );
475 foreach (int hash in nameHashes)
477 #if RESOURCE_FILE_FORMAT_DEBUG
478 Console.Write("Name hashes: ");
479 foreach(int hash in nameHashes)
480 Console.Write(hash.ToString("x")+" ");
484 // Write relative positions of all the names in the file.
485 // Note: this data is 4 byte aligned, occuring immediately
486 // after the 8 byte aligned name hashes (whose length may
487 // potentially be odd).
488 Contract.Assert((bw.BaseStream.Position & 3) == 0, "ResourceWriter: Name positions array won't be 4 byte aligned! Ack!");
489 #if RESOURCE_FILE_FORMAT_DEBUG
490 bw.Write(new byte[] { (byte) 'P', (byte) 'O', (byte) 'S', (byte) '-', (byte) '-', (byte) '-', (byte) '-', (byte) '>' } );
492 foreach (int pos in namePositions)
494 #if RESOURCE_FILE_FORMAT_DEBUG
495 Console.Write("Name positions: ");
496 foreach(int pos in namePositions)
497 Console.Write(pos.ToString("x")+" ");
501 // Flush all BinaryWriters to their underlying streams.
506 // Write offset to data section
507 int startOfDataSection = (int)(bw.Seek(0, SeekOrigin.Current) + nameSection.Length);
508 startOfDataSection += 4; // We're writing an int to store this data, adding more bytes to the header
509 BCLDebug.Log("RESMGRFILEFORMAT", "Generate: start of DataSection: 0x" + startOfDataSection.ToString("x", CultureInfo.InvariantCulture) + " nameSection length: " + nameSection.Length);
510 bw.Write(startOfDataSection);
512 // Write name section.
513 bw.Write(nameSection.GetBuffer(), 0, (int)nameSection.Length);
516 // Write data section.
517 Contract.Assert(startOfDataSection == bw.Seek(0, SeekOrigin.Current), "ResourceWriter::Generate - start of data section is wrong!");
518 dataSection.Position = 0;
519 dataSection.CopyTo(bw.BaseStream);
521 } // using(dataSection) <--- Closes dataSection, which was opened w/ FileOptions.DeleteOnClose
524 // Indicate we've called Generate
525 _resourceList = null;
528 // Finds the ResourceTypeCode for a type, or adds this type to the
530 private ResourceTypeCode FindTypeCode(Object value, List<String> types)
533 return ResourceTypeCode.Null;
535 Type type = value.GetType();
536 if (type == typeof(String))
537 return ResourceTypeCode.String;
538 else if (type == typeof(Int32))
539 return ResourceTypeCode.Int32;
540 else if (type == typeof(Boolean))
541 return ResourceTypeCode.Boolean;
542 else if (type == typeof(Char))
543 return ResourceTypeCode.Char;
544 else if (type == typeof(Byte))
545 return ResourceTypeCode.Byte;
546 else if (type == typeof(SByte))
547 return ResourceTypeCode.SByte;
548 else if (type == typeof(Int16))
549 return ResourceTypeCode.Int16;
550 else if (type == typeof(Int64))
551 return ResourceTypeCode.Int64;
552 else if (type == typeof(UInt16))
553 return ResourceTypeCode.UInt16;
554 else if (type == typeof(UInt32))
555 return ResourceTypeCode.UInt32;
556 else if (type == typeof(UInt64))
557 return ResourceTypeCode.UInt64;
558 else if (type == typeof(Single))
559 return ResourceTypeCode.Single;
560 else if (type == typeof(Double))
561 return ResourceTypeCode.Double;
562 else if (type == typeof (Decimal))
563 return ResourceTypeCode.Decimal;
564 else if (type == typeof(DateTime))
565 return ResourceTypeCode.DateTime;
566 else if (type == typeof(TimeSpan))
567 return ResourceTypeCode.TimeSpan;
568 else if (type == typeof(byte[]))
569 return ResourceTypeCode.ByteArray;
570 else if (type == typeof(StreamWrapper))
571 return ResourceTypeCode.Stream;
574 // This is a user type, or a precanned resource. Find type
575 // table index. If not there, add new element.
577 if (type == typeof(PrecannedResource)) {
578 typeName = ((PrecannedResource)value).TypeName;
579 if (typeName.StartsWith("ResourceTypeCode.", StringComparison.Ordinal)) {
580 typeName = typeName.Substring(17); // Remove through '.'
581 ResourceTypeCode typeCode = (ResourceTypeCode)Enum.Parse(typeof(ResourceTypeCode), typeName);
587 typeName = MultitargetingHelpers.GetAssemblyQualifiedName(type, typeConverter);
590 int typeIndex = types.IndexOf(typeName);
591 if (typeIndex == -1) {
592 typeIndex = types.Count;
596 return (ResourceTypeCode)(typeIndex + ResourceTypeCode.StartOfUserTypes);
599 #if FEATURE_SERIALIZATION
600 private void WriteValue(ResourceTypeCode typeCode, Object value, BinaryWriter writer, IFormatter objFormatter)
602 private void WriteValue(ResourceTypeCode typeCode, Object value, BinaryWriter writer)
603 #endif // FEATURE_SERIALIZATION
605 Contract.Requires(writer != null);
608 case ResourceTypeCode.Null:
611 case ResourceTypeCode.String:
612 writer.Write((String) value);
615 case ResourceTypeCode.Boolean:
616 writer.Write((bool) value);
619 case ResourceTypeCode.Char:
620 writer.Write((UInt16) (char) value);
623 case ResourceTypeCode.Byte:
624 writer.Write((byte) value);
627 case ResourceTypeCode.SByte:
628 writer.Write((sbyte) value);
631 case ResourceTypeCode.Int16:
632 writer.Write((Int16) value);
635 case ResourceTypeCode.UInt16:
636 writer.Write((UInt16) value);
639 case ResourceTypeCode.Int32:
640 writer.Write((Int32) value);
643 case ResourceTypeCode.UInt32:
644 writer.Write((UInt32) value);
647 case ResourceTypeCode.Int64:
648 writer.Write((Int64) value);
651 case ResourceTypeCode.UInt64:
652 writer.Write((UInt64) value);
655 case ResourceTypeCode.Single:
656 writer.Write((Single) value);
659 case ResourceTypeCode.Double:
660 writer.Write((Double) value);
663 case ResourceTypeCode.Decimal:
664 writer.Write((Decimal) value);
667 case ResourceTypeCode.DateTime:
668 // Use DateTime's ToBinary & FromBinary.
669 Int64 data = ((DateTime) value).ToBinary();
673 case ResourceTypeCode.TimeSpan:
674 writer.Write(((TimeSpan) value).Ticks);
678 case ResourceTypeCode.ByteArray:
680 byte[] bytes = (byte[]) value;
681 writer.Write(bytes.Length);
682 writer.Write(bytes, 0, bytes.Length);
686 case ResourceTypeCode.Stream:
688 StreamWrapper sw = (StreamWrapper)value;
689 if (sw.m_stream.GetType() == typeof(MemoryStream))
691 MemoryStream ms = (MemoryStream)sw.m_stream;
692 if (ms.Length > Int32.MaxValue)
693 throw new ArgumentException(Environment.GetResourceString("ArgumentOutOfRange_StreamLength"));
695 ms.InternalGetOriginAndLength(out offset, out len);
696 byte[] bytes = ms.InternalGetBuffer();
698 writer.Write(bytes, offset, len);
702 Stream s = sw.m_stream;
703 // we've already verified that the Stream is seekable
704 if (s.Length > Int32.MaxValue)
705 throw new ArgumentException(Environment.GetResourceString("ArgumentOutOfRange_StreamLength"));
708 writer.Write((int)s.Length);
709 byte[] buffer = new byte[_DefaultBufferSize];
711 while ((read = s.Read(buffer, 0, buffer.Length)) != 0)
713 writer.Write(buffer, 0, read);
715 if (sw.m_closeAfterWrite)
724 Contract.Assert(typeCode >= ResourceTypeCode.StartOfUserTypes, String.Format(CultureInfo.InvariantCulture, "ResourceReader: Unsupported ResourceTypeCode in .resources file! {0}", typeCode));
725 #if FEATURE_SERIALIZATION
726 objFormatter.Serialize(writer.BaseStream, value);
729 throw new NotSupportedException(Environment.GetResourceString("NotSupported_ResourceObjectSerialization"));
730 #endif // FEATURE_SERIALIZATION
734 private static void Write7BitEncodedInt(BinaryWriter store, int value) {
735 Contract.Requires(store != null);
736 // Write out an int 7 bits at a time. The high bit of the byte,
737 // when on, tells reader to continue reading more bytes.
738 uint v = (uint) value; // support negative numbers
740 store.Write((byte) (v | 0x80));
743 store.Write((byte)v);