context: // 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();
  					if (string.IsNullOrEmpty(xmlReader.GetAttribute("redirect"))) {
  						this.fileName = fileName;
  						encoding = GetEncoding(xmlReader.Encoding);
  						ReadXmlDoc(xmlReader);
  					} else {
  						string? redirectionTarget = GetRedirectionTarget(fileName, xmlReader.GetAttribute("redirect"));
  						if (!(redirectionTarget is 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);
  									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)
  		{
  			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);
  
gt: 				if (File.Exists(englishXmlDocFile)) {