File: System\UriBuilder.cs
Web Access
Project: src\src\libraries\System.Private.Uri\src\System.Private.Uri.csproj (System.Private.Uri)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
 
namespace System
{
    public class UriBuilder
    {
        private string _scheme = "http";
        private string _username = string.Empty;
        private string _password = string.Empty;
        private string _host = "localhost";
        private int _port = -1;
        private string _path = "/";
        private string _query = string.Empty;
        private string _fragment = string.Empty;
 
        private bool _changed = true;
        private Uri? _uri;
 
        public UriBuilder()
        {
        }
 
        public UriBuilder([StringSyntax(StringSyntaxAttribute.Uri)] string uri)
        {
            // setting allowRelative=true for a string like www.acme.org
            _uri = new Uri(uri, UriKind.RelativeOrAbsolute);
 
            if (!_uri.IsAbsoluteUri)
            {
                _uri = new Uri(Uri.UriSchemeHttp + Uri.SchemeDelimiter + uri);
            }
 
            SetFieldsFromUri();
        }
 
        public UriBuilder(Uri uri)
        {
            ArgumentNullException.ThrowIfNull(uri);
 
            _uri = uri;
            SetFieldsFromUri();
        }
 
        public UriBuilder(string? schemeName, string? hostName)
        {
            Scheme = schemeName;
            Host = hostName;
        }
 
        public UriBuilder(string? scheme, string? host, int portNumber)
            : this(scheme, host)
        {
            Port = portNumber;
        }
 
        public UriBuilder(string? scheme, string? host, int port, string? pathValue)
            : this(scheme, host, port)
        {
            Path = pathValue;
        }
 
        public UriBuilder(string? scheme, string? host, int port, string? path, string? extraValue)
            : this(scheme, host, port, path)
        {
            if (!string.IsNullOrEmpty(extraValue))
            {
                if (extraValue[0] == '#')
                {
                    _fragment = extraValue;
                }
                else if (extraValue[0] == '?')
                {
                    int fragmentIndex = extraValue.IndexOf('#');
                    if (fragmentIndex == -1)
                    {
                        _query = extraValue;
                    }
                    else
                    {
                        _query = extraValue.Substring(0, fragmentIndex);
                        _fragment = extraValue.Substring(fragmentIndex);
                    }
                }
                else
                {
                    throw new ArgumentException(SR.Argument_ExtraNotValid, nameof(extraValue));
                }
 
                if (_query.Length == 1)
                {
                    _query = string.Empty;
                }
 
                if (_fragment.Length == 1)
                {
                    _fragment = string.Empty;
                }
            }
        }
 
        [AllowNull]
        public string Scheme
        {
            get => _scheme;
            set
            {
                value ??= string.Empty;
 
                if (value.Length != 0)
                {
                    if (!Uri.CheckSchemeName(value))
                    {
                        int index = value.IndexOf(':');
                        if (index != -1)
                        {
                            value = value.Substring(0, index);
                        }
 
                        if (!Uri.CheckSchemeName(value))
                        {
                            throw new ArgumentException(SR.net_uri_BadScheme, nameof(value));
                        }
                    }
 
                    value = value.ToLowerInvariant();
                }
 
                _scheme = value;
                _changed = true;
            }
        }
 
        [AllowNull]
        public string UserName
        {
            get => _username;
            set
            {
                _username = value ?? string.Empty;
                _changed = true;
            }
        }
 
        [AllowNull]
        public string Password
        {
            get => _password;
            set
            {
                _password = value ?? string.Empty;
                _changed = true;
            }
        }
 
        [AllowNull]
        public string Host
        {
            get => _host;
            set
            {
                if (!string.IsNullOrEmpty(value) && value.Contains(':') && value[0] != '[')
                {
                    //probable ipv6 address - Note: this is only supported for cases where the authority is inet-based.
                    value = "[" + value + "]";
                }
 
                _host = value ?? string.Empty;
                _changed = true;
            }
        }
 
        public int Port
        {
            get => _port;
            set
            {
                ArgumentOutOfRangeException.ThrowIfLessThan(value, -1);
                ArgumentOutOfRangeException.ThrowIfGreaterThan(value, 0xFFFF);
                _port = value;
                _changed = true;
            }
        }
 
        [AllowNull]
        public string Path
        {
            get => _path;
            set
            {
                _path = string.IsNullOrEmpty(value)
                    ? "/"
                    : Uri.InternalEscapeString(value.Replace('\\', '/'));
                _changed = true;
            }
        }
 
        [AllowNull]
        public string Query
        {
            get => _query;
            set
            {
                if (!string.IsNullOrEmpty(value) && value[0] != '?')
                {
                    value = '?' + value;
                }
 
                _query = value ?? string.Empty;
                _changed = true;
            }
        }
 
        [AllowNull]
        public string Fragment
        {
            get => _fragment;
            set
            {
                if (!string.IsNullOrEmpty(value) && value[0] != '#')
                {
                    value = '#' + value;
                }
 
                _fragment = value ?? string.Empty;
                _changed = true;
            }
        }
 
        public Uri Uri
        {
            get
            {
                if (_changed)
                {
                    _uri = new Uri(ToString());
                    SetFieldsFromUri();
                    _changed = false;
                }
                else
                {
                    Debug.Assert(_uri is not null);
                }
                return _uri;
            }
        }
 
        public override bool Equals([NotNullWhen(true)] object? rparam) => rparam is not null && Uri.Equals(rparam.ToString());
 
        public override int GetHashCode() => Uri.GetHashCode();
 
        // The following characters ("/" / "\" / "?" / "#" / "@") are from the gen-delims group.
        // We have to escape them to avoid corrupting the rest of the Uri string.
        // Other characters like spaces or non-ASCII will be escaped by Uri, we can ignore them here.
        private static readonly SearchValues<char> s_userInfoReservedChars =
            SearchValues.Create(@"/\?#@");
 
        private static string EncodeUserInfo(string input)
        {
            if (!input.AsSpan().ContainsAny(s_userInfoReservedChars))
            {
                return input;
            }
 
            return input
                .Replace("/", "%2F", StringComparison.Ordinal)
                .Replace(@"\", "%5C", StringComparison.Ordinal)
                .Replace("?", "%3F", StringComparison.Ordinal)
                .Replace("#", "%23", StringComparison.Ordinal)
                .Replace("@", "%40", StringComparison.Ordinal);
        }
 
        private void SetFieldsFromUri()
        {
            Debug.Assert(_uri is not null);
            _scheme = _uri.Scheme;
            _host = _uri.Host;
            _port = _uri.Port;
            _path = _uri.AbsolutePath;
            _query = _uri.Query;
            _fragment = _uri.Fragment;
 
            string userInfo = _uri.UserInfo;
 
            if (userInfo.Length > 0)
            {
                int index = userInfo.IndexOf(':');
 
                if (index != -1)
                {
                    _password = userInfo.Substring(index + 1);
                    _username = userInfo.Substring(0, index);
                }
                else
                {
                    _username = userInfo;
                }
            }
        }
 
        public override string ToString()
        {
            if (UserName.Length == 0 && Password.Length != 0)
            {
                throw new UriFormatException(SR.net_uri_BadUserPassword);
            }
 
            var vsb = new ValueStringBuilder(stackalloc char[Uri.StackallocThreshold]);
 
            string scheme = Scheme;
            string host = Host;
 
            if (scheme.Length != 0)
            {
                UriParser? syntax = UriParser.GetSyntax(scheme);
                string schemeDelimiter;
                if (syntax is null)
                {
                    schemeDelimiter = host.Length == 0 ? ":" : Uri.SchemeDelimiter;
                }
                else
                {
                    schemeDelimiter = syntax.InFact(UriSyntaxFlags.MustHaveAuthority)
                        || (host.Length != 0 && syntax.NotAny(UriSyntaxFlags.MailToLikeUri) && syntax.InFact(UriSyntaxFlags.OptionalAuthority))
                            ? Uri.SchemeDelimiter
                            : ":";
                }
 
                vsb.Append(scheme);
                vsb.Append(schemeDelimiter);
            }
 
            string username = EncodeUserInfo(UserName);
            if (username.Length != 0)
            {
                vsb.Append(username);
 
                string password = EncodeUserInfo(Password);
                if (password.Length != 0)
                {
                    vsb.Append(':');
                    vsb.Append(password);
                }
 
                vsb.Append('@');
            }
 
            if (host.Length != 0)
            {
                vsb.Append(host);
 
                if (_port != -1)
                {
                    vsb.Append(':');
 
                    const int MaxUshortLength = 5;
                    bool success = _port.TryFormat(vsb.AppendSpan(MaxUshortLength), out int charsWritten);
                    Debug.Assert(success);
                    vsb.Length -= MaxUshortLength - charsWritten;
                }
            }
 
            var path = Path;
            if (path.Length != 0)
            {
                if (!path.StartsWith('/') && host.Length != 0)
                {
                    vsb.Append('/');
                }
 
                vsb.Append(path);
            }
 
            vsb.Append(Query);
 
            vsb.Append(Fragment);
 
            return vsb.ToString();
        }
    }
}