context: /*
    KeePass Password Safe - The Open-Source Password Manager
    Copyright (C) 2003-2019 Dominik Reichl <dominik.reichl@t-online.de>
  
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.
  
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
  
    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
  */
  
  using System;
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Globalization;
  using System.IO;
  using System.Text;
  
  using KeePassLib.Native;
  
  namespace KeePassLib.Utility
  {
  	/// <summary>
  	/// A class containing various static path utility helper methods (like
  	/// stripping extension from a file, etc.).
  	/// </summary>
  	public static class UrlUtil
  	{
  		private static readonly char[] m_vDirSeps = new char[] {
  			'\\', '/', UrlUtil.LocalDirSepChar };
  		private static readonly char[] m_vPathTrimCharsWs = new char[] {
  			'\"', ' ', '\t', '\r', '\n' };
  
  		public static char LocalDirSepChar
  		{
  			get { return Path.DirectorySeparatorChar; }
  		}
  
  		/// <summary>
  		/// Get the directory (path) of a file name. The returned string may be
  		/// terminated by a directory separator character. Example:
  		/// passing <c>C:\\My Documents\\My File.kdb</c> in <paramref name="strFile" />
  		/// and <c>true</c> to <paramref name="bAppendTerminatingChar"/>
  		/// would produce this string: <c>C:\\My Documents\\</c>.
  		/// </summary>
  		/// <param name="strFile">Full path of a file.</param>
  		/// <param name="bAppendTerminatingChar">Append a terminating directory separator
  		/// character to the returned path.</param>
  		/// <param name="bEnsureValidDirSpec">If <c>true</c>, the returned path
  		/// is guaranteed to be a valid directory path (for example <c>X:\\</c> instead
  		/// of <c>X:</c>, overriding <paramref name="bAppendTerminatingChar" />).
  		/// This should only be set to <c>true</c>, if the returned path is directly
  		/// passed to some directory API.</param>
  		/// <returns>Directory of the file.</returns>
  		public static string GetFileDirectory(string strFile, bool bAppendTerminatingChar,
  			bool bEnsureValidDirSpec)
  		{
  			Debug.Assert(strFile != null);
  			if(strFile == null) throw new ArgumentNullException("strFile");
  
  			int nLastSep = strFile.LastIndexOfAny(m_vDirSeps);
  			if(nLastSep < 0) return string.Empty; // No directory
  
  			if(bEnsureValidDirSpec && (nLastSep == 2) && (strFile[1] == ':') &&
  				(strFile[2] == '\\')) // Length >= 3 and Windows root directory
  				bAppendTerminatingChar = true;
  
  			if(!bAppendTerminatingChar) return strFile.Substring(0, nLastSep);
  			return EnsureTerminatingSeparator(strFile.Substring(0, nLastSep),
  				(strFile[nLastSep] == '/'));
  		}
  
  		/// <summary>
  		/// Gets the file name of the specified file (full path). Example:
  		/// if <paramref name="strPath" /> is <c>C:\\My Documents\\My File.kdb</c>
  		/// the returned string is <c>My File.kdb</c>.
  		/// </summary>
  		/// <param name="strPath">Full path of a file.</param>
  		/// <returns>File name of the specified file. The return value is
  		/// an empty string (<c>""</c>) if the input parameter is <c>null</c>.</returns>
  		public static string GetFileName(string strPath)
  		{
  			Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath");
  
  			int nLastSep = strPath.LastIndexOfAny(m_vDirSeps);
  
  			if(nLastSep < 0) return strPath;
  			if(nLastSep >= (strPath.Length - 1)) return string.Empty;
  
  			return strPath.Substring(nLastSep + 1);
  		}
  
  		/// <summary>
  		/// Strip the extension of a file.
  		/// </summary>
  		/// <param name="strPath">Full path of a file with extension.</param>
  		/// <returns>File name without extension.</returns>
  		public static string StripExtension(string strPath)
  		{
  			Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath");
  
  			int nLastDirSep = strPath.LastIndexOfAny(m_vDirSeps);
  			int nLastExtDot = strPath.LastIndexOf('.');
  
  			if(nLastExtDot <= nLastDirSep) return strPath;
  
  			return strPath.Substring(0, nLastExtDot);
  		}
  
  		/// <summary>
  		/// Get the extension of a file.
  		/// </summary>
  		/// <param name="strPath">Full path of a file with extension.</param>
  		/// <returns>Extension without prepending dot.</returns>
  		public static string GetExtension(string strPath)
  		{
  			Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath");
  
  			int nLastDirSep = strPath.LastIndexOfAny(m_vDirSeps);
  			int nLastExtDot = strPath.LastIndexOf('.');
  
  			if(nLastExtDot <= nLastDirSep) return string.Empty;
  			if(nLastExtDot == (strPath.Length - 1)) return string.Empty;
  
  			return strPath.Substring(nLastExtDot + 1);
  		}
  
  		/// <summary>
  		/// Ensure that a path is terminated with a directory separator character.
  		/// </summary>
  		/// <param name="strPath">Input path.</param>
  		/// <param name="bUrl">If <c>true</c>, a slash (<c>/</c>) is appended to
  		/// the string if it's not terminated already. If <c>false</c>, the
  		/// default system directory separator character is used.</param>
  		/// <returns>Path having a directory separator as last character.</returns>
  		public static string EnsureTerminatingSeparator(string strPath, bool bUrl)
  		{
  			Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath");
  
  			int nLength = strPath.Length;
  			if(nLength <= 0) return string.Empty;
  
  			char chLast = strPath[nLength - 1];
  
  			for(int i = 0; i < m_vDirSeps.Length; ++i)
  			{
  				if(chLast == m_vDirSeps[i]) return strPath;
  			}
  
  			if(bUrl) return (strPath + '/');
  			return (strPath + UrlUtil.LocalDirSepChar);
  		}
  
  		/* /// <summary>
  		/// File access mode enumeration. Used by the <c>FileAccessible</c>
  		/// method.
  		/// </summary>
  		public enum FileAccessMode
  		{
  			/// <summary>
  			/// Opening a file in read mode. The specified file must exist.
  			/// </summary>
  			Read = 0,
  
  			/// <summary>
  			/// Opening a file in create mode. If the file exists already, it
  			/// will be overwritten. If it doesn't exist, it will be created.
  			/// The return value is <c>true</c>, if data can be written to the
  			/// file.
  			/// </summary>
  			Create
  		} */
  
  		/* /// <summary>
  		/// Test if a specified path is accessible, either in read or write mode.
  		/// </summary>
  		/// <param name="strFilePath">Path to test.</param>
  		/// <param name="fMode">Requested file access mode.</param>
  		/// <returns>Returns <c>true</c> if the specified path is accessible in
  		/// the requested mode, otherwise the return value is <c>false</c>.</returns>
  		public static bool FileAccessible(string strFilePath, FileAccessMode fMode)
  		{
  			Debug.Assert(strFilePath != null);
  			if(strFilePath == null) throw new ArgumentNullException("strFilePath");
  
  			if(fMode == FileAccessMode.Read)
  			{
  				FileStream fs;
  
  				try { fs = File.OpenRead(strFilePath); }
  				catch(Exception) { return false; }
  				if(fs == null) return false;
  
  				fs.Close();
  				return true;
  			}
  			else if(fMode == FileAccessMode.Create)
  			{
  				FileStream fs;
  
  				try { fs = File.Create(strFilePath); }
  				catch(Exception) { return false; }
  				if(fs == null) return false;
  
  				fs.Close();
  				return true;
  			}
  
  			return false;
  		} */
  
  		public static string GetQuotedAppPath(string strPath)
  		{
  			if(strPath == null) { Debug.Assert(false); return string.Empty; }
  
  			// int nFirst = strPath.IndexOf('\"');
  			// int nSecond = strPath.IndexOf('\"', nFirst + 1);
  			// if((nFirst >= 0) && (nSecond >= 0))
  			//	return strPath.Substring(nFirst + 1, nSecond - nFirst - 1);
  			// return strPath;
  
  			string str = strPath.Trim();
  			if(str.Length <= 1) return str;
  			if(str[0] != '\"') return str;
  
  			int iSecond = str.IndexOf('\"', 1);
  			if(iSecond <= 0) return str;
  
  			return str.Substring(1, iSecond - 1);
  		}
  
  		public static string FileUrlToPath(string strUrl)
  		{
  			Debug.Assert(strUrl != null);
  			if(strUrl == null) throw new ArgumentNullException("strUrl");
  
  			string str = strUrl;
  			if(str.StartsWith(@"file:///", StrUtil.CaseIgnoreCmp))
  				str = str.Substring(8, str.Length - 8);
  
  			str = str.Replace('/', UrlUtil.LocalDirSepChar);
  
  			return str;
  		}
  
  		public static bool UnhideFile(string strFile)
  		{
  #if KeePassLibSD
  			return false;
  #else
  			if(strFile == null) throw new ArgumentNullException("strFile");
  
  			try
  			{
  				FileAttributes fa = File.GetAttributes(strFile);
  				if((long)(fa & FileAttributes.Hidden) == 0) return false;
  
  				return HideFile(strFile, false);
  			}
  			catch(Exception) { }
  
  			return false;
  #endif
  		}
  
  		public static bool HideFile(string strFile, bool bHide)
  		{
  #if KeePassLibSD
  			return false;
  #else
  			if(strFile == null) throw new ArgumentNullException("strFile");
  
  			try
  			{
  				FileAttributes fa = File.GetAttributes(strFile);
  
  				if(bHide) fa = ((fa & ~FileAttributes.Normal) | FileAttributes.Hidden);
  				else // Unhide
  				{
  					fa &= ~FileAttributes.Hidden;
  					if((long)fa == 0) fa = FileAttributes.Normal;
  				}
  
  				File.SetAttributes(strFile, fa);
  				return true;
  			}
  			catch(Exception) { }
  
  			return false;
  #endif
  		}
  
  		public static string MakeRelativePath(string strBaseFile, string strTargetFile)
  		{
  			if(strBaseFile == null) throw new ArgumentNullException("strBasePath");
  			if(strTargetFile == null) throw new ArgumentNullException("strTargetPath");
  			if(strBaseFile.Length == 0) return strTargetFile;
  			if(strTargetFile.Length == 0) return string.Empty;
  
  			// Test whether on different Windows drives
  			if((strBaseFile.Length >= 3) && (strTargetFile.Length >= 3))
  			{
  				if((strBaseFile[1] == ':') && (strTargetFile[1] == ':') &&
  					(strBaseFile[2] == '\\') && (strTargetFile[2] == '\\') &&
  					(strBaseFile[0] != strTargetFile[0]))
  					return strTargetFile;
  			}
  
  #if (!KeePassLibSD && !KeePassUAP)
  			if(NativeLib.IsUnix())
  			{
  #endif
  				bool bBaseUnc = IsUncPath(strBaseFile);
  				bool bTargetUnc = IsUncPath(strTargetFile);
  				if((!bBaseUnc && bTargetUnc) || (bBaseUnc && !bTargetUnc))
  					return strTargetFile;
  
  				string strBase = GetShortestAbsolutePath(strBaseFile);
  				string strTarget = GetShortestAbsolutePath(strTargetFile);
  				string[] vBase = strBase.Split(m_vDirSeps);
  				string[] vTarget = strTarget.Split(m_vDirSeps);
  
  				int i = 0;
  				while((i < (vBase.Length - 1)) && (i < (vTarget.Length - 1)) &&
  					(vBase[i] == vTarget[i])) { ++i; }
  
  				StringBuilder sbRel = new StringBuilder();
  				for(int j = i; j < (vBase.Length - 1); ++j)
  				{
  					if(sbRel.Length > 0) sbRel.Append(UrlUtil.LocalDirSepChar);
  					sbRel.Append("..");
  				}
  				for(int k = i; k < vTarget.Length; ++k)
  				{
  					if(sbRel.Length > 0) sbRel.Append(UrlUtil.LocalDirSepChar);
  					sbRel.Append(vTarget[k]);
  				}
  
  				return sbRel.ToString();
  #if (!KeePassLibSD && !KeePassUAP)
  			}
  
  			try // Windows
  			{
  				const int nMaxPath = NativeMethods.MAX_PATH * 2;
  				StringBuilder sb = new StringBuilder(nMaxPath + 2);
  				if(!NativeMethods.PathRelativePathTo(sb, strBaseFile, 0,
  					strTargetFile, 0))
  					return strTargetFile;
  
  				string str = sb.ToString();
  				while(str.StartsWith(".\\")) str = str.Substring(2, str.Length - 2);
  
  				return str;
  			}
  			catch(Exception) { Debug.Assert(false); }
  			return strTargetFile;
  #endif
  		}
  
  		public static string MakeAbsolutePath(string strBaseFile, string strTargetFile)
  		{
  			if(strBaseFile == null) throw new ArgumentNullException("strBasePath");
  			if(strTargetFile == null) throw new ArgumentNullException("strTargetPath");
  			if(strBaseFile.Length == 0) return strTargetFile;
  			if(strTargetFile.Length == 0) return string.Empty;
  
  			if(IsAbsolutePath(strTargetFile)) return strTargetFile;
  
  			string strBaseDir = GetFileDirectory(strBaseFile, true, false);
  			return GetShortestAbsolutePath(strBaseDir + strTargetFile);
  		}
  
  		public static bool IsAbsolutePath(string strPath)
  		{
  			if(strPath == null) throw new ArgumentNullException("strPath");
  			if(strPath.Length == 0) return false;
  
  			if(IsUncPath(strPath)) return true;
  
  			try { return Path.IsPathRooted(strPath); }
  			catch(Exception) { Debug.Assert(false); }
  
  			return true;
  		}
  
  		public static string GetShortestAbsolutePath(string strPath)
  		{
  			if(strPath == null) throw new ArgumentNullException("strPath");
  			if(strPath.Length == 0) return string.Empty;
  
  			// Path.GetFullPath is incompatible with UNC paths traversing over
  			// different server shares (which are created by PathRelativePathTo);
  			// we need to build the absolute path on our own...
  			if(IsUncPath(strPath))
  			{
  				char chSep = strPath[0];
  				Debug.Assert(Array.IndexOf<char>(m_vDirSeps, chSep) >= 0);
  
  				List<string> l = new List<string>();
  #if !KeePassLibSD
  				string[] v = strPath.Split(m_vDirSeps, StringSplitOptions.None);
  #else
  				string[] v = strPath.Split(m_vDirSeps);
  #endif
  				Debug.Assert((v.Length >= 3) && (v[0].Length == 0) &&
  					(v[1].Length == 0));
  
  				foreach(string strPart in v)
  				{
  					if(strPart.Equals(".")) continue;
  					else if(strPart.Equals(".."))
  					{
  						if(l.Count > 0) l.RemoveAt(l.Count - 1);
  						else { Debug.Assert(false); }
  					}
  					else l.Add(strPart); // Do not ignore zero length parts
  				}
  
  				StringBuilder sb = new StringBuilder();
  				for(int i = 0; i < l.Count; ++i)
  				{
  					// Don't test length of sb, might be 0 due to initial UNC seps
  					if(i > 0) sb.Append(chSep);
  
  					sb.Append(l[i]);
  				}
  
  				return sb.ToString();
  			}
  
  			string str;
  			try { str = Path.GetFullPath(strPath); }
  			catch(Exception) { Debug.Assert(false); return strPath; }
  
  			Debug.Assert(str.IndexOf("\\..\\") < 0);
  			foreach(char ch in m_vDirSeps)
  			{
  				string strSep = new string(ch, 1);
  
gt: 				str = str.Replace(strSep + "." + strSep, strSep);