|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Reflection;
using System.Text;
using System.Xml;
namespace System.Resources;
/// <summary>
/// ResX resource writer. See the text in "ResourceSchema" for more information.
/// </summary>
public class ResXResourceWriter : IResourceWriter
{
internal const string TypeStr = "type";
internal const string NameStr = "name";
internal const string DataStr = "data";
internal const string MetadataStr = "metadata";
internal const string MimeTypeStr = "mimetype";
internal const string ValueStr = "value";
internal const string ResHeaderStr = "resheader";
internal const string VersionStr = "version";
internal const string ResMimeTypeStr = "resmimetype";
internal const string ReaderStr = "reader";
internal const string WriterStr = "writer";
internal const string CommentStr = "comment";
internal const string AssemblyStr = "assembly";
internal const string AliasStr = "alias";
private Dictionary<string, string?>? _cachedAliases;
public static readonly string BinSerializedObjectMimeType = "application/x-microsoft.net.object.binary.base64";
public static readonly string SoapSerializedObjectMimeType = "application/x-microsoft.net.object.soap.base64";
public static readonly string DefaultSerializedObjectMimeType = BinSerializedObjectMimeType;
public static readonly string ByteArraySerializedObjectMimeType = "application/x-microsoft.net.object.bytearray.base64";
public static readonly string ResMimeType = "text/microsoft-resx";
public static readonly string Version = "2.0";
public static readonly string ResourceSchema = """
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space"/>
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
""";
private readonly string? _fileName;
private Stream? _stream;
private TextWriter? _textWriter;
private XmlTextWriter? _xmlTextWriter;
private bool _hasBeenSaved;
private readonly Func<Type?, string>? _typeNameConverter; // no public property to be consistent with ResXDataNode class.
/// <summary>
/// A path that, if prepended to the relative file path specified in a <see cref="ResXFileRef"/> object,
/// yields an absolute path to an XML resource file.
/// </summary>
public string? BasePath { get; set; }
/// <summary>
/// Creates a new ResXResourceWriter that will write to the specified file.
/// </summary>
public ResXResourceWriter(string fileName) => _fileName = fileName;
public ResXResourceWriter(string fileName, Func<Type?, string> typeNameConverter)
{
_fileName = fileName;
_typeNameConverter = typeNameConverter;
}
/// <summary>
/// Creates a new ResXResourceWriter that will write to the specified stream.
/// </summary>
public ResXResourceWriter(Stream stream) => _stream = stream;
public ResXResourceWriter(Stream stream, Func<Type?, string> typeNameConverter)
{
_stream = stream;
_typeNameConverter = typeNameConverter;
}
/// <summary>
/// Initializes a new instance of the <see cref="ResXResourceWriter"/> class that writes to the specified
/// <see cref="TextWriter"/> object.
/// </summary>
public ResXResourceWriter(TextWriter textWriter) => _textWriter = textWriter;
public ResXResourceWriter(TextWriter textWriter, Func<Type?, string> typeNameConverter)
{
_textWriter = textWriter;
_typeNameConverter = typeNameConverter;
}
~ResXResourceWriter()
{
Dispose(disposing: false);
}
[MemberNotNull(nameof(_xmlTextWriter))]
private void InitializeWriter()
{
if (_xmlTextWriter is not null)
{
return;
}
bool writeHeaderRequired = false;
if (_textWriter is not null)
{
_textWriter.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
writeHeaderRequired = true;
_xmlTextWriter = new XmlTextWriter(_textWriter);
}
else if (_stream is not null)
{
_xmlTextWriter = new XmlTextWriter(_stream, Encoding.UTF8);
}
else
{
Debug.Assert(_fileName is not null, "Nothing to output to");
_xmlTextWriter = new XmlTextWriter(_fileName, Encoding.UTF8);
}
_xmlTextWriter.Formatting = Formatting.Indented;
_xmlTextWriter.Indentation = 2;
if (!writeHeaderRequired)
{
_xmlTextWriter.WriteStartDocument(); // writes <?xml version="1.0" encoding="utf-8"?>
}
_xmlTextWriter.WriteStartElement("root");
XmlTextReader reader = new(new StringReader(ResourceSchema))
{
WhitespaceHandling = WhitespaceHandling.None
};
_xmlTextWriter.WriteNode(reader, true);
_xmlTextWriter.WriteStartElement(ResHeaderStr);
{
_xmlTextWriter.WriteAttributeString(NameStr, ResMimeTypeStr);
_xmlTextWriter.WriteStartElement(ValueStr);
{
_xmlTextWriter.WriteString(ResMimeType);
}
_xmlTextWriter.WriteEndElement();
}
_xmlTextWriter.WriteEndElement();
_xmlTextWriter.WriteStartElement(ResHeaderStr);
{
_xmlTextWriter.WriteAttributeString(NameStr, VersionStr);
_xmlTextWriter.WriteStartElement(ValueStr);
{
_xmlTextWriter.WriteString(Version);
}
_xmlTextWriter.WriteEndElement();
}
_xmlTextWriter.WriteEndElement();
_xmlTextWriter.WriteStartElement(ResHeaderStr);
{
_xmlTextWriter.WriteAttributeString(NameStr, ReaderStr);
_xmlTextWriter.WriteStartElement(ValueStr);
{
_xmlTextWriter.WriteString(MultitargetUtil.GetAssemblyQualifiedName(typeof(ResXResourceReader), _typeNameConverter));
}
_xmlTextWriter.WriteEndElement();
}
_xmlTextWriter.WriteEndElement();
_xmlTextWriter.WriteStartElement(ResHeaderStr);
{
_xmlTextWriter.WriteAttributeString(NameStr, WriterStr);
_xmlTextWriter.WriteStartElement(ValueStr);
{
_xmlTextWriter.WriteString(MultitargetUtil.GetAssemblyQualifiedName(typeof(ResXResourceWriter), _typeNameConverter));
}
_xmlTextWriter.WriteEndElement();
}
_xmlTextWriter.WriteEndElement();
}
private XmlTextWriter Writer
{
get
{
InitializeWriter();
return _xmlTextWriter;
}
}
/// <summary>
/// Adds aliases to the resource file.
/// </summary>
public virtual void AddAlias(string? aliasName, AssemblyName assemblyName)
{
ArgumentNullException.ThrowIfNull(assemblyName);
_cachedAliases ??= [];
_cachedAliases[assemblyName.FullName] = aliasName;
}
/// <summary>
/// Adds the given value to the collection of metadata. These name/value pairs
/// will be emitted to the <metadata> elements in the .resx file.
/// </summary>
public void AddMetadata(string name, byte[] value) => AddDataRow(MetadataStr, name, value);
/// <summary>
/// Adds the given value to the collection of metadata. These name/value pairs
/// will be emitted to the <metadata> elements in the .resx file.
/// </summary>
public void AddMetadata(string name, string? value) => AddDataRow(MetadataStr, name, value);
/// <summary>
/// Adds the given value to the collection of metadata. These name/value pairs
/// will be emitted to the <metadata> elements in the .resx file.
/// </summary>
public void AddMetadata(string name, object? value) => AddDataRow(MetadataStr, name, value);
/// <summary>
/// Adds a blob resource to the resources.
/// </summary>
public void AddResource(string name, byte[]? value) => AddDataRow(DataStr, name, value);
/// <summary>
/// Adds a resource to the resources. If the resource is a string, it will be saved that way, otherwise it
/// will be serialized and stored as in binary.
/// </summary>
public void AddResource(string name, object? value)
{
if (value is ResXDataNode node)
{
AddResource(node);
}
else
{
AddDataRow(DataStr, name, value);
}
}
/// <summary>
/// Adds a string resource to the resources.
/// </summary>
public void AddResource(string name, string? value) => AddDataRow(DataStr, name, value);
/// <summary>
/// Adds a string resource to the resources.
/// </summary>
public void AddResource(ResXDataNode node)
{
// Clone the node to work on a copy.
ResXDataNode nodeClone = node.DeepClone();
ResXFileRef? fileRef = nodeClone.FileRef;
if (fileRef is not null && !string.IsNullOrEmpty(BasePath))
{
string modifiedBasePath = Path.EndsInDirectorySeparator(BasePath)
? BasePath
: $"{BasePath}{Path.DirectorySeparatorChar}";
fileRef.MakeFilePathRelative(modifiedBasePath);
}
DataNodeInfo info = nodeClone.GetDataNodeInfo();
AddDataRow(DataStr, info.Name, info.ValueData, info.TypeName, info.MimeType, info.Comment);
}
/// <summary>
/// Adds a blob resource to the resources.
/// </summary>
private void AddDataRow(string elementName, string name, byte[]? value)
=> AddDataRow(
elementName,
name,
value is null ? null : ToBase64WrappedString(value),
MultitargetUtil.GetAssemblyQualifiedName(typeof(byte[]), _typeNameConverter),
mimeType: null,
comment: null);
/// <summary>
/// Adds a resource to the resources. If the resource is a string, it will be saved that way, otherwise it
/// will be serialized and stored as in binary.
/// </summary>
private void AddDataRow(string elementName, string name, object? value)
{
switch (value)
{
case string str:
AddDataRow(elementName, name, str);
break;
case byte[] bytes:
AddDataRow(elementName, name, bytes);
break;
case ResXFileRef fileRef:
{
ResXDataNode node = new(name, fileRef, _typeNameConverter);
DataNodeInfo info = node.GetDataNodeInfo();
AddDataRow(elementName, info.Name, info.ValueData, info.TypeName, info.MimeType, info.Comment);
break;
}
default:
{
ResXDataNode node = new(name, value, _typeNameConverter);
DataNodeInfo info = node.GetDataNodeInfo();
AddDataRow(elementName, info.Name, info.ValueData, info.TypeName, info.MimeType, info.Comment);
break;
}
}
}
/// <summary>
/// Adds a string resource to the resources.
/// </summary>
private void AddDataRow(string elementName, string name, string? value)
{
// if it's a null string, set it here as a resxnullref
string? typeName =
value is null
? MultitargetUtil.GetAssemblyQualifiedName(typeof(ResXNullRef), _typeNameConverter)
: null;
AddDataRow(elementName, name, value, typeName, null, null);
}
/// <summary>
/// Adds a new row to the Resources table. This helper is used because
/// we want to always late bind to the columns for greater flexibility.
/// </summary>
private void AddDataRow(string elementName, string name, string? value, string? type, string? mimeType, string? comment)
{
if (_hasBeenSaved)
{
throw new InvalidOperationException(SR.ResXResourceWriterSaved);
}
string? alias = null;
if (!string.IsNullOrEmpty(type) && elementName == DataStr)
{
string? assemblyName = GetFullName(type);
if (string.IsNullOrEmpty(assemblyName))
{
try
{
Type? typeObject = Type.GetType(type);
if (typeObject == typeof(string))
{
type = null;
}
else if (typeObject is not null)
{
string? qualifiedName = MultitargetUtil.GetAssemblyQualifiedName(typeObject, _typeNameConverter);
if (qualifiedName is not null)
{
// Let this throw argument null for bad type name (to match old behavior)
assemblyName = GetFullName(qualifiedName);
alias = GetAliasFromName(new AssemblyName(assemblyName!));
}
}
}
catch
{
}
}
else
{
// Let this throw argument null for bad type name (to match old behavior)
alias = GetAliasFromName(new AssemblyName(GetFullName(type)!));
}
}
Writer.WriteStartElement(elementName);
{
Writer.WriteAttributeString(NameStr, name);
if (!string.IsNullOrEmpty(alias) && !string.IsNullOrEmpty(type) && elementName == DataStr)
{
Writer.WriteAttributeString(TypeStr, $"{GetTypeName(type)}, {alias}");
}
else
{
if (type is not null)
{
Writer.WriteAttributeString(TypeStr, type);
}
}
if (mimeType is not null)
{
Writer.WriteAttributeString(MimeTypeStr, mimeType);
}
if ((type is null && mimeType is null) || (type is not null && type.StartsWith("System.Char", StringComparison.Ordinal)))
{
Writer.WriteAttributeString("xml", "space", null, "preserve");
}
Writer.WriteStartElement(ValueStr);
{
if (!string.IsNullOrEmpty(value))
{
Writer.WriteString(value);
}
}
Writer.WriteEndElement();
if (!string.IsNullOrEmpty(comment))
{
Writer.WriteStartElement(CommentStr);
{
Writer.WriteString(comment);
}
Writer.WriteEndElement();
}
}
Writer.WriteEndElement();
}
private void AddAssemblyRow(string elementName, string? alias, string? name)
{
Writer.WriteStartElement(elementName);
{
if (!string.IsNullOrEmpty(alias))
{
Writer.WriteAttributeString(AliasStr, alias);
}
if (!string.IsNullOrEmpty(name))
{
Writer.WriteAttributeString(NameStr, name);
}
}
Writer.WriteEndElement();
}
private string? GetAliasFromName(AssemblyName assemblyName)
{
_cachedAliases ??= [];
if (!_cachedAliases.TryGetValue(assemblyName.FullName, out string? alias) || string.IsNullOrEmpty(alias))
{
alias = assemblyName.Name;
AddAlias(alias, assemblyName);
AddAssemblyRow(AssemblyStr, alias, assemblyName.FullName);
}
return alias;
}
/// <summary>
/// Closes any files or streams locked by the writer.
/// </summary>
public void Close() => Dispose();
public virtual void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (!_hasBeenSaved)
{
Generate();
}
_xmlTextWriter?.Close();
_xmlTextWriter = null;
_stream?.Close();
_stream = null;
_textWriter?.Close();
_textWriter = null;
}
}
private static string GetTypeName(string typeName)
{
int indexStart = typeName.IndexOf(',');
return (indexStart == -1) ? typeName : typeName[..indexStart];
}
private static string? GetFullName(string typeName)
{
int indexStart = typeName.IndexOf(',');
return indexStart == -1 ? null : typeName[(indexStart + 2)..];
}
private static string ToBase64WrappedString(byte[] data)
{
const int lineWrap = 80;
const string prefix = " ";
string raw = Convert.ToBase64String(data);
if (raw.Length > lineWrap)
{
// Word wrap on lineWrap chars, \r\n.
StringBuilder output = new(raw.Length + (raw.Length / lineWrap) * 3);
int current = 0;
for (; current < raw.Length - lineWrap; current += lineWrap)
{
output.AppendLine();
output.Append(prefix);
output.Append(raw, current, lineWrap);
}
output.AppendLine();
output.Append(prefix);
output.Append(raw, current, raw.Length - current);
output.AppendLine();
return output.ToString();
}
return raw;
}
/// <summary>
/// Writes the resources out to the file or stream.
/// </summary>
public void Generate()
{
if (_hasBeenSaved)
{
throw new InvalidOperationException(SR.ResXResourceWriterSaved);
}
_hasBeenSaved = true;
Writer.WriteEndElement();
Writer.Flush();
}
}
|