Browse Source

Merge branch '3.2.x'

pull/1213/head
Daniel Grunwald 7 years ago
parent
commit
ec964bf4ba
  1. 72
      ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs
  2. 2
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  3. 540
      ICSharpCode.Decompiler/Util/ResXResourceWriter.cs
  4. 526
      ICSharpCode.Decompiler/Util/ResourcesFile.cs
  5. 10
      ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs
  6. 9
      ILSpy/Languages/CSharpLanguage.cs
  7. 42
      ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs
  8. 99
      doc/Resources.txt

72
ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs

@ -21,7 +21,6 @@ using System.Collections; @@ -21,7 +21,6 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Resources;
using System.Threading.Tasks;
using System.Xml;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
@ -31,6 +30,7 @@ using ICSharpCode.Decompiler.TypeSystem; @@ -31,6 +30,7 @@ using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util;
using Mono.Cecil;
using System.Threading;
using System.Text;
namespace ICSharpCode.Decompiler.CSharp
{
@ -330,23 +330,40 @@ namespace ICSharpCode.Decompiler.CSharp @@ -330,23 +330,40 @@ namespace ICSharpCode.Decompiler.CSharp
Stream stream = r.GetResourceStream();
stream.Position = 0;
IEnumerable<DictionaryEntry> entries;
if (r.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) {
if (GetEntries(stream, out entries) && entries.All(e => e.Value is Stream)) {
foreach (var pair in entries) {
string fileName = Path.Combine(((string)pair.Key).Split('/').Select(p => CleanUpFileName(p)).ToArray());
string dirName = Path.GetDirectoryName(fileName);
if (!string.IsNullOrEmpty(dirName) && directories.Add(dirName)) {
Directory.CreateDirectory(Path.Combine(targetDirectory, dirName));
bool decodedIntoIndividualFiles;
var individualResources = new List<Tuple<string, string>>();
try {
var resourcesFile = new ResourcesFile(stream);
if (resourcesFile.AllEntriesAreStreams()) {
foreach (var (name, value) in resourcesFile) {
string fileName = Path.Combine(name.Split('/').Select(p => CleanUpFileName(p)).ToArray());
string dirName = Path.GetDirectoryName(fileName);
if (!string.IsNullOrEmpty(dirName) && directories.Add(dirName)) {
Directory.CreateDirectory(Path.Combine(targetDirectory, dirName));
}
Stream entryStream = (Stream)value;
entryStream.Position = 0;
individualResources.AddRange(
WriteResourceToFile(Path.Combine(targetDirectory, fileName), (string)name, entryStream));
}
Stream entryStream = (Stream)pair.Value;
entryStream.Position = 0;
WriteResourceToFile(Path.Combine(targetDirectory, fileName), (string)pair.Key, entryStream);
decodedIntoIndividualFiles = true;
} else {
decodedIntoIndividualFiles = false;
}
} catch (BadImageFormatException) {
decodedIntoIndividualFiles = false;
}
if (decodedIntoIndividualFiles) {
foreach (var entry in individualResources) {
yield return entry;
}
} else {
stream.Position = 0;
string fileName = GetFileNameForResource(Path.ChangeExtension(r.Name, ".resource"));
WriteResourceToFile(fileName, r.Name, stream);
string fileName = Path.ChangeExtension(GetFileNameForResource(r.Name), ".resource");
foreach (var entry in WriteResourceToFile(fileName, r.Name, stream)) {
yield return entry;
}
}
} else {
string fileName = GetFileNameForResource(r.Name);
@ -381,17 +398,6 @@ namespace ICSharpCode.Decompiler.CSharp @@ -381,17 +398,6 @@ namespace ICSharpCode.Decompiler.CSharp
}
return fileName;
}
bool GetEntries(Stream stream, out IEnumerable<DictionaryEntry> entries)
{
try {
entries = new ResourceSet(stream).Cast<DictionaryEntry>();
return true;
} catch (ArgumentException) {
entries = null;
return false;
}
}
#endregion
/// <summary>
@ -406,9 +412,21 @@ namespace ICSharpCode.Decompiler.CSharp @@ -406,9 +412,21 @@ namespace ICSharpCode.Decompiler.CSharp
if (pos > 0)
text = text.Substring(0, pos);
text = text.Trim();
foreach (char c in Path.GetInvalidFileNameChars())
text = text.Replace(c, '-');
return text;
// Whitelist allowed characters, replace everything else:
StringBuilder b = new StringBuilder(text.Length);
foreach (var c in text) {
if (char.IsLetterOrDigit(c) || c == '-' || c == '_')
b.Append(c);
else if (c == '.' && b.Length > 0 && b[b.Length - 1] != '.')
b.Append('.'); // allow dot, but never two in a row
else
b.Append('-');
if (b.Length >= 64)
break; // limit to 64 chars
}
if (b.Length == 0)
b.Append('-');
return b.ToString();
}
public static string GetPlatformName(ModuleDefinition module)

2
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

@ -336,6 +336,8 @@ @@ -336,6 +336,8 @@
<Compile Include="Util\GraphVizGraph.cs" />
<Compile Include="Util\KeyComparer.cs" />
<Compile Include="Util\LongDict.cs" />
<Compile Include="Util\ResourcesFile.cs" />
<Compile Include="Util\ResXResourceWriter.cs" />
<Compile Include="Util\UnicodeNewline.cs" />
<Compile Include="FlowAnalysis\ControlFlowNode.cs" />
<Compile Include="FlowAnalysis\DataFlowVisitor.cs" />

540
ICSharpCode.Decompiler/Util/ResXResourceWriter.cs

@ -0,0 +1,540 @@ @@ -0,0 +1,540 @@
// Copyright (c) 2018 Daniel Grunwald
// This file is based on the Mono implementation of ResXResourceWriter.
// It is modified to add support for "ResourceSerializedObject" values.
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// Copyright (c) 2004-2005 Novell, Inc.
//
// Authors:
// Duncan Mak duncan@ximian.com
// Gonzalo Paniagua Javier gonzalo@ximian.com
// Peter Bartok pbartok@novell.com
// Gary Barnett gary.barnett.mono@gmail.com
// includes code by Mike Krüger and Lluis Sanchez
using System.ComponentModel;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Xml;
using System;
namespace ICSharpCode.Decompiler.Util
{
#if INSIDE_SYSTEM_WEB
internal
#else
public
#endif
class ResXResourceWriter : IDisposable
{
#region Local Variables
private string filename;
private Stream stream;
private TextWriter textwriter;
private XmlTextWriter writer;
private bool written;
private string base_path;
#endregion // Local Variables
#region Static Fields
public static readonly string BinSerializedObjectMimeType = "application/x-microsoft.net.object.binary.base64";
public static readonly string ByteArraySerializedObjectMimeType = "application/x-microsoft.net.object.bytearray.base64";
public static readonly string DefaultSerializedObjectMimeType = BinSerializedObjectMimeType;
public static readonly string ResMimeType = "text/microsoft-resx";
public static readonly string ResourceSchema = schema;
public static readonly string SoapSerializedObjectMimeType = "application/x-microsoft.net.object.soap.base64";
public static readonly string Version = "2.0";
#endregion // Static Fields
#region Constructors & Destructor
public ResXResourceWriter(Stream stream)
{
if (stream == null)
throw new ArgumentNullException("stream");
if (!stream.CanWrite)
throw new ArgumentException("stream is not writable.", "stream");
this.stream = stream;
}
public ResXResourceWriter(TextWriter textWriter)
{
if (textWriter == null)
throw new ArgumentNullException("textWriter");
this.textwriter = textWriter;
}
public ResXResourceWriter(string fileName)
{
if (fileName == null)
throw new ArgumentNullException("fileName");
this.filename = fileName;
}
~ResXResourceWriter()
{
Dispose(false);
}
#endregion // Constructors & Destructor
const string WinFormsAssemblyName = ", System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089";
const string ResXNullRefTypeName = "System.Resources.ResXNullRef" + WinFormsAssemblyName;
void InitWriter()
{
if (filename != null)
stream = File.Open(filename, FileMode.Create);
if (textwriter == null)
textwriter = new StreamWriter(stream, Encoding.UTF8);
writer = new XmlTextWriter(textwriter);
writer.Formatting = Formatting.Indented;
writer.WriteStartDocument();
writer.WriteStartElement("root");
writer.WriteRaw(schema);
WriteHeader("resmimetype", "text/microsoft-resx");
WriteHeader("version", "1.3");
WriteHeader("reader", "System.Resources.ResXResourceReader" + WinFormsAssemblyName);
WriteHeader("writer", "System.Resources.ResXResourceWriter" + WinFormsAssemblyName);
}
void WriteHeader(string name, string value)
{
writer.WriteStartElement("resheader");
writer.WriteAttributeString("name", name);
writer.WriteStartElement("value");
writer.WriteString(value);
writer.WriteEndElement();
writer.WriteEndElement();
}
void WriteNiceBase64(byte[] value, int offset, int length)
{
string b64;
StringBuilder sb;
int pos;
int inc;
string ins;
b64 = Convert.ToBase64String(value, offset, length);
// Wild guess; two extra newlines, and one newline/tab pair for every 80 chars
sb = new StringBuilder(b64, b64.Length + ((b64.Length + 160) / 80) * 3);
pos = 0;
inc = 80 + Environment.NewLine.Length + 1;
ins = Environment.NewLine + "\t";
while (pos < sb.Length) {
sb.Insert(pos, ins);
pos += inc;
}
sb.Insert(sb.Length, Environment.NewLine);
writer.WriteString(sb.ToString());
}
void WriteBytes(string name, Type type, byte[] value, int offset, int length, string comment)
{
writer.WriteStartElement("data");
writer.WriteAttributeString("name", name);
if (type != null) {
writer.WriteAttributeString("type", type.AssemblyQualifiedName);
// byte[] should never get a mimetype, otherwise MS.NET won't be able
// to parse the data.
if (type != typeof(byte[]))
writer.WriteAttributeString("mimetype", ByteArraySerializedObjectMimeType);
writer.WriteStartElement("value");
WriteNiceBase64(value, offset, length);
} else {
writer.WriteAttributeString("mimetype", BinSerializedObjectMimeType);
writer.WriteStartElement("value");
writer.WriteBase64(value, offset, length);
}
writer.WriteEndElement();
if (!(comment == null || comment.Equals(String.Empty))) {
writer.WriteStartElement("comment");
writer.WriteString(comment);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
void WriteBytes(string name, Type type, byte[] value, string comment)
{
WriteBytes(name, type, value, 0, value.Length, comment);
}
void WriteString(string name, string value)
{
WriteString(name, value, null);
}
void WriteString(string name, string value, string type)
{
WriteString(name, value, type, String.Empty);
}
void WriteString(string name, string value, string type, string comment)
{
writer.WriteStartElement("data");
writer.WriteAttributeString("name", name);
if (type != null)
writer.WriteAttributeString("type", type);
writer.WriteStartElement("value");
writer.WriteString(value);
writer.WriteEndElement();
if (!(comment == null || comment.Equals(String.Empty))) {
writer.WriteStartElement("comment");
writer.WriteString(comment);
writer.WriteEndElement();
}
writer.WriteEndElement();
writer.WriteWhitespace("\n ");
}
public void AddResource(string name, byte[] value)
{
if (name == null)
throw new ArgumentNullException("name");
if (value == null)
throw new ArgumentNullException("value");
if (written)
throw new InvalidOperationException("The resource is already generated.");
if (writer == null)
InitWriter();
WriteBytes(name, value.GetType(), value, null);
}
public void AddResource(string name, object value)
{
AddResource(name, value, String.Empty);
}
private void AddResource(string name, object value, string comment)
{
if (value is string) {
AddResource(name, (string)value, comment);
return;
}
if (name == null)
throw new ArgumentNullException("name");
if (written)
throw new InvalidOperationException("The resource is already generated.");
if (writer == null)
InitWriter();
if (value is byte[]) {
WriteBytes(name, value.GetType(), (byte[])value, comment);
return;
}
if (value is ResourceSerializedObject rso) {
var bytes = rso.GetBytes();
WriteBytes(name, null, bytes, 0, bytes.Length, comment);
return;
}
if (value == null) {
// nulls written as ResXNullRef
WriteString(name, "", ResXNullRefTypeName, comment);
return;
}
if (value != null && !value.GetType().IsSerializable)
throw new InvalidOperationException(String.Format("The element '{0}' of type '{1}' is not serializable.", name, value.GetType().Name));
TypeConverter converter = TypeDescriptor.GetConverter(value);
if (converter != null && converter.CanConvertTo(typeof(string)) && converter.CanConvertFrom(typeof(string))) {
string str = (string)converter.ConvertToInvariantString(value);
WriteString(name, str, value.GetType().AssemblyQualifiedName, comment);
return;
}
if (converter != null && converter.CanConvertTo(typeof(byte[])) && converter.CanConvertFrom(typeof(byte[]))) {
byte[] b = (byte[])converter.ConvertTo(value, typeof(byte[]));
WriteBytes(name, value.GetType(), b, comment);
return;
}
MemoryStream ms = new MemoryStream();
BinaryFormatter fmt = new BinaryFormatter();
try {
fmt.Serialize(ms, value);
} catch (Exception e) {
throw new InvalidOperationException("Cannot add a " + value.GetType() +
"because it cannot be serialized: " +
e.Message);
}
WriteBytes(name, null, ms.GetBuffer(), 0, (int)ms.Length, comment);
ms.Close();
}
public void AddResource(string name, string value)
{
AddResource(name, value, string.Empty);
}
private void AddResource(string name, string value, string comment)
{
if (name == null)
throw new ArgumentNullException("name");
if (value == null)
throw new ArgumentNullException("value");
if (written)
throw new InvalidOperationException("The resource is already generated.");
if (writer == null)
InitWriter();
WriteString(name, value, null, comment);
}
public void AddMetadata(string name, string value)
{
if (name == null)
throw new ArgumentNullException("name");
if (value == null)
throw new ArgumentNullException("value");
if (written)
throw new InvalidOperationException("The resource is already generated.");
if (writer == null)
InitWriter();
writer.WriteStartElement("metadata");
writer.WriteAttributeString("name", name);
writer.WriteAttributeString("xml:space", "preserve");
writer.WriteElementString("value", value);
writer.WriteEndElement();
}
public void AddMetadata(string name, byte[] value)
{
if (name == null)
throw new ArgumentNullException("name");
if (value == null)
throw new ArgumentNullException("value");
if (written)
throw new InvalidOperationException("The resource is already generated.");
if (writer == null)
InitWriter();
writer.WriteStartElement("metadata");
writer.WriteAttributeString("name", name);
writer.WriteAttributeString("type", value.GetType().AssemblyQualifiedName);
writer.WriteStartElement("value");
WriteNiceBase64(value, 0, value.Length);
writer.WriteEndElement();
writer.WriteEndElement();
}
public void AddMetadata(string name, object value)
{
if (value is string) {
AddMetadata(name, (string)value);
return;
}
if (value is byte[]) {
AddMetadata(name, (byte[])value);
return;
}
if (name == null)
throw new ArgumentNullException("name");
if (value == null)
throw new ArgumentNullException("value");
if (!value.GetType().IsSerializable)
throw new InvalidOperationException(String.Format("The element '{0}' of type '{1}' is not serializable.", name, value.GetType().Name));
if (written)
throw new InvalidOperationException("The resource is already generated.");
if (writer == null)
InitWriter();
Type type = value.GetType();
TypeConverter converter = TypeDescriptor.GetConverter(value);
if (converter != null && converter.CanConvertTo(typeof(string)) && converter.CanConvertFrom(typeof(string))) {
string str = (string)converter.ConvertToInvariantString(value);
writer.WriteStartElement("metadata");
writer.WriteAttributeString("name", name);
if (type != null)
writer.WriteAttributeString("type", type.AssemblyQualifiedName);
writer.WriteStartElement("value");
writer.WriteString(str);
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteWhitespace("\n ");
return;
}
if (converter != null && converter.CanConvertTo(typeof(byte[])) && converter.CanConvertFrom(typeof(byte[]))) {
byte[] b = (byte[])converter.ConvertTo(value, typeof(byte[]));
writer.WriteStartElement("metadata");
writer.WriteAttributeString("name", name);
if (type != null) {
writer.WriteAttributeString("type", type.AssemblyQualifiedName);
writer.WriteAttributeString("mimetype", ByteArraySerializedObjectMimeType);
writer.WriteStartElement("value");
WriteNiceBase64(b, 0, b.Length);
} else {
writer.WriteAttributeString("mimetype", BinSerializedObjectMimeType);
writer.WriteStartElement("value");
writer.WriteBase64(b, 0, b.Length);
}
writer.WriteEndElement();
writer.WriteEndElement();
return;
}
MemoryStream ms = new MemoryStream();
BinaryFormatter fmt = new BinaryFormatter();
try {
fmt.Serialize(ms, value);
} catch (Exception e) {
throw new InvalidOperationException("Cannot add a " + value.GetType() +
"because it cannot be serialized: " +
e.Message);
}
writer.WriteStartElement("metadata");
writer.WriteAttributeString("name", name);
if (type != null) {
writer.WriteAttributeString("type", type.AssemblyQualifiedName);
writer.WriteAttributeString("mimetype", ByteArraySerializedObjectMimeType);
writer.WriteStartElement("value");
WriteNiceBase64(ms.GetBuffer(), 0, ms.GetBuffer().Length);
} else {
writer.WriteAttributeString("mimetype", BinSerializedObjectMimeType);
writer.WriteStartElement("value");
writer.WriteBase64(ms.GetBuffer(), 0, ms.GetBuffer().Length);
}
writer.WriteEndElement();
writer.WriteEndElement();
ms.Close();
}
public void Close()
{
if (!written) {
Generate();
}
if (writer != null) {
writer.Close();
stream = null;
filename = null;
textwriter = null;
}
}
public virtual void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public void Generate()
{
if (written)
throw new InvalidOperationException("The resource is already generated.");
written = true;
writer.WriteEndElement();
writer.Flush();
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
Close();
}
static string schema = @"
<xsd:schema id='root' xmlns='' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:msdata='urn:schemas-microsoft-com:xml-msdata'>
<xsd:element name='root' msdata:IsDataSet='true'>
<xsd:complexType>
<xsd:choice maxOccurs='unbounded'>
<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' msdata:Ordinal='1' />
<xsd:attribute name='type' type='xsd:string' msdata:Ordinal='3' />
<xsd:attribute name='mimetype' type='xsd:string' msdata:Ordinal='4' />
</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>
".Replace("'", "\"").Replace("\t", " ");
#region Public Properties
public string BasePath {
get { return base_path; }
set { base_path = value; }
}
#endregion
}
}

526
ICSharpCode.Decompiler/Util/ResourcesFile.cs

@ -0,0 +1,526 @@ @@ -0,0 +1,526 @@
// Copyright (c) 2018 Daniel Grunwald
// Based on the .NET Core ResourceReader; make available under the MIT license
// by the .NET Foundation.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
namespace ICSharpCode.Decompiler.Util
{
/// <summary>
/// .resources file.
/// </summary>
public class ResourcesFile : IEnumerable<KeyValuePair<string, object>>, IDisposable
{
sealed class MyBinaryReader : BinaryReader
{
public MyBinaryReader(Stream input, bool leaveOpen) : base(input, Encoding.UTF8, leaveOpen)
{
}
// upgrade from protected to public visibility
public new int Read7BitEncodedInt()
{
return base.Read7BitEncodedInt();
}
public void Seek(long pos, SeekOrigin origin)
{
BaseStream.Seek(pos, origin);
}
}
enum ResourceTypeCode
{
Null = 0,
String = 1,
Boolean = 2,
Char = 3,
Byte = 4,
SByte = 5,
Int16 = 6,
UInt16 = 7,
Int32 = 8,
UInt32 = 9,
Int64 = 10,
UInt64 = 11,
Single = 12,
Double = 13,
Decimal = 14,
DateTime = 0xF,
TimeSpan = 0x10,
LastPrimitive = 0x10,
ByteArray = 0x20,
Stream = 33,
StartOfUserTypes = 0x40
}
/// <summary>Holds the number used to identify resource files.</summary>
public const int MagicNumber = unchecked((int)0xBEEFCACE);
const int ResourceSetVersion = 2;
readonly MyBinaryReader reader;
readonly int version;
readonly int numResources;
readonly string[] typeTable;
readonly int[] namePositions;
readonly long fileStartPosition;
readonly long nameSectionPosition;
readonly long dataSectionPosition;
long[] startPositions;
/// <summary>
/// Creates a new ResourcesFile.
/// </summary>
/// <param name="stream">Input stream.</param>
/// <param name="leaveOpen">Whether the stream should be help open when the ResourcesFile is disposed.</param>
/// <remarks>
/// The stream is must be held open while the ResourcesFile is in use.
/// The stream must be seekable; any operation using the ResourcesFile will end up seeking the stream.
/// </remarks>
public ResourcesFile(Stream stream, bool leaveOpen = true)
{
fileStartPosition = stream.Position;
reader = new MyBinaryReader(stream, leaveOpen);
const string ResourcesHeaderCorrupted = "Resources header corrupted.";
// Read ResourceManager header
// Check for magic number
int magicNum = reader.ReadInt32();
if (magicNum != MagicNumber)
throw new BadImageFormatException("Not a .resources file - invalid magic number");
// Assuming this is ResourceManager header V1 or greater, hopefully
// after the version number there is a number of bytes to skip
// to bypass the rest of the ResMgr header. For V2 or greater, we
// use this to skip to the end of the header
int resMgrHeaderVersion = reader.ReadInt32();
int numBytesToSkip = reader.ReadInt32();
if (numBytesToSkip < 0 || resMgrHeaderVersion < 0) {
throw new BadImageFormatException(ResourcesHeaderCorrupted);
}
if (resMgrHeaderVersion > 1) {
reader.BaseStream.Seek(numBytesToSkip, SeekOrigin.Current);
} else {
// We don't care about numBytesToSkip; read the rest of the header
// readerType:
reader.ReadString();
// resourceSetType:
reader.ReadString();
}
// Read RuntimeResourceSet header
// Do file version check
version = reader.ReadInt32();
if (version != ResourceSetVersion && version != 1)
throw new BadImageFormatException($"Unsupported resource set version: {version}");
numResources = reader.ReadInt32();
if (numResources < 0) {
throw new BadImageFormatException(ResourcesHeaderCorrupted);
}
// Read type positions into type positions array.
// But delay initialize the type table.
int numTypes = reader.ReadInt32();
if (numTypes < 0) {
throw new BadImageFormatException(ResourcesHeaderCorrupted);
}
typeTable = new string[numTypes];
for (int i = 0; i < numTypes; i++) {
typeTable[i] = reader.ReadString();
}
// Prepare to read in the array of name hashes
// Note that the name hashes array is aligned to 8 bytes so
// we can use pointers into it on 64 bit machines. (4 bytes
// may be sufficient, but let's plan for the future)
// Skip over alignment stuff. All public .resources files
// should be aligned No need to verify the byte values.
long pos = reader.BaseStream.Position - fileStartPosition;
int alignBytes = unchecked((int)pos) & 7;
if (alignBytes != 0) {
for (int i = 0; i < 8 - alignBytes; i++) {
reader.ReadByte();
}
}
// Skip over the array of name hashes
try {
reader.Seek(checked(4 * numResources), SeekOrigin.Current);
} catch (OverflowException) {
throw new BadImageFormatException(ResourcesHeaderCorrupted);
}
// Read in the array of relative positions for all the names.
namePositions = new int[numResources];
for (int i = 0; i < numResources; i++) {
int namePosition = reader.ReadInt32();
if (namePosition < 0) {
throw new BadImageFormatException(ResourcesHeaderCorrupted);
}
namePositions[i] = namePosition;
}
// Read location of data section.
int dataSectionOffset = reader.ReadInt32();
if (dataSectionOffset < 0) {
throw new BadImageFormatException(ResourcesHeaderCorrupted);
}
// Store current location as start of name section
nameSectionPosition = reader.BaseStream.Position;
dataSectionPosition = fileStartPosition + dataSectionOffset;
// _nameSectionOffset should be <= _dataSectionOffset; if not, it's corrupt
if (dataSectionPosition < nameSectionPosition) {
throw new BadImageFormatException(ResourcesHeaderCorrupted);
}
}
public void Dispose()
{
reader.Dispose();
}
public int ResourceCount => numResources;
public string GetResourceName(int index)
{
return GetResourceName(index, out _);
}
int GetResourceDataOffset(int index)
{
GetResourceName(index, out int dataOffset);
return dataOffset;
}
string GetResourceName(int index, out int dataOffset)
{
long pos = nameSectionPosition + namePositions[index];
byte[] bytes;
lock (reader) {
reader.Seek(pos, SeekOrigin.Begin);
// Can't use reader.ReadString, since it's using UTF-8!
int byteLen = reader.Read7BitEncodedInt();
if (byteLen < 0) {
throw new BadImageFormatException("Resource name has negative length");
}
bytes = new byte[byteLen];
// We must read byteLen bytes, or we have a corrupted file.
// Use a blocking read in case the stream doesn't give us back
// everything immediately.
int count = byteLen;
while (count > 0) {
int n = reader.Read(bytes, byteLen - count, count);
if (n == 0)
throw new BadImageFormatException("End of stream within a resource name");
count -= n;
}
dataOffset = reader.ReadInt32();
if (dataOffset < 0) {
throw new BadImageFormatException("Negative data offset");
}
}
return Encoding.Unicode.GetString(bytes);
}
internal bool AllEntriesAreStreams()
{
if (version != 2)
return false;
lock (reader) {
for (int i = 0; i < numResources; i++) {
int dataOffset = GetResourceDataOffset(i);
reader.Seek(dataSectionPosition + dataOffset, SeekOrigin.Begin);
var typeCode = (ResourceTypeCode)reader.Read7BitEncodedInt();
if (typeCode != ResourceTypeCode.Stream)
return false;
}
}
return true;
}
object LoadObject(int dataOffset)
{
try {
lock (reader) {
if (version == 1) {
return LoadObjectV1(dataOffset);
} else {
return LoadObjectV2(dataOffset);
}
}
} catch (EndOfStreamException e) {
throw new BadImageFormatException("Invalid resource file", e);
}
}
string FindType(int typeIndex)
{
if (typeIndex < 0 || typeIndex >= typeTable.Length)
throw new BadImageFormatException("Type index out of bounds");
return typeTable[typeIndex];
}
// This takes a virtual offset into the data section and reads an Object
// from that location.
// Anyone who calls LoadObject should make sure they take a lock so
// no one can cause us to do a seek in here.
private object LoadObjectV1(int dataOffset)
{
Debug.Assert(System.Threading.Monitor.IsEntered(reader));
reader.Seek(dataSectionPosition + dataOffset, SeekOrigin.Begin);
int typeIndex = reader.Read7BitEncodedInt();
if (typeIndex == -1)
return null;
string typeName = FindType(typeIndex);
int comma = typeName.IndexOf(',');
if (comma > 0) {
// strip assembly name
typeName = typeName.Substring(0, comma);
}
switch (typeName) {
case "System.String":
return reader.ReadString();
case "System.Byte":
return reader.ReadByte();
case "System.SByte":
return reader.ReadSByte();
case "System.Int16":
return reader.ReadInt16();
case "System.UInt16":
return reader.ReadUInt16();
case "System.Int32":
return reader.ReadInt32();
case "System.UInt32":
return reader.ReadUInt32();
case "System.Int64":
return reader.ReadInt64();
case "System.UInt64":
return reader.ReadUInt64();
case "System.Single":
return reader.ReadSingle();
case "System.Double":
return reader.ReadDouble();
case "System.DateTime":
// Ideally we should use DateTime's ToBinary & FromBinary,
// but we can't for compatibility reasons.
return new DateTime(reader.ReadInt64());
case "System.TimeSpan":
return new TimeSpan(reader.ReadInt64());
case "System.Decimal":
int[] bits = new int[4];
for (int i = 0; i < bits.Length; i++)
bits[i] = reader.ReadInt32();
return new decimal(bits);
default:
return new ResourceSerializedObject(FindType(typeIndex), this, reader.BaseStream.Position);
}
}
private object LoadObjectV2(int dataOffset)
{
Debug.Assert(System.Threading.Monitor.IsEntered(reader));
reader.Seek(dataSectionPosition + dataOffset, SeekOrigin.Begin);
var typeCode = (ResourceTypeCode)reader.Read7BitEncodedInt();
switch (typeCode) {
case ResourceTypeCode.Null:
return null;
case ResourceTypeCode.String:
return reader.ReadString();
case ResourceTypeCode.Boolean:
return reader.ReadBoolean();
case ResourceTypeCode.Char:
return (char)reader.ReadUInt16();
case ResourceTypeCode.Byte:
return reader.ReadByte();
case ResourceTypeCode.SByte:
return reader.ReadSByte();
case ResourceTypeCode.Int16:
return reader.ReadInt16();
case ResourceTypeCode.UInt16:
return reader.ReadUInt16();
case ResourceTypeCode.Int32:
return reader.ReadInt32();
case ResourceTypeCode.UInt32:
return reader.ReadUInt32();
case ResourceTypeCode.Int64:
return reader.ReadInt64();
case ResourceTypeCode.UInt64:
return reader.ReadUInt64();
case ResourceTypeCode.Single:
return reader.ReadSingle();
case ResourceTypeCode.Double:
return reader.ReadDouble();
case ResourceTypeCode.Decimal:
return reader.ReadDecimal();
case ResourceTypeCode.DateTime:
// Use DateTime's ToBinary & FromBinary.
long data = reader.ReadInt64();
return DateTime.FromBinary(data);
case ResourceTypeCode.TimeSpan:
long ticks = reader.ReadInt64();
return new TimeSpan(ticks);
// Special types
case ResourceTypeCode.ByteArray: {
int len = reader.ReadInt32();
if (len < 0) {
throw new BadImageFormatException("Resource with negative length");
}
return reader.ReadBytes(len);
}
case ResourceTypeCode.Stream: {
int len = reader.ReadInt32();
if (len < 0) {
throw new BadImageFormatException("Resource with negative length");
}
byte[] bytes = reader.ReadBytes(len);
return new MemoryStream(bytes, writable: false);
}
default:
if (typeCode < ResourceTypeCode.StartOfUserTypes) {
throw new BadImageFormatException("Invalid typeCode");
}
return new ResourceSerializedObject(FindType(typeCode - ResourceTypeCode.StartOfUserTypes), this, reader.BaseStream.Position);
}
}
public object GetResourceValue(int index)
{
GetResourceName(index, out int dataOffset);
return LoadObject(dataOffset);
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
for (int i = 0; i < numResources; i++) {
string name = GetResourceName(i, out int dataOffset);
object val = LoadObject(dataOffset);
yield return new KeyValuePair<string, object>(name, val);
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
long[] GetStartPositions()
{
long[] positions = LazyInit.VolatileRead(ref startPositions);
if (positions != null)
return positions;
lock (reader) {
// double-checked locking
positions = LazyInit.VolatileRead(ref startPositions);
if (positions != null)
return positions;
positions = new long[numResources * 2];
int outPos = 0;
for (int i = 0; i < numResources; i++) {
positions[outPos++] = nameSectionPosition + namePositions[i];
positions[outPos++] = dataSectionPosition + GetResourceDataOffset(i);
}
Array.Sort(positions);
return LazyInit.GetOrSet(ref startPositions, positions);
}
}
internal byte[] GetBytesForSerializedObject(long pos)
{
long[] positions = GetStartPositions();
int i = Array.BinarySearch(positions, pos);
if (i < 0) {
// 'pos' the the start position of the serialized object data
// This is the position after the type code, so it should not appear in the 'positions' array.
// Set i to the index of the next position after 'pos'.
i = ~i;
// Note: if 'pos' does exist in the array, that means the stream has length 0,
// so we keep the i that we found.
}
lock (reader) {
long endPos;
if (i == positions.Length) {
endPos = reader.BaseStream.Length;
} else {
endPos = positions[i];
}
int len = (int)(endPos - pos);
reader.Seek(pos, SeekOrigin.Begin);
return reader.ReadBytes(len);
}
}
}
public class ResourceSerializedObject
{
public string TypeName { get; }
readonly ResourcesFile file;
readonly long position;
internal ResourceSerializedObject(string typeName, ResourcesFile file, long position)
{
this.TypeName = typeName;
this.file = file;
this.position = position;
}
/// <summary>
/// Gets a stream that starts with the serialized object data.
/// </summary>
public Stream GetStream()
{
return new MemoryStream(file.GetBytesForSerializedObject(position), writable: false);
}
/// <summary>
/// Gets the serialized object data.
/// </summary>
public byte[] GetBytes()
{
return file.GetBytesForSerializedObject(position);
}
}
}

10
ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs

@ -5,10 +5,10 @@ using System; @@ -5,10 +5,10 @@ using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Resources;
using System.Threading;
using System.Xml.Linq;
using ICSharpCode.Decompiler.Tests.Helpers;
using ICSharpCode.Decompiler.Util;
using Mono.Cecil;
using NUnit.Framework;
@ -143,14 +143,14 @@ namespace ILSpy.BamlDecompiler.Tests @@ -143,14 +143,14 @@ namespace ILSpy.BamlDecompiler.Tests
if (er != null) {
Stream s = er.GetResourceStream();
s.Position = 0;
ResourceReader reader;
ResourcesFile resources;
try {
reader = new ResourceReader(s);
resources = new ResourcesFile(s);
} catch (ArgumentException) {
return null;
}
foreach (DictionaryEntry entry in reader.Cast<DictionaryEntry>().OrderBy(e => e.Key.ToString())) {
if (entry.Key.ToString() == name) {
foreach (var entry in resources.OrderBy(e => e.Key)) {
if (entry.Key == name) {
if (entry.Value is Stream)
return (Stream)entry.Value;
if (entry.Value is byte[])

9
ILSpy/Languages/CSharpLanguage.cs

@ -17,12 +17,10 @@ @@ -17,12 +17,10 @@
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Resources;
using ICSharpCode.Decompiler;
using Mono.Cecil;
@ -34,6 +32,7 @@ using System.Windows; @@ -34,6 +32,7 @@ using System.Windows;
using System.Windows.Controls;
using ICSharpCode.ILSpy.TreeNodes;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.Util;
namespace ICSharpCode.ILSpy
{
@ -400,11 +399,11 @@ namespace ICSharpCode.ILSpy @@ -400,11 +399,11 @@ namespace ICSharpCode.ILSpy
protected override IEnumerable<Tuple<string, string>> WriteResourceToFile(string fileName, string resourceName, Stream entryStream)
{
if (fileName.EndsWith(".resource", StringComparison.OrdinalIgnoreCase)) {
using (ResourceReader reader = new ResourceReader(entryStream))
fileName = Path.ChangeExtension(fileName, ".resx");
using (FileStream fs = new FileStream(Path.Combine(targetDirectory, fileName), FileMode.Create, FileAccess.Write))
using (ResXResourceWriter writer = new ResXResourceWriter(fs)) {
foreach (DictionaryEntry entry in reader) {
writer.AddResource((string)entry.Key, entry.Value);
foreach (var entry in new ResourcesFile(entryStream)) {
writer.AddResource(entry.Key, entry.Value);
}
}
return new[] { Tuple.Create("EmbeddedResource", fileName) };

42
ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs

@ -17,15 +17,12 @@ @@ -17,15 +17,12 @@
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Resources;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Util;
using ICSharpCode.ILSpy.Controls;
using ICSharpCode.ILSpy.TextView;
using Microsoft.Win32;
@ -73,44 +70,40 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -73,44 +70,40 @@ namespace ICSharpCode.ILSpy.TreeNodes
if (er != null) {
Stream s = er.GetResourceStream();
s.Position = 0;
ResourceReader reader;
try {
reader = new ResourceReader(s);
}
catch (ArgumentException) {
return;
}
foreach (DictionaryEntry entry in reader.Cast<DictionaryEntry>().OrderBy(e => e.Key.ToString())) {
ProcessResourceEntry(entry);
foreach (var entry in new ResourcesFile(s)) {
ProcessResourceEntry(entry);
}
} catch (BadImageFormatException) {
// ignore errors
}
}
}
private void ProcessResourceEntry(DictionaryEntry entry)
private void ProcessResourceEntry(KeyValuePair<string, object> entry)
{
var keyString = entry.Key.ToString();
if (entry.Value is String) {
stringTableEntries.Add(new KeyValuePair<string, string>(keyString, (string)entry.Value));
stringTableEntries.Add(new KeyValuePair<string, string>(entry.Key, (string)entry.Value));
return;
}
if (entry.Value is byte[]) {
Children.Add(ResourceEntryNode.Create(keyString, new MemoryStream((byte[])entry.Value)));
Children.Add(ResourceEntryNode.Create(entry.Key, new MemoryStream((byte[])entry.Value)));
return;
}
var node = ResourceEntryNode.Create(keyString, entry.Value);
var node = ResourceEntryNode.Create(entry.Key, entry.Value);
if (node != null) {
Children.Add(node);
return;
}
string entryType = entry.Value.GetType().FullName;
if (entry.Value is System.Globalization.CultureInfo) {
otherEntries.Add(new SerializedObjectRepresentation(keyString, entryType, ((System.Globalization.CultureInfo)entry.Value).DisplayName));
if (entry.Value == null) {
otherEntries.Add(new SerializedObjectRepresentation(entry.Key, "null", ""));
} else if (entry.Value is ResourceSerializedObject so) {
otherEntries.Add(new SerializedObjectRepresentation(entry.Key, so.TypeName, "<serialized>"));
} else {
otherEntries.Add(new SerializedObjectRepresentation(keyString, entryType, entry.Value.ToString()));
otherEntries.Add(new SerializedObjectRepresentation(entry.Key, entry.Value.GetType().FullName, entry.Value.ToString()));
}
}
@ -131,10 +124,9 @@ namespace ICSharpCode.ILSpy.TreeNodes @@ -131,10 +124,9 @@ namespace ICSharpCode.ILSpy.TreeNodes
}
break;
case 2:
var reader = new ResourceReader(s);
using (var writer = new ResXResourceWriter(dlg.OpenFile())) {
foreach (DictionaryEntry entry in reader) {
writer.AddResource(entry.Key.ToString(), entry.Value);
foreach (var entry in new ResourcesFile(s)) {
writer.AddResource(entry.Key, entry.Value);
}
}
break;

99
doc/Resources.txt

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
System.Resources.ResourceReader is unsuitable for deserializing resources in the ILSpy context,
because it tries to deserialize custom resource types, which fails if the types are in a non-GAC assembly.
So we are instead using our own "class ResourcesFile", which is based on the
.NET Core ResourceReader implementation.
struct ResourcesFileFormat {
int32 magicNum; [check == ResourceManager.MagicNumber]
// ResourceManager header:
int32 resMgrHeaderVersion; [check >= 0]
int32 numBytesToSkip; [check >= 0]
if (resMgrHeaderVersion <= 1) {
string readerType;
string resourceSetType;
} else {
byte _[numBytesToSkip];
}
// RuntimeResourceSet header:
int32 version; [check in (1, 2)]
int32 numResources; [check >=0]
int32 numTypes; [check >=0]
string typeName[numTypes];
.align 8;
int32 nameHashes[numResources];
int32 namePositions[numResources]; [check >= 0]
int32 dataSectionOffset; [check >= current position in file]
byte remainderOfFile[];
}
// normal strings in this file format are stored as:
struct string {
compressedint len;
byte value[len]; // interpret as UTF-8
}
// NameEntry #i is stored starting at remainderOfFile[namePositions[i]]
// (that is, namePositions is interpreted relative to the start of the remainderOfFile array)
struct NameEntry {
compressedint len;
byte name[len]; // interpret as UTF-16
int32 dataOffset; [check >= 0]
}
// Found at position ResourcesFileFormat.dataSectionOffset+NameEntry.dataOffset in the file.
struct ValueEntry {
if (version == 1) {
compressedint typeIndex;
if (typeIndex == -1) {
// no value stored; value is implicitly null
} else {
switch (typeName[typeIndex]) {
case string:
case int, uint, long, ulong, sbyte, byte, short, ushort:
case float:
case double:
T value; // value directly stored
case DateTime:
int64 value; // new DateTime(_store.ReadInt64())
case TimeSpan:
int64 value; // new TimeSpan(_store.ReadInt64())
case decimal:
int32 value[4];
default:
byte data[...]; // BinaryFormatter-serialized data using typeName[typeIndex]
}
}
} else if (version == 2) {
compressedint typeCode;
// note: unlike v1, no lookup into the typeName array!
switch (typeCode) {
case null:
// no value stored; value is implicitly null
case string:
case bool:
case int, uint, long, ulong, sbyte, byte, short, ushort:
T value;
case char:
uint16 value;
case float:
case double:
case decimal:
T value;
case DateTime:
int64 value; // DateTime.FromBinary(_store.ReadInt64())
case TimeSpan:
int64 value; // new TimeSpan(_store.ReadInt64())
case ResourceTypeCode.ByteArray:
int32 len;
byte value[len];
case ResourceTypeCode.Stream:
int32 len;
byte value[len];
case >= ResourceTypeCode.StartOfUserTypes:
byte data[...]; // BinaryFormatter-serialized data using typeName[typeCode - ResourceTypeCode.StartOfUserTypes]
}
}
}
Loading…
Cancel
Save