diff --git a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/ForLoopTests.xml b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/ForLoopTests.xml index aebd4ff9f..1d41df003 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/ForLoopTests.xml +++ b/ICSharpCode.Decompiler.Tests/TestCases/PdbGen/ForLoopTests.xml @@ -1,7 +1,7 @@ - - - directories = new HashSet(Platform.FileNameComparer); - readonly IProjectFileWriter projectWriter; public void DecompileProject(PEFile moduleDefinition, string targetDirectory, CancellationToken cancellationToken = default(CancellationToken)) @@ -252,7 +253,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler #region WriteResourceFilesInProject protected virtual IEnumerable<(string itemType, string fileName)> WriteResourceFilesInProject(Metadata.PEFile module) { - foreach (var r in module.Resources.Where(r => r.ResourceType == Metadata.ResourceType.Embedded)) + foreach (var r in module.Resources.Where(r => r.ResourceType == ResourceType.Embedded)) { Stream stream = r.TryOpenStream(); stream.Position = 0; @@ -268,7 +269,8 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { foreach (var (name, value) in resourcesFile) { - string fileName = Path.Combine(name.Split('/').Select(p => CleanUpFileName(p)).ToArray()); + string fileName = CleanUpFileName(name) + .Replace('/', Path.DirectorySeparatorChar); string dirName = Path.GetDirectoryName(fileName); if (!string.IsNullOrEmpty(dirName) && directories.Add(dirName)) { @@ -359,8 +361,15 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler string GetFileNameForResource(string fullName) { - string[] splitName = fullName.Split('.'); - string fileName = CleanUpFileName(fullName); + // Clean up the name first and ensure the length does not exceed the maximum length + // supported by the OS. + fullName = CleanUpDirectoryName(fullName); + // The purpose of the below algorithm is to "maximize" the directory name and "minimize" the file name. + // That is, a full name of the form "Namespace1.Namespace2{...}.NamespaceN.ResourceName" is split such that + // the directory part Namespace1\Namespace2\... reuses as many existing directories as + // possible, and only the remaining name parts are used as prefix for the filename. + string[] splitName = fullName.Split(Path.DirectorySeparatorChar); + string fileName = fullName; string separator = Path.DirectorySeparatorChar.ToString(); for (int i = splitName.Length - 1; i > 0; i--) { @@ -368,7 +377,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler if (directories.Contains(ns)) { string name = string.Join(".", splitName, i, splitName.Length - i); - fileName = Path.Combine(ns, CleanUpFileName(name)); + fileName = Path.Combine(ns, name); break; } } @@ -526,10 +535,50 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler } #endregion + static readonly Lazy<(bool longPathsEnabled, int maxPathLength, int maxSegmentLength)> longPathSupport = + new Lazy<(bool longPathsEnabled, int maxPathLength, int maxSegmentLength)>(GetLongPathSupport, isThreadSafe: true); + + static (bool longPathsEnabled, int maxPathLength, int maxSegmentLength) GetLongPathSupport() + { + try + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + case PlatformID.Unix: + return (true, int.MaxValue, int.MaxValue); + case PlatformID.Win32NT: + const string key = @"SYSTEM\CurrentControlSet\Control\FileSystem"; + var fileSystem = Registry.LocalMachine.OpenSubKey(key); + var value = (int?)fileSystem.GetValue("LongPathsEnabled"); + if (value == 1) + { + return (true, int.MaxValue, 255); + } + return (false, 200, 30); + default: + return (false, 200, 30); + } + } + catch + { + return (false, 200, 30); + } + } + /// /// Cleans up a node name for use as a file name. /// public static string CleanUpFileName(string text) + { + return CleanUpFileName(text, separateAtDots: false); + } + + /// + /// Cleans up a node name for use as a file system name. If is active, + /// dots are seen as segment separators. Each segment is limited to 255 characters. + /// + static string CleanUpFileName(string text, bool separateAtDots) { int pos = text.IndexOf(':'); if (pos > 0) @@ -540,16 +589,38 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler text = text.Trim(); // Whitelist allowed characters, replace everything else: StringBuilder b = new StringBuilder(text.Length); + int currentSegmentLength = 0; + var (supportsLongPaths, maxPathLength, maxSegmentLength) = longPathSupport.Value; foreach (var c in text) { + currentSegmentLength++; if (char.IsLetterOrDigit(c) || c == '-' || c == '_') - b.Append(c); + { + // if the current segment exceeds 255 characters, + // skip until the end of the segment. + if (currentSegmentLength <= maxSegmentLength) + b.Append(c); + } else if (c == '.' && b.Length > 0 && b[b.Length - 1] != '.') - b.Append('.'); // allow dot, but never two in a row + { + // if the current segment exceeds 255 characters, + // skip until the end of the segment. + if (separateAtDots || currentSegmentLength <= maxSegmentLength) + b.Append('.'); // allow dot, but never two in a row + + // Reset length at end of segment. + if (separateAtDots) + currentSegmentLength = 0; + } else - b.Append('-'); - if (b.Length >= 200) - break; // limit to 200 chars + { + // if the current segment exceeds 255 characters, + // skip until the end of the segment. + if (currentSegmentLength <= maxSegmentLength) + b.Append('-'); + } + if (b.Length >= maxPathLength && !supportsLongPaths) + break; // limit to 200 chars, if long paths are not supported. } if (b.Length == 0) b.Append('-'); @@ -567,7 +638,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// public static string CleanUpDirectoryName(string text) { - return CleanUpFileName(text).Replace('.', Path.DirectorySeparatorChar); + return CleanUpFileName(text, separateAtDots: true).Replace('.', Path.DirectorySeparatorChar); } static bool IsReservedFileSystemName(string name) diff --git a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs index fdc87c352..23223a805 100644 --- a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs +++ b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs @@ -66,7 +66,7 @@ namespace ICSharpCode.Decompiler.DebugInfo string BuildFileNameFromTypeName(TypeDefinitionHandle handle) { var typeName = handle.GetFullTypeName(reader).TopLevelTypeName; - return Path.Combine(WholeProjectDecompiler.CleanUpFileName(typeName.Namespace), WholeProjectDecompiler.CleanUpFileName(typeName.Name) + ".cs"); + return Path.Combine(WholeProjectDecompiler.CleanUpDirectoryName(typeName.Namespace), WholeProjectDecompiler.CleanUpFileName(typeName.Name) + ".cs"); } foreach (var sourceFile in reader.GetTopLevelTypeDefinitions().GroupBy(BuildFileNameFromTypeName)) diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index b16589e62..fc8b7b9df 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -53,6 +53,7 @@ + diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index d0b19b041..d54fe2675 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -13,6 +13,7 @@ True Images\ILSpy-Large.ico + app.manifest True ..\ICSharpCode.Decompiler\ICSharpCode.Decompiler.snk diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index 48bfc9c98..1fb3da6b4 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -2010,6 +2010,17 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Failed to decompile the assemblies {0} because the namespace directory structure is nested too deep. + /// + ///If you are using Windows 10.0.14393 (Windows 10 version 1607) or later, you can enable "Long path support" by creating a REG_DWORD registry key named "LongPathsEnabled" with value 0x1 at "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem" (see https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation for more information).. + /// + public static string ProjectExportPathTooLong { + get { + return ResourceManager.GetString("ProjectExportPathTooLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to for ex. property getter/setter access. To get optimal decompilation results, please manually add the missing references to the list of loaded assemblies.. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 74fa044d9..7e03f44e0 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -691,6 +691,11 @@ Please disable all filters that might hide the item (i.e. activate "View > Sh A SDK-style project was generated. Learn more at https://docs.microsoft.com/en-us/nuget/resources/check-project-format. + + Failed to decompile the assemblies {0} because the namespace directory structure is nested too deep. + +If you are using Windows 10.0.14393 (Windows 10 version 1607) or later, you can enable "Long path support" by creating a REG_DWORD registry key named "LongPathsEnabled" with value 0x1 at "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem" (see https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation for more information). + for ex. property getter/setter access. To get optimal decompilation results, please manually add the missing references to the list of loaded assemblies. diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index bf2db7517..a02cfe87b 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -229,6 +229,13 @@ namespace ICSharpCode.ILSpy statusOutput.Add("-------------"); statusOutput.Add($"Failed to decompile the assembly '{loadedAssembly.FileName}':{Environment.NewLine}{e.Message}"); } + catch (PathTooLongException e) + { + statusOutput.Add("-------------"); + statusOutput.Add(string.Format(Properties.Resources.ProjectExportPathTooLong, loadedAssembly.FileName) + + Environment.NewLine + Environment.NewLine + + e.ToString()); + } catch (Exception e) when (!(e is OperationCanceledException)) { statusOutput.Add("-------------"); diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index 714f6f9c1..a6fbac1a8 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -1079,26 +1079,40 @@ namespace ICSharpCode.ILSpy.TextView { bool originalProjectFormatSetting = context.Options.DecompilerSettings.UseSdkStyleProjectFormat; context.Options.EscapeInvalidIdentifiers = true; + AvalonEditTextOutput output = new AvalonEditTextOutput { + EnableHyperlinks = true, + Title = string.Join(", ", context.TreeNodes.Select(n => n.Text)) + }; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); - using (StreamWriter w = new StreamWriter(fileName)) + try { - try + using (StreamWriter w = new StreamWriter(fileName)) { - DecompileNodes(context, new PlainTextOutput(w)); - } - catch (OperationCanceledException) - { - w.WriteLine(); - w.WriteLine(Properties.Resources.DecompilationWasCancelled); - throw; + try + { + DecompileNodes(context, new PlainTextOutput(w)); + } + catch (OperationCanceledException) + { + w.WriteLine(); + w.WriteLine(Properties.Resources.DecompilationWasCancelled); + throw; + } + catch (PathTooLongException pathTooLong) when (context.Options.SaveAsProjectDirectory != null) + { + output.WriteLine(Properties.Resources.ProjectExportPathTooLong, string.Join(", ", context.TreeNodes.Select(n => n.Text))); + output.WriteLine(); + output.WriteLine(pathTooLong.ToString()); + tcs.SetResult(output); + return; + } } } - stopwatch.Stop(); - AvalonEditTextOutput output = new AvalonEditTextOutput { - EnableHyperlinks = true, - Title = string.Join(", ", context.TreeNodes.Select(n => n.Text)) - }; + finally + { + stopwatch.Stop(); + } output.WriteLine(Properties.Resources.DecompilationCompleteInF1Seconds, stopwatch.Elapsed.TotalSeconds); if (context.Options.SaveAsProjectDirectory != null) @@ -1106,9 +1120,13 @@ namespace ICSharpCode.ILSpy.TextView output.WriteLine(); bool useSdkStyleProjectFormat = context.Options.DecompilerSettings.UseSdkStyleProjectFormat; if (useSdkStyleProjectFormat) + { output.WriteLine(Properties.Resources.ProjectExportFormatSDKHint); + } else + { output.WriteLine(Properties.Resources.ProjectExportFormatNonSDKHint); + } output.WriteLine(Properties.Resources.ProjectExportFormatChangeSettingHint); if (originalProjectFormatSetting != useSdkStyleProjectFormat) { diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 100eec589..19b4df1b8 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -342,11 +342,11 @@ namespace ICSharpCode.ILSpy.TreeNodes { HandleException(badImage, "This file does not contain a managed assembly."); } - catch (FileNotFoundException fileNotFound) + catch (FileNotFoundException fileNotFound) when (options.SaveAsProjectDirectory == null) { HandleException(fileNotFound, "The file was not found."); } - catch (DirectoryNotFoundException dirNotFound) + catch (DirectoryNotFoundException dirNotFound) when (options.SaveAsProjectDirectory == null) { HandleException(dirNotFound, "The directory was not found."); } diff --git a/ILSpy/app.manifest b/ILSpy/app.manifest index 21bd8c5ff..62208eaa1 100644 --- a/ILSpy/app.manifest +++ b/ILSpy/app.manifest @@ -1,10 +1,10 @@  - - - - - - - - - + + + + - - - - - + + - - + + - - + + - - + + - - + + - - + + - - - - - PerMonitorV2, PerMonitor - True - - - true - - + + + + PerMonitorV2, PerMonitor + True - - - - + True + + + + + + + - - + +