diff --git a/Orm/Xtensive.Orm.Tests.Core/UrlInfoTest.cs b/Orm/Xtensive.Orm.Tests.Core/UrlInfoTest.cs index 0f6dddd64..cd3e8ebb6 100644 --- a/Orm/Xtensive.Orm.Tests.Core/UrlInfoTest.cs +++ b/Orm/Xtensive.Orm.Tests.Core/UrlInfoTest.cs @@ -6,26 +6,53 @@ using NUnit.Framework; -namespace Xtensive.Orm.Tests.Core +namespace Xtensive.Orm.Tests.Core; + +[TestFixture] +public class UrlInfoTest { - [TestFixture] - public class UrlInfoTest + [Test] + public void CombinedTest() { - [Test] - public void CombinedTest() - { - UrlInfo a1 = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl?someParameter=someValue&someParameter2=someValue2"); - UrlInfo a2 = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl?someParameter=someValue&someParameter2=someValue2"); - UrlInfo aX = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl?someParameter2=someValue2&someParameter=someValue"); - UrlInfo b = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl"); + UrlInfo a1 = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl?someParameter=someValue&someParameter2=someValue2"); + UrlInfo a2 = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl?someParameter=someValue&someParameter2=someValue2"); + UrlInfo aX = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl?someParameter2=someValue2&someParameter=someValue"); + UrlInfo b = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl"); + + Assert.IsTrue(a1.GetHashCode()==a2.GetHashCode()); + Assert.IsTrue(a1.GetHashCode()!=b.GetHashCode()); - Assert.IsTrue(a1.GetHashCode()==a2.GetHashCode()); - Assert.IsTrue(a1.GetHashCode()!=aX.GetHashCode()); - Assert.IsTrue(a1.GetHashCode()!=b.GetHashCode()); + Assert.IsTrue(a1.Equals(a2)); + Assert.IsFalse(a1.Equals(b)); + } - Assert.IsTrue(a1.Equals(a2)); - Assert.IsFalse(a1.Equals(aX)); - Assert.IsFalse(a1.Equals(b)); - } + [Test] + public void WithTest() + { + UrlInfo a1 = UrlInfo.Parse("tcp://user:password@someHost:1000/someUrl/someUrl?p3=v3&p4=v4&p1=v1&p2=v2"); + var a2 = a1 with { + Port = 2000, + Password = "xxx", + Protocol = "unkProto", + Resource = "other/resource", + Params = new Dictionary { { "a", "b" } } + }; + Assert.AreEqual("unkProto://user:xxx@someHost:2000/other/resource?a=b", a2.ToString()); + Assert.IsTrue(a2.Equals(a2)); + Assert.IsFalse(a1.Equals(a2)); + var a3 = UrlInfo.Parse("unkProto://user:xxx@someHost:2000/other/resource?a=b"); + Assert.IsTrue(a2 == a3); + } + + [Test] + public void TestUrlProps() + { + var url = UrlInfo.Parse("sqlserver://int:xxx@127.0.0.1:51571/db"); + Assert.AreEqual("sqlserver", url.Protocol); + Assert.AreEqual("int", url.User); + Assert.AreEqual("xxx", url.Password); + Assert.AreEqual("127.0.0.1", url.Host); + Assert.AreEqual(51571, url.Port); + Assert.AreEqual("db", url.Resource); } -} \ No newline at end of file +} diff --git a/Orm/Xtensive.Orm/Orm/UrlInfo.cs b/Orm/Xtensive.Orm/Orm/UrlInfo.cs index dca896c9a..eb10f4df4 100644 --- a/Orm/Xtensive.Orm/Orm/UrlInfo.cs +++ b/Orm/Xtensive.Orm/Orm/UrlInfo.cs @@ -4,455 +4,394 @@ // Created by: Alex Yakunin // Created: 2007.06.08 -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; -using System.Runtime.Serialization; -using System.Security; using System.Text; using System.Text.RegularExpressions; using Xtensive.Core; using Xtensive.Comparison; - -namespace Xtensive.Orm +namespace Xtensive.Orm; + +/// +/// Holds an URL and provides easy access to its different parts. +/// +/// +/// +/// The common URL format that would be converted +/// to the can be represented +/// in the BNF form as following: +/// +/// url ::= protocol://[user[:password]@]host[:port]/resource[?parameters] +/// protocol ::= alphanumx[protocol] +/// user ::= alphanumx[user] +/// password ::= alphanumx[password] +/// host ::= hostname | hostnum +/// port ::= digits +/// resource ::= name +/// parameters ::= parameter[&parameter] +/// +/// hostname ::= name[.hostname] +/// hostnum ::= digits.digits.digits.digits +/// +/// parameter ::= name=[name] +/// +/// name ::= alpanumx[name] +/// +/// digits ::= digit[digits] +/// alphanumx ::= alphanum | escape | $ | - | _ | . | + | ! | * | " | ' | ( | ) | , | ; | # | space +/// alphanum ::= alpha | digit +/// escape ::= % hex hex +/// hex ::= digit | a | b | c | d | e | f | A | B | C | D | E | F +/// digit ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 +/// alpha ::= /* represents any unicode alpa character */ +/// +/// +/// +/// This not fully precise notation because it slightly simplified to be shorter. +/// But it almost completely reflects URL parser +/// capabilities. +/// +/// +/// Here you can see several valid URL samples: +///
+/// tcp://localhost/
+/// tcp://server:40000/myResource
+/// tcp://admin:admin@localhost:40000/myResource?askTimeout=60
+/// 
+///
+///
+[Serializable] +[DebuggerDisplay("{Url}")] +[TypeConverter(typeof(UrlInfoConverter))] +public sealed record UrlInfo +( +) : IComparable { + private static readonly Regex Pattern = new Regex( + @"^(?'proto'[^:]*[^sS])(?'secure'[sS]?)://" + + @"((?'username'[^:@]*)" + + @"(:(?'password'[^@]*))?@)?" + + @"(?'host'[^:/]*)" + + @"(:(?'port'\d+))?" + + @"/(?'resource'[^?]*)?" + + @"(\?(?'params'.*))?", + RegexOptions.Compiled|RegexOptions.Singleline); + /// - /// Holds an URL and provides easy access to its different parts. + /// Gets an URL this instance describes. /// - /// - /// - /// The common URL format that would be converted - /// to the can be represented - /// in the BNF form as following: - /// - /// url ::= protocol://[user[:password]@]host[:port]/resource[?parameters] - /// protocol ::= alphanumx[protocol] - /// user ::= alphanumx[user] - /// password ::= alphanumx[password] - /// host ::= hostname | hostnum - /// port ::= digits - /// resource ::= name - /// parameters ::= parameter[&parameter] - /// - /// hostname ::= name[.hostname] - /// hostnum ::= digits.digits.digits.digits - /// - /// parameter ::= name=[name] - /// - /// name ::= alpanumx[name] - /// - /// digits ::= digit[digits] - /// alphanumx ::= alphanum | escape | $ | - | _ | . | + | ! | * | " | ' | ( | ) | , | ; | # | space - /// alphanum ::= alpha | digit - /// escape ::= % hex hex - /// hex ::= digit | a | b | c | d | e | f | A | B | C | D | E | F - /// digit ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 - /// alpha ::= /* represents any unicode alpa character */ - /// - /// - /// - /// This not fully precise notation because it slightly simplified to be shorter. - /// But it almost completely reflects URL parser - /// capabilities. - /// - /// - /// Here you can see several valid URL samples: - ///
-  /// tcp://localhost/
-  /// tcp://server:40000/myResource
-  /// tcp://admin:admin@localhost:40000/myResource?askTimeout=60
-  /// 
- ///
- ///
- [Serializable] - [DebuggerDisplay("{url}")] - [TypeConverter(typeof(UrlInfoConverter))] - public class UrlInfo : - IEquatable, - IComparable, - ISerializable + public string Url { - private static readonly Regex Pattern = new Regex( - @"^(?'proto'[^:]*[^sS])(?'secure'[sS]?)://" + - @"((?'username'[^:@]*)" + - @"(:(?'password'[^@]*))?@)?" + - @"(?'host'[^:/]*)" + - @"(:(?'port'\d+))?" + - @"/(?'resource'[^?]*)?" + - @"(\?(?'params'.*))?", - RegexOptions.Compiled|RegexOptions.Singleline); - - private string url = string.Empty; - private string protocol = string.Empty; - private bool secure = false; - private string host = string.Empty; - private int port; - private string resource = string.Empty; - private string user = string.Empty; - private string password = string.Empty; - private ReadOnlyDictionary parameters; - - #region Properties: Url, Protocol, Host, etc... - - /// - /// Gets an URL this instance describes. - /// - public string Url - { - [DebuggerStepThrough] - get { return url; } - } + get { + if (field is null) { + StringBuilder sb = new(100); + sb.Append($"{Protocol}{(Secure ? "s" : "")}://"); + if (!string.IsNullOrEmpty(User)) { + sb.Append(UrlEncode(User)); + if (!string.IsNullOrEmpty(Password)) { + sb.Append($":{UrlEncode(Password)}"); + } + sb.Append('@'); + } - /// - /// Gets the protocol part of the current - /// (e.g. "tcp" is the protocol part of the "tcp://admin:password@localhost/resource" URL). - /// - public string Protocol - { - [DebuggerStepThrough] - get { return protocol; } - } + sb.Append(UrlEncode(Host)); + if (Port != 0) { + sb.Append($":{Port}"); + } - /// - /// Gets the security part of the current - /// Scheme with 's' suffix is secure. - /// - public bool Secure - { - [DebuggerStepThrough] - get => secure; - } + if (!string.IsNullOrEmpty(Resource)) { + sb.Append($"/{Resource}"); + } - /// - /// Gets the host part of the current - /// (e.g. "localhost" is the host part of the "tcp://admin:password@localhost/resource" URL). - /// - public string Host - { - [DebuggerStepThrough] - get { return host; } + if (Params.Count > 0) { + sb.Append('?'); + sb.Append(string.Join("&", Params.Select(kv =>$"{UrlEncode(kv.Key)}={UrlEncode(kv.Value)}"))); + } + field = sb.ToString(); + } + return field; } + private init; + } - /// - /// Gets the port part of the current - /// (e.g. 40000 is the port part of the "tcp://admin:password@localhost:40000/resource" URL). - /// - public int Port - { - [DebuggerStepThrough] - get { return port; } + /// + /// Gets the protocol part of the current + /// (e.g. "tcp" is the protocol part of the "tcp://admin:password@localhost/resource" URL). + /// + public string Protocol + { + [DebuggerStepThrough] get => field; + init { + field = value; + Url = null; } + } = string.Empty; - /// - /// Gets the resource name part of the current - /// (e.g. "resource" is the resource name part of the "tcp://admin:password@localhost/resource" URL). - /// - public string Resource - { - [DebuggerStepThrough] - get { return resource; } + /// + /// Gets the security part of the current + /// Scheme with 's' suffix is secure. + /// + public bool Secure + { + [DebuggerStepThrough] get => field; + init { + field = value; + Url = null; } + } - /// - /// Gets the user name part of the current - /// (e.g. "admin" is the user name part of the "tcp://admin:password@localhost/resource" URL). - /// - public string User - { - [DebuggerStepThrough] - get { return user; } + /// + /// Gets the host part of the current + /// (e.g. "localhost" is the host part of the "tcp://admin:password@localhost/resource" URL). + /// + public string Host + { + [DebuggerStepThrough] get; + init { + field = value; + Url = null; } + } = string.Empty; - /// - /// Gets the password part of the current - /// (e.g. "password" is the password part of the "tcp://admin:password@localhost/resource" URL). - /// - public string Password - { - [DebuggerStepThrough] - get { return password; } + /// + /// Gets the port part of the current + /// (e.g. 40000 is the port part of the "tcp://admin:password@localhost:40000/resource" URL). + /// + public int Port + { + [DebuggerStepThrough] get; + init { + field = value; + Url = null; } + } - /// - /// Gets additional parameters of the current - /// (e.g. "param1=value1&param2=value2" is the additional parameters part - /// of the "tcp://admin:password@localhost/resource?param1=value1&param2=value2" URL). - /// - /// - /// The mentioned part of the is parsed - /// and represented in a form. - /// - public IReadOnlyDictionary Params - { - [DebuggerStepThrough] - get { return parameters; } + /// + /// Gets the resource name part of the current + /// (e.g. "resource" is the resource name part of the "tcp://admin:password@localhost/resource" URL). + /// + public string Resource + { + [DebuggerStepThrough] get; + init { + field = value; + Url = null; } + } = string.Empty; - #endregion - - /// - /// Splits URL into parts (protocol, host, port, resource, user, password) and set all - /// derived values to the corresponding properties of the instance. - /// - /// URL to split - /// - /// The expected URL format is as the following: - /// proto://[[user[:password]@]host[:port]]/resource. - /// Note that the empty URL will cause an exception. - /// - /// Specified is invalid (cannot be parsed). - public static UrlInfo Parse(string url) - { - var result = new UrlInfo(); - Parse(url, result); - return result; + /// + /// Gets the user name part of the current + /// (e.g. "admin" is the user name part of the "tcp://admin:password@localhost/resource" URL). + /// + public string User + { + [DebuggerStepThrough] get => field; + init { + field = value; + Url = null; } + } = string.Empty; - private static void Parse(string url, UrlInfo info) - { - try { - string tUrl = url; - if (tUrl.Length==0) - tUrl = ":///"; - - var result = Pattern.Match(tUrl); - if (!result.Success) - throw Exceptions.InvalidUrl(url, "url"); - - int @port = 0; - - if (result.Result("${port}").Length!=0) - @port = int.Parse(result.Result("${port}")); - if (@port<0 || @port>65535) - throw Exceptions.InvalidUrl(url, "port"); - - string tParams = result.Result("${params}"); - string[] aParams = tParams.Split('&'); - var @params = new Dictionary(); - if (tParams!=string.Empty) { - foreach (string sPair in aParams) { - string[] aNameValue = sPair.Split(new char[] {'='}, 2); - if (aNameValue.Length!=2) - throw Exceptions.InvalidUrl(url, "parameters"); - @params.Add(UrlDecode(aNameValue[0]), UrlDecode(aNameValue[1])); - } - } - - info.url = url; - info.user = UrlDecode(result.Result("${username}")); - info.password = UrlDecode(result.Result("${password}")); - info.resource = UrlDecode(result.Result("${resource}")); - info.host = UrlDecode(result.Result("${host}")); - info.protocol = UrlDecode(result.Result("${proto}")); - info.secure = !string.IsNullOrEmpty(result.Result("${secure}")); - info.port = @port; - info.parameters = new ReadOnlyDictionary(@params); - } - catch (Exception e) { - if (e is ArgumentException || e is InvalidOperationException) - throw; - else - throw Exceptions.InvalidUrl(url, "url"); - } + /// + /// Gets the password part of the current + /// (e.g. "password" is the password part of the "tcp://admin:password@localhost/resource" URL). + /// + public string Password + { + [DebuggerStepThrough] get => field; + init { + field = value; + Url = null; } + } = string.Empty; - #region Nested type: UrlDecoder - - private class UrlDecoder - { - // Fields - private int m_bufferSize; - private byte[] m_byteBuffer; - private char[] m_charBuffer; - private Encoding m_encoding; - private int m_numBytes; - private int m_numChars; - - // Methods - internal UrlDecoder(int bufferSize, Encoding encoding) - { - m_bufferSize = bufferSize; - m_encoding = encoding; - m_charBuffer = new char[bufferSize]; - } - - internal void AddByte(byte b) - { - if (m_byteBuffer==null) - m_byteBuffer = new byte[m_bufferSize]; - m_byteBuffer[m_numBytes++] = b; - } - - internal void AddChar(char ch) - { - if (m_numBytes>0) - FlushBytes(); - m_charBuffer[m_numChars++] = ch; - } + /// + /// Gets additional parameters of the current + /// (e.g. "param1=value1&param2=value2" is the additional parameters part + /// of the "tcp://admin:password@localhost/resource?param1=value1&param2=value2" URL). + /// + /// + /// The mentioned part of the is parsed + /// and represented in a form. + /// + public IReadOnlyDictionary Params + { + [DebuggerStepThrough] + get => field; + init { + field = value; + Url = null; + } + } - private void FlushBytes() - { - if (m_numBytes>0) { - m_numChars += m_encoding.GetChars(m_byteBuffer, 0, m_numBytes, m_charBuffer, m_numChars); - m_numBytes = 0; + /// + /// Splits URL into parts (protocol, host, port, resource, user, password) and set all + /// derived values to the corresponding properties of the instance. + /// + /// URL to split + /// + /// The expected URL format is as the following: + /// proto://[[user[:password]@]host[:port]]/resource. + /// Note that the empty URL will cause an exception. + /// + /// Specified is invalid (cannot be parsed). + public static UrlInfo Parse(string url) + { + try { + string tUrl = url; + if (tUrl.Length==0) + tUrl = ":///"; + + var result = Pattern.Match(tUrl); + if (!result.Success) + throw Exceptions.InvalidUrl(url, "url"); + + int @port = 0; + + if (result.Result("${port}").Length!=0) + @port = int.Parse(result.Result("${port}")); + if (@port<0 || @port>65535) + throw Exceptions.InvalidUrl(url, "port"); + + string tParams = result.Result("${params}"); + string[] aParams = tParams.Split('&'); + var parameters = new SortedDictionary(); + if (tParams!=string.Empty) { + foreach (string sPair in aParams) { + string[] aNameValue = sPair.Split(new char[] {'='}, 2); + if (aNameValue.Length!=2) + throw Exceptions.InvalidUrl(url, "parameters"); + parameters.Add(UrlDecode(aNameValue[0]), UrlDecode(aNameValue[1])); } } - internal string GetString() - { - if (m_numBytes>0) - FlushBytes(); - if (m_numChars>0) - return new string(m_charBuffer, 0, m_numChars); - return string.Empty; - } + return new UrlInfo { + User = UrlDecode(result.Result("${username}")), + Password = UrlDecode(result.Result("${password}")), + Resource = UrlDecode(result.Result("${resource}")), + Host = UrlDecode(result.Result("${host}")), + Protocol = UrlDecode(result.Result("${proto}")), + Secure = !string.IsNullOrEmpty(result.Result("${secure}")), + Port = @port, + Params = parameters + }; } - - #endregion - - #region Private \ internal methods - - private static string UrlDecode(string str) - { - return UrlDecode(str, Encoding.UTF8); + catch (Exception e) when (!(e is ArgumentException or InvalidOperationException)) { + throw Exceptions.InvalidUrl(url, "url"); } + } - private static string UrlDecode(string s, Encoding e) + private class UrlDecoder + { + // Fields + private int m_bufferSize; + private byte[] m_byteBuffer; + private char[] m_charBuffer; + private Encoding m_encoding; + private int m_numBytes; + private int m_numChars; + + // Methods + internal UrlDecoder(int bufferSize, Encoding encoding) { - int len = s.Length; - UrlDecoder decoder = new UrlDecoder(len, e); - for (int i = 0; i=0 && num8>=0) { - byte num9 = (byte)((num7 << 4)|num8); - i += 2; - decoder.AddByte(num9); - continue; - } - } - loc_1: - if ((c&0xff80)=='\0') - decoder.AddByte((byte)c); - else - decoder.AddChar(c); - } - return decoder.GetString().Trim(); + m_bufferSize = bufferSize; + m_encoding = encoding; + m_charBuffer = new char[bufferSize]; } - private static int HexToInt(char h) + internal void AddByte(byte b) { - if (h>='0' && h<='9') - return h-'0'; - if (h<'a' || h>'f') { - if (h>='A' && h<='F') - return h-'A'+'\n'; - return -1; - } - return h-'a'+'\n'; + if (m_byteBuffer==null) + m_byteBuffer = new byte[m_bufferSize]; + m_byteBuffer[m_numBytes++] = b; } - #endregion - - #region IComparable<...>, IEquatable<...> methods - - /// - public bool Equals(UrlInfo other) + internal void AddChar(char ch) { - return AdvancedComparerStruct.System.Equals(url, other.url); + if (m_numBytes>0) + FlushBytes(); + m_charBuffer[m_numChars++] = ch; } - /// - public int CompareTo(UrlInfo other) + private void FlushBytes() { - return AdvancedComparerStruct.System.Compare(url, other.url); + if (m_numBytes>0) { + m_numChars += m_encoding.GetChars(m_byteBuffer, 0, m_numBytes, m_charBuffer, m_numChars); + m_numBytes = 0; + } } - #endregion - - #region Equals, GetHashCode, ==, != - - /// - public override bool Equals(object obj) => obj is UrlInfo other && Equals(other); - - /// - public override int GetHashCode() => url?.GetHashCode() ?? 0; - - /// - /// Checks specified objects for equality. - /// - /// - /// - /// - public static bool operator ==(UrlInfo left, UrlInfo right) => left?.Equals(right) ?? right is null; - - /// - /// Checks specified objects for inequality. - /// - /// - /// - /// - public static bool operator !=(UrlInfo left, UrlInfo right) => !(left == right); - - #endregion - - /// - public override string ToString() + internal string GetString() { - return url; + if (m_numBytes>0) + FlushBytes(); + if (m_numChars>0) + return new string(m_charBuffer, 0, m_numChars); + return string.Empty; } + } + private static string UrlEncode(string str) => Uri.EscapeDataString(str); - // Constructors - - private UrlInfo() - { + private static string UrlDecode(string s, Encoding e = null) + { + e ??= Encoding.UTF8; + int len = s.Length; + UrlDecoder decoder = new UrlDecoder(len, e); + for (int i = 0; i=0 && num8>=0) { + byte num9 = (byte)((num7 << 4)|num8); + i += 2; + decoder.AddByte(num9); + continue; + } + } + loc_1: + if ((c&0xff80)=='\0') + decoder.AddByte((byte)c); + else + decoder.AddChar(c); } + return decoder.GetString().Trim(); + } - #region ISerializable members, deserializing constructor + private static int HexToInt(char h) => + h is >= '0' and <= '9' ? h - '0' + : h is >= 'a' and <= 'f' ? h - 'a' + 10 + : h is >= 'A' and <= 'F' ? h - 'A' + 10 + : -1; - /// - /// Deserilizing constructor. - /// - /// The source (see ) for this deserialization. - /// The to populate the data from. - protected UrlInfo(SerializationInfo info, StreamingContext context) - { - Parse(info.GetString("Url"), this); - } + /// + public bool Equals(UrlInfo other) => + AdvancedComparerStruct.System.Equals(Url, other.Url); - /// - /// Populates a with the data needed to serialize the target object. - /// - /// The destination (see ) for this serialization. - /// The to populate with data. - /// The caller does not have the required permission. - [SecurityCritical] - public virtual void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue("Url", url); - } + /// + public int CompareTo(UrlInfo other) => + AdvancedComparerStruct.System.Compare(Url, other.Url); - #endregion - } + /// + public override int GetHashCode() => Url.GetHashCode(); + + /// + public override string ToString() => Url; }