1 /*
2   KeePass Password Safe - The Open-Source Password Manager
3   Copyright (C) 2003-2021 Dominik Reichl <dominik.reichl@t-online.de>
4 
5   This program is free software; you can redistribute it and/or modify
6   it under the terms of the GNU General Public License as published by
7   the Free Software Foundation; either version 2 of the License, or
8   (at your option) any later version.
9 
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18 */
19 
20 using System;
21 using System.Collections.Generic;
22 using System.Diagnostics;
23 using System.Globalization;
24 using System.IO;
25 using System.Text;
26 using System.Text.RegularExpressions;
27 
28 using KeePassLib.Native;
29 
30 namespace KeePassLib.Utility
31 {
32 	/// <summary>
33 	/// A class containing various static path utility helper methods (like
34 	/// stripping extension from a file, etc.).
35 	/// </summary>
36 	public static class UrlUtil
37 	{
38 		private static readonly char[] g_vPathTrimCharsWs = new char[] {
39 			'\"', ' ', '\t', '\r', '\n' };
40 
41 		public static char LocalDirSepChar
42 		{
43 			get { return Path.DirectorySeparatorChar; }
44 		}
45 
46 		private static char[] g_vDirSepChars = null;
47 		private static char[] DirSepChars
48 		{
49 			get
50 			{
51 				if(g_vDirSepChars == null)
52 				{
53 					List<char> l = new List<char>();
54 					l.Add('/'); // For URLs, also on Windows
55 
56 					// On Unix-like systems, '\\' is not a separator
57 					if(!NativeLib.IsUnix()) l.Add('\\');
58 
59 					if(!l.Contains(UrlUtil.LocalDirSepChar))
60 					{
61 						Debug.Assert(false);
62 						l.Add(UrlUtil.LocalDirSepChar);
63 					}
64 
65 					g_vDirSepChars = l.ToArray();
66 				}
67 
68 				return g_vDirSepChars;
69 			}
70 		}
71 
72 		/// <summary>
73 		/// Get the directory (path) of a file name. The returned string may be
74 		/// terminated by a directory separator character. Example:
75 		/// passing <c>C:\\My Documents\\My File.kdb</c> in <paramref name="strFile" />
76 		/// and <c>true</c> to <paramref name="bAppendTerminatingChar"/>
77 		/// would produce this string: <c>C:\\My Documents\\</c>.
78 		/// </summary>
79 		/// <param name="strFile">Full path of a file.</param>
80 		/// <param name="bAppendTerminatingChar">Append a terminating directory separator
81 		/// character to the returned path.</param>
82 		/// <param name="bEnsureValidDirSpec">If <c>true</c>, the returned path
83 		/// is guaranteed to be a valid directory path (for example <c>X:\\</c> instead
84 		/// of <c>X:</c>, overriding <paramref name="bAppendTerminatingChar" />).
85 		/// This should only be set to <c>true</c>, if the returned path is directly
86 		/// passed to some directory API.</param>
87 		/// <returns>Directory of the file.</returns>
GetFileDirectory(string strFile, bool bAppendTerminatingChar, bool bEnsureValidDirSpec)88 		public static string GetFileDirectory(string strFile, bool bAppendTerminatingChar,
89 			bool bEnsureValidDirSpec)
90 		{
91 			Debug.Assert(strFile != null);
92 			if(strFile == null) throw new ArgumentNullException("strFile");
93 
94 			int nLastSep = strFile.LastIndexOfAny(UrlUtil.DirSepChars);
95 			if(nLastSep < 0) return string.Empty; // No directory
96 
97 			if(bEnsureValidDirSpec && (nLastSep == 2) && (strFile[1] == ':') &&
98 				(strFile[2] == '\\')) // Length >= 3 and Windows root directory
99 				bAppendTerminatingChar = true;
100 
101 			if(!bAppendTerminatingChar) return strFile.Substring(0, nLastSep);
102 			return EnsureTerminatingSeparator(strFile.Substring(0, nLastSep),
103 				(strFile[nLastSep] == '/'));
104 		}
105 
106 		/// <summary>
107 		/// Gets the file name of the specified file (full path). Example:
108 		/// if <paramref name="strPath" /> is <c>C:\\My Documents\\My File.kdb</c>
109 		/// the returned string is <c>My File.kdb</c>.
110 		/// </summary>
111 		/// <param name="strPath">Full path of a file.</param>
112 		/// <returns>File name of the specified file. The return value is
113 		/// an empty string (<c>""</c>) if the input parameter is <c>null</c>.</returns>
GetFileName(string strPath)114 		public static string GetFileName(string strPath)
115 		{
116 			Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath");
117 
118 			int nLastSep = strPath.LastIndexOfAny(UrlUtil.DirSepChars);
119 
120 			if(nLastSep < 0) return strPath;
121 			if(nLastSep >= (strPath.Length - 1)) return string.Empty;
122 
123 			return strPath.Substring(nLastSep + 1);
124 		}
125 
126 		/// <summary>
127 		/// Strip the extension of a file.
128 		/// </summary>
129 		/// <param name="strPath">Full path of a file with extension.</param>
130 		/// <returns>File name without extension.</returns>
StripExtension(string strPath)131 		public static string StripExtension(string strPath)
132 		{
133 			Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath");
134 
135 			int nLastDirSep = strPath.LastIndexOfAny(UrlUtil.DirSepChars);
136 			int nLastExtDot = strPath.LastIndexOf('.');
137 
138 			if(nLastExtDot <= nLastDirSep) return strPath;
139 
140 			return strPath.Substring(0, nLastExtDot);
141 		}
142 
143 		/// <summary>
144 		/// Get the extension of a file.
145 		/// </summary>
146 		/// <param name="strPath">Full path of a file with extension.</param>
147 		/// <returns>Extension without prepending dot.</returns>
GetExtension(string strPath)148 		public static string GetExtension(string strPath)
149 		{
150 			Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath");
151 
152 			int nLastDirSep = strPath.LastIndexOfAny(UrlUtil.DirSepChars);
153 			int nLastExtDot = strPath.LastIndexOf('.');
154 
155 			if(nLastExtDot <= nLastDirSep) return string.Empty;
156 			if(nLastExtDot == (strPath.Length - 1)) return string.Empty;
157 
158 			return strPath.Substring(nLastExtDot + 1);
159 		}
160 
161 		/// <summary>
162 		/// Ensure that a path is terminated with a directory separator character.
163 		/// </summary>
164 		/// <param name="strPath">Input path.</param>
165 		/// <param name="bUrl">If <c>true</c>, a slash (<c>/</c>) is appended to
166 		/// the string if it's not terminated already. If <c>false</c>, the
167 		/// default system directory separator character is used.</param>
168 		/// <returns>Path having a directory separator as last character.</returns>
EnsureTerminatingSeparator(string strPath, bool bUrl)169 		public static string EnsureTerminatingSeparator(string strPath, bool bUrl)
170 		{
171 			Debug.Assert(strPath != null); if(strPath == null) throw new ArgumentNullException("strPath");
172 
173 			int nLength = strPath.Length;
174 			if(nLength <= 0) return string.Empty;
175 
176 			char chLast = strPath[nLength - 1];
177 			if(Array.IndexOf<char>(UrlUtil.DirSepChars, chLast) >= 0)
178 				return strPath;
179 
180 			if(bUrl) return (strPath + '/');
181 			return (strPath + UrlUtil.LocalDirSepChar);
182 		}
183 
184 		/* /// <summary>
185 		/// File access mode enumeration. Used by the <c>FileAccessible</c>
186 		/// method.
187 		/// </summary>
188 		public enum FileAccessMode
189 		{
190 			/// <summary>
191 			/// Opening a file in read mode. The specified file must exist.
192 			/// </summary>
193 			Read = 0,
194 
195 			/// <summary>
196 			/// Opening a file in create mode. If the file exists already, it
197 			/// will be overwritten. If it doesn't exist, it will be created.
198 			/// The return value is <c>true</c>, if data can be written to the
199 			/// file.
200 			/// </summary>
201 			Create
202 		} */
203 
204 		/* /// <summary>
205 		/// Test if a specified path is accessible, either in read or write mode.
206 		/// </summary>
207 		/// <param name="strFilePath">Path to test.</param>
208 		/// <param name="fMode">Requested file access mode.</param>
209 		/// <returns>Returns <c>true</c> if the specified path is accessible in
210 		/// the requested mode, otherwise the return value is <c>false</c>.</returns>
211 		public static bool FileAccessible(string strFilePath, FileAccessMode fMode)
212 		{
213 			Debug.Assert(strFilePath != null);
214 			if(strFilePath == null) throw new ArgumentNullException("strFilePath");
215 
216 			if(fMode == FileAccessMode.Read)
217 			{
218 				FileStream fs;
219 
220 				try { fs = File.OpenRead(strFilePath); }
221 				catch(Exception) { return false; }
222 				if(fs == null) return false;
223 
224 				fs.Close();
225 				return true;
226 			}
227 			else if(fMode == FileAccessMode.Create)
228 			{
229 				FileStream fs;
230 
231 				try { fs = File.Create(strFilePath); }
232 				catch(Exception) { return false; }
233 				if(fs == null) return false;
234 
235 				fs.Close();
236 				return true;
237 			}
238 
239 			return false;
240 		} */
241 
IndexOfSecondEnclQuote(string str)242 		internal static int IndexOfSecondEnclQuote(string str)
243 		{
244 			if(str == null) { Debug.Assert(false); return -1; }
245 			if(str.Length <= 1) return -1;
246 			if(str[0] != '\"') { Debug.Assert(false); return -1; }
247 
248 			if(NativeLib.IsUnix())
249 			{
250 				// Find non-escaped quote
251 				string strFlt = str.Replace("\\\\", new string(
252 					StrUtil.GetUnusedChar(str + "\\\""), 2)); // Same length
253 				Match m = Regex.Match(strFlt, "[^\\\\]\\u0022");
254 				int i = (((m != null) && m.Success) ? m.Index : -1);
255 				return ((i >= 0) ? (i + 1) : -1); // Index of quote
256 			}
257 
258 			// Windows does not allow quotes in folder/file names
259 			return str.IndexOf('\"', 1);
260 		}
261 
GetQuotedAppPath(string strPath)262 		public static string GetQuotedAppPath(string strPath)
263 		{
264 			if(strPath == null) { Debug.Assert(false); return string.Empty; }
265 
266 			string str = strPath.Trim();
267 			if(str.Length <= 1) return str;
268 			if(str[0] != '\"') return str;
269 
270 			int iSecond = IndexOfSecondEnclQuote(str);
271 			if(iSecond <= 0) return str;
272 
273 			return str.Substring(1, iSecond - 1);
274 		}
275 
FileUrlToPath(string strUrl)276 		public static string FileUrlToPath(string strUrl)
277 		{
278 			if(strUrl == null) { Debug.Assert(false); throw new ArgumentNullException("strUrl"); }
279 			if(strUrl.Length == 0) { Debug.Assert(false); return string.Empty; }
280 
281 			if(!strUrl.StartsWith(Uri.UriSchemeFile + ":", StrUtil.CaseIgnoreCmp))
282 			{
283 				Debug.Assert(false);
284 				return strUrl;
285 			}
286 
287 			try
288 			{
289 				Uri uri = new Uri(strUrl);
290 				string str = uri.LocalPath;
291 				if(!string.IsNullOrEmpty(str)) return str;
292 			}
293 			catch(Exception) { Debug.Assert(false); }
294 
295 			Debug.Assert(false);
296 			return strUrl;
297 		}
298 
UnhideFile(string strFile)299 		public static bool UnhideFile(string strFile)
300 		{
301 #if KeePassLibSD
302 			return false;
303 #else
304 			if(strFile == null) throw new ArgumentNullException("strFile");
305 
306 			try
307 			{
308 				FileAttributes fa = File.GetAttributes(strFile);
309 				if((long)(fa & FileAttributes.Hidden) == 0) return false;
310 
311 				return HideFile(strFile, false);
312 			}
313 			catch(Exception) { }
314 
315 			return false;
316 #endif
317 		}
318 
HideFile(string strFile, bool bHide)319 		public static bool HideFile(string strFile, bool bHide)
320 		{
321 #if KeePassLibSD
322 			return false;
323 #else
324 			if(strFile == null) throw new ArgumentNullException("strFile");
325 
326 			try
327 			{
328 				FileAttributes fa = File.GetAttributes(strFile);
329 
330 				if(bHide) fa = ((fa & ~FileAttributes.Normal) | FileAttributes.Hidden);
331 				else // Unhide
332 				{
333 					fa &= ~FileAttributes.Hidden;
334 					if((long)fa == 0) fa = FileAttributes.Normal;
335 				}
336 
337 				File.SetAttributes(strFile, fa);
338 				return true;
339 			}
340 			catch(Exception) { }
341 
342 			return false;
343 #endif
344 		}
345 
MakeRelativePath(string strBaseFile, string strTargetFile)346 		public static string MakeRelativePath(string strBaseFile, string strTargetFile)
347 		{
348 			if(strBaseFile == null) throw new ArgumentNullException("strBasePath");
349 			if(strTargetFile == null) throw new ArgumentNullException("strTargetPath");
350 			if(strBaseFile.Length == 0) return strTargetFile;
351 			if(strTargetFile.Length == 0) return string.Empty;
352 
353 			// Test whether on different Windows drives
354 			if((strBaseFile.Length >= 3) && (strTargetFile.Length >= 3))
355 			{
356 				if((strBaseFile[1] == ':') && (strTargetFile[1] == ':') &&
357 					(strBaseFile[2] == '\\') && (strTargetFile[2] == '\\') &&
358 					(strBaseFile[0] != strTargetFile[0]))
359 					return strTargetFile;
360 			}
361 
362 #if (!KeePassLibSD && !KeePassUAP)
363 			if(NativeLib.IsUnix())
364 			{
365 #endif
366 				bool bBaseUnc = IsUncPath(strBaseFile);
367 				bool bTargetUnc = IsUncPath(strTargetFile);
368 				if((!bBaseUnc && bTargetUnc) || (bBaseUnc && !bTargetUnc))
369 					return strTargetFile;
370 
371 				string strBase = GetShortestAbsolutePath(strBaseFile);
372 				string strTarget = GetShortestAbsolutePath(strTargetFile);
373 				string[] vBase = strBase.Split(UrlUtil.DirSepChars);
374 				string[] vTarget = strTarget.Split(UrlUtil.DirSepChars);
375 
376 				int i = 0;
377 				while((i < (vBase.Length - 1)) && (i < (vTarget.Length - 1)) &&
378 					(vBase[i] == vTarget[i])) { ++i; }
379 
380 				StringBuilder sbRel = new StringBuilder();
381 				for(int j = i; j < (vBase.Length - 1); ++j)
382 				{
383 					if(sbRel.Length > 0) sbRel.Append(UrlUtil.LocalDirSepChar);
384 					sbRel.Append("..");
385 				}
386 				for(int k = i; k < vTarget.Length; ++k)
387 				{
388 					if(sbRel.Length > 0) sbRel.Append(UrlUtil.LocalDirSepChar);
389 					sbRel.Append(vTarget[k]);
390 				}
391 
392 				return sbRel.ToString();
393 #if (!KeePassLibSD && !KeePassUAP)
394 			}
395 
396 			try // Windows
397 			{
398 				const int nMaxPath = NativeMethods.MAX_PATH * 2;
399 				StringBuilder sb = new StringBuilder(nMaxPath + 2);
400 				if(!NativeMethods.PathRelativePathTo(sb, strBaseFile, 0,
401 					strTargetFile, 0))
402 					return strTargetFile;
403 
404 				string str = sb.ToString();
405 				while(str.StartsWith(".\\")) str = str.Substring(2, str.Length - 2);
406 
407 				return str;
408 			}
409 			catch(Exception) { Debug.Assert(false); }
410 			return strTargetFile;
411 #endif
412 		}
413 
MakeAbsolutePath(string strBaseFile, string strTargetFile)414 		public static string MakeAbsolutePath(string strBaseFile, string strTargetFile)
415 		{
416 			if(strBaseFile == null) throw new ArgumentNullException("strBasePath");
417 			if(strTargetFile == null) throw new ArgumentNullException("strTargetPath");
418 			if(strBaseFile.Length == 0) return strTargetFile;
419 			if(strTargetFile.Length == 0) return string.Empty;
420 
421 			if(IsAbsolutePath(strTargetFile)) return strTargetFile;
422 
423 			string strBaseDir = GetFileDirectory(strBaseFile, true, false);
424 			return GetShortestAbsolutePath(strBaseDir + strTargetFile);
425 		}
426 
IsAbsolutePath(string strPath)427 		public static bool IsAbsolutePath(string strPath)
428 		{
429 			if(strPath == null) throw new ArgumentNullException("strPath");
430 			if(strPath.Length == 0) return false;
431 
432 			if(IsUncPath(strPath)) return true;
433 
434 			try { return Path.IsPathRooted(strPath); }
435 			catch(Exception) { Debug.Assert(false); }
436 
437 			return true;
438 		}
439 
GetShortestAbsolutePath(string strPath)440 		public static string GetShortestAbsolutePath(string strPath)
441 		{
442 			if(strPath == null) throw new ArgumentNullException("strPath");
443 			if(strPath.Length == 0) return string.Empty;
444 
445 			// Path.GetFullPath is incompatible with UNC paths traversing over
446 			// different server shares (which are created by PathRelativePathTo);
447 			// we need to build the absolute path on our own...
448 			if(IsUncPath(strPath))
449 			{
450 				char chSep = strPath[0];
451 				char[] vSep = ((chSep == '/') ? (new char[] { '/' }) :
452 					(new char[] { '\\', '/' }));
453 
454 				List<string> l = new List<string>();
455 #if !KeePassLibSD
456 				string[] v = strPath.Split(vSep, StringSplitOptions.None);
457 #else
458 				string[] v = strPath.Split(vSep);
459 #endif
460 				Debug.Assert((v.Length >= 3) && (v[0].Length == 0) &&
461 					(v[1].Length == 0));
462 
463 				foreach(string strPart in v)
464 				{
465 					if(strPart.Equals(".")) continue;
466 					else if(strPart.Equals(".."))
467 					{
468 						if(l.Count > 0) l.RemoveAt(l.Count - 1);
469 						else { Debug.Assert(false); }
470 					}
471 					else l.Add(strPart); // Do not ignore zero length parts
472 				}
473 
474 				StringBuilder sb = new StringBuilder();
475 				for(int i = 0; i < l.Count; ++i)
476 				{
477 					// Don't test length of sb, might be 0 due to initial UNC seps
478 					if(i > 0) sb.Append(chSep);
479 
480 					sb.Append(l[i]);
481 				}
482 
483 				return sb.ToString();
484 			}
485 
486 			string str;
487 			try { str = Path.GetFullPath(strPath); }
488 			catch(Exception) { Debug.Assert(false); return strPath; }
489 
490 			Debug.Assert((str.IndexOf("\\..\\") < 0) || NativeLib.IsUnix());
491 			foreach(char ch in UrlUtil.DirSepChars)
492 			{
493 				string strSep = new string(ch, 1);
494 				str = str.Replace(strSep + "." + strSep, strSep);
495 			}
496 
497 			return str;
498 		}
499 
IsUriChar(char ch)500 		internal static bool IsUriChar(char ch)
501 		{
502 			Debug.Assert(((ulong)'!' == 0x21) && ((ulong)'~' == 0x7E) &&
503 				((ulong)'`' == 0x60));
504 			if((ch < '!') || (ch > '~')) return false;
505 
506 			bool b = true;
507 			switch(ch)
508 			{
509 				case '\"':
510 				case '<':
511 				case '>':
512 				case '\\':
513 				case '^':
514 				case '`':
515 				case '{':
516 				case '|':
517 				case '}':
518 					b = false;
519 					break;
520 
521 				default: break;
522 			}
523 
524 			return b;
525 		}
526 
IsUriSchemeChar(char ch)527 		internal static bool IsUriSchemeChar(char ch)
528 		{
529 			return (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z')) ||
530 				((ch >= '0') && (ch <= '9')) || (ch == '+') || (ch == '-') ||
531 				(ch == '.'));
532 		}
533 
GetUrlLength(string strText, int iOffset)534 		public static int GetUrlLength(string strText, int iOffset)
535 		{
536 			return GetUrlLength(strText, iOffset, false);
537 		}
538 
GetUrlLength(string strText, int iOffset, bool bExclStdTerm)539 		internal static int GetUrlLength(string strText, int iOffset, bool bExclStdTerm)
540 		{
541 			if(strText == null) throw new ArgumentNullException("strText");
542 			if(iOffset < 0) throw new ArgumentOutOfRangeException("iOffset");
543 
544 			int i = iOffset, n = strText.Length;
545 			if(iOffset > n) throw new ArgumentOutOfRangeException("iOffset");
546 
547 			while(i < n)
548 			{
549 				char ch = strText[i];
550 				if(!IsUriChar(ch))
551 				{
552 					if((ch == '\"') || (ch == '>') || (ch == '}'))
553 						bExclStdTerm = false;
554 					break;
555 				}
556 
557 				++i;
558 			}
559 
560 			if(bExclStdTerm)
561 			{
562 				while(i != iOffset)
563 				{
564 					char ch = strText[i - 1];
565 
566 					bool bIsStdTerm = false;
567 					switch(ch)
568 					{
569 						case '!':
570 						case ',':
571 						case '.':
572 						case ':':
573 						case ';':
574 						case '?':
575 							bIsStdTerm = true;
576 							break;
577 
578 						default: break;
579 					}
580 
581 					if(bIsStdTerm) --i;
582 					else break;
583 				}
584 			}
585 
586 			return (i - iOffset);
587 		}
588 
589 		private static readonly string[] g_vKnownSchemes = new string[] {
590 			"callto", "file", "ftp", "http", "https", "ldap", "ldaps",
591 			"mailto", "news", "nntp", "sftp", "tel", "telnet"
592 		};
593 		// This method only knows some popular schemes; subset of
594 		// https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
IsKnownScheme(string strScheme)595 		internal static bool IsKnownScheme(string strScheme)
596 		{
597 			if(strScheme == null) { Debug.Assert(false); return false; }
598 
599 			string str = strScheme.ToLowerInvariant();
600 			return (Array.IndexOf(g_vKnownSchemes, str) >= 0);
601 		}
602 
GetScheme(string strUrl)603 		internal static string GetScheme(string strUrl)
604 		{
605 			if(string.IsNullOrEmpty(strUrl)) return string.Empty;
606 
607 			int i = strUrl.IndexOf(':');
608 			if(i > 0) return strUrl.Substring(0, i);
609 
610 			return string.Empty;
611 		}
612 
RemoveScheme(string strUrl)613 		public static string RemoveScheme(string strUrl)
614 		{
615 			if(string.IsNullOrEmpty(strUrl)) return string.Empty;
616 
617 			int i = strUrl.IndexOf(':');
618 			if(i < 0) return strUrl; // No scheme to remove
619 			++i;
620 
621 			// A single '/' indicates a path (absolute) and should not be removed
622 			if(((i + 1) < strUrl.Length) && (strUrl[i] == '/') &&
623 				(strUrl[i + 1] == '/'))
624 				i += 2; // Skip authority prefix
625 
626 			return strUrl.Substring(i);
627 		}
628 
ConvertSeparators(string strPath)629 		public static string ConvertSeparators(string strPath)
630 		{
631 			return ConvertSeparators(strPath, UrlUtil.LocalDirSepChar);
632 		}
633 
ConvertSeparators(string strPath, char chSeparator)634 		public static string ConvertSeparators(string strPath, char chSeparator)
635 		{
636 			if(string.IsNullOrEmpty(strPath)) return string.Empty;
637 
638 			strPath = strPath.Replace('/', chSeparator);
639 			strPath = strPath.Replace('\\', chSeparator);
640 
641 			return strPath;
642 		}
643 
IsUncPath(string strPath)644 		public static bool IsUncPath(string strPath)
645 		{
646 			if(strPath == null) throw new ArgumentNullException("strPath");
647 
648 			return (strPath.StartsWith("\\\\") || strPath.StartsWith("//"));
649 		}
650 
FilterFileName(string strName)651 		public static string FilterFileName(string strName)
652 		{
653 			if(string.IsNullOrEmpty(strName)) { Debug.Assert(false); return string.Empty; }
654 
655 			// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
656 
657 			StringBuilder sb = new StringBuilder(strName.Length);
658 			foreach(char ch in strName)
659 			{
660 				if(ch < '\u0020') continue;
661 
662 				switch(ch)
663 				{
664 					case '\"':
665 					case '*':
666 					case ':':
667 					case '?':
668 						break;
669 
670 					case '/':
671 					case '\\':
672 					case '|':
673 						sb.Append('-');
674 						break;
675 
676 					case '<':
677 						sb.Append('(');
678 						break;
679 
680 					case '>':
681 						sb.Append(')');
682 						break;
683 
684 					default: sb.Append(ch); break;
685 				}
686 			}
687 
688 			// Trim trailing spaces and periods
689 			for(int i = sb.Length - 1; i >= 0; --i)
690 			{
691 				char ch = sb[i];
692 				if((ch == ' ') || (ch == '.')) sb.Remove(i, 1);
693 				else break;
694 			}
695 
696 			return sb.ToString();
697 		}
698 
699 		/// <summary>
700 		/// Get the host component of a URL.
701 		/// This method is faster and more fault-tolerant than creating
702 		/// an <code>Uri</code> object and querying its <code>Host</code>
703 		/// property.
704 		/// </summary>
705 		/// <example>
706 		/// For the input <code>s://u:p@d.tld:p/p?q#f</code> the return
707 		/// value is <code>d.tld</code>.
708 		/// </example>
GetHost(string strUrl)709 		public static string GetHost(string strUrl)
710 		{
711 			if(strUrl == null) { Debug.Assert(false); return string.Empty; }
712 
713 			StringBuilder sb = new StringBuilder();
714 			bool bInExtHost = false;
715 			for(int i = 0; i < strUrl.Length; ++i)
716 			{
717 				char ch = strUrl[i];
718 				if(bInExtHost)
719 				{
720 					if(ch == '/')
721 					{
722 						if(sb.Length == 0) { } // Ignore leading '/'s
723 						else break;
724 					}
725 					else sb.Append(ch);
726 				}
727 				else // !bInExtHost
728 				{
729 					if(ch == ':') bInExtHost = true;
730 				}
731 			}
732 
733 			string str = sb.ToString();
734 			if(str.Length == 0) str = strUrl;
735 
736 			// Remove the login part
737 			int nLoginLen = str.IndexOf('@');
738 			if(nLoginLen >= 0) str = str.Substring(nLoginLen + 1);
739 
740 			// Remove the port
741 			int iPort = str.LastIndexOf(':');
742 			if(iPort >= 0) str = str.Substring(0, iPort);
743 
744 			return str;
745 		}
746 
AssemblyEquals(string strExt, string strShort)747 		public static bool AssemblyEquals(string strExt, string strShort)
748 		{
749 			if((strExt == null) || (strShort == null)) { Debug.Assert(false); return false; }
750 
751 			if(strExt.Equals(strShort, StrUtil.CaseIgnoreCmp) ||
752 				strExt.StartsWith(strShort + ",", StrUtil.CaseIgnoreCmp))
753 				return true;
754 
755 			if(!strShort.EndsWith(".dll", StrUtil.CaseIgnoreCmp))
756 			{
757 				if(strExt.Equals(strShort + ".dll", StrUtil.CaseIgnoreCmp) ||
758 					strExt.StartsWith(strShort + ".dll,", StrUtil.CaseIgnoreCmp))
759 					return true;
760 			}
761 
762 			if(!strShort.EndsWith(".exe", StrUtil.CaseIgnoreCmp))
763 			{
764 				if(strExt.Equals(strShort + ".exe", StrUtil.CaseIgnoreCmp) ||
765 					strExt.StartsWith(strShort + ".exe,", StrUtil.CaseIgnoreCmp))
766 					return true;
767 			}
768 
769 			return false;
770 		}
771 
GetTempPath()772 		public static string GetTempPath()
773 		{
774 			string strDir;
775 			if(NativeLib.IsUnix())
776 				strDir = NativeMethods.GetUserRuntimeDir();
777 #if KeePassUAP
778 			else strDir = Windows.Storage.ApplicationData.Current.TemporaryFolder.Path;
779 #else
780 			else strDir = Path.GetTempPath();
781 #endif
782 
783 			try
784 			{
785 				if(!Directory.Exists(strDir)) Directory.CreateDirectory(strDir);
786 			}
787 			catch(Exception) { Debug.Assert(false); }
788 
789 			return strDir;
790 		}
791 
792 #if !KeePassLibSD
793 		// Structurally mostly equivalent to UrlUtil.GetFileInfos
GetFilePaths(string strDir, string strPattern, SearchOption opt)794 		public static List<string> GetFilePaths(string strDir, string strPattern,
795 			SearchOption opt)
796 		{
797 			List<string> l = new List<string>();
798 			if(strDir == null) { Debug.Assert(false); return l; }
799 			if(strPattern == null) { Debug.Assert(false); return l; }
800 
801 			string[] v = Directory.GetFiles(strDir, strPattern, opt);
802 			if(v == null) { Debug.Assert(false); return l; }
803 
804 			// Only accept files with the correct extension; GetFiles may
805 			// return additional files, see GetFiles documentation
806 			string strExt = GetExtension(strPattern);
807 			if(!string.IsNullOrEmpty(strExt) && (strExt.IndexOf('*') < 0) &&
808 				(strExt.IndexOf('?') < 0))
809 			{
810 				strExt = "." + strExt;
811 
812 				foreach(string strPathRaw in v)
813 				{
814 					if(strPathRaw == null) { Debug.Assert(false); continue; }
815 					string strPath = strPathRaw.Trim(g_vPathTrimCharsWs);
816 					if(strPath.Length == 0) { Debug.Assert(false); continue; }
817 					Debug.Assert(strPath == strPathRaw);
818 
819 					if(strPath.EndsWith(strExt, StrUtil.CaseIgnoreCmp))
820 						l.Add(strPathRaw);
821 				}
822 			}
823 			else l.AddRange(v);
824 
825 			return l;
826 		}
827 
828 		// Structurally mostly equivalent to UrlUtil.GetFilePaths
GetFileInfos(DirectoryInfo di, string strPattern, SearchOption opt)829 		public static List<FileInfo> GetFileInfos(DirectoryInfo di, string strPattern,
830 			SearchOption opt)
831 		{
832 			List<FileInfo> l = new List<FileInfo>();
833 			if(di == null) { Debug.Assert(false); return l; }
834 			if(strPattern == null) { Debug.Assert(false); return l; }
835 
836 			FileInfo[] v = di.GetFiles(strPattern, opt);
837 			if(v == null) { Debug.Assert(false); return l; }
838 
839 			// Only accept files with the correct extension; GetFiles may
840 			// return additional files, see GetFiles documentation
841 			string strExt = GetExtension(strPattern);
842 			if(!string.IsNullOrEmpty(strExt) && (strExt.IndexOf('*') < 0) &&
843 				(strExt.IndexOf('?') < 0))
844 			{
845 				strExt = "." + strExt;
846 
847 				foreach(FileInfo fi in v)
848 				{
849 					if(fi == null) { Debug.Assert(false); continue; }
850 					string strPathRaw = fi.FullName;
851 					if(strPathRaw == null) { Debug.Assert(false); continue; }
852 					string strPath = strPathRaw.Trim(g_vPathTrimCharsWs);
853 					if(strPath.Length == 0) { Debug.Assert(false); continue; }
854 					Debug.Assert(strPath == strPathRaw);
855 
856 					if(strPath.EndsWith(strExt, StrUtil.CaseIgnoreCmp))
857 						l.Add(fi);
858 				}
859 			}
860 			else l.AddRange(v);
861 
862 			return l;
863 		}
864 #endif
865 
866 		/// <summary>
867 		/// Expand shell variables in a string.
868 		/// <paramref name="vParams" />[0] is the value of <c>%1</c>, etc.
869 		/// </summary>
ExpandShellVariables(string strText, string[] vParams, bool bEncParamsToArgs)870 		internal static string ExpandShellVariables(string strText, string[] vParams,
871 			bool bEncParamsToArgs)
872 		{
873 			if(strText == null) { Debug.Assert(false); return string.Empty; }
874 
875 			string[] v = vParams;
876 			if(v == null) { Debug.Assert(false); v = new string[0]; }
877 			if(bEncParamsToArgs)
878 			{
879 				for(int i = 0; i < v.Length; ++i)
880 					v[i] = NativeLib.EncodeDataToArgs(v[i] ?? string.Empty);
881 			}
882 
883 			string str = strText;
884 			NumberFormatInfo nfi = NumberFormatInfo.InvariantInfo;
885 
886 			string strPctPlh = Guid.NewGuid().ToString();
887 			str = str.Replace("%%", strPctPlh);
888 
889 			for(int i = 0; i <= 9; ++i)
890 			{
891 				string strPlh = "%" + i.ToString(nfi);
892 
893 				string strValue = string.Empty;
894 				if((i > 0) && ((i - 1) < v.Length))
895 					strValue = (v[i - 1] ?? string.Empty);
896 
897 				str = str.Replace(strPlh, strValue);
898 
899 				if(i == 1)
900 				{
901 					// %L is replaced by the long version of %1; e.g.
902 					// HKEY_CLASSES_ROOT\\IE.AssocFile.URL\\Shell\\Open\\Command
903 					str = str.Replace("%L", strValue);
904 					str = str.Replace("%l", strValue);
905 				}
906 			}
907 
908 			if(str.IndexOf("%*") >= 0)
909 			{
910 				StringBuilder sb = new StringBuilder();
911 				foreach(string strValue in v)
912 				{
913 					if(!string.IsNullOrEmpty(strValue))
914 					{
915 						if(sb.Length > 0) sb.Append(' ');
916 						sb.Append(strValue);
917 					}
918 				}
919 
920 				str = str.Replace("%*", sb.ToString());
921 			}
922 
923 			str = str.Replace(strPctPlh, "%");
924 			return str;
925 		}
926 
GetDriveLetter(string strPath)927 		public static char GetDriveLetter(string strPath)
928 		{
929 			if(strPath == null) throw new ArgumentNullException("strPath");
930 
931 			Debug.Assert(default(char) == '\0');
932 			if(strPath.Length < 3) return '\0';
933 			if((strPath[1] != ':') || (strPath[2] != '\\')) return '\0';
934 
935 			char ch = char.ToUpperInvariant(strPath[0]);
936 			return (((ch >= 'A') && (ch <= 'Z')) ? ch : '\0');
937 		}
938 
GetSafeFileName(string strName)939 		internal static string GetSafeFileName(string strName)
940 		{
941 			Debug.Assert(!string.IsNullOrEmpty(strName));
942 
943 			string str = FilterFileName(GetFileName(strName ?? string.Empty));
944 
945 			if(string.IsNullOrEmpty(str))
946 			{
947 				Debug.Assert(false);
948 				return "File.dat";
949 			}
950 			return str;
951 		}
952 
GetCanonicalUri(string strUri)953 		internal static string GetCanonicalUri(string strUri)
954 		{
955 			if(string.IsNullOrEmpty(strUri)) { Debug.Assert(false); return strUri; }
956 
957 			try
958 			{
959 				Uri uri = new Uri(strUri);
960 
961 				if(uri.IsAbsoluteUri) return uri.AbsoluteUri;
962 				else { Debug.Assert(false); }
963 			}
964 			catch(Exception) { Debug.Assert(false); }
965 
966 			return strUri;
967 		}
968 
ParseQuery(string strQuery)969 		internal static Dictionary<string, string> ParseQuery(string strQuery)
970 		{
971 			Dictionary<string, string> d = new Dictionary<string, string>();
972 			if(string.IsNullOrEmpty(strQuery)) return d;
973 
974 			string[] vKvps = strQuery.Split(new char[] { '?', '&' });
975 			if(vKvps == null) { Debug.Assert(false); return d; }
976 
977 			foreach(string strKvp in vKvps)
978 			{
979 				if(string.IsNullOrEmpty(strKvp)) continue;
980 
981 				string strKey = strKvp, strValue = string.Empty;
982 				int iSep = strKvp.IndexOf('=');
983 				if(iSep >= 0)
984 				{
985 					strKey = strKvp.Substring(0, iSep);
986 					strValue = strKvp.Substring(iSep + 1);
987 				}
988 
989 				strKey = Uri.UnescapeDataString(strKey);
990 				strValue = Uri.UnescapeDataString(strValue);
991 
992 				d[strKey] = strValue;
993 			}
994 
995 			return d;
996 		}
997 	}
998 }
999