// 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.Xml; using ICSharpCode.NRefactory.Editor; using ICSharpCode.NRefactory.TypeSystem; namespace ICSharpCode.NRefactory.Documentation { /// /// Provides documentation from an .xml file (as generated by the Microsoft C# compiler). /// /// /// 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. /// [Serializable] public class XmlDocumentationProvider : IDocumentationProvider, IDeserializationCallback { #region Cache sealed class XmlDocumentationCache { readonly KeyValuePair[] entries; int pos; public XmlDocumentationCache(int size = 50) { if (size <= 0) throw new ArgumentOutOfRangeException("size", size, "Value must be positive"); this.entries = new KeyValuePair[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(key, value); if (pos == entries.Length) pos = 0; } } #endregion [Serializable] struct IndexEntry : IComparable { /// /// Hash code of the documentation tag /// internal readonly int HashCode; /// /// Position in the .xml file where the documentation starts /// 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; volatile IndexEntry[] index; // SORTED array of index entries #region Constructor / Redirection support /// /// Creates a new XmlDocumentationProvider. /// /// Name of the .xml file. /// Error reading from XML file (or from redirected file) /// Invalid XML file 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; 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 this.fileName = redirectionTarget; 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; } /// /// Given the assembly file name, looks up the XML documentation file name. /// Returns null if no XML documentation file is found. /// 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); List indexList = new List(); 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; int currentLine = 1; public LinePositionMapper(FileStream fs) { 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(); if (b == '\n') { currentLine++; } } return checked((int)fs.Position); } } static void ReadMembersSection(XmlTextReader reader, LinePositionMapper linePosMapper, List 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; } } } /// /// 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) /// static int GetHashCode(string key) { unchecked { int h = 0; foreach (char c in key) { h = (h << 5) - h + c; } return h; } } #endregion #region GetDocumentation /// /// Get the documentation for the member with the specified documentation key. /// 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 GetDocumentation for entity /// public DocumentationComment GetDocumentation(IEntity entity) { string xmlDoc = GetDocumentation(IdStringProvider.GetIdString(entity)); if (xmlDoc != null) { return new DocumentationComment(new StringTextSource(xmlDoc), new SimpleTypeResolveContext(entity)); } else { return null; } } #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; using (XmlTextReader r = new XmlTextReader(fs, XmlNodeType.Element, null)) { 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(); } } }