|
// 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;
using System.ComponentModel.Design;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
using System.Xml;
namespace System.Resources;
/// <summary>
/// ResX resource reader.
/// </summary>
public partial class ResXResourceReader : IResourceReader
{
private readonly string? _fileName;
private TextReader? _reader;
private Stream? _stream;
private string? _fileContents;
private readonly AssemblyName[]? _assemblyNames;
private string? _basePath;
private bool _isReaderDirty;
private readonly ITypeResolutionService? _typeResolver;
private readonly IAliasResolver _aliasResolver;
private Dictionary<string, object>? _resData;
private Dictionary<string, object>? _resMetadata;
#pragma warning disable IDE0052 // Remove unread private members - for diagnostics
private string? _resHeaderVersion;
#pragma warning restore IDE0052
private string? _resHeaderMimeType;
private string? _resHeaderReaderType;
private string? _resHeaderWriterType;
private bool _useResXDataNodes;
private ResXResourceReader(ITypeResolutionService? typeResolver)
{
_typeResolver = typeResolver;
_aliasResolver = new ReaderAliasResolver();
}
private ResXResourceReader(AssemblyName[] assemblyNames)
{
_assemblyNames = assemblyNames;
_aliasResolver = new ReaderAliasResolver();
}
public ResXResourceReader(string fileName) : this(fileName, typeResolver: null, aliasResolver: null)
{
}
public ResXResourceReader(string fileName, ITypeResolutionService? typeResolver)
: this(fileName, typeResolver, aliasResolver: null)
{
}
internal ResXResourceReader(string fileName, ITypeResolutionService? typeResolver, IAliasResolver? aliasResolver)
{
_fileName = fileName;
_typeResolver = typeResolver;
_aliasResolver = aliasResolver ?? new ReaderAliasResolver();
}
public ResXResourceReader(TextReader reader) : this(reader, typeResolver: null, aliasResolver: null)
{
}
public ResXResourceReader(TextReader reader, ITypeResolutionService typeResolver)
: this(reader, typeResolver, aliasResolver: null)
{
}
internal ResXResourceReader(TextReader reader, ITypeResolutionService? typeResolver, IAliasResolver? aliasResolver)
{
_reader = reader;
_typeResolver = typeResolver;
_aliasResolver = aliasResolver ?? new ReaderAliasResolver();
}
public ResXResourceReader(Stream stream) : this(stream, typeResolver: null, aliasResolver: null)
{
}
public ResXResourceReader(Stream stream, ITypeResolutionService typeResolver)
: this(stream, typeResolver, aliasResolver: null)
{
}
internal ResXResourceReader(Stream stream, ITypeResolutionService? typeResolver, IAliasResolver? aliasResolver)
{
_stream = stream;
_typeResolver = typeResolver;
_aliasResolver = aliasResolver ?? new ReaderAliasResolver();
}
public ResXResourceReader(Stream stream, AssemblyName[] assemblyNames)
: this(stream, assemblyNames, aliasResolver: null)
{
}
internal ResXResourceReader(Stream stream, AssemblyName[] assemblyNames, IAliasResolver? aliasResolver)
{
_stream = stream;
_assemblyNames = assemblyNames;
_aliasResolver = aliasResolver ?? new ReaderAliasResolver();
}
public ResXResourceReader(TextReader reader, AssemblyName[] assemblyNames) : this(reader, assemblyNames, null)
{
}
internal ResXResourceReader(TextReader reader, AssemblyName[] assemblyNames, IAliasResolver? aliasResolver)
{
_reader = reader;
_assemblyNames = assemblyNames;
_aliasResolver = aliasResolver ?? new ReaderAliasResolver();
}
public ResXResourceReader(string fileName, AssemblyName[] assemblyNames)
: this(fileName, assemblyNames, aliasResolver: null)
{
}
internal ResXResourceReader(string fileName, AssemblyName[] assemblyNames, IAliasResolver? aliasResolver)
{
_fileName = fileName;
_assemblyNames = assemblyNames;
_aliasResolver = aliasResolver ?? new ReaderAliasResolver();
}
~ResXResourceReader()
{
Dispose(false);
}
/// <summary>
/// BasePath for relatives filepaths with ResXFileRefs.
/// </summary>
public string? BasePath
{
get => _basePath;
set
{
if (_isReaderDirty)
{
throw new InvalidOperationException(SR.InvalidResXBasePathOperation);
}
_basePath = value;
}
}
/// <summary>
/// ResXFileRef's TypeConverter automatically unwraps it, creates the referenced
/// object and returns it. This property gives the user control over whether this unwrapping should
/// happen, or a ResXFileRef object should be returned. Default is true for backwards compatibility
/// and common case scenarios.
/// </summary>
public bool UseResXDataNodes
{
get => _useResXDataNodes;
set
{
if (_isReaderDirty)
{
throw new InvalidOperationException(SR.InvalidResXBasePathOperation);
}
_useResXDataNodes = value;
}
}
/// <summary>
/// Closes any files or streams being used by the reader.
/// </summary>
public void Close()
{
((IDisposable)this).Dispose();
}
void IDisposable.Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_fileName is not null && _stream is not null)
{
_stream.Close();
_stream = null;
}
if (_reader is not null)
{
_reader.Close();
_reader = null;
}
}
}
private static void SetupNameTable(XmlReader reader)
{
reader.NameTable.Add(ResXResourceWriter.TypeStr);
reader.NameTable.Add(ResXResourceWriter.NameStr);
reader.NameTable.Add(ResXResourceWriter.DataStr);
reader.NameTable.Add(ResXResourceWriter.MetadataStr);
reader.NameTable.Add(ResXResourceWriter.MimeTypeStr);
reader.NameTable.Add(ResXResourceWriter.ValueStr);
reader.NameTable.Add(ResXResourceWriter.ResHeaderStr);
reader.NameTable.Add(ResXResourceWriter.VersionStr);
reader.NameTable.Add(ResXResourceWriter.ResMimeTypeStr);
reader.NameTable.Add(ResXResourceWriter.ReaderStr);
reader.NameTable.Add(ResXResourceWriter.WriterStr);
reader.NameTable.Add(ResXResourceWriter.BinSerializedObjectMimeType);
reader.NameTable.Add(ResXResourceWriter.SoapSerializedObjectMimeType);
reader.NameTable.Add(ResXResourceWriter.AssemblyStr);
reader.NameTable.Add(ResXResourceWriter.AliasStr);
}
/// <summary>
/// Demand loads the resource data.
/// </summary>
[MemberNotNull(nameof(_resData))]
[MemberNotNull(nameof(_resMetadata))]
private void EnsureResData()
{
if (_resData is not null && _resMetadata is not null)
{
return;
}
Debug.Assert(_resData is null && _resMetadata is null);
_resData = [];
_resMetadata = [];
XmlTextReader? contentReader = null;
try
{
// Create the reader and parse the XML
if (_fileContents is not null)
{
contentReader = new XmlTextReader(new StringReader(_fileContents));
}
else if (_reader is not null)
{
contentReader = new XmlTextReader(_reader);
}
else if (_fileName is not null || _stream is not null)
{
_stream ??= new FileStream(_fileName!, FileMode.Open, FileAccess.Read, FileShare.Read);
contentReader = new XmlTextReader(_stream);
}
Debug.Assert(contentReader is not null);
if (contentReader is not null)
{
SetupNameTable(contentReader);
contentReader.WhitespaceHandling = WhitespaceHandling.None;
ParseXml(contentReader);
}
}
finally
{
if (_fileName is not null && _stream is not null)
{
_stream.Close();
_stream = null;
}
}
}
/// <summary>
/// Creates a reader with the specified file contents.
/// </summary>
public static ResXResourceReader FromFileContents(string fileContents)
=> FromFileContents(fileContents, (ITypeResolutionService?)null);
/// <summary>
/// Creates a reader with the specified file contents.
/// </summary>
public static ResXResourceReader FromFileContents(string fileContents, ITypeResolutionService? typeResolver)
=> new(typeResolver)
{
_fileContents = fileContents
};
/// <summary>
/// Creates a reader with the specified file contents.
/// </summary>
public static ResXResourceReader FromFileContents(string fileContents, AssemblyName[] assemblyNames)
=> new(assemblyNames)
{
_fileContents = fileContents
};
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public IDictionaryEnumerator GetEnumerator()
{
_isReaderDirty = true;
EnsureResData();
return ((IDictionary)_resData).GetEnumerator();
}
/// <summary>
/// Returns a dictionary enumerator that can be used to enumerate the <metadata> elements in the .resx file.
/// </summary>
public IDictionaryEnumerator GetMetadataEnumerator()
{
EnsureResData();
return ((IDictionary)_resMetadata).GetEnumerator();
}
/// <summary>
/// Attempts to return the line and column (Y, X) of the XML reader.
/// </summary>
private static Point GetPosition(XmlReader reader)
{
Point pt = default;
if (reader is IXmlLineInfo lineInfo)
{
pt.Y = lineInfo.LineNumber;
pt.X = lineInfo.LinePosition;
}
return pt;
}
private void ParseXml(XmlTextReader reader)
{
Debug.Assert(_resData is not null);
Debug.Assert(_resMetadata is not null);
bool success = false;
try
{
try
{
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
string s = reader.LocalName;
if (reader.LocalName.Equals(ResXResourceWriter.AssemblyStr))
{
ParseAssemblyNode(reader);
}
else if (reader.LocalName.Equals(ResXResourceWriter.DataStr))
{
ParseDataNode(reader, isMetaData: false);
}
else if (reader.LocalName.Equals(ResXResourceWriter.ResHeaderStr))
{
ParseResHeaderNode(reader);
}
else if (reader.LocalName.Equals(ResXResourceWriter.MetadataStr))
{
ParseDataNode(reader, isMetaData: true);
}
}
}
success = true;
}
catch (SerializationException se)
{
Point pt = GetPosition(reader);
string newMessage = string.Format(SR.SerializationException, reader[ResXResourceWriter.TypeStr], pt.Y, pt.X, se.Message);
XmlException xml = new(newMessage, se, pt.Y, pt.X);
throw new SerializationException(newMessage, xml);
}
catch (TargetInvocationException tie)
{
Point pt = GetPosition(reader);
string newMessage = string.Format(SR.InvocationException, reader[ResXResourceWriter.TypeStr], pt.Y, pt.X, tie.InnerException?.Message);
XmlException xml = new(newMessage, tie.InnerException, pt.Y, pt.X);
throw new TargetInvocationException(newMessage, xml);
}
catch (XmlException e)
{
throw new ArgumentException(string.Format(SR.InvalidResXFile, e.Message), e);
}
catch (Exception e)
{
if (e.IsCriticalException())
{
throw;
}
else
{
Point pt = GetPosition(reader);
XmlException xmlEx = new(e.Message, e, pt.Y, pt.X);
throw new ArgumentException(string.Format(SR.InvalidResXFile, xmlEx.Message), xmlEx);
}
}
}
finally
{
if (!success)
{
_resData = null;
_resMetadata = null;
}
}
bool validFile = false;
if (_resHeaderMimeType == ResXResourceWriter.ResMimeType)
{
Type readerType = typeof(ResXResourceReader);
Type writerType = typeof(ResXResourceWriter);
string? readerTypeName = GetPrefix(_resHeaderReaderType);
string? writerTypeName = GetPrefix(_resHeaderWriterType);
if (readerTypeName is not null
&& writerTypeName is not null
&& readerTypeName.Equals(readerType.FullName)
&& writerTypeName.Equals(writerType.FullName))
{
validFile = true;
}
}
if (!validFile)
{
_resData = null;
_resMetadata = null;
throw new ArgumentException(SR.InvalidResXFileReaderWriterTypes);
}
static string? GetPrefix(string? typeName)
{
if (typeName is null)
{
return null;
}
int index = typeName.IndexOf(',');
if (index != -1)
{
return typeName.AsSpan(0, index).Trim().ToString();
}
return typeName;
}
}
private void ParseResHeaderNode(XmlTextReader reader)
{
string? name = reader[ResXResourceWriter.NameStr];
if (name is null)
{
return;
}
reader.ReadStartElement();
// The "1.1" schema requires the correct casing of the strings in the resheader, however the "1.0" schema
// had a different casing. By checking the Equals first, we should see significant performance improvements.
if (name == ResXResourceWriter.VersionStr)
{
_resHeaderVersion = reader.NodeType == XmlNodeType.Element ? reader.ReadElementString() : reader.Value.Trim();
}
else if (name == ResXResourceWriter.ResMimeTypeStr)
{
_resHeaderMimeType = reader.NodeType == XmlNodeType.Element ? reader.ReadElementString() : reader.Value.Trim();
}
else if (name == ResXResourceWriter.ReaderStr)
{
_resHeaderReaderType = reader.NodeType == XmlNodeType.Element ? reader.ReadElementString() : reader.Value.Trim();
}
else if (name == ResXResourceWriter.WriterStr)
{
_resHeaderWriterType = reader.NodeType == XmlNodeType.Element ? reader.ReadElementString() : reader.Value.Trim();
}
else
{
switch (name.ToLower(CultureInfo.InvariantCulture))
{
case ResXResourceWriter.VersionStr:
_resHeaderVersion = reader.NodeType == XmlNodeType.Element ? reader.ReadElementString() : reader.Value.Trim();
break;
case ResXResourceWriter.ResMimeTypeStr:
_resHeaderMimeType = reader.NodeType == XmlNodeType.Element ? reader.ReadElementString() : reader.Value.Trim();
break;
case ResXResourceWriter.ReaderStr:
_resHeaderReaderType = reader.NodeType == XmlNodeType.Element ? reader.ReadElementString() : reader.Value.Trim();
break;
case ResXResourceWriter.WriterStr:
_resHeaderWriterType = reader.NodeType == XmlNodeType.Element ? reader.ReadElementString() : reader.Value.Trim();
break;
}
}
}
private void ParseAssemblyNode(XmlReader reader)
{
string? alias = reader[ResXResourceWriter.AliasStr];
string? typeName = reader[ResXResourceWriter.NameStr];
// Let this throw out the way it historically did (argument null)
AssemblyName assemblyName = new(typeName!);
if (string.IsNullOrEmpty(alias))
{
alias = assemblyName.Name;
}
_aliasResolver.PushAlias(alias, assemblyName);
}
private void ParseDataNode(XmlTextReader reader, bool isMetaData)
{
Debug.Assert(_resData is not null);
Debug.Assert(_resMetadata is not null);
DataNodeInfo nodeInfo = new DataNodeInfo
{
Name = reader[ResXResourceWriter.NameStr] ?? string.Empty
};
string? typeName = reader[ResXResourceWriter.TypeStr];
string? alias = null;
AssemblyName? assemblyName = null;
if (!string.IsNullOrEmpty(typeName))
{
alias = GetAliasFromTypeName(typeName);
}
if (!string.IsNullOrEmpty(alias))
{
assemblyName = _aliasResolver.ResolveAlias(alias);
}
nodeInfo.TypeName = assemblyName is not null && typeName is not null
? $"{GetTypeFromTypeName(typeName)}, {assemblyName.FullName}"
: reader[ResXResourceWriter.TypeStr];
nodeInfo.MimeType = reader[ResXResourceWriter.MimeTypeStr];
nodeInfo.ReaderPosition = GetPosition(reader);
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.EndElement
&& (reader.LocalName.Equals(ResXResourceWriter.DataStr) || reader.LocalName.Equals(ResXResourceWriter.MetadataStr)))
{
// We just found </data>, quit or </metadata>
break;
}
// Could be a <value> or a <comment>
if (reader.NodeType != XmlNodeType.Element)
{
// No <xxxx> tag, just the inside of <data> as text.
nodeInfo.ValueData = reader.Value.Trim();
continue;
}
if (reader.Name.Equals(ResXResourceWriter.CommentStr))
{
nodeInfo.Comment = reader.ReadString();
continue;
}
if (!reader.Name.Equals(ResXResourceWriter.ValueStr))
{
continue;
}
WhitespaceHandling oldValue = reader.WhitespaceHandling;
try
{
// Based on the documentation at https://learn.microsoft.com/dotnet/api/system.xml.xmltextreader.whitespacehandling
// this is ok because:
//
// "Because the XmlTextReader does not have DTD information available to it,
// SignificantWhitespace nodes are only returned within the an xml:space='preserve' scope."
//
// The xml:space would not be present for anything else than string and char (see ResXResourceWriter)
// so this would not cause any breaking change while reading data from Everett (we never output
// xml:space then) or from whidbey that is not specifically either a string or a char.
// However please note that manually editing a resx file in Everett and in Whidbey because of the addition
// of xml:space=preserve might have different consequences.
reader.WhitespaceHandling = WhitespaceHandling.Significant;
nodeInfo.ValueData = reader.ReadString();
}
finally
{
reader.WhitespaceHandling = oldValue;
}
}
if (nodeInfo.Name is null)
{
throw new ArgumentException(string.Format(SR.InvalidResXResourceNoName, nodeInfo.ValueData));
}
ResXDataNode dataNode = new(nodeInfo, BasePath);
if (UseResXDataNodes)
{
_resData[nodeInfo.Name] = dataNode;
}
else
{
IDictionary data = isMetaData ? _resMetadata : _resData;
data[nodeInfo.Name] = _assemblyNames is null ? dataNode.GetValue(_typeResolver) : dataNode.GetValue(_assemblyNames);
}
}
private static string GetAliasFromTypeName(string typeName)
{
int indexStart = typeName.IndexOf(',');
return typeName[(indexStart + 2)..];
}
private static string GetTypeFromTypeName(string typeName)
{
int indexStart = typeName.IndexOf(',');
return typeName[..indexStart];
}
}
|