443 lines
15 KiB
C#
443 lines
15 KiB
C#
![]() |
// Copyright (c) 2010-2013 AlphaSierraPapa for the SharpDevelop Team
|
||
|
//
|
||
|
// 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.Generic;
|
||
|
using System.Diagnostics;
|
||
|
using System.IO;
|
||
|
using System.Runtime.Serialization;
|
||
|
using System.Text;
|
||
|
using System.Xml;
|
||
|
|
||
|
namespace dnSpy.Contracts.Decompiler.XmlDoc {
|
||
|
/// <summary>
|
||
|
/// Provides documentation from an .xml file (as generated by the Microsoft C# compiler).
|
||
|
/// </summary>
|
||
|
/// <remarks>
|
||
|
/// This class first creates an in-memory index of the .xml file, and then uses that to read only the requested members.
|
||
|
/// This way, we avoid keeping all the documentation in memory.
|
||
|
/// The .xml file is only opened when necessary, the file handle is not kept open all the time.
|
||
|
/// If the .xml file is changed, the index will automatically be recreated.
|
||
|
/// </remarks>
|
||
|
[Serializable]
|
||
|
public class XmlDocumentationProvider : IDeserializationCallback
|
||
|
{
|
||
|
#region Cache
|
||
|
sealed class XmlDocumentationCache
|
||
|
{
|
||
|
readonly KeyValuePair<string, string?>[] entries;
|
||
|
int pos;
|
||
|
|
||
|
public XmlDocumentationCache(int size = 50)
|
||
|
{
|
||
|
if (size <= 0)
|
||
|
throw new ArgumentOutOfRangeException(nameof(size), size, "Value must be positive");
|
||
|
entries = new KeyValuePair<string, string?>[size];
|
||
|
}
|
||
|
|
||
|
internal bool TryGet(string key, out string? value)
|
||
|
{
|
||
|
foreach (var pair in entries) {
|
||
|
if (pair.Key == key) {
|
||
|
value = pair.Value;
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
value = null;
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
internal void Add(string key, string? value)
|
||
|
{
|
||
|
entries[pos++] = new KeyValuePair<string, string?>(key, value);
|
||
|
if (pos == entries.Length)
|
||
|
pos = 0;
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
[Serializable]
|
||
|
struct IndexEntry : IComparable<IndexEntry>
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// Hash code of the documentation tag
|
||
|
/// </summary>
|
||
|
internal readonly int HashCode;
|
||
|
|
||
|
/// <summary>
|
||
|
/// Position in the .xml file where the documentation starts
|
||
|
/// </summary>
|
||
|
internal readonly int PositionInFile;
|
||
|
|
||
|
internal IndexEntry(int hashCode, int positionInFile)
|
||
|
{
|
||
|
HashCode = hashCode;
|
||
|
PositionInFile = positionInFile;
|
||
|
}
|
||
|
|
||
|
public int CompareTo(IndexEntry other) => HashCode.CompareTo(other.HashCode);
|
||
|
}
|
||
|
|
||
|
[NonSerialized]
|
||
|
XmlDocumentationCache cache = new XmlDocumentationCache();
|
||
|
|
||
|
readonly string fileName;
|
||
|
readonly Encoding encoding;
|
||
|
volatile IndexEntry[] index; // SORTED array of index entries
|
||
|
|
||
|
/// <summary>
|
||
|
/// Creates a new XmlDocumentationProvider. Can return null if we couldn't read the file.
|
||
|
/// </summary>
|
||
|
/// <param name="fileName">Name of the .xml file.</param>
|
||
|
/// <returns>null if we couldn't create it</returns>
|
||
|
public static XmlDocumentationProvider? Create(string fileName)
|
||
|
{
|
||
|
if (fileName is null)
|
||
|
return null;
|
||
|
try {
|
||
|
return new XmlDocumentationProvider(fileName);
|
||
|
} catch {
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
#region Constructor / Redirection support
|
||
|
Encoding GetEncoding(Encoding encoding) {
|
||
|
var clone = (Encoding)encoding.Clone();
|
||
|
Debug.Assert(!clone.IsReadOnly);
|
||
|
if (clone.IsReadOnly)
|
||
|
return encoding;
|
||
|
clone.EncoderFallback = new EncoderReplacementFallback("\uFFFD");
|
||
|
clone.DecoderFallback = new DecoderReplacementFallback("\uFFFD");
|
||
|
return clone;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Creates a new XmlDocumentationProvider.
|
||
|
/// </summary>
|
||
|
/// <param name="fileName">Name of the .xml file.</param>
|
||
|
/// <exception cref="IOException">Error reading from XML file (or from redirected file)</exception>
|
||
|
/// <exception cref="XmlException">Invalid XML file</exception>
|
||
|
public XmlDocumentationProvider(string fileName)
|
||
|
{
|
||
|
if (fileName is null)
|
||
|
throw new ArgumentNullException(nameof(fileName));
|
||
|
|
||
|
index = new IndexEntry[0];
|
||
|
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
|
||
|
using (XmlTextReader xmlReader = new XmlTextReader(fs)) {
|
||
|
xmlReader.XmlResolver = null; // no DTD resolving
|
||
|
xmlReader.MoveToContent();
|
||
|
var redirectAttr = (string?)xmlReader.GetAttribute("redirect");
|
||
|
if (string2.IsNullOrEmpty(redirectAttr)) {
|
||
|
this.fileName = fileName;
|
||
|
encoding = GetEncoding(xmlReader.Encoding ?? throw new InvalidOperationException());
|
||
|
ReadXmlDoc(xmlReader);
|
||
|
} else {
|
||
|
var redirectionTarget = GetRedirectionTarget(fileName, redirectAttr);
|
||
|
if (redirectionTarget is not null) {
|
||
|
//Debug.WriteLine("XmlDoc " + fileName + " is redirecting to " + redirectionTarget);
|
||
|
using (FileStream redirectedFs = new FileStream(redirectionTarget, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
|
||
|
using (XmlTextReader redirectedXmlReader = new XmlTextReader(redirectedFs)) {
|
||
|
redirectedXmlReader.XmlResolver = null; // no DTD resolving
|
||
|
redirectedXmlReader.MoveToContent();
|
||
|
this.fileName = redirectionTarget;
|
||
|
encoding = GetEncoding(redirectedXmlReader.Encoding ?? throw new InvalidOperationException());
|
||
|
ReadXmlDoc(redirectedXmlReader);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
throw new XmlException("XmlDoc " + fileName + " is redirecting to " + redirectAttr + ", but that file was not found.");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static string? GetRedirectionTarget(string xmlFileName, string target)
|
||
|
{
|
||
|
var programFilesDir = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||
|
if (string.IsNullOrEmpty(programFilesDir))
|
||
|
programFilesDir = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||
|
programFilesDir = AppendDirectorySeparator(programFilesDir);
|
||
|
|
||
|
string corSysDir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();
|
||
|
corSysDir = AppendDirectorySeparator(corSysDir);
|
||
|
|
||
|
var fileName = target.Replace ("%PROGRAMFILESDIR%", programFilesDir)
|
||
|
.Replace ("%CORSYSDIR%", corSysDir);
|
||
|
if (!Path.IsPathRooted (fileName))
|
||
|
fileName = Path.Combine (Path.GetDirectoryName (xmlFileName)!, fileName);
|
||
|
return LookupLocalizedXmlDoc(fileName);
|
||
|
}
|
||
|
|
||
|
static string AppendDirectorySeparator(string dir)
|
||
|
{
|
||
|
if (dir.EndsWith("\\", StringComparison.Ordinal) || dir.EndsWith("/", StringComparison.Ordinal))
|
||
|
return dir;
|
||
|
else
|
||
|
return dir + Path.DirectorySeparatorChar;
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Given the assembly file name, looks up the XML documentation file name.
|
||
|
/// Returns null if no XML documentation file is found.
|
||
|
/// </summary>
|
||
|
public static string? LookupLocalizedXmlDoc(string fileName)
|
||
|
{
|
||
|
string xmlFileName = Path.ChangeExtension(fileName, ".xml");
|
||
|
string currentCulture = System.Threading.Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName;
|
||
|
string localizedXmlDocFile = GetLocalizedName(xmlFileName, currentCulture);
|
||
|
|
||
|
//Debug.WriteLine("Try find XMLDoc @" + localizedXmlDocFile);
|
||
|
if (File.Exists(localizedXmlDocFile)) {
|
||
|
return localizedXmlDocFile;
|
||
|
}
|
||
|
//Debug.WriteLine("Try find XMLDoc @" + xmlFileName);
|
||
|
if (File.Exists(xmlFileName)) {
|
||
|
return xmlFileName;
|
||
|
}
|
||
|
if (currentCulture != "en") {
|
||
|
string englishXmlDocFile = GetLocalizedName(xmlFileName, "en");
|
||
|
//Debug.WriteLine("Try find XMLDoc @" + englishXmlDocFile);
|
||
|
if (File.Exists(englishXmlDocFile)) {
|
||
|
return englishXmlDocFile;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
static string GetLocalizedName(string fileName, string language)
|
||
|
{
|
||
|
string localizedXmlDocFile = Path.GetDirectoryName(fileName)!;
|
||
|
localizedXmlDocFile = Path.Combine(localizedXmlDocFile, language);
|
||
|
localizedXmlDocFile = Path.Combine(localizedXmlDocFile, Path.GetFileName(fileName));
|
||
|
return localizedXmlDocFile;
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Load / Create Index
|
||
|
void ReadXmlDoc(XmlTextReader reader)
|
||
|
{
|
||
|
//lastWriteDate = File.GetLastWriteTimeUtc(fileName);
|
||
|
// Open up a second file stream for the line<->position mapping
|
||
|
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
|
||
|
LinePositionMapper linePosMapper = new LinePositionMapper(fs, encoding);
|
||
|
List<IndexEntry> indexList = new List<IndexEntry>();
|
||
|
while (reader.Read()) {
|
||
|
if (reader.IsStartElement()) {
|
||
|
switch (reader.LocalName) {
|
||
|
case "members":
|
||
|
ReadMembersSection(reader, linePosMapper, indexList);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
indexList.Sort();
|
||
|
index = indexList.ToArray(); // volatile write
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sealed class LinePositionMapper
|
||
|
{
|
||
|
readonly FileStream fs;
|
||
|
readonly Decoder decoder;
|
||
|
int currentLine = 1;
|
||
|
|
||
|
// buffers for use with Decoder:
|
||
|
readonly byte[] input = new byte[1];
|
||
|
readonly char[] output = new char[2];
|
||
|
|
||
|
public LinePositionMapper(FileStream fs, Encoding encoding)
|
||
|
{
|
||
|
decoder = encoding.GetDecoder();
|
||
|
this.fs = fs;
|
||
|
}
|
||
|
|
||
|
public int GetPositionForLine(int line)
|
||
|
{
|
||
|
Debug.Assert(line >= currentLine);
|
||
|
var inputLocal = input;
|
||
|
var outputLocal = output;
|
||
|
while (line > currentLine) {
|
||
|
int b = fs.ReadByte();
|
||
|
if (b < 0)
|
||
|
throw new EndOfStreamException();
|
||
|
inputLocal[0] = (byte)b;
|
||
|
decoder.Convert(inputLocal, 0, 1, outputLocal, 0, outputLocal.Length, false, out int bytesUsed, out int charsUsed, out bool completed);
|
||
|
Debug.Assert(bytesUsed == 1);
|
||
|
for (int i = 0; i < charsUsed; i++) {
|
||
|
if (outputLocal[i] == '\n')
|
||
|
currentLine++;
|
||
|
}
|
||
|
}
|
||
|
return checked((int)fs.Position);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void ReadMembersSection(XmlTextReader reader, LinePositionMapper linePosMapper, List<IndexEntry> indexList)
|
||
|
{
|
||
|
while (reader.Read()) {
|
||
|
switch (reader.NodeType) {
|
||
|
case XmlNodeType.EndElement:
|
||
|
if (reader.LocalName == "members") {
|
||
|
return;
|
||
|
}
|
||
|
break;
|
||
|
case XmlNodeType.Element:
|
||
|
if (reader.LocalName == "member") {
|
||
|
int pos = linePosMapper.GetPositionForLine(reader.LineNumber) + Math.Max(reader.LinePosition - 2, 0);
|
||
|
var memberAttr = (string?)reader.GetAttribute("name");
|
||
|
if (memberAttr is not null)
|
||
|
indexList.Add(new IndexEntry(GetHashCode(memberAttr), pos));
|
||
|
reader.Skip();
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Hash algorithm used for the index.
|
||
|
/// This is a custom implementation so that old index files work correctly
|
||
|
/// even when the .NET string.GetHashCode implementation changes
|
||
|
/// (e.g. due to .NET 4.5 hash randomization)
|
||
|
/// </summary>
|
||
|
static int GetHashCode(string key)
|
||
|
{
|
||
|
unchecked {
|
||
|
int h = 0;
|
||
|
foreach (char c in key) {
|
||
|
h = (h << 5) - h + c;
|
||
|
}
|
||
|
return h;
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region GetDocumentation
|
||
|
/// <summary>
|
||
|
/// Get the documentation for the member with the specified documentation key.
|
||
|
/// </summary>
|
||
|
public string? GetDocumentation(string key)
|
||
|
{
|
||
|
if (key is null)
|
||
|
return null;
|
||
|
return GetDocumentation(key, true);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Get the documentation for the member with the specified documentation key.
|
||
|
/// </summary>
|
||
|
public string? GetDocumentation(StringBuilder? key)
|
||
|
{
|
||
|
if (key is null)
|
||
|
return null;
|
||
|
//TODO: Try to prevent ToString()
|
||
|
return GetDocumentation(key.ToString(), true);
|
||
|
}
|
||
|
|
||
|
string? GetDocumentation(string key, bool allowReload)
|
||
|
{
|
||
|
int hashcode = GetHashCode(key);
|
||
|
var index = this.index; // read volatile field
|
||
|
// index is sorted, so we can use binary search
|
||
|
int m = Array.BinarySearch(index, new IndexEntry(hashcode, 0));
|
||
|
if (m < 0)
|
||
|
return null;
|
||
|
// correct hash code found.
|
||
|
// possibly there are multiple items with the same hash, so go to the first.
|
||
|
while (--m >= 0 && index[m].HashCode == hashcode);
|
||
|
// m is now 1 before the first item with the correct hash
|
||
|
|
||
|
XmlDocumentationCache cache = this.cache;
|
||
|
lock (cache) {
|
||
|
if (!cache.TryGet(key, out string? val)) {
|
||
|
try {
|
||
|
// go through all items that have the correct hash
|
||
|
while (++m < index.Length && index[m].HashCode == hashcode) {
|
||
|
val = LoadDocumentation(key, index[m].PositionInFile);
|
||
|
if (val is not null)
|
||
|
break;
|
||
|
}
|
||
|
// cache the result (even if it is null)
|
||
|
cache.Add(key, val);
|
||
|
} catch (IOException) {
|
||
|
// may happen if the documentation file was deleted/is inaccessible/changed (EndOfStreamException)
|
||
|
return allowReload ? ReloadAndGetDocumentation(key) : null;
|
||
|
} catch (XmlException) {
|
||
|
// may happen if the documentation file was changed so that the file position no longer starts on a valid XML element
|
||
|
return allowReload ? ReloadAndGetDocumentation(key) : null;
|
||
|
}
|
||
|
}
|
||
|
return val;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
string? ReloadAndGetDocumentation(string key)
|
||
|
{
|
||
|
try {
|
||
|
// Reload the index
|
||
|
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
|
||
|
using (XmlTextReader xmlReader = new XmlTextReader(fs)) {
|
||
|
xmlReader.XmlResolver = null; // no DTD resolving
|
||
|
xmlReader.MoveToContent();
|
||
|
ReadXmlDoc(xmlReader);
|
||
|
}
|
||
|
}
|
||
|
} catch (IOException) {
|
||
|
// Ignore errors on reload; IEntity.Documentation callers aren't prepared to handle exceptions
|
||
|
index = new IndexEntry[0]; // clear index to avoid future load attempts
|
||
|
return null;
|
||
|
} catch (XmlException) {
|
||
|
index = new IndexEntry[0]; // clear index to avoid future load attempts
|
||
|
return null;
|
||
|
}
|
||
|
return GetDocumentation(key, allowReload: false); // prevent infinite reload loops
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region Load / Read XML
|
||
|
string? LoadDocumentation(string key, int positionInFile)
|
||
|
{
|
||
|
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete)) {
|
||
|
fs.Position = positionInFile;
|
||
|
var context = new XmlParserContext(null, null, null, XmlSpace.None) { Encoding = encoding };
|
||
|
using (XmlTextReader r = new XmlTextReader(fs, XmlNodeType.Element, context)) {
|
||
|
r.XmlResolver = null; // no DTD resolving
|
||
|
while (r.Read()) {
|
||
|
if (r.NodeType == XmlNodeType.Element) {
|
||
|
var memberAttr = (string?)r.GetAttribute("name");
|
||
|
if (memberAttr == key) {
|
||
|
return r.ReadInnerXml();
|
||
|
} else {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
/// <inheritdoc/>
|
||
|
public virtual void OnDeserialization(object? sender) => cache = new XmlDocumentationCache();
|
||
|
}
|
||
|
}
|