Browse Source

Fix #1192: Use custom ResourcesFile implementation

This avoids deserializing objects embedded in .resources files within the ILSpy process.
pull/1213/head
Daniel Grunwald 7 years ago
parent
commit
c17c3c739f
  1. 62
      ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs
  2. 1
      ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
  3. 466
      ICSharpCode.Decompiler/Util/ResourcesFile.cs
  4. 1
      ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs
  5. 8
      ILSpy/Languages/CSharpLanguage.cs
  6. 39
      ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs
  7. 99
      doc/Resources.txt

62
ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs

@ -21,7 +21,6 @@ using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Resources;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.CSharp.OutputVisitor;
@ -31,6 +30,7 @@ using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.Decompiler.Util; using ICSharpCode.Decompiler.Util;
using Mono.Cecil; using Mono.Cecil;
using System.Threading; using System.Threading;
using System.Text;
namespace ICSharpCode.Decompiler.CSharp namespace ICSharpCode.Decompiler.CSharp
{ {
@ -330,20 +330,29 @@ namespace ICSharpCode.Decompiler.CSharp
Stream stream = r.GetResourceStream(); Stream stream = r.GetResourceStream();
stream.Position = 0; stream.Position = 0;
IEnumerable<DictionaryEntry> entries;
if (r.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) { if (r.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) {
if (GetEntries(stream, out entries) && entries.All(e => e.Value is Stream)) { bool decodedIntoIndividualFiles;
foreach (var pair in entries) { try {
string fileName = Path.Combine(((string)pair.Key).Split('/').Select(p => CleanUpFileName(p)).ToArray()); var resourcesFile = new ResourcesFile(stream);
string dirName = Path.GetDirectoryName(fileName); if (resourcesFile.AllEntriesAreStreams()) {
if (!string.IsNullOrEmpty(dirName) && directories.Add(dirName)) { foreach (var (name, value) in resourcesFile) {
Directory.CreateDirectory(Path.Combine(targetDirectory, dirName)); 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;
WriteResourceToFile(Path.Combine(targetDirectory, fileName), (string)name, entryStream);
} }
Stream entryStream = (Stream)pair.Value; decodedIntoIndividualFiles = true;
entryStream.Position = 0; } else {
WriteResourceToFile(Path.Combine(targetDirectory, fileName), (string)pair.Key, entryStream); decodedIntoIndividualFiles = false;
} }
} else { } catch (BadImageFormatException) {
decodedIntoIndividualFiles = false;
}
if (!decodedIntoIndividualFiles) {
stream.Position = 0; stream.Position = 0;
string fileName = Path.ChangeExtension(GetFileNameForResource(r.Name), ".resource"); string fileName = Path.ChangeExtension(GetFileNameForResource(r.Name), ".resource");
WriteResourceToFile(fileName, r.Name, stream); WriteResourceToFile(fileName, r.Name, stream);
@ -381,17 +390,6 @@ namespace ICSharpCode.Decompiler.CSharp
} }
return fileName; 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 #endregion
/// <summary> /// <summary>
@ -406,9 +404,21 @@ namespace ICSharpCode.Decompiler.CSharp
if (pos > 0) if (pos > 0)
text = text.Substring(0, pos); text = text.Substring(0, pos);
text = text.Trim(); text = text.Trim();
foreach (char c in Path.GetInvalidFileNameChars()) // Whitelist allowed characters, replace everything else:
text = text.Replace(c, '-'); StringBuilder b = new StringBuilder(text.Length);
return text; 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) public static string GetPlatformName(ModuleDefinition module)

1
ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj

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

466
ICSharpCode.Decompiler/Util/ResourcesFile.cs

@ -0,0 +1,466 @@
// 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;
/// <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 _);
}
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;
for (int i = 0; i < numResources; i++) {
GetResourceName(i, out int dataOffset);
lock (reader) {
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), reader);
}
}
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), reader);
}
}
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();
}
}
public class ResourceSerializedObject
{
public string TypeName { get; }
readonly Stream stream;
readonly long position;
internal ResourceSerializedObject(string typeName, BinaryReader reader)
{
this.TypeName = typeName;
this.stream = reader.BaseStream;
this.position = stream.Position;
}
/// <summary>
/// Gets a stream that starts with the serialized object data.
/// </summary>
public Stream GetStream()
{
stream.Seek(position, SeekOrigin.Begin);
return stream;
}
}
}

1
ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs

@ -5,7 +5,6 @@ using System;
using System.Collections; using System.Collections;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Resources;
using System.Threading; using System.Threading;
using System.Xml.Linq; using System.Xml.Linq;
using ICSharpCode.Decompiler.Tests.Helpers; using ICSharpCode.Decompiler.Tests.Helpers;

8
ILSpy/Languages/CSharpLanguage.cs

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

39
ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs

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

99
doc/Resources.txt

@ -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