From c17c3c739f339563749f73f0a4f2d1d65516c797 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sat, 30 Jun 2018 16:13:03 +0200 Subject: [PATCH] Fix #1192: Use custom ResourcesFile implementation This avoids deserializing objects embedded in .resources files within the ILSpy process. --- .../CSharp/WholeProjectDecompiler.cs | 62 ++- .../ICSharpCode.Decompiler.csproj | 1 + ICSharpCode.Decompiler/Util/ResourcesFile.cs | 466 ++++++++++++++++++ ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs | 1 - ILSpy/Languages/CSharpLanguage.cs | 8 +- .../ResourceNodes/ResourcesFileTreeNode.cs | 39 +- doc/Resources.txt | 99 ++++ 7 files changed, 623 insertions(+), 53 deletions(-) create mode 100644 ICSharpCode.Decompiler/Util/ResourcesFile.cs create mode 100644 doc/Resources.txt diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs index 5fee20b49..0de65b57e 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs @@ -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; using ICSharpCode.Decompiler.Util; using Mono.Cecil; using System.Threading; +using System.Text; namespace ICSharpCode.Decompiler.CSharp { @@ -330,20 +330,29 @@ namespace ICSharpCode.Decompiler.CSharp Stream stream = r.GetResourceStream(); stream.Position = 0; - IEnumerable 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; + 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; + 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; } - } else { + } catch (BadImageFormatException) { + decodedIntoIndividualFiles = false; + } + if (!decodedIntoIndividualFiles) { stream.Position = 0; string fileName = Path.ChangeExtension(GetFileNameForResource(r.Name), ".resource"); WriteResourceToFile(fileName, r.Name, stream); @@ -381,17 +390,6 @@ namespace ICSharpCode.Decompiler.CSharp } return fileName; } - - bool GetEntries(Stream stream, out IEnumerable entries) - { - try { - entries = new ResourceSet(stream).Cast(); - return true; - } catch (ArgumentException) { - entries = null; - return false; - } - } #endregion /// @@ -406,9 +404,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) diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index c84c31bbf..cfc4fdcec 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -336,6 +336,7 @@ + diff --git a/ICSharpCode.Decompiler/Util/ResourcesFile.cs b/ICSharpCode.Decompiler/Util/ResourcesFile.cs new file mode 100644 index 000000000..1eb19d22c --- /dev/null +++ b/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 +{ + /// + /// .resources file. + /// + public class ResourcesFile : IEnumerable>, 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 + } + + /// Holds the number used to identify resource files. + 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; + + /// + /// Creates a new ResourcesFile. + /// + /// Input stream. + /// Whether the stream should be help open when the ResourcesFile is disposed. + /// + /// 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. + /// + 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> GetEnumerator() + { + for (int i = 0; i < numResources; i++) { + string name = GetResourceName(i, out int dataOffset); + object val = LoadObject(dataOffset); + yield return new KeyValuePair(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; + } + + /// + /// Gets a stream that starts with the serialized object data. + /// + public Stream GetStream() + { + stream.Seek(position, SeekOrigin.Begin); + return stream; + } + } +} diff --git a/ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs b/ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs index 806891b49..a44d75149 100644 --- a/ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs +++ b/ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs @@ -5,7 +5,6 @@ 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; diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 308a1ed5c..1e852e313 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -22,7 +22,6 @@ 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 +33,8 @@ using System.Windows; using System.Windows.Controls; using ICSharpCode.ILSpy.TreeNodes; using ICSharpCode.Decompiler.CSharp.Transforms; +using System.Resources; +using ICSharpCode.Decompiler.Util; namespace ICSharpCode.ILSpy { @@ -400,11 +401,10 @@ namespace ICSharpCode.ILSpy protected override IEnumerable> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) { 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 (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) }; diff --git a/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs b/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs index b9512a771..2a80c215a 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs @@ -24,8 +24,8 @@ 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 +73,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().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 entry) { - var keyString = entry.Key.ToString(); - if (entry.Value is String) { - stringTableEntries.Add(new KeyValuePair(keyString, (string)entry.Value)); + stringTableEntries.Add(new KeyValuePair(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, "")); } 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; 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; diff --git a/doc/Resources.txt b/doc/Resources.txt new file mode 100644 index 000000000..a7b2f4df8 --- /dev/null +++ b/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] + } + } +} \ No newline at end of file