/* KeePass Password Safe - The Open-Source Password Manager Copyright (C) 2003-2020 Dominik Reichl 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 System.Text.RegularExpressions; #if ModernKeePassLib using Windows.Storage; #endif using ModernKeePassLib.Native; namespace ModernKeePassLib.Utility { /// /// A class containing various static path utility helper methods (like /// stripping extension from a file, etc.). /// public static class UrlUtil { private static readonly char[] g_vPathTrimCharsWs = new char[] { '\"', ' ', '\t', '\r', '\n' }; public static char LocalDirSepChar { #if ModernKeePassLib get { return '\\'; } #else get { return Path.DirectorySeparatorChar; } #endif } private static char[] g_vDirSepChars = null; private static char[] DirSepChars { get { if(g_vDirSepChars == null) { List l = new List(); l.Add('/'); // For URLs, also on Windows // On Unix-like systems, '\\' is not a separator if(!NativeLib.IsUnix()) l.Add('\\'); if(!l.Contains(UrlUtil.LocalDirSepChar)) { Debug.Assert(false); l.Add(UrlUtil.LocalDirSepChar); } g_vDirSepChars = l.ToArray(); } return g_vDirSepChars; } } /// /// Get the directory (path) of a file name. The returned string may be /// terminated by a directory separator character. Example: /// passing C:\\My Documents\\My File.kdb in /// and true to /// would produce this string: C:\\My Documents\\. /// /// Full path of a file. /// Append a terminating directory separator /// character to the returned path. /// If true, the returned path /// is guaranteed to be a valid directory path (for example X:\\ instead /// of X:, overriding ). /// This should only be set to true, if the returned path is directly /// passed to some directory API. /// Directory of the file. 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(UrlUtil.DirSepChars); 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] == '/')); } /// /// Gets the file name of the specified file (full path). Example: /// if is C:\\My Documents\\My File.kdb /// the returned string is My File.kdb. /// /// Full path of a file. /// File name of the specified file. The return value is /// an empty string ("") if the input parameter is null. public static string GetFileName(string strPath) { Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath"); int nLastSep = strPath.LastIndexOfAny(UrlUtil.DirSepChars); if(nLastSep < 0) return strPath; if(nLastSep >= (strPath.Length - 1)) return string.Empty; return strPath.Substring(nLastSep + 1); } /// /// Strip the extension of a file. /// /// Full path of a file with extension. /// File name without extension. public static string StripExtension(string strPath) { Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath"); int nLastDirSep = strPath.LastIndexOfAny(UrlUtil.DirSepChars); int nLastExtDot = strPath.LastIndexOf('.'); if(nLastExtDot <= nLastDirSep) return strPath; return strPath.Substring(0, nLastExtDot); } /// /// Get the extension of a file. /// /// Full path of a file with extension. /// Extension without prepending dot. public static string GetExtension(string strPath) { Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath"); int nLastDirSep = strPath.LastIndexOfAny(UrlUtil.DirSepChars); int nLastExtDot = strPath.LastIndexOf('.'); if(nLastExtDot <= nLastDirSep) return string.Empty; if(nLastExtDot == (strPath.Length - 1)) return string.Empty; return strPath.Substring(nLastExtDot + 1); } /// /// Ensure that a path is terminated with a directory separator character. /// /// Input path. /// If true, a slash (/) is appended to /// the string if it's not terminated already. If false, the /// default system directory separator character is used. /// Path having a directory separator as last character. 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]; if(Array.IndexOf(UrlUtil.DirSepChars, chLast) >= 0) return strPath; if(bUrl) return (strPath + '/'); return (strPath + UrlUtil.LocalDirSepChar); } /* /// /// File access mode enumeration. Used by the FileAccessible /// method. /// public enum FileAccessMode { /// /// Opening a file in read mode. The specified file must exist. /// Read = 0, /// /// 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 true, if data can be written to the /// file. /// Create } */ /* /// /// Test if a specified path is accessible, either in read or write mode. /// /// Path to test. /// Requested file access mode. /// Returns true if the specified path is accessible in /// the requested mode, otherwise the return value is false. 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; } */ internal static int IndexOfSecondEnclQuote(string str) { if(str == null) { Debug.Assert(false); return -1; } if(str.Length <= 1) return -1; if(str[0] != '\"') { Debug.Assert(false); return -1; } if(NativeLib.IsUnix()) { // Find non-escaped quote string strFlt = str.Replace("\\\\", new string( StrUtil.GetUnusedChar(str + "\\\""), 2)); // Same length Match m = Regex.Match(strFlt, "[^\\\\]\\u0022"); int i = (((m != null) && m.Success) ? m.Index : -1); return ((i >= 0) ? (i + 1) : -1); // Index of quote } // Windows does not allow quotes in folder/file names return str.IndexOf('\"', 1); } public static string GetQuotedAppPath(string strPath) { if(strPath == null) { Debug.Assert(false); return string.Empty; } string str = strPath.Trim(); if(str.Length <= 1) return str; if(str[0] != '\"') return str; int iSecond = IndexOfSecondEnclQuote(str); if(iSecond <= 0) return str; return str.Substring(1, iSecond - 1); } public static string FileUrlToPath(string strUrl) { if(strUrl == null) { Debug.Assert(false); throw new ArgumentNullException("strUrl"); } if(strUrl.Length == 0) { Debug.Assert(false); return string.Empty; } #if !ModernKeePassLib if(!strUrl.StartsWith(Uri.UriSchemeFile + ":", StrUtil.CaseIgnoreCmp)) { Debug.Assert(false); return strUrl; } #endif try { Uri uri = new Uri(strUrl); string str = uri.LocalPath; if(!string.IsNullOrEmpty(str)) return str; } catch(Exception) { Debug.Assert(false); } Debug.Assert(false); return strUrl; } public static bool UnhideFile(string strFile) { #if ModernKeePassLib || 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 ModernKeePassLib || 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 && !ModernKeePassLib) 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(UrlUtil.DirSepChars); string[] vTarget = strTarget.Split(UrlUtil.DirSepChars); 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 && !ModernKeePassLib) } 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]; char[] vSep = ((chSep == '/') ? (new char[] { '/' }) : (new char[] { '\\', '/' })); List l = new List(); #if !KeePassLibSD string[] v = strPath.Split(vSep, StringSplitOptions.None); #else string[] v = strPath.Split(vSep); #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; #if ModernKeePassLib try { var dirT = StorageFolder.GetFolderFromPathAsync( strPath).GetResults(); str = dirT.Path; } #else try { str = Path.GetFullPath(strPath); } #endif catch(Exception) { Debug.Assert(false); return strPath; } Debug.Assert((str.IndexOf("\\..\\") < 0) || NativeLib.IsUnix()); foreach(char ch in UrlUtil.DirSepChars) { string strSep = new string(ch, 1); str = str.Replace(strSep + "." + strSep, strSep); } return str; } public static int GetUrlLength(string strText, int nOffset) { if(strText == null) throw new ArgumentNullException("strText"); if(nOffset > strText.Length) throw new ArgumentException(); // Not >= (0 len) int iPosition = nOffset, nLength = 0, nStrLen = strText.Length; while(iPosition < nStrLen) { char ch = strText[iPosition]; ++iPosition; if((ch == ' ') || (ch == '\t') || (ch == '\r') || (ch == '\n')) break; ++nLength; } return nLength; } internal static string GetScheme(string strUrl) { if(string.IsNullOrEmpty(strUrl)) return string.Empty; int i = strUrl.IndexOf(':'); if(i > 0) return strUrl.Substring(0, i); return string.Empty; } public static string RemoveScheme(string strUrl) { if(string.IsNullOrEmpty(strUrl)) return string.Empty; int i = strUrl.IndexOf(':'); if(i < 0) return strUrl; // No scheme to remove ++i; // A single '/' indicates a path (absolute) and should not be removed if(((i + 1) < strUrl.Length) && (strUrl[i] == '/') && (strUrl[i + 1] == '/')) i += 2; // Skip authority prefix return strUrl.Substring(i); } public static string ConvertSeparators(string strPath) { return ConvertSeparators(strPath, UrlUtil.LocalDirSepChar); } public static string ConvertSeparators(string strPath, char chSeparator) { if(string.IsNullOrEmpty(strPath)) return string.Empty; strPath = strPath.Replace('/', chSeparator); strPath = strPath.Replace('\\', chSeparator); return strPath; } public static bool IsUncPath(string strPath) { if(strPath == null) throw new ArgumentNullException("strPath"); return (strPath.StartsWith("\\\\") || strPath.StartsWith("//")); } public static string FilterFileName(string strName) { if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return string.Empty; } // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file StringBuilder sb = new StringBuilder(strName.Length); foreach(char ch in strName) { if(ch < '\u0020') continue; switch(ch) { case '\"': case '*': case ':': case '?': break; case '/': case '\\': case '|': sb.Append('-'); break; case '<': sb.Append('('); break; case '>': sb.Append(')'); break; default: sb.Append(ch); break; } } // Trim trailing spaces and periods for(int i = sb.Length - 1; i >= 0; --i) { char ch = sb[i]; if((ch == ' ') || (ch == '.')) sb.Remove(i, 1); else break; } return sb.ToString(); } /// /// Get the host component of a URL. /// This method is faster and more fault-tolerant than creating /// an Uri object and querying its Host /// property. /// /// /// For the input s://u:p@d.tld:p/p?q#f the return /// value is d.tld. /// public static string GetHost(string strUrl) { if(strUrl == null) { Debug.Assert(false); return string.Empty; } StringBuilder sb = new StringBuilder(); bool bInExtHost = false; for(int i = 0; i < strUrl.Length; ++i) { char ch = strUrl[i]; if(bInExtHost) { if(ch == '/') { if(sb.Length == 0) { } // Ignore leading '/'s else break; } else sb.Append(ch); } else // !bInExtHost { if(ch == ':') bInExtHost = true; } } string str = sb.ToString(); if(str.Length == 0) str = strUrl; // Remove the login part int nLoginLen = str.IndexOf('@'); if(nLoginLen >= 0) str = str.Substring(nLoginLen + 1); // Remove the port int iPort = str.LastIndexOf(':'); if(iPort >= 0) str = str.Substring(0, iPort); return str; } public static bool AssemblyEquals(string strExt, string strShort) { if((strExt == null) || (strShort == null)) { Debug.Assert(false); return false; } if(strExt.Equals(strShort, StrUtil.CaseIgnoreCmp) || strExt.StartsWith(strShort + ",", StrUtil.CaseIgnoreCmp)) return true; if(!strShort.EndsWith(".dll", StrUtil.CaseIgnoreCmp)) { if(strExt.Equals(strShort + ".dll", StrUtil.CaseIgnoreCmp) || strExt.StartsWith(strShort + ".dll,", StrUtil.CaseIgnoreCmp)) return true; } if(!strShort.EndsWith(".exe", StrUtil.CaseIgnoreCmp)) { if(strExt.Equals(strShort + ".exe", StrUtil.CaseIgnoreCmp) || strExt.StartsWith(strShort + ".exe,", StrUtil.CaseIgnoreCmp)) return true; } return false; } public static string GetTempPath() { string strDir; if(NativeLib.IsUnix()) strDir = NativeMethods.GetUserRuntimeDir(); #if KeePassUAP || ModernKeePassLib else strDir = Windows.Storage.ApplicationData.Current.TemporaryFolder.Path; #else else strDir = Path.GetTempPath(); try { if(!Directory.Exists(strDir)) Directory.CreateDirectory(strDir); } catch(Exception) { Debug.Assert(false); } #endif return strDir; } #if !ModernKeePassLib && !KeePassLibSD // Structurally mostly equivalent to UrlUtil.GetFileInfos public static List GetFilePaths(string strDir, string strPattern, SearchOption opt) { List l = new List(); if(strDir == null) { Debug.Assert(false); return l; } if(strPattern == null) { Debug.Assert(false); return l; } string[] v = Directory.GetFiles(strDir, strPattern, opt); if(v == null) { Debug.Assert(false); return l; } // Only accept files with the correct extension; GetFiles may // return additional files, see GetFiles documentation string strExt = GetExtension(strPattern); if(!string.IsNullOrEmpty(strExt) && (strExt.IndexOf('*') < 0) && (strExt.IndexOf('?') < 0)) { strExt = "." + strExt; foreach(string strPathRaw in v) { if(strPathRaw == null) { Debug.Assert(false); continue; } string strPath = strPathRaw.Trim(g_vPathTrimCharsWs); if(strPath.Length == 0) { Debug.Assert(false); continue; } Debug.Assert(strPath == strPathRaw); if(strPath.EndsWith(strExt, StrUtil.CaseIgnoreCmp)) l.Add(strPathRaw); } } else l.AddRange(v); return l; } // Structurally mostly equivalent to UrlUtil.GetFilePaths public static List GetFileInfos(DirectoryInfo di, string strPattern, SearchOption opt) { List l = new List(); if(di == null) { Debug.Assert(false); return l; } if(strPattern == null) { Debug.Assert(false); return l; } FileInfo[] v = di.GetFiles(strPattern, opt); if(v == null) { Debug.Assert(false); return l; } // Only accept files with the correct extension; GetFiles may // return additional files, see GetFiles documentation string strExt = GetExtension(strPattern); if(!string.IsNullOrEmpty(strExt) && (strExt.IndexOf('*') < 0) && (strExt.IndexOf('?') < 0)) { strExt = "." + strExt; foreach(FileInfo fi in v) { if(fi == null) { Debug.Assert(false); continue; } string strPathRaw = fi.FullName; if(strPathRaw == null) { Debug.Assert(false); continue; } string strPath = strPathRaw.Trim(g_vPathTrimCharsWs); if(strPath.Length == 0) { Debug.Assert(false); continue; } Debug.Assert(strPath == strPathRaw); if(strPath.EndsWith(strExt, StrUtil.CaseIgnoreCmp)) l.Add(fi); } } else l.AddRange(v); return l; } #endif /// /// Expand shell variables in a string. /// [0] is the value of %1, etc. /// internal static string ExpandShellVariables(string strText, string[] vParams, bool bEncParamsToArgs) { if(strText == null) { Debug.Assert(false); return string.Empty; } string[] v = vParams; if(v == null) { Debug.Assert(false); v = new string[0]; } if(bEncParamsToArgs) { for(int i = 0; i < v.Length; ++i) v[i] = NativeLib.EncodeDataToArgs(v[i] ?? string.Empty); } string str = strText; NumberFormatInfo nfi = NumberFormatInfo.InvariantInfo; string strPctPlh = Guid.NewGuid().ToString(); str = str.Replace("%%", strPctPlh); for(int i = 0; i <= 9; ++i) { string strPlh = "%" + i.ToString(nfi); string strValue = string.Empty; if((i > 0) && ((i - 1) < v.Length)) strValue = (v[i - 1] ?? string.Empty); str = str.Replace(strPlh, strValue); if(i == 1) { // %L is replaced by the long version of %1; e.g. // HKEY_CLASSES_ROOT\\IE.AssocFile.URL\\Shell\\Open\\Command str = str.Replace("%L", strValue); str = str.Replace("%l", strValue); } } if(str.IndexOf("%*") >= 0) { StringBuilder sb = new StringBuilder(); foreach(string strValue in v) { if(!string.IsNullOrEmpty(strValue)) { if(sb.Length > 0) sb.Append(' '); sb.Append(strValue); } } str = str.Replace("%*", sb.ToString()); } str = str.Replace(strPctPlh, "%"); return str; } public static char GetDriveLetter(string strPath) { if(strPath == null) throw new ArgumentNullException("strPath"); Debug.Assert(default(char) == '\0'); if(strPath.Length < 3) return '\0'; if((strPath[1] != ':') || (strPath[2] != '\\')) return '\0'; char ch = char.ToUpperInvariant(strPath[0]); return (((ch >= 'A') && (ch <= 'Z')) ? ch : '\0'); } internal static string GetSafeFileName(string strName) { Debug.Assert(!string.IsNullOrEmpty(strName)); string str = FilterFileName(GetFileName(strName ?? string.Empty)); if(string.IsNullOrEmpty(str)) { Debug.Assert(false); return "File.dat"; } return str; } internal static string GetCanonicalUri(string strUri) { if(string.IsNullOrEmpty(strUri)) { Debug.Assert(false); return strUri; } try { Uri uri = new Uri(strUri); if(uri.IsAbsoluteUri) return uri.AbsoluteUri; else { Debug.Assert(false); } } catch(Exception) { Debug.Assert(false); } return strUri; } } }