From 1c0e61a4c3964515654910286caf94d04211de8b Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Fri, 29 Jun 2018 20:22:53 +0200 Subject: [PATCH 1/6] Fix #1187: ArgumentException: Illegal characters in path. --- ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs index a36ae5869..5fee20b49 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs @@ -345,7 +345,7 @@ namespace ICSharpCode.Decompiler.CSharp } } else { stream.Position = 0; - string fileName = GetFileNameForResource(Path.ChangeExtension(r.Name, ".resource")); + string fileName = Path.ChangeExtension(GetFileNameForResource(r.Name), ".resource"); WriteResourceToFile(fileName, r.Name, stream); } } else { From c17c3c739f339563749f73f0a4f2d1d65516c797 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sat, 30 Jun 2018 16:13:03 +0200 Subject: [PATCH 2/6] 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 From 22f7e34761ef7cd21b27c5c1ae69eab7c4d25ae0 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sat, 30 Jun 2018 17:24:00 +0200 Subject: [PATCH 3/6] Add Mono's ResXResourceWriter, so that we can write serialized objects in resources back out. --- .../ICSharpCode.Decompiler.csproj | 1 + .../Util/ResXResourceWriter.cs | 540 ++++++++++++++++++ ICSharpCode.Decompiler/Util/ResourcesFile.cs | 84 ++- ILSpy/Languages/CSharpLanguage.cs | 2 - .../ResourceNodes/ResourcesFileTreeNode.cs | 3 - 5 files changed, 613 insertions(+), 17 deletions(-) create mode 100644 ICSharpCode.Decompiler/Util/ResXResourceWriter.cs diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index cfc4fdcec..5f868a243 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -337,6 +337,7 @@ + diff --git a/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs b/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs new file mode 100644 index 000000000..c600830fe --- /dev/null +++ b/ICSharpCode.Decompiler/Util/ResXResourceWriter.cs @@ -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 = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + +".Replace("'", "\"").Replace("\t", " "); + + #region Public Properties + public string BasePath { + get { return base_path; } + set { base_path = value; } + } + #endregion + } +} diff --git a/ICSharpCode.Decompiler/Util/ResourcesFile.cs b/ICSharpCode.Decompiler/Util/ResourcesFile.cs index 1eb19d22c..85e18bf4d 100644 --- a/ICSharpCode.Decompiler/Util/ResourcesFile.cs +++ b/ICSharpCode.Decompiler/Util/ResourcesFile.cs @@ -87,6 +87,7 @@ namespace ICSharpCode.Decompiler.Util readonly long fileStartPosition; readonly long nameSectionPosition; readonly long dataSectionPosition; + long[] startPositions; /// /// Creates a new ResourcesFile. @@ -210,6 +211,12 @@ namespace ICSharpCode.Decompiler.Util 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]; @@ -239,14 +246,14 @@ namespace ICSharpCode.Decompiler.Util } 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) { + 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) @@ -330,7 +337,7 @@ namespace ICSharpCode.Decompiler.Util bits[i] = reader.ReadInt32(); return new decimal(bits); default: - return new ResourceSerializedObject(FindType(typeIndex), reader); + return new ResourceSerializedObject(FindType(typeIndex), this, reader.BaseStream.Position); } } @@ -416,7 +423,7 @@ namespace ICSharpCode.Decompiler.Util if (typeCode < ResourceTypeCode.StartOfUserTypes) { throw new BadImageFormatException("Invalid typeCode"); } - return new ResourceSerializedObject(FindType(typeCode - ResourceTypeCode.StartOfUserTypes), reader); + return new ResourceSerializedObject(FindType(typeCode - ResourceTypeCode.StartOfUserTypes), this, reader.BaseStream.Position); } } @@ -439,19 +446,65 @@ namespace ICSharpCode.Decompiler.Util { 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 Stream stream; + readonly ResourcesFile file; readonly long position; - internal ResourceSerializedObject(string typeName, BinaryReader reader) + internal ResourceSerializedObject(string typeName, ResourcesFile file, long position) { this.TypeName = typeName; - this.stream = reader.BaseStream; - this.position = stream.Position; + this.file = file; + this.position = position; } /// @@ -459,8 +512,15 @@ namespace ICSharpCode.Decompiler.Util /// public Stream GetStream() { - stream.Seek(position, SeekOrigin.Begin); - return stream; + return new MemoryStream(file.GetBytesForSerializedObject(position), writable: false); + } + + /// + /// Gets the serialized object data. + /// + public byte[] GetBytes() + { + return file.GetBytesForSerializedObject(position); } } } diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 1e852e313..1ad580be5 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -17,7 +17,6 @@ // DEALINGS IN THE SOFTWARE. using System; -using System.Collections; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; @@ -33,7 +32,6 @@ 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 diff --git a/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs b/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs index 2a80c215a..da7dc0911 100644 --- a/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs +++ b/ILSpy/TreeNodes/ResourceNodes/ResourcesFileTreeNode.cs @@ -17,13 +17,10 @@ // 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; From 80a4d951a74e3d6e740f399239a1e5e59ee1eea0 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sat, 30 Jun 2018 21:30:45 +0200 Subject: [PATCH 4/6] Use .resx file extension when saving resources in XML format. --- .../CSharp/WholeProjectDecompiler.cs | 14 +++++++++++--- ILSpy/Languages/CSharpLanguage.cs | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs index 0de65b57e..313cc9c16 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs @@ -332,6 +332,7 @@ namespace ICSharpCode.Decompiler.CSharp if (r.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) { bool decodedIntoIndividualFiles; + var individualResources = new List>(); try { var resourcesFile = new ResourcesFile(stream); if (resourcesFile.AllEntriesAreStreams()) { @@ -343,7 +344,8 @@ namespace ICSharpCode.Decompiler.CSharp } Stream entryStream = (Stream)value; entryStream.Position = 0; - WriteResourceToFile(Path.Combine(targetDirectory, fileName), (string)name, entryStream); + individualResources.AddRange( + WriteResourceToFile(Path.Combine(targetDirectory, fileName), (string)name, entryStream)); } decodedIntoIndividualFiles = true; } else { @@ -352,10 +354,16 @@ namespace ICSharpCode.Decompiler.CSharp } catch (BadImageFormatException) { decodedIntoIndividualFiles = false; } - if (!decodedIntoIndividualFiles) { + if (decodedIntoIndividualFiles) { + foreach (var entry in individualResources) { + yield return entry; + } + } else { stream.Position = 0; string fileName = Path.ChangeExtension(GetFileNameForResource(r.Name), ".resource"); - WriteResourceToFile(fileName, r.Name, stream); + foreach (var entry in WriteResourceToFile(fileName, r.Name, stream)) { + yield return entry; + } } } else { string fileName = GetFileNameForResource(r.Name); diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 1ad580be5..b277239c6 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -399,6 +399,7 @@ namespace ICSharpCode.ILSpy protected override IEnumerable> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) { if (fileName.EndsWith(".resource", StringComparison.OrdinalIgnoreCase)) { + 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 (var entry in new ResourcesFile(entryStream)) { From a6e5b82506af49b318f0c7b8cd13baaa574a5d13 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 1 Jul 2018 13:06:18 +0200 Subject: [PATCH 5/6] Set VersionName = null --- ILSpy/Properties/AssemblyInfo.template.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ILSpy/Properties/AssemblyInfo.template.cs b/ILSpy/Properties/AssemblyInfo.template.cs index e6b5f2bb4..fde34be9d 100644 --- a/ILSpy/Properties/AssemblyInfo.template.cs +++ b/ILSpy/Properties/AssemblyInfo.template.cs @@ -42,7 +42,7 @@ internal static class RevisionClass public const string Minor = "2"; public const string Build = "0"; public const string Revision = "$INSERTREVISION$"; - public const string VersionName = "rc"; + public const string VersionName = null; public const string FullVersion = Major + "." + Minor + "." + Build + ".$INSERTREVISION$$INSERTBRANCHPOSTFIX$$INSERTVERSIONNAMEPOSTFIX$"; } From 60726e97e96f0eb966ae720e9bb60a7de97932c0 Mon Sep 17 00:00:00 2001 From: Daniel Grunwald Date: Sun, 1 Jul 2018 14:50:25 +0200 Subject: [PATCH 6/6] Fix BamlDecompiler.Tests --- ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs b/ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs index a44d75149..65557f741 100644 --- a/ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs +++ b/ILSpy.BamlDecompiler.Tests/BamlTestRunner.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Xml.Linq; using ICSharpCode.Decompiler.Tests.Helpers; +using ICSharpCode.Decompiler.Util; using Mono.Cecil; using NUnit.Framework; @@ -142,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().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[])