mirror of https://github.com/icsharpcode/ILSpy.git
				
				
			
			You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							410 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
	
	
							410 lines
						
					
					
						
							14 KiB
						
					
					
				// 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 ICSharpCode.Decompiler.Documentation | 
						|
{ | 
						|
	/// <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("size", size, "Value must be positive"); | 
						|
				this.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) | 
						|
			{ | 
						|
				this.HashCode = hashCode; | 
						|
				this.PositionInFile = positionInFile; | 
						|
			} | 
						|
			 | 
						|
			public int CompareTo(IndexEntry other) | 
						|
			{ | 
						|
				return this.HashCode.CompareTo(other.HashCode); | 
						|
			} | 
						|
		} | 
						|
		 | 
						|
		[NonSerialized] | 
						|
		XmlDocumentationCache cache = new XmlDocumentationCache(); | 
						|
		 | 
						|
		readonly string fileName; | 
						|
		readonly Encoding encoding; | 
						|
		volatile IndexEntry[] index; // SORTED array of index entries | 
						|
		 | 
						|
		#region Constructor / Redirection support | 
						|
		/// <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 == null) | 
						|
				throw new ArgumentNullException("fileName"); | 
						|
			 | 
						|
			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(); | 
						|
					if (string.IsNullOrEmpty(xmlReader.GetAttribute("redirect"))) { | 
						|
						this.fileName = fileName; | 
						|
						this.encoding = xmlReader.Encoding; | 
						|
						ReadXmlDoc(xmlReader); | 
						|
					} else { | 
						|
						string redirectionTarget = GetRedirectionTarget(fileName, xmlReader.GetAttribute("redirect")); | 
						|
						if (redirectionTarget != 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; | 
						|
									this.encoding = redirectedXmlReader.Encoding; | 
						|
									ReadXmlDoc(redirectedXmlReader); | 
						|
								} | 
						|
							} | 
						|
						} else { | 
						|
							throw new XmlException("XmlDoc " + fileName + " is redirecting to " + xmlReader.GetAttribute("redirect") + ", but that file was not found."); | 
						|
						} | 
						|
					} | 
						|
				} | 
						|
			} | 
						|
		} | 
						|
		 | 
						|
		static string GetRedirectionTarget(string xmlFileName, string target) | 
						|
		{ | 
						|
			string programFilesDir = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); | 
						|
			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(); | 
						|
				this.index = indexList.ToArray(); // volatile write | 
						|
			} | 
						|
		} | 
						|
		 | 
						|
		sealed class LinePositionMapper | 
						|
		{ | 
						|
			readonly FileStream fs; | 
						|
			readonly Decoder decoder; | 
						|
			int currentLine = 1; | 
						|
			char prevChar = '\0'; | 
						|
			 | 
						|
			// buffers for use with Decoder: | 
						|
			byte[] input = new byte[1]; | 
						|
			char[] output = new char[1]; | 
						|
			 | 
						|
			public LinePositionMapper(FileStream fs, Encoding encoding) | 
						|
			{ | 
						|
				this.decoder = encoding.GetDecoder(); | 
						|
				this.fs = fs; | 
						|
			} | 
						|
			 | 
						|
			public int GetPositionForLine(int line) | 
						|
			{ | 
						|
				Debug.Assert(line >= currentLine); | 
						|
				while (line > currentLine) { | 
						|
					int b = fs.ReadByte(); | 
						|
					if (b < 0) | 
						|
						throw new EndOfStreamException(); | 
						|
					int bytesUsed, charsUsed; | 
						|
					bool completed; | 
						|
					input[0] = (byte)b; | 
						|
					decoder.Convert(input, 0, 1, output, 0, 1, false, out bytesUsed, out charsUsed, out completed); | 
						|
					Debug.Assert(bytesUsed == 1); | 
						|
					if (charsUsed == 1) { | 
						|
						if ((prevChar != '\r' && output[0] == '\n') || output[0] == '\r') | 
						|
							currentLine++; | 
						|
						prevChar = output[0]; | 
						|
					} | 
						|
				} | 
						|
				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); | 
						|
							string memberAttr = reader.GetAttribute("name"); | 
						|
							if (memberAttr != 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 == null) | 
						|
				throw new ArgumentNullException("key"); | 
						|
			return GetDocumentation(key, 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) { | 
						|
				string val; | 
						|
				if (!cache.TryGet(key, out 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 != 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 | 
						|
				this.index = new IndexEntry[0]; // clear index to avoid future load attempts | 
						|
				return null; | 
						|
			} catch (XmlException) { | 
						|
				this.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) { | 
						|
							string memberAttr = r.GetAttribute("name"); | 
						|
							if (memberAttr == key) { | 
						|
								return r.ReadInnerXml(); | 
						|
							} else { | 
						|
								return null; | 
						|
							} | 
						|
						} | 
						|
					} | 
						|
					return null; | 
						|
				} | 
						|
			} | 
						|
		} | 
						|
		#endregion | 
						|
		 | 
						|
		public virtual void OnDeserialization(object sender) | 
						|
		{ | 
						|
			cache = new XmlDocumentationCache(); | 
						|
		} | 
						|
	} | 
						|
}
 | 
						|
 |