File: System\Net\IPAddressParser.cs
Web Access
Project: src\src\libraries\System.Net.Primitives\src\System.Net.Primitives.csproj (System.Net.Primitives)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Globalization;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
 
namespace System.Net
{
    internal static class IPAddressParser
    {
        internal const int MaxIPv4StringLength = 15; // 4 numbers separated by 3 periods, with up to 3 digits per number
        internal const int MaxIPv6StringLength = 65;
 
        internal static IPAddress? Parse<TChar>(ReadOnlySpan<TChar> ipSpan, bool tryParse)
            where TChar : unmanaged, IBinaryInteger<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(byte) || typeof(TChar) == typeof(char));
 
            if (ipSpan.Contains(TChar.CreateTruncating(':')))
            {
                // The address is parsed as IPv6 if and only if it contains a colon. This is valid because
                // we don't support/parse a port specification at the end of an IPv4 address.
                Span<ushort> numbers = stackalloc ushort[IPAddressParserStatics.IPv6AddressShorts];
                numbers.Clear();
                if (TryParseIPv6(ipSpan, numbers, IPAddressParserStatics.IPv6AddressShorts, out uint scope))
                {
                    return new IPAddress(numbers, scope);
                }
            }
            else if (TryParseIpv4(ipSpan, out long address))
            {
                return new IPAddress(address);
            }
 
            if (tryParse)
            {
                return null;
            }
 
            throw new FormatException(SR.dns_bad_ip_address, new SocketException(SocketError.InvalidArgument));
        }
 
        private static unsafe bool TryParseIpv4<TChar>(ReadOnlySpan<TChar> ipSpan, out long address)
            where TChar : unmanaged, IBinaryInteger<TChar>
        {
            int end = ipSpan.Length;
            long tmpAddr;
 
            fixed (TChar* ipStringPtr = &MemoryMarshal.GetReference(ipSpan))
            {
                tmpAddr = IPv4AddressHelper.ParseNonCanonical(ipStringPtr, 0, ref end, notImplicitFile: true);
            }
 
            if (tmpAddr != IPv4AddressHelper.Invalid && end == ipSpan.Length)
            {
                // IPv4AddressHelper.ParseNonCanonical returns the bytes in host order.
                // Convert to network order and return success.
                address = (uint)IPAddress.HostToNetworkOrder(unchecked((int)tmpAddr));
                return true;
            }
 
            // Failed to parse the address.
            address = 0;
            return false;
        }
 
        private static unsafe bool TryParseIPv6<TChar>(ReadOnlySpan<TChar> ipSpan, Span<ushort> numbers, int numbersLength, out uint scope)
            where TChar : unmanaged, IBinaryInteger<TChar>
        {
            Debug.Assert(typeof(TChar) == typeof(char) || typeof(TChar) == typeof(byte));
            Debug.Assert(numbersLength >= IPAddressParserStatics.IPv6AddressShorts);
 
            int end = ipSpan.Length;
            bool isValid = false;
            fixed (TChar* ipStringPtr = &MemoryMarshal.GetReference(ipSpan))
            {
                isValid = IPv6AddressHelper.IsValidStrict(ipStringPtr, 0, ref end);
            }
 
            scope = 0;
            if (isValid || (end != ipSpan.Length))
            {
                IPv6AddressHelper.Parse(ipSpan, numbers, out ReadOnlySpan<TChar> scopeIdSpan);
 
                if (scopeIdSpan.Length > 1)
                {
                    bool parsedNumericScope = false;
                    scopeIdSpan = scopeIdSpan.Slice(1);
 
                    // scopeId is a numeric value
                    if (typeof(TChar) == typeof(byte))
                    {
                        ReadOnlySpan<byte> castScopeIdSpan = MemoryMarshal.Cast<TChar, byte>(scopeIdSpan);
 
                        parsedNumericScope = uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope);
                    }
                    else if (typeof(TChar) == typeof(char))
                    {
                        ReadOnlySpan<char> castScopeIdSpan = MemoryMarshal.Cast<TChar, char>(scopeIdSpan);
 
                        parsedNumericScope = uint.TryParse(castScopeIdSpan, NumberStyles.None, CultureInfo.InvariantCulture, out scope);
                    }
 
                    if (parsedNumericScope)
                    {
                        return true;
                    }
                    else
                    {
                        uint interfaceIndex = InterfaceInfoPal.InterfaceNameToIndex(scopeIdSpan);
 
                        if (interfaceIndex > 0)
                        {
                            scope = interfaceIndex;
                            return true; // scopeId is a known interface name
                        }
                    }
 
                    // scopeId is an unknown interface name
                }
 
                // scopeId is not presented
                return true;
            }
 
            return false;
        }
 
        internal static int FormatIPv4Address<TChar>(uint address, Span<TChar> addressString)
            where TChar : unmanaged, IBinaryInteger<TChar>
        {
            address = (uint)IPAddress.NetworkToHostOrder(unchecked((int)address));
 
            int pos = FormatByte(address >> 24, addressString);
            addressString[pos++] = TChar.CreateTruncating('.');
            pos += FormatByte(address >> 16, addressString.Slice(pos));
            addressString[pos++] = TChar.CreateTruncating('.');
            pos += FormatByte(address >> 8, addressString.Slice(pos));
            addressString[pos++] = TChar.CreateTruncating('.');
            pos += FormatByte(address, addressString.Slice(pos));
 
            return pos;
 
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            static int FormatByte(uint number, Span<TChar> addressString)
            {
                number &= 0xFF;
 
                if (number >= 10)
                {
                    uint hundreds, tens;
                    if (number >= 100)
                    {
                        (uint hundredsAndTens, number) = Math.DivRem(number, 10);
                        (hundreds, tens) = Math.DivRem(hundredsAndTens, 10);
 
                        addressString[2] = TChar.CreateTruncating('0' + number);
                        addressString[1] = TChar.CreateTruncating('0' + tens);
                        addressString[0] = TChar.CreateTruncating('0' + hundreds);
                        return 3;
                    }
 
                    (tens, number) = Math.DivRem(number, 10);
                    addressString[1] = TChar.CreateTruncating('0' + number);
                    addressString[0] = TChar.CreateTruncating('0' + tens);
                    return 2;
                }
 
                addressString[0] = TChar.CreateTruncating('0' + number);
                return 1;
            }
        }
 
        internal static int FormatIPv6Address<TChar>(ushort[] address, uint scopeId, Span<TChar> destination)
            where TChar : unmanaged, IBinaryInteger<TChar>
        {
            int pos = 0;
 
            if (IPv6AddressHelper.ShouldHaveIpv4Embedded(address))
            {
                // We need to treat the last 2 ushorts as a 4-byte IPv4 address,
                // so output the first 6 ushorts normally, followed by the IPv4 address.
                AppendSections(address.AsSpan(0, 6), destination, ref pos);
                if (destination[pos - 1] != TChar.CreateTruncating(':'))
                {
                    destination[pos++] = TChar.CreateTruncating(':');
                }
 
                pos += FormatIPv4Address(ExtractIPv4Address(address), destination.Slice(pos));
            }
            else
            {
                // No IPv4 address.  Output all 8 sections as part of the IPv6 address
                // with normal formatting rules.
                AppendSections(address.AsSpan(0, 8), destination, ref pos);
            }
 
            // If there's a scope ID, append it.
            if (scopeId != 0)
            {
                destination[pos++] = TChar.CreateTruncating('%');
 
                // TODO https://github.com/dotnet/runtime/issues/84527: Use UInt32 TryFormat for both char and byte once IUtf8SpanFormattable implementation exists
                Span<TChar> chars = stackalloc TChar[10];
                int bytesPos = 10;
                do
                {
                    (scopeId, uint digit) = Math.DivRem(scopeId, 10);
                    chars[--bytesPos] = TChar.CreateTruncating('0' + digit);
                }
                while (scopeId != 0);
                Span<TChar> used = chars.Slice(bytesPos);
                used.CopyTo(destination.Slice(pos));
                pos += used.Length;
            }
 
            return pos;
 
            // Appends each of the numbers in address in indexed range [fromInclusive, toExclusive),
            // while also replacing the longest sequence of 0s found in that range with "::", as long
            // as the sequence is more than one 0.
            static void AppendSections(ReadOnlySpan<ushort> address, Span<TChar> destination, ref int offset)
            {
                // Find the longest sequence of zeros to be combined into a "::"
                (int zeroStart, int zeroEnd) = IPv6AddressHelper.FindCompressionRange(address);
                bool needsColon = false;
 
                // Handle a zero sequence if there is one
                if (zeroStart >= 0)
                {
                    // Output all of the numbers before the zero sequence
                    for (int i = 0; i < zeroStart; i++)
                    {
                        if (needsColon)
                        {
                            destination[offset++] = TChar.CreateTruncating(':');
                        }
                        needsColon = true;
                        AppendHex(address[i], destination, ref offset);
                    }
 
                    // Output the zero sequence if there is one
                    destination[offset++] = TChar.CreateTruncating(':');
                    destination[offset++] = TChar.CreateTruncating(':');
                    needsColon = false;
                }
 
                // Output everything after the zero sequence
                for (int i = zeroEnd; i < address.Length; i++)
                {
                    if (needsColon)
                    {
                        destination[offset++] = TChar.CreateTruncating(':');
                    }
                    needsColon = true;
                    AppendHex(address[i], destination, ref offset);
                }
            }
 
            // Appends a number as hexadecimal (without the leading "0x")
            static void AppendHex(ushort value, Span<TChar> destination, ref int offset)
            {
                if ((value & 0xFFF0) != 0)
                {
                    if ((value & 0xFF00) != 0)
                    {
                        if ((value & 0xF000) != 0)
                        {
                            destination[offset++] = TChar.CreateTruncating(HexConverter.ToCharLower(value >> 12));
                        }
 
                        destination[offset++] = TChar.CreateTruncating(HexConverter.ToCharLower(value >> 8));
                    }
 
                    destination[offset++] = TChar.CreateTruncating(HexConverter.ToCharLower(value >> 4));
                }
 
                destination[offset++] = TChar.CreateTruncating(HexConverter.ToCharLower(value));
            }
        }
 
        /// <summary>Extracts the IPv4 address from the end of the IPv6 address byte array.</summary>
        private static uint ExtractIPv4Address(ushort[] address)
        {
            uint ipv4address = (uint)address[6] << 16 | address[7];
            return (uint)IPAddress.HostToNetworkOrder(unchecked((int)ipv4address));
        }
    }
}