/*
* Copyright (c) 2023 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN 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 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN 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 ProtonVPN. If not, see .
*/
using System.Globalization;
using System.Net;
using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using ProtonVPN.Common.Core.Networking;
namespace ProtonVPN.Common.Core.Extensions;
public static class StringExtensions
{
private static readonly Regex _base64KeyRegex = new("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$");
private static readonly Dictionary _diacriticsMap = new()
{
{ "äæǽ", "ae" },
{ "öœ", "oe" },
{ "ü", "ue" },
{ "Ä", "Ae" },
{ "Ü", "Ue" },
{ "Ö", "Oe" },
{ "ÀÁÂÃÄÅǺĀĂĄǍΑΆẢẠẦẪẨẬẰẮẴẲẶА", "A" },
{ "àáâãåǻāăąǎªαάảạầấẫẩậằắẵẳặа", "a" },
{ "Б", "B" },
{ "б", "b" },
{ "ÇĆĈĊČ", "C" },
{ "çćĉċč", "c" },
{ "Д", "D" },
{ "д", "d" },
{ "ÐĎĐΔ", "Dj" },
{ "ðďđδ", "dj" },
{ "ÈÉÊËĒĔĖĘĚΕΈẼẺẸỀẾỄỂỆЕЭ", "E" },
{ "èéêëēĕėęěέεẽẻẹềếễểệеэ", "e" },
{ "Ф", "F" },
{ "ф", "f" },
{ "ĜĞĠĢΓГҐ", "G" },
{ "ĝğġģγгґ", "g" },
{ "ĤĦ", "H" },
{ "ĥħ", "h" },
{ "ÌÍÎÏĨĪĬǏĮİΗΉΊΙΪỈỊИЫ", "I" },
{ "ìíîïĩīĭǐįıηήίιϊỉịиыї", "i" },
{ "Ĵ", "J" },
{ "ĵ", "j" },
{ "ĶΚК", "K" },
{ "ķκк", "k" },
{ "ĹĻĽĿŁΛЛ", "L" },
{ "ĺļľŀłλл", "l" },
{ "М", "M" },
{ "м", "m" },
{ "ÑŃŅŇΝН", "N" },
{ "ñńņňʼnνн", "n" },
{ "ÒÓÔÕŌŎǑŐƠØǾΟΌΩΏỎỌỒỐỖỔỘỜỚỠỞỢО", "O" },
{ "òóôõōŏǒőơøǿºοόωώỏọồốỗổộờớỡởợо", "o" },
{ "П", "P" },
{ "п", "p" },
{ "ŔŖŘΡР", "R" },
{ "ŕŗřρр", "r" },
{ "ŚŜŞȘŠΣС", "S" },
{ "śŝşșšſσςс", "s" },
{ "ȚŢŤŦτТ", "T" },
{ "țţťŧт", "t" },
{ "ÙÚÛŨŪŬŮŰŲƯǓǕǗǙǛŨỦỤỪỨỮỬỰУ", "U" },
{ "ùúûũūŭůűųưǔǖǘǚǜυύϋủụừứữửựу", "u" },
{ "ÝŸŶΥΎΫỲỸỶỴЙ", "Y" },
{ "ýÿŷỳỹỷỵй", "y" },
{ "В", "V" },
{ "в", "v" },
{ "Ŵ", "W" },
{ "ŵ", "w" },
{ "ŹŻŽΖЗ", "Z" },
{ "źżžζз", "z" },
{ "ÆǼ", "AE" },
{ "ß", "ss" },
{ "IJ", "IJ" },
{ "ij", "ij" },
{ "Œ", "OE" },
{ "ƒ", "f" },
{ "ξ", "ks" },
{ "π", "p" },
{ "β", "v" },
{ "μ", "m" },
{ "ψ", "ps" },
{ "Ё", "Yo" },
{ "ё", "yo" },
{ "Є", "Ye" },
{ "є", "ye" },
{ "Ї", "Yi" },
{ "Ж", "Zh" },
{ "ж", "zh" },
{ "Х", "Kh" },
{ "х", "kh" },
{ "Ц", "Ts" },
{ "ц", "ts" },
{ "Ч", "Ch" },
{ "ч", "ch" },
{ "Ш", "Sh" },
{ "ш", "sh" },
{ "Щ", "Shch" },
{ "щ", "shch" },
{ "ЪъЬь", "" },
{ "Ю", "Yu" },
{ "ю", "yu" },
{ "Я", "Ya" },
{ "я", "ya" },
};
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}
public static bool? ToBoolOrNull(this string value)
{
return bool.TryParse(value, out bool result) ? result : null;
}
public static bool EqualsIgnoringCase(this string value, string other)
{
return value.Equals(other, StringComparison.OrdinalIgnoreCase);
}
public static bool ContainsIgnoringCase(this string value, string other)
{
return value != null && value.IndexOf(other, StringComparison.OrdinalIgnoreCase) >= 0;
}
public static string RemoveDiacritics(this string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
value = value.Normalize(NormalizationForm.FormD);
StringBuilder sb = new();
foreach (char c in value.Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark))
{
// Some of the diacritics can't be automatically remapped, therefore requires manual mapping.
bool isMapped = false;
foreach (KeyValuePair kvp in _diacriticsMap.Where(kvp => kvp.Key.Contains(c)))
{
sb.Append(kvp.Value);
isMapped = true;
break;
}
if (!isMapped)
{
sb.Append(c);
}
}
return sb.ToString().Normalize(NormalizationForm.FormC);
}
public static bool ContainsIgnoringCase(this IEnumerable collection, string other)
{
return collection != null && collection.Any(e => e.Equals(other, StringComparison.OrdinalIgnoreCase));
}
public static bool StartsWithIgnoringCase(this string value, string other)
{
return value != null && value.StartsWith(other, StringComparison.OrdinalIgnoreCase);
}
public static bool EndsWithIgnoringCase(this string value, string other)
{
return value != null && value.EndsWith(other, StringComparison.OrdinalIgnoreCase);
}
public static bool IsNotNullAndContains(this string value, string other)
{
return value != null && value.Contains(other);
}
public static string FirstCharToUpper(this string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
return value.First().ToString().ToUpper() + value.Substring(1);
}
public static string TrimEnd(this string value, string ending)
{
if (!value.EndsWith(ending))
{
return value;
}
return value.Remove(value.LastIndexOf(ending));
}
public static string GetLastChars(this string value, int length)
{
return length >= value.Length ? value : value.Substring(value.Length - length);
}
public static string? GetFirstChars(this string? value, int maxLength)
{
return value?.Length > maxLength ? value.Substring(0, maxLength) : value;
}
public static byte[] HexStringToByteArray(this string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
public static bool IsValidIpAddressFormat(this string ip)
{
return IPAddress.TryParse(ip, out IPAddress? parsedIpAddress) &&
ip.EqualsIgnoringCase(parsedIpAddress.ToString());
}
public static bool IsValidIpAddress(this string ip)
{
return NetworkAddress.TryParse(ip, out NetworkAddress address)
&& address.IsSingleIp;
}
public static bool IsValidIpAddressOrRange(this string ip)
{
return NetworkAddress.TryParse(ip, out _);
}
public static bool IsValidBase64Key(this string key)
{
return _base64KeyRegex.IsMatch(key);
}
public static bool IsValidUrl(this string url)
{
return !string.IsNullOrWhiteSpace(url)
&& Uri.IsWellFormedUriString(url.ToFormattedUrl(), UriKind.Absolute);
}
public static bool IsValidPath(this string path)
{
return !string.IsNullOrWhiteSpace(path)
&& Path.Exists(path);
}
public static string ToFormattedUrl(this string url)
{
// Check if protocol is already defined, otherwise use https by default.
return string.IsNullOrWhiteSpace(url) || url.Contains(":/")
? url
: $"https://{url}";
}
public static uint ToIPAddressBytes(this string value)
{
if (IPAddress.TryParse(value, out IPAddress? address))
{
return BitConverter.ToUInt32(address.GetAddressBytes(), 0);
}
return 0;
}
public static string FormatIfNotEmpty(this string value, string format)
{
if (value.IsNullOrEmpty())
{
return value;
}
return string.Format(format, value);
}
public static IEnumerable? SplitToEnumerable(this string value, char separator)
{
return string.IsNullOrWhiteSpace(value)
? null
: value.Split(separator).Select(e => e.Trim());
}
public static HashSet? SplitToHashSet(this string value, char separator)
{
return value.SplitToEnumerable(separator)?.ToHashSet();
}
public static List? SplitToList(this string value, char separator)
{
return value.SplitToEnumerable(separator)?.ToList();
}
public static bool IsHttpUri(this string uriString, out Uri? uri)
{
return Uri.TryCreate(uriString, UriKind.Absolute, out uri) && (uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeHttp)
|| Uri.TryCreate($"https://{uriString}", UriKind.Absolute, out uri);
}
public static bool IsValidEmailAddress(this string email)
{
if (string.IsNullOrWhiteSpace(email))
{
return false;
}
try
{
email = Regex.Replace(email, @"(@)(.+)$", DomainMapper,
RegexOptions.None, TimeSpan.FromMilliseconds(200));
string DomainMapper(Match match)
{
IdnMapping idn = new();
string domainName = idn.GetAscii(match.Groups[2].Value);
return match.Groups[1].Value + domainName;
}
}
catch (RegexMatchTimeoutException)
{
return false;
}
catch (ArgumentException)
{
return false;
}
try
{
return Regex.IsMatch(email,
@"^[^@\s]+@[^@\s]+\.[^@\s]+$",
RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250));
}
catch (RegexMatchTimeoutException)
{
return false;
}
}
public static SecureString ToSecureString(this string? value)
{
SecureString secureString = new();
if (!string.IsNullOrEmpty(value))
{
foreach (char c in value)
{
secureString.AppendChar(c);
}
}
secureString.MakeReadOnly();
return secureString;
}
public static string NormalizeCountryCode(this string countryCode)
{
if (string.Equals(countryCode, "GB", StringComparison.OrdinalIgnoreCase))
{
// Normalize GB to UK for consistency
return "UK";
}
return countryCode.Trim().ToUpperInvariant();
}
public static int IndexOfFirstDigit(this string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return -1;
}
for (int i = 0; i < text.Length; i++)
{
if (char.IsDigit(text[i]))
{
return i;
}
}
return -1;
}
}