|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace System.Resources
#if RESOURCES_EXTENSIONS
.Extensions
#endif
{
// Generates a binary .resources file in the system default format
// from name and value pairs. Create one with a unique file name,
// call AddResource() at least once, then call Generate() to write
// the .resources file to disk, then call Dispose() to close the file.
//
// The resources generally aren't written out in the same order
// they were added.
//
// See the RuntimeResourceSet overview for details on the system
// default file format.
//
public sealed partial class
#if RESOURCES_EXTENSIONS
PreserializedResourceWriter
#else
ResourceWriter
#endif
: IResourceWriter
{
// An initial size for our internal sorted list, to avoid extra resizes.
private const int AverageNameSize = 20 * 2; // chars in little endian Unicode
internal const string ResourceReaderFullyQualifiedName = "System.Resources.ResourceReader, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
private const string ResSetTypeName = "System.Resources.RuntimeResourceSet";
private const int ResSetVersion = 2;
private SortedDictionary<string, object?>? _resourceList;
private Stream _output;
private Dictionary<string, object?> _caseInsensitiveDups;
private Dictionary<string, PrecannedResource>? _preserializedData;
public
#if RESOURCES_EXTENSIONS
PreserializedResourceWriter(string fileName)
#else
ResourceWriter(string fileName)
#endif
{
if (fileName is null)
{
throw new ArgumentNullException(nameof(fileName));
}
_output = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
_resourceList = new SortedDictionary<string, object?>(FastResourceComparer.Default);
_caseInsensitiveDups = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
}
public
#if RESOURCES_EXTENSIONS
PreserializedResourceWriter(Stream stream)
#else
ResourceWriter(Stream stream)
#endif
{
if (stream is null)
{
throw new ArgumentNullException(nameof(stream));
}
if (!stream.CanWrite)
{
throw new ArgumentException(SR.Argument_StreamNotWritable);
}
_output = stream;
_resourceList = new SortedDictionary<string, object?>(FastResourceComparer.Default);
_caseInsensitiveDups = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
}
// Adds a string resource to the list of resources to be written to a file.
// They aren't written until Generate() is called.
//
public void AddResource(string name, string? value)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
if (_resourceList == null)
{
throw new InvalidOperationException(SR.InvalidOperation_ResourceWriterSaved);
}
// Check for duplicate resources whose names vary only by case.
_caseInsensitiveDups.Add(name, null);
_resourceList.Add(name, value);
}
// Adds a resource of type Object to the list of resources to be
// written to a file. They aren't written until Generate() is called.
//
public void AddResource(string name, object? value)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
if (_resourceList == null)
{
throw new InvalidOperationException(SR.InvalidOperation_ResourceWriterSaved);
}
// needed for binary compat
if (value != null && value is Stream)
{
AddResourceInternal(name, (Stream)value, false);
}
else
{
// Check for duplicate resources whose names vary only by case.
_caseInsensitiveDups.Add(name, null);
_resourceList.Add(name, value);
}
}
// Adds a resource of type Stream to the list of resources to be
// written to a file. They aren't written until Generate() is called.
// closeAfterWrite parameter indicates whether to close the stream when done.
//
public void AddResource(string name, Stream? value, bool closeAfterWrite = false)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
if (_resourceList == null)
{
throw new InvalidOperationException(SR.InvalidOperation_ResourceWriterSaved);
}
AddResourceInternal(name, value, closeAfterWrite);
}
private void AddResourceInternal(string name, Stream? value, bool closeAfterWrite)
{
Debug.Assert(_resourceList != null);
if (value == null)
{
// Check for duplicate resources whose names vary only by case.
_caseInsensitiveDups.Add(name, null);
_resourceList.Add(name, value);
}
else
{
// make sure the Stream is seekable
if (!value.CanSeek)
throw new ArgumentException(SR.NotSupported_UnseekableStream);
// Check for duplicate resources whose names vary only by case.
_caseInsensitiveDups.Add(name, null);
_resourceList.Add(name, new StreamWrapper(value, closeAfterWrite));
}
}
// Adds a named byte array as a resource to the list of resources to
// be written to a file. They aren't written until Generate() is called.
//
public void AddResource(string name, byte[]? value)
{
if (name is null)
{
throw new ArgumentNullException(nameof(name));
}
if (_resourceList == null)
{
throw new InvalidOperationException(SR.InvalidOperation_ResourceWriterSaved);
}
// Check for duplicate resources whose names vary only by case.
_caseInsensitiveDups.Add(name, null);
_resourceList.Add(name, value);
}
private void AddResourceData(string name, string typeName, object data)
{
if (_resourceList == null)
throw new InvalidOperationException(SR.InvalidOperation_ResourceWriterSaved);
// Check for duplicate resources whose names vary only by case.
_caseInsensitiveDups.Add(name, null);
_preserializedData ??= new Dictionary<string, PrecannedResource>(FastResourceComparer.Default);
_preserializedData.Add(name, new PrecannedResource(typeName, data));
}
// For cases where users can't create an instance of the deserialized
// type in memory, and need to pass us serialized blobs instead.
// LocStudio's managed code parser will do this in some cases.
private sealed class PrecannedResource
{
internal readonly string TypeName;
internal readonly object Data;
internal PrecannedResource(string typeName, object data)
{
TypeName = typeName;
Data = data;
}
}
private sealed class StreamWrapper
{
internal readonly Stream Stream;
internal readonly bool CloseAfterWrite;
internal StreamWrapper(Stream s, bool closeAfterWrite)
{
Stream = s;
CloseAfterWrite = closeAfterWrite;
}
}
public void Close()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (disposing)
{
if (_resourceList != null)
{
Generate();
}
_output?.Dispose();
}
_output = null!;
_caseInsensitiveDups = null!;
}
public void Dispose()
{
Dispose(true);
}
// After calling AddResource, Generate() writes out all resources to the
// output stream in the system default format.
// If an exception occurs during object serialization or during IO,
// the .resources file is closed and deleted, since it is most likely
// invalid.
public void Generate()
{
if (_resourceList == null)
throw new InvalidOperationException(SR.InvalidOperation_ResourceWriterSaved);
BinaryWriter bw = new BinaryWriter(_output, Encoding.UTF8);
List<string> typeNames = new List<string>();
// Write out the ResourceManager header
// Write out magic number
bw.Write(ResourceManager.MagicNumber);
// Write out ResourceManager header version number
bw.Write(ResourceManager.HeaderVersionNumber);
MemoryStream resMgrHeaderBlob = new MemoryStream(240);
BinaryWriter resMgrHeaderPart = new BinaryWriter(resMgrHeaderBlob);
// Write out class name of IResourceReader capable of handling
// this file.
resMgrHeaderPart.Write(ResourceReaderTypeName);
// Write out class name of the ResourceSet class best suited to
// handling this file.
// This needs to be the same even with multi-targeting. It's the
// full name -- not the assembly qualified name.
resMgrHeaderPart.Write(ResourceSetTypeName);
resMgrHeaderPart.Flush();
// Write number of bytes to skip over to get past ResMgr header
bw.Write((int)resMgrHeaderBlob.Length);
// Write the rest of the ResMgr header
Debug.Assert(resMgrHeaderBlob.Length > 0, "ResourceWriter: Expected non empty header");
resMgrHeaderBlob.Seek(0, SeekOrigin.Begin);
resMgrHeaderBlob.CopyTo(bw.BaseStream, (int)resMgrHeaderBlob.Length);
// End ResourceManager header
// Write out the RuntimeResourceSet header
// Version number
bw.Write(ResSetVersion);
// number of resources
int numResources = _resourceList.Count;
if (_preserializedData != null)
numResources += _preserializedData.Count;
bw.Write(numResources);
// Store values in temporary streams to write at end of file.
int[] nameHashes = new int[numResources];
int[] namePositions = new int[numResources];
int curNameNumber = 0;
MemoryStream nameSection = new MemoryStream(numResources * AverageNameSize);
BinaryWriter names = new BinaryWriter(nameSection, Encoding.Unicode);
Stream dataSection = new MemoryStream(); // Either a FileStream or a MemoryStream
using (dataSection)
{
BinaryWriter data = new BinaryWriter(dataSection, Encoding.UTF8);
if (_preserializedData != null)
{
foreach (KeyValuePair<string, PrecannedResource> entry in _preserializedData)
{
_resourceList.Add(entry.Key, entry.Value);
}
}
// Write resource name and position to the file, and the value
// to our temporary buffer. Save Type as well.
foreach (var item in _resourceList)
{
nameHashes[curNameNumber] = FastResourceComparer.HashFunction(item.Key);
namePositions[curNameNumber++] = (int)names.Seek(0, SeekOrigin.Current);
names.Write(item.Key); // key
names.Write((int)data.Seek(0, SeekOrigin.Current)); // virtual offset of value.
object? value = item.Value;
ResourceTypeCode typeCode = FindTypeCode(value, typeNames);
// Write out type code
data.Write7BitEncodedInt((int)typeCode);
var userProvidedResource = value as PrecannedResource;
if (userProvidedResource != null)
{
WriteData(data, userProvidedResource.Data);
}
else
{
WriteValue(typeCode, value, data);
}
}
// At this point, the ResourceManager header has been written.
// Finish RuntimeResourceSet header
// The reader expects a list of user defined type names
// following the size of the list, write 0 for this
// writer implementation
bw.Write(typeNames.Count);
foreach (var typeName in typeNames)
{
bw.Write(typeName);
}
// Write out the name-related items for lookup.
// Note that the hash array and the namePositions array must
// be sorted in parallel.
Array.Sort(nameHashes, namePositions);
// Prepare to write sorted name hashes (alignment fixup)
// Note: For 64-bit machines, these MUST be aligned on 8 byte
// boundaries! Pointers on IA64 must be aligned! And we'll
// run faster on X86 machines too.
bw.Flush();
int alignBytes = ((int)bw.BaseStream.Position) & 7;
if (alignBytes > 0)
{
for (int i = 0; i < 8 - alignBytes; i++)
bw.Write("PAD"[i % 3]);
}
// Write out sorted name hashes.
// Align to 8 bytes.
Debug.Assert((bw.BaseStream.Position & 7) == 0, "ResourceWriter: Name hashes array won't be 8 byte aligned! Ack!");
foreach (int hash in nameHashes)
{
bw.Write(hash);
}
// Write relative positions of all the names in the file.
// Note: this data is 4 byte aligned, occurring immediately
// after the 8 byte aligned name hashes (whose length may
// potentially be odd).
Debug.Assert((bw.BaseStream.Position & 3) == 0, "ResourceWriter: Name positions array won't be 4 byte aligned! Ack!");
foreach (int pos in namePositions)
{
bw.Write(pos);
}
// Flush all BinaryWriters to their underlying streams.
bw.Flush();
names.Flush();
data.Flush();
// Write offset to data section
int startOfDataSection = (int)(bw.Seek(0, SeekOrigin.Current) + nameSection.Length);
startOfDataSection += 4; // We're writing an int to store this data, adding more bytes to the header
bw.Write(startOfDataSection);
// Write name section.
if (nameSection.Length > 0)
{
nameSection.Seek(0, SeekOrigin.Begin);
nameSection.CopyTo(bw.BaseStream, (int)nameSection.Length);
}
names.Dispose();
// Write data section.
Debug.Assert(startOfDataSection == bw.Seek(0, SeekOrigin.Current), "ResourceWriter::Generate - start of data section is wrong!");
dataSection.Position = 0;
dataSection.CopyTo(bw.BaseStream);
data.Dispose();
} // using(dataSection) <--- Closes dataSection, which was opened w/ FileOptions.DeleteOnClose
bw.Flush();
// Indicate we've called Generate
_resourceList = null;
}
// Finds the ResourceTypeCode for a type, or adds this type to the
// types list.
private static ResourceTypeCode FindTypeCode(object? value, List<string> types)
{
if (value == null)
return ResourceTypeCode.Null;
Type type = value.GetType();
if (type == typeof(string))
return ResourceTypeCode.String;
else if (type == typeof(int))
return ResourceTypeCode.Int32;
else if (type == typeof(bool))
return ResourceTypeCode.Boolean;
else if (type == typeof(char))
return ResourceTypeCode.Char;
else if (type == typeof(byte))
return ResourceTypeCode.Byte;
else if (type == typeof(sbyte))
return ResourceTypeCode.SByte;
else if (type == typeof(short))
return ResourceTypeCode.Int16;
else if (type == typeof(long))
return ResourceTypeCode.Int64;
else if (type == typeof(ushort))
return ResourceTypeCode.UInt16;
else if (type == typeof(uint))
return ResourceTypeCode.UInt32;
else if (type == typeof(ulong))
return ResourceTypeCode.UInt64;
else if (type == typeof(float))
return ResourceTypeCode.Single;
else if (type == typeof(double))
return ResourceTypeCode.Double;
else if (type == typeof(decimal))
return ResourceTypeCode.Decimal;
else if (type == typeof(DateTime))
return ResourceTypeCode.DateTime;
else if (type == typeof(TimeSpan))
return ResourceTypeCode.TimeSpan;
else if (type == typeof(byte[]))
return ResourceTypeCode.ByteArray;
else if (type == typeof(StreamWrapper))
return ResourceTypeCode.Stream;
// This is a user type, or a precanned resource. Find type
// table index. If not there, add new element.
string typeName;
if (type == typeof(PrecannedResource))
{
typeName = ((PrecannedResource)value).TypeName;
if (typeName.StartsWith("ResourceTypeCode.", StringComparison.Ordinal))
{
typeName = typeName.Substring(17); // Remove through '.'
#if NET
ResourceTypeCode typeCode = Enum.Parse<ResourceTypeCode>(typeName);
#else
ResourceTypeCode typeCode = (ResourceTypeCode)Enum.Parse(typeof(ResourceTypeCode), typeName);
#endif
return typeCode;
}
}
else
{
// not a preserialized resource
throw new PlatformNotSupportedException(SR.NotSupported_BinarySerializedResources);
}
int typeIndex = types.IndexOf(typeName);
if (typeIndex == -1)
{
typeIndex = types.Count;
types.Add(typeName);
}
return (ResourceTypeCode)(typeIndex + ResourceTypeCode.StartOfUserTypes);
}
private static void WriteValue(ResourceTypeCode typeCode, object? value, BinaryWriter writer)
{
Debug.Assert(writer != null);
switch (typeCode)
{
case ResourceTypeCode.Null:
break;
case ResourceTypeCode.String:
writer.Write((string)value!);
break;
case ResourceTypeCode.Boolean:
writer.Write((bool)value!);
break;
case ResourceTypeCode.Char:
writer.Write((ushort)(char)value!);
break;
case ResourceTypeCode.Byte:
writer.Write((byte)value!);
break;
case ResourceTypeCode.SByte:
writer.Write((sbyte)value!);
break;
case ResourceTypeCode.Int16:
writer.Write((short)value!);
break;
case ResourceTypeCode.UInt16:
writer.Write((ushort)value!);
break;
case ResourceTypeCode.Int32:
writer.Write((int)value!);
break;
case ResourceTypeCode.UInt32:
writer.Write((uint)value!);
break;
case ResourceTypeCode.Int64:
writer.Write((long)value!);
break;
case ResourceTypeCode.UInt64:
writer.Write((ulong)value!);
break;
case ResourceTypeCode.Single:
writer.Write((float)value!);
break;
case ResourceTypeCode.Double:
writer.Write((double)value!);
break;
case ResourceTypeCode.Decimal:
writer.Write((decimal)value!);
break;
case ResourceTypeCode.DateTime:
// Use DateTime's ToBinary & FromBinary.
long data = ((DateTime)value!).ToBinary();
writer.Write(data);
break;
case ResourceTypeCode.TimeSpan:
writer.Write(((TimeSpan)value!).Ticks);
break;
// Special Types
case ResourceTypeCode.ByteArray:
{
byte[] bytes = (byte[])value!;
writer.Write(bytes.Length);
writer.Write(bytes, 0, bytes.Length);
break;
}
case ResourceTypeCode.Stream:
{
StreamWrapper sw = (StreamWrapper)value!;
if (sw.Stream.GetType() == typeof(MemoryStream))
{
MemoryStream ms = (MemoryStream)sw.Stream;
if (ms.Length > int.MaxValue)
throw new ArgumentException(SR.ArgumentOutOfRange_StreamLength);
byte[] arr = ms.ToArray();
writer.Write(arr.Length);
writer.Write(arr, 0, arr.Length);
}
else
{
Stream s = sw.Stream;
// we've already verified that the Stream is seekable
if (s.Length > int.MaxValue)
throw new ArgumentException(SR.ArgumentOutOfRange_StreamLength);
s.Position = 0;
writer.Write((int)s.Length);
byte[] buffer = new byte[4096];
int read;
while ((read = s.Read(buffer, 0, buffer.Length)) != 0)
{
writer.Write(buffer, 0, read);
}
if (sw.CloseAfterWrite)
{
s.Close();
}
}
break;
}
default:
Debug.Assert(typeCode >= ResourceTypeCode.StartOfUserTypes, $"ResourceReader: Unsupported ResourceTypeCode in .resources file! {typeCode}");
throw new PlatformNotSupportedException(SR.NotSupported_BinarySerializedResources);
}
}
}
}
|