< Summary

Information
Class: IceRpc.Ice.Operations.IceProxyIceDecoderExtensions
Assembly: IceRpc.Ice
File(s): /home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Ice/Operations/IceProxyIceDecoderExtensions.cs
Tag: 2300_35243572715
Line coverage
81%
Covered lines: 173
Uncovered lines: 38
Coverable lines: 211
Total lines: 364
Line coverage: 81.9%
Branch coverage
76%
Covered branches: 60
Total branches: 78
Branch coverage: 76.9%
Method coverage
100%
Covered methods: 9
Fully covered methods: 6
Total methods: 9
Method coverage: 100%
Full method coverage: 66.6%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
DecodeProxy(...)100%22100%
CreateProxy(...)100%22100%
DecodeServiceAddress(...)100%22100%
DecodeServerAddress(...)62.5%473275.3%
DecodeUriServerAddress()100%11100%
DecodeServiceAddressCore(...)75%232081.03%
DecodeTcpServerAddressBody(...)75%10868.18%
EscapeAdapterId(...)100%1212100%

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc.Ice/Operations/IceProxyIceDecoderExtensions.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Ice.Codec;
 4using IceRpc.Ice.Internal;
 5using IceRpc.Ice.Operations.Internal;
 6using IceRpc.Internal;
 7using System.Buffers;
 8using System.Collections.Immutable;
 9using System.Diagnostics;
 10using System.Globalization;
 11using System.Runtime.CompilerServices;
 12using System.Text;
 13
 14namespace IceRpc.Ice.Operations;
 15
 16/// <summary>Provides extension methods for <see cref="IceDecoder" /> to decode proxies.</summary>
 17public static class IceProxyIceDecoderExtensions
 18{
 19    // The printable ASCII range. Characters outside this range must be percent-escaped in a service address parameter
 20    // value.
 21    private const char FirstValidChar = '\x21';
 22    private const char LastValidChar = '\x7E';
 23
 24    // Same as ServiceAddress's _notValidInParamValue, plus '%' which must be escaped to make the percent-escaped
 25    // value unambiguously decodable.
 226    private static readonly SearchValues<char> _mustEscapeInAdapterId = SearchValues.Create("\"<>#%&\\^`{|}");
 27
 28    /// <summary>Decodes a proxy struct.</summary>
 29    /// <typeparam name="TProxy">The type of the proxy struct to decode.</typeparam>
 30    /// <param name="decoder">The Ice decoder.</param>
 31    /// <returns>The decoded proxy, or <see langword="null" />.</returns>
 32    public static TProxy? DecodeProxy<TProxy>(this ref IceDecoder decoder) where TProxy : struct, IIceProxy =>
 6133        decoder.DecodeServiceAddress() is ServiceAddress serviceAddress ?
 6134            CreateProxy<TProxy>(serviceAddress, decoder.DecodingContext) : null;
 35
 36    private static TProxy CreateProxy<TProxy>(ServiceAddress serviceAddress, object? decodingContext)
 37        where TProxy : struct, IIceProxy
 4938    {
 4939        Debug.Assert(serviceAddress.Protocol is not null, "The Ice encoding does not support relative proxies.");
 40
 4941        if (decodingContext is null)
 3842        {
 3843            return new TProxy { Invoker = InvalidInvoker.Instance, ServiceAddress = serviceAddress };
 44        }
 45        else
 1146        {
 1147            var baseProxy = (IIceProxy)decodingContext;
 1148            return new TProxy
 1149            {
 1150                EncodeOptions = baseProxy.EncodeOptions,
 1151                Invoker = baseProxy.Invoker,
 1152                ServiceAddress = serviceAddress
 1153            };
 54        }
 4955    }
 56
 57    /// <summary>Decodes a service address.</summary>
 58    /// <param name="decoder">The Ice decoder.</param>
 59    /// <returns>The decoded service address, or <see langword="null" />.</returns>
 60    private static ServiceAddress? DecodeServiceAddress(this ref IceDecoder decoder)
 6161    {
 6162        string path = new Identity(ref decoder).ToPath();
 6163        return path != "/" ? decoder.DecodeServiceAddressCore(path) : null;
 5764    }
 65
 66    /// <summary>Decodes a server address.</summary>
 67    /// <param name="decoder">The Ice decoder.</param>
 68    /// <param name="protocol">The protocol of this server address.</param>
 69    /// <returns>The server address decoded by this decoder.</returns>
 70    private static ServerAddress DecodeServerAddress(this ref IceDecoder decoder, Protocol protocol)
 3771    {
 72        // With the Ice encoding, the ice server addresses are transport-specific, with a transport-specific encoding.
 73
 3774        ServerAddress? serverAddress = null;
 3775        short transportCodeValue = decoder.DecodeShort();
 3776        if (transportCodeValue < 0)
 077        {
 078            throw new InvalidDataException($"Received invalid transport code: {transportCodeValue}.");
 79        }
 3780        var transportCode = (TransportCode)transportCodeValue;
 81
 3782        int size = decoder.DecodeInt();
 3783        if (size < 6)
 084        {
 085            throw new InvalidDataException($"The Ice encapsulation's size ({size}) is too small.");
 86        }
 87
 88        // Remove 6 bytes from the encapsulation size (4 for encapsulation size, 2 for encoding).
 3789        size -= 6;
 90
 3791        byte encodingMajor = decoder.DecodeByte();
 3792        byte encodingMinor = decoder.DecodeByte();
 93
 3794        if (decoder.Remaining < size)
 095        {
 096            throw new InvalidDataException($"The Ice encapsulation's size ({size}) is too big.");
 97        }
 98
 3799        if (encodingMajor == 1 && encodingMinor <= 1)
 37100        {
 37101            long oldPos = decoder.Consumed;
 102
 37103            if (protocol == Protocol.Ice)
 18104            {
 18105                switch (transportCode)
 106                {
 107                    case TransportCode.Tcp:
 11108                        serverAddress = decoder.DecodeTcpServerAddressBody(IceProxyIceEncoderExtensions.TcpName);
 11109                        break;
 110
 111                    case TransportCode.Ssl:
 2112                        serverAddress = decoder.DecodeTcpServerAddressBody(IceProxyIceEncoderExtensions.SslName);
 2113                        break;
 114
 115                    case TransportCode.Uri:
 3116                        serverAddress = DecodeUriServerAddress(decoder.DecodeString());
 1117                        if (serverAddress.Value.Protocol != protocol)
 0118                        {
 0119                            throw new InvalidDataException(
 0120                                $"Expected {protocol} server address but received '{serverAddress.Value}'.");
 121                        }
 1122                        break;
 123
 124                    default:
 2125                        if (size == 0)
 0126                        {
 0127                            throw new InvalidDataException("Received opaque server address with an empty body.");
 128                        }
 129
 130                        // Create a server address for transport opaque
 2131                        ImmutableDictionary<string, string>.Builder builder =
 2132                            ImmutableDictionary.CreateBuilder<string, string>();
 133
 2134                        if (encodingMinor == 0)
 1135                        {
 1136                            builder.Add("e", "1.0");
 1137                        }
 138                        // else no e
 139
 2140                        builder.Add("t", ((short)transportCode).ToString(CultureInfo.InvariantCulture));
 2141                        {
 2142                            using IMemoryOwner<byte> memoryOwner = MemoryPool<byte>.Shared.Rent(size);
 2143                            Span<byte> span = memoryOwner.Memory.Span[0..size];
 2144                            decoder.CopyTo(span);
 2145                            string value = Convert.ToBase64String(span);
 2146                            builder.Add("v", value);
 2147                            decoder.IncreaseCollectionAllocation(value.Length, Unsafe.SizeOf<char>());
 2148                        }
 149
 2150                        serverAddress = new ServerAddress(
 2151                            Protocol.Ice,
 2152                            host: "opaque", // not a real host obviously
 2153                            port: Protocol.Ice.DefaultPort,
 2154                            transport: IceProxyIceEncoderExtensions.OpaqueName,
 2155                            builder.ToImmutable());
 2156                        break;
 157                }
 16158            }
 19159            else if (transportCode == TransportCode.Uri)
 19160            {
 161                // The server addresses of an Ice-encoded icerpc proxies only use TransportCode.Uri.
 19162                serverAddress = DecodeUriServerAddress(decoder.DecodeString());
 17163                if (serverAddress.Value.Protocol != protocol)
 0164                {
 0165                    throw new InvalidDataException(
 0166                        $"Expected {protocol} server address but received '{serverAddress.Value}'.");
 167                }
 17168            }
 169
 33170            if (serverAddress is not null)
 33171            {
 172                // Make sure we read the full encapsulation.
 33173                if (decoder.Consumed != oldPos + size)
 0174                {
 0175                    throw new InvalidDataException(
 0176                        $"There are {oldPos + size - decoder.Consumed} bytes left in server address encapsulation.");
 177                }
 33178            }
 33179        }
 180
 33181        if (serverAddress is null)
 0182        {
 0183            throw new InvalidDataException(
 0184                $"Cannot decode server address for protocol '{protocol}' and transport '{transportCode.ToString().ToLowe
 185        }
 186
 33187        return serverAddress.Value;
 188
 189        static ServerAddress DecodeUriServerAddress(string uriString)
 22190        {
 191            try
 22192            {
 22193                return new ServerAddress(new Uri(uriString));
 194            }
 4195            catch (Exception exception) when (exception is UriFormatException or ArgumentException)
 4196            {
 4197                throw new InvalidDataException(
 4198                    $"Received invalid server address URI '{uriString}'.",
 4199                    exception);
 200            }
 18201        }
 33202    }
 203
 204    /// <summary>Decodes a service address encoded with the Ice encoding.</summary>
 205    /// <param name="decoder">The Ice decoder.</param>
 206    /// <param name="path">The decoded path.</param>
 207    /// <returns>The decoded service address.</returns>
 208    private static ServiceAddress DecodeServiceAddressCore(this ref IceDecoder decoder, string path)
 53209    {
 210        // With the Ice encoding, a service address is encoded as a kind of discriminated union with:
 211        // - Identity
 212        // - If Identity is not the null identity:
 213        //     - the fragment, invocation mode, secure, protocol major and minor, and the encoding major and minor
 214        //     - a sequence of server addresses (can be empty)
 215        //     - an adapter ID string present only when the sequence of server addresses is empty
 216
 53217        string fragment = decoder.DecodeFacet().ToFragment();
 53218        _ = decoder.DecodeInvocationMode();
 53219        _ = decoder.DecodeBool();
 53220        byte protocolMajor = decoder.DecodeByte();
 53221        byte protocolMinor = decoder.DecodeByte();
 53222        decoder.Skip(2); // skip encoding major and minor
 223
 53224        Protocol protocol = protocolMajor == Protocol.Ice.ByteValue ? Protocol.Ice :
 53225            (protocolMajor == Protocol.IceRpc.ByteValue ? Protocol.IceRpc :
 53226                throw new InvalidDataException(
 53227                    $"Received service address with invalid protocolMajor value: {protocolMajor}"));
 228
 53229        if (protocolMinor != 0)
 0230        {
 0231            throw new InvalidDataException(
 0232                $"Received service address with invalid protocolMinor value: {protocolMinor}");
 233        }
 234
 53235        int count = decoder.DecodeSize();
 53236        ServerAddress? serverAddress = null;
 53237        IEnumerable<ServerAddress> altServerAddresses = ImmutableList<ServerAddress>.Empty;
 238
 53239        ImmutableDictionary<string, string> serviceAddressParams = ImmutableDictionary<string, string>.Empty;
 240
 53241        if (count == 0)
 17242        {
 17243            if (decoder.DecodeString() is string adapterId && adapterId.Length > 0)
 10244            {
 10245                serviceAddressParams =
 10246                    serviceAddressParams.Add("adapter-id", EscapeAdapterId(adapterId));
 10247            }
 17248        }
 249        else
 36250        {
 36251            serverAddress = decoder.DecodeServerAddress(protocol);
 32252            if (count >= 2)
 1253            {
 254                // An Ice-encoded server address consumes at least 8 bytes (2 bytes for the server address type and 6
 255                // bytes for the encapsulation header). SizeOf ServerAddress is large but less than 8 * 8.
 1256                decoder.IncreaseCollectionAllocation(count, Unsafe.SizeOf<ServerAddress>());
 257
 1258                var serverAddressArray = new ServerAddress[count - 1];
 4259                for (int i = 0; i < count - 1; ++i)
 1260                {
 1261                    serverAddressArray[i] = decoder.DecodeServerAddress(protocol);
 1262                }
 1263                altServerAddresses = serverAddressArray;
 1264            }
 32265        }
 266
 267        try
 49268        {
 49269            if (!protocol.HasFragment && fragment.Length > 0)
 0270            {
 0271                throw new InvalidDataException($"Unexpected fragment in {protocol} service address.");
 272            }
 273
 49274            return new ServiceAddress(
 49275                protocol,
 49276                path,
 49277                serverAddress,
 49278                altServerAddresses.ToImmutableList(),
 49279                serviceAddressParams,
 49280                fragment);
 281        }
 0282        catch (InvalidDataException)
 0283        {
 0284            throw;
 285        }
 0286        catch (Exception exception)
 0287        {
 0288            throw new InvalidDataException("Received invalid service address.", exception);
 289        }
 49290    }
 291
 292    /// <summary>Decodes the body of a tcp or ssl server address.</summary>
 293    private static ServerAddress DecodeTcpServerAddressBody(this ref IceDecoder decoder, string transport)
 13294    {
 13295        var body = new TcpServerAddressBody(ref decoder);
 296
 297        // The host name in an Ice tcp/ssl server address is never bracketed, even when it's an IPv6 address.
 13298        if (body.Host.StartsWith('[') || Uri.CheckHostName(body.Host) == UriHostNameType.Unknown)
 0299        {
 0300            throw new InvalidDataException($"Received service address with invalid host '{body.Host}'.");
 301        }
 302
 13303        ImmutableDictionary<string, string> parameters = ImmutableDictionary<string, string>.Empty;
 13304        if (body.Timeout != IceProxyIceEncoderExtensions.DefaultTcpTimeout)
 4305        {
 4306            parameters = parameters.Add("t", body.Timeout.ToString(CultureInfo.InvariantCulture));
 4307        }
 13308        if (body.Compress)
 1309        {
 1310            parameters = parameters.Add("z", "");
 1311        }
 312
 313        try
 13314        {
 13315            return new ServerAddress(Protocol.Ice, body.Host, checked((ushort)body.Port), transport, parameters);
 316        }
 0317        catch (OverflowException exception)
 0318        {
 0319            throw new InvalidDataException(
 0320                "Cannot decode a server address with a port number larger than 65,535.",
 0321                exception);
 322        }
 13323    }
 324
 325    /// <summary>Percent-encodes only the characters that are invalid in a service address parameter value: characters
 326    /// outside the printable ASCII range <c>\x21..\x7E</c>, characters in
 327    /// <see cref="_mustEscapeInAdapterId" />, and the <c>%</c> character itself (which must be escaped to make the
 328    /// result unambiguously decodable).</summary>
 329    /// <param name="value">The raw adapter ID, as decoded from the wire.</param>
 330    /// <returns>An escaped string suitable for use as the value of the <c>adapter-id</c> service address parameter.
 331    /// </returns>
 332    /// <remarks>This is intentionally narrower than <see cref="Uri.EscapeDataString(string)" />, which over-escapes
 333    /// characters that are valid in service address parameter values such as <c>/</c>, <c>:</c>, and <c>@</c>.
 334    /// </remarks>
 335    private static string EscapeAdapterId(string value)
 10336    {
 10337        ReadOnlySpan<char> span = value.AsSpan();
 338
 339        // Fast path: nothing to escape. Adapter IDs are usually pure ASCII so we almost always take this path.
 10340        if (span.IndexOfAnyExceptInRange(FirstValidChar, LastValidChar) == -1 &&
 10341            span.IndexOfAny(_mustEscapeInAdapterId) == -1)
 5342        {
 5343            return value;
 344        }
 345
 346        // Slow path. Encode the whole string to UTF-8 bytes, then percent-escape every byte that is not a valid
 347        // unescaped char. UTF-8 continuation bytes (>= 0x80) naturally fall in the escape branch, so multi-byte
 348        // code points are handled without any surrogate-pair logic here.
 5349        byte[] utf8 = Encoding.UTF8.GetBytes(value);
 5350        var sb = new StringBuilder(utf8.Length + 8);
 111351        foreach (byte b in utf8)
 48352        {
 48353            if (b >= FirstValidChar && b <= LastValidChar && !_mustEscapeInAdapterId.Contains((char)b))
 40354            {
 40355                sb.Append((char)b);
 40356            }
 357            else
 8358            {
 8359                sb.Append('%').Append(b.ToString("X2", CultureInfo.InvariantCulture));
 8360            }
 48361        }
 5362        return sb.ToString();
 10363    }
 364}