< Summary

Line coverage
88%
Covered lines: 494
Uncovered lines: 63
Coverable lines: 557
Total lines: 1077
Line coverage: 88.6%
Branch coverage
80%
Covered branches: 189
Total branches: 234
Branch coverage: 80.7%
Method coverage
94%
Covered methods: 37
Fully covered methods: 20
Total methods: 39
Method coverage: 94.8%
Full method coverage: 51.2%

Metrics

File(s)

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Ice/Codec/IceDecoder.Class.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Ice.Codec.Internal;
 4using System.Collections.Immutable;
 5using System.ComponentModel;
 6using System.Diagnostics;
 7using System.Globalization;
 8using System.Runtime.CompilerServices;
 9using static IceRpc.Ice.Codec.Internal.IceEncodingDefinitions;
 10
 11namespace IceRpc.Ice.Codec;
 12
 13/// <summary>Provides methods to decode data encoded with Ice.</summary>
 14public ref partial struct IceDecoder
 15{
 16    /// <summary>Decodes a class instance.</summary>
 17    /// <typeparam name="T">The class type.</typeparam>
 18    /// <returns>The class instance, or <see langword="null" />.</returns>
 19    public T? DecodeClass<T>() where T : IceClass
 29820    {
 29821        IceClass? obj = DecodeClass();
 22
 19623        if (obj is T result)
 7024        {
 7025            return result;
 26        }
 12627        else if (obj is null)
 12628        {
 12629            return null;
 30        }
 031        throw new InvalidDataException(
 032            $"Decoded instance of type '{obj.GetType()}' but expected instance of type '{typeof(T)}'.");
 19633    }
 34
 35    /// <summary>Decodes an Ice exception.</summary>
 36    /// <param name="message">The error message. It's used only when this method fails to find an exception class to
 37    /// instantiate.</param>
 38    /// <returns>The decoded Ice exception.</returns>
 39    public IceException DecodeException(string? message = null)
 3140    {
 3141        Debug.Assert(_classContext.Current.InstanceType == InstanceType.None);
 3142        _classContext.Current.InstanceType = InstanceType.Exception;
 43
 44        // We can decode the indirection table (if there is one) immediately after decoding each slice header
 45        // because the indirection table cannot reference the exception itself.
 46        // Each slice contains its type ID as a string.
 47
 3148        string? mostDerivedTypeId = null;
 3149        IActivator activator = _activator ?? _defaultActivator;
 50        IceException? iceException;
 51
 52        do
 3453        {
 54            // The type ID is always decoded for an exception and cannot be null.
 3455            string? typeId = DecodeSliceHeaderIntoCurrent();
 3456            Debug.Assert(typeId is not null);
 3457            mostDerivedTypeId ??= typeId;
 58
 3459            DecodeIndirectionTableIntoCurrent(); // we decode the indirection table immediately.
 60
 3461            iceException = activator.CreateInstance(typeId) as IceException;
 3462            if (iceException is null && SkipSlice(typeId))
 163            {
 64                // Cannot decode this exception. The message should be set only when the exception was received over
 65                // icerpc.
 166                throw new InvalidDataException(
 167                    message is null || message.Length == 0 ?
 168                    $"The dispatch returned an Ice exception with type ID '{mostDerivedTypeId}' that the configured acti
 169                    $"The dispatch returned an Ice exception with type ID '{mostDerivedTypeId}' that the configured acti
 70            }
 3371        }
 3372        while (iceException is null);
 73
 3074        _classContext.Current.FirstSlice = true;
 3075        iceException.Decode(ref this);
 3076        _classContext.Current = default;
 3077        return iceException;
 3078    }
 79
 80    /// <summary>Tells the decoder the end of a class or exception slice was reached.</summary>
 81    [EditorBrowsable(EditorBrowsableState.Never)]
 82    public void EndSlice()
 18583    {
 84        // Note that EndSlice is not called when we call SkipSlice.
 18585        Debug.Assert(_classContext.Current.InstanceType != InstanceType.None);
 86
 18587        if ((_classContext.Current.SliceFlags & SliceFlags.HasTaggedFields) != 0)
 2788        {
 2789            SkipTagged();
 2790        }
 18591        if ((_classContext.Current.SliceFlags & SliceFlags.HasIndirectionTable) != 0)
 1892        {
 1893            Debug.Assert(_classContext.Current.PosAfterIndirectionTable is not null &&
 1894                         _classContext.Current.IndirectionTable is not null);
 95
 1896            _reader.Advance(_classContext.Current.PosAfterIndirectionTable.Value - _reader.Consumed);
 1897            _classContext.Current.PosAfterIndirectionTable = null;
 1898            _classContext.Current.IndirectionTable = null;
 1899        }
 185100    }
 101
 102    /// <summary>Marks the start of the decoding of a class or remote exception slice.</summary>
 103    [EditorBrowsable(EditorBrowsableState.Never)]
 104    public void StartSlice()
 285105    {
 285106        Debug.Assert(_classContext.Current.InstanceType != InstanceType.None);
 285107        if (_classContext.Current.FirstSlice)
 185108        {
 185109            _classContext.Current.FirstSlice = false;
 185110        }
 111        else
 100112        {
 100113            _ = DecodeSliceHeaderIntoCurrent();
 100114            DecodeIndirectionTableIntoCurrent();
 100115        }
 285116    }
 117
 118    /// <summary>Decodes a class instance.</summary>
 119    /// <returns>The class instance. Can be <see langword="null" />.</returns>
 120    private IceClass? DecodeClass()
 298121    {
 298122        int index = DecodeSize();
 298123        if (index < 0)
 0124        {
 0125            throw new InvalidDataException($"Found invalid index {index} while decoding a class.");
 126        }
 298127        else if (index == 0)
 126128        {
 126129            return null;
 130        }
 172131        else if (_classContext.Current.InstanceType != InstanceType.None &&
 172132            (_classContext.Current.SliceFlags & SliceFlags.HasIndirectionTable) != 0)
 21133        {
 134            // When decoding an instance within a slice and there is an indirection table, we have an index within
 135            // this indirection table.
 136            // We need to decrement index since position 0 in the indirection table corresponds to index 1.
 21137            index--;
 138
 139            // If the right-hand side is null, the comparison simply evaluates to false.
 21140            if (index < _classContext.Current.IndirectionTable?.Length)
 21141            {
 21142                return _classContext.Current.IndirectionTable[index];
 143            }
 144            else
 0145            {
 0146                throw new InvalidDataException("The index is too big for the indirection table.");
 147            }
 148        }
 149        else
 151150        {
 151151            return DecodeInstance(index);
 152        }
 196153    }
 154
 155    /// <summary>Decodes an indirection table without updating _current.</summary>
 156    /// <returns>The indirection table.</returns>
 157    private IceClass[] DecodeIndirectionTable()
 28158    {
 28159        int size = DecodeSize();
 28160        if (size == 0)
 0161        {
 0162            throw new InvalidDataException("Invalid empty indirection table.");
 163        }
 28164        IncreaseCollectionAllocation(size, Unsafe.SizeOf<IceClass>());
 28165        var indirectionTable = new IceClass[size];
 116166        for (int i = 0; i < indirectionTable.Length; ++i)
 30167        {
 30168            int index = DecodeSize();
 30169            if (index < 1)
 0170            {
 0171                throw new InvalidDataException($"Found invalid index {index} decoding the indirection table.");
 172            }
 30173            indirectionTable[i] = DecodeInstance(index);
 30174        }
 28175        return indirectionTable;
 28176    }
 177
 178    /// <summary>Decodes the indirection table into _current's fields if there is an indirection table.
 179    /// Precondition: called after decoding the header of the current slice. This method does not change _pos.
 180    /// </summary>
 181    private void DecodeIndirectionTableIntoCurrent()
 289182    {
 289183        Debug.Assert(_classContext.Current.IndirectionTable is null);
 289184        if ((_classContext.Current.SliceFlags & SliceFlags.HasIndirectionTable) != 0)
 20185        {
 20186            if ((_classContext.Current.SliceFlags & SliceFlags.HasSliceSize) == 0)
 0187            {
 0188                throw new InvalidDataException("The Ice has indirection table flag but has not size flag.");
 189            }
 190
 20191            long savedPos = _reader.Consumed;
 20192            _reader.Advance(_classContext.Current.SliceSize);
 20193            _classContext.Current.IndirectionTable = DecodeIndirectionTable();
 20194            _classContext.Current.PosAfterIndirectionTable = _reader.Consumed;
 20195            _reader.Rewind(_reader.Consumed - savedPos);
 20196        }
 289197    }
 198
 199    /// <summary>Decodes a class instance.</summary>
 200    /// <param name="index">The index of the class instance. If greater than 1, it's a reference to a previously
 201    /// seen class; if 1, the class instance's bytes are next. Cannot be 0 or less.</param>
 202    private IceClass DecodeInstance(int index)
 181203    {
 181204        Debug.Assert(index > 0);
 205
 181206        if (index > 1)
 17207        {
 17208            if (_classContext.InstanceMap is not null && _classContext.InstanceMap.Count > index - 2)
 17209            {
 17210                return _classContext.InstanceMap[index - 2];
 211            }
 0212            throw new InvalidDataException($"Cannot find instance index {index} in the instance map.");
 213        }
 214
 164215        if (++_currentDepth > _maxDepth)
 1216        {
 1217            throw new InvalidDataException("The maximum decoder depth was reached while decoding a class.");
 218        }
 219
 220        // Save current in case we're decoding a nested instance.
 163221        InstanceData previousCurrent = _classContext.Current;
 163222        _classContext.Current = default;
 163223        _classContext.Current.InstanceType = InstanceType.Class;
 224
 163225        IceClass? instance = null;
 163226        _classContext.InstanceMap ??= new List<IceClass>();
 227
 163228        bool decodeIndirectionTable = true;
 163229        IActivator activator = _activator ?? _defaultActivator;
 230        do
 181231        {
 232            // Decode the slice header.
 181233            string? typeId = DecodeSliceHeaderIntoCurrent();
 234
 235            // We cannot decode the indirection table at this point as it may reference the new instance that is
 236            // not created yet.
 181237            if (typeId is not null)
 181238            {
 181239                instance = activator.CreateInstance(typeId) as IceClass;
 181240            }
 241
 181242            if (instance is null && SkipSlice(typeId))
 7243            {
 244                // Ice off what we don't understand.
 7245                instance = new UnknownIceClass();
 246                // Don't decode the indirection table as it's the last entry in DeferredIndirectionTableList.
 7247                decodeIndirectionTable = false;
 7248            }
 180249        }
 180250        while (instance is null);
 251
 252        // Add the instance to the map/list of instances. This must be done before decoding the instances (for
 253        // circular references).
 162254        _classContext.InstanceMap!.Add(instance);
 255
 256        // Decode all the deferred indirection tables now that the instance is inserted in _instanceMap.
 162257        if (_classContext.Current.DeferredIndirectionTableList?.Count > 0)
 13258        {
 13259            long savedPos = _reader.Consumed;
 260
 13261            Debug.Assert(_classContext.Current.Slices?.Count ==
 13262                _classContext.Current.DeferredIndirectionTableList.Count);
 76263            for (int i = 0; i < _classContext.Current.DeferredIndirectionTableList.Count; ++i)
 25264            {
 25265                long pos = _classContext.Current.DeferredIndirectionTableList[i];
 25266                if (pos > 0)
 8267                {
 8268                    long distance = pos - _reader.Consumed;
 8269                    if (distance > 0)
 2270                    {
 2271                        _reader.Advance(distance);
 2272                    }
 273                    else
 6274                    {
 6275                        _reader.Rewind(-distance);
 6276                    }
 8277                    _classContext.Current.Slices[i].Instances = DecodeIndirectionTable();
 8278                }
 279                // else remains empty
 25280            }
 281
 13282            _reader.Advance(savedPos - _reader.Consumed);
 13283        }
 284
 162285        if (decodeIndirectionTable)
 155286        {
 155287            DecodeIndirectionTableIntoCurrent();
 155288        }
 289
 162290        instance.UnknownSlices = _classContext.Current.Slices?.ToImmutableList() ?? ImmutableList<SliceInfo>.Empty;
 162291        _classContext.Current.FirstSlice = true;
 162292        instance.Decode(ref this);
 293
 62294        _classContext.Current = previousCurrent;
 62295        --_currentDepth;
 62296        return instance;
 79297    }
 298
 299    /// <summary>Decodes the header of the current slice into _current.</summary>
 300    /// <returns>The type ID or the compact ID of the current slice.</returns>
 301    private string? DecodeSliceHeaderIntoCurrent()
 315302    {
 315303        _classContext.Current.SliceFlags = (SliceFlags)DecodeByte();
 304
 305        string? typeId;
 306        // Decode the type ID. For class slices, the type ID is encoded as a string or as an index or as a compact
 307        // ID, for exceptions it's always encoded as a string.
 315308        if (_classContext.Current.InstanceType == InstanceType.Class)
 276309        {
 276310            typeId = DecodeTypeId(_classContext.Current.SliceFlags.GetTypeIdKind());
 311
 276312            if (typeId is null)
 62313            {
 62314                if ((_classContext.Current.SliceFlags & SliceFlags.HasSliceSize) != 0)
 0315                {
 0316                    throw new InvalidDataException(
 0317                        "Invalid Ice flags; an Ice in compact format cannot carry a size.");
 318                }
 62319            }
 276320        }
 321        else
 39322        {
 323            // Exception slices always include the type ID, even when using the compact format.
 39324            typeId = DecodeString();
 39325        }
 326
 327        // Decode the slice size if available.
 315328        if ((_classContext.Current.SliceFlags & SliceFlags.HasSliceSize) != 0)
 129329        {
 129330            _classContext.Current.SliceSize = DecodeSliceSize();
 129331        }
 332        else
 186333        {
 186334            _classContext.Current.SliceSize = 0;
 186335        }
 336
 337        // Clear other per-slice fields:
 315338        _classContext.Current.IndirectionTable = null;
 315339        _classContext.Current.PosAfterIndirectionTable = null;
 340
 315341        return typeId;
 315342    }
 343
 344    /// <summary>Decodes the size of the current slice.</summary>
 345    /// <returns>The slice of the current slice, not including the size length.</returns>
 346    private int DecodeSliceSize()
 149347    {
 149348        int size = DecodeInt();
 149349        if (size < 4)
 0350        {
 0351            throw new InvalidDataException($"Invalid Ice size: {size}.");
 352        }
 353        // The encoded size includes the size length.
 149354        return size - 4;
 149355    }
 356
 357    /// <summary>Decodes the type ID of a class instance.</summary>
 358    /// <param name="typeIdKind">The kind of type ID to decode.</param>
 359    /// <returns>The type ID or the compact ID, if any.</returns>
 360    private string? DecodeTypeId(TypeIdKind typeIdKind)
 296361    {
 296362        _classContext.TypeIdMap ??= new List<string>();
 363
 296364        switch (typeIdKind)
 365        {
 366            case TypeIdKind.Index:
 131367                int index = DecodeSize();
 131368                if (index > 0 && index - 1 < _classContext.TypeIdMap.Count)
 131369                {
 370                    // The encoded type-id indexes start at 1, not 0.
 131371                    return _classContext.TypeIdMap[index - 1];
 372                }
 0373                throw new InvalidDataException($"Decoded invalid type ID index {index}.");
 374
 375            case TypeIdKind.String:
 70376                string typeId = DecodeString();
 377
 378                // The typeIds of slices in indirection tables can be decoded several times: when we skip the
 379                // indirection table and later on when we decode it. We only want to add this type ID to the list and
 380                // assign it an index when it's the first time we decode it, so we save the largest position we
 381                // decode to figure out when to add to the list.
 70382                if (_reader.Consumed > _classContext.PosAfterLatestInsertedTypeId)
 62383                {
 62384                    _classContext.PosAfterLatestInsertedTypeId = _reader.Consumed;
 62385                    _classContext.TypeIdMap.Add(typeId);
 62386                }
 70387                return typeId;
 388
 389            case TypeIdKind.CompactId:
 33390                return DecodeSize().ToString(CultureInfo.InvariantCulture);
 391
 392            default:
 393                // TypeIdKind has only 4 possible values.
 62394                Debug.Assert(typeIdKind == TypeIdKind.None);
 62395                return null;
 396        }
 296397    }
 398
 399    /// <summary>Skips the indirection table. The caller must save the current position before calling
 400    /// SkipIndirectionTable (to decode the indirection table at a later point) except when the caller is
 401    /// SkipIndirectionTable itself.</summary>
 402    private void SkipIndirectionTable()
 11403    {
 404        // We should never skip an exception's indirection table
 11405        Debug.Assert(_classContext.Current.InstanceType == InstanceType.Class);
 406
 11407        int tableSize = DecodeSize();
 38408        for (int i = 0; i < tableSize; ++i)
 11409        {
 11410            int index = DecodeSize();
 11411            if (index <= 0)
 0412            {
 0413                throw new InvalidDataException($"Decoded invalid index {index} in indirection table.");
 414            }
 11415            if (index == 1)
 9416            {
 9417                if (++_currentDepth > _maxDepth)
 1418                {
 1419                    throw new InvalidDataException("Maximum decoder depth reached while decoding a class.");
 420                }
 421
 422                // Decode/skip this instance
 423                SliceFlags sliceFlags;
 424                do
 20425                {
 20426                    sliceFlags = (SliceFlags)DecodeByte();
 427
 428                    // Skip type ID - can update _typeIdMap
 20429                    _ = DecodeTypeId(sliceFlags.GetTypeIdKind());
 430
 431                    // Decode the slice size, then skip the slice
 20432                    if ((sliceFlags & SliceFlags.HasSliceSize) == 0)
 0433                    {
 0434                        throw new InvalidDataException("The Ice size flag is missing.");
 435                    }
 20436                    _reader.Advance(DecodeSliceSize());
 437
 438                    // If this slice has an indirection table, skip it too.
 20439                    if ((sliceFlags & SliceFlags.HasIndirectionTable) != 0)
 2440                    {
 2441                        SkipIndirectionTable();
 0442                    }
 18443                }
 18444                while ((sliceFlags & SliceFlags.IsLastSlice) == 0);
 6445                _currentDepth--;
 6446            }
 8447        }
 8448    }
 449
 450    /// <summary>Skips and saves the body of the current slice (save only for classes); also skips and save the
 451    /// indirection table (if any).</summary>
 452    /// <param name="typeId">The type ID or compact ID of the current slice.</param>
 453    /// <returns><see langword="true" /> when the current slice is the last slice; otherwise, <see langword="false" />.
 454    /// </returns>
 455    private bool SkipSlice(string? typeId)
 30456    {
 30457        if (typeId is null)
 0458        {
 0459            throw new InvalidDataException("Cannot skip a class slice with no type ID.");
 460        }
 461
 30462        if ((_classContext.Current.SliceFlags & SliceFlags.HasSliceSize) == 0)
 0463        {
 0464            throw new InvalidDataException(
 0465                $"The configured activator cannot find a class for type ID '{typeId}' and the compact format prevents sl
 466        }
 467
 30468        bool hasTaggedFields = (_classContext.Current.SliceFlags & SliceFlags.HasTaggedFields) != 0;
 469        byte[] bytes;
 30470        if (hasTaggedFields)
 4471        {
 472            // Don't include the tag end marker. It will be re-written by IceEncoder.EndSlice when the sliced data
 473            // is re-written.
 4474            bytes = new byte[_classContext.Current.SliceSize - 1];
 4475            CopyTo(bytes.AsSpan());
 4476            Skip(1);
 4477        }
 478        else
 26479        {
 26480            bytes = new byte[_classContext.Current.SliceSize];
 26481            CopyTo(bytes.AsSpan());
 26482        }
 483
 30484        bool hasIndirectionTable = (_classContext.Current.SliceFlags & SliceFlags.HasIndirectionTable) != 0;
 485
 486        // SkipSlice for a class skips the indirection table and preserves its position in
 487        // _current.DeferredIndirectionTableList for later decoding.
 30488        if (_classContext.Current.InstanceType == InstanceType.Class)
 26489        {
 26490            _classContext.Current.DeferredIndirectionTableList ??= new List<long>();
 26491            if (hasIndirectionTable)
 9492            {
 9493                long savedPos = _reader.Consumed;
 9494                SkipIndirectionTable();
 495
 496                // we want to later read the deepest first
 8497                _classContext.Current.DeferredIndirectionTableList.Add(savedPos);
 8498            }
 499            else
 17500            {
 17501                _classContext.Current.DeferredIndirectionTableList.Add(0); // keep a slot for each slice
 17502            }
 503
 25504            var info = new SliceInfo(
 25505                typeId,
 25506                new ReadOnlyMemory<byte>(bytes),
 25507                _classContext.Current.IndirectionTable ?? Array.Empty<IceClass>(),
 25508                hasTaggedFields);
 509
 25510            _classContext.Current.Slices ??= new List<SliceInfo>();
 25511            _classContext.Current.Slices.Add(info);
 25512        }
 4513        else if (hasIndirectionTable)
 2514        {
 2515            Debug.Assert(_classContext.Current.PosAfterIndirectionTable is not null);
 516
 517            // Move past indirection table
 2518            _reader.Advance(_classContext.Current.PosAfterIndirectionTable.Value - _reader.Consumed);
 2519            _classContext.Current.PosAfterIndirectionTable = null;
 2520        }
 521
 522        // If we decoded the indirection table previously, we don't need it anymore since we're skipping this slice.
 29523        _classContext.Current.IndirectionTable = null;
 524
 29525        return (_classContext.Current.SliceFlags & SliceFlags.IsLastSlice) != 0;
 29526    }
 527
 528    /// <summary>Holds various fields used for class and exception decoding.</summary>
 529    private struct ClassContext
 530    {
 531        // Data for the class or exception instance that is currently getting decoded.
 532        internal InstanceData Current;
 533
 534        // Map of class instance ID to class instance.
 535        // When decoding a buffer:
 536        //  - Instance ID = 0 means null
 537        //  - Instance ID = 1 means the instance is encoded inline afterwards
 538        //  - Instance ID > 1 means a reference to a previously decoded instance, found in this map.
 539        // Since the map is actually a list, we use instance ID - 2 to lookup an instance.
 540        internal List<IceClass>? InstanceMap;
 541
 542        // See DecodeTypeId.
 543        internal long PosAfterLatestInsertedTypeId;
 544
 545        // Map of type ID index to type ID sequence, used only for classes.
 546        // We assign a type ID index (starting with 1) to each type ID (type ID sequence) we decode, in order.
 547        // Since this map is a list, we lookup a previously assigned type ID (type ID sequence) with
 548        // _typeIdMap[index - 1].
 549        internal List<string>? TypeIdMap;
 550    }
 551
 552    private struct InstanceData
 553    {
 554        // Instance fields
 555
 556        internal List<long>? DeferredIndirectionTableList;
 557        internal InstanceType InstanceType;
 558        internal List<SliceInfo>? Slices; // Preserved slices.
 559
 560        // Slice fields
 561
 562        internal bool FirstSlice;
 563        internal IceClass[]? IndirectionTable; // Indirection table of the current slice
 564        internal long? PosAfterIndirectionTable;
 565
 566        internal SliceFlags SliceFlags;
 567        internal int SliceSize;
 568    }
 569
 570    private enum InstanceType : byte
 571    {
 572        None = 0,
 573        Class,
 574        Exception
 575    }
 576}

/home/runner/work/icerpc-csharp/icerpc-csharp/src/IceRpc/Ice/Codec/IceDecoder.cs

#LineLine coverage
 1// Copyright (c) ZeroC, Inc.
 2
 3using IceRpc.Ice.Codec.Internal;
 4using System.Buffers;
 5using System.Diagnostics;
 6using System.Runtime.CompilerServices;
 7using System.Runtime.InteropServices;
 8using System.Text;
 9using static IceRpc.Ice.Codec.Internal.IceEncodingDefinitions;
 10
 11namespace IceRpc.Ice.Codec;
 12
 13/// <summary>Provides methods to decode data encoded with Ice.</summary>
 14public ref partial struct IceDecoder
 15{
 16    /// <summary>Gets the number of bytes decoded in the underlying buffer.</summary>
 544917    public readonly long Consumed => _reader.Consumed;
 18
 19    /// <summary>Gets the decoding context.</summary>
 20    /// <remarks>The decoding context is a kind of cookie: the code that creates the decoder can store this context in
 21    /// the decoder for later retrieval.</remarks>
 4422    public object? DecodingContext { get; }
 23
 24    /// <summary>Gets a value indicating whether this decoder has reached the end of its underlying buffer.</summary>
 25    /// <value><see langword="true" /> when this decoder has reached the end of its underlying buffer; otherwise
 26    /// <see langword="false" />.</value>
 467927    public readonly bool End => _reader.End;
 28
 29    /// <summary>Gets the number of bytes remaining in the underlying buffer.</summary>
 30    /// <value>The number of bytes remaining in the underlying buffer.</value>
 3831    public readonly long Remaining => _reader.Remaining;
 32
 33    private const string EndOfBufferMessage = "Attempting to decode past the end of the Ice decoder buffer.";
 34
 735    private static readonly IActivator _defaultActivator =
 736        ActivatorFactory.Instance.Get(typeof(IceDecoder).Assembly);
 37
 738    private static readonly UTF8Encoding _utf8 =
 739        new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); // no BOM
 40
 41    private readonly IActivator? _activator;
 42
 43    private ClassContext _classContext;
 44
 45    // The number of bytes already allocated for strings, dictionaries, and sequences.
 46    private int _currentCollectionAllocation;
 47
 48    // The current depth when decoding a class recursively.
 49    private int _currentDepth;
 50
 51    // The maximum number of bytes that can be allocated for strings, dictionaries, and sequences.
 52    private readonly int _maxCollectionAllocation;
 53
 54    // The maximum depth when decoding a class recursively.
 55    private readonly int _maxDepth;
 56
 57    // The sequence reader.
 58    private SequenceReader<byte> _reader;
 59
 60    /// <summary>Constructs a new Ice decoder over a byte buffer.</summary>
 61    /// <param name="buffer">The byte buffer.</param>
 62    /// <param name="decodingContext">The decoding context.</param>
 63    /// <param name="maxCollectionAllocation">The maximum cumulative allocation in bytes when decoding strings,
 64    /// sequences, and dictionaries from this buffer.<c>-1</c> (the default) is equivalent to 8 times the buffer
 65    /// length, clamped to <see cref="int.MaxValue" />.</param>
 66    /// <param name="activator">The activator for decoding classes and exceptions.</param>
 67    /// <param name="maxDepth">The maximum depth when decoding a class recursively. The default is <c>3</c>.</param>
 68    public IceDecoder(
 69        ReadOnlySequence<byte> buffer,
 70        object? decodingContext = null,
 71        int maxCollectionAllocation = -1,
 72        IActivator? activator = null,
 73        int maxDepth = 3)
 633874    {
 633875        DecodingContext = decodingContext;
 76
 633877        _currentCollectionAllocation = 0;
 78
 633879        _maxCollectionAllocation = maxCollectionAllocation == -1 ?
 633880            (buffer.Length > int.MaxValue / 8 ? int.MaxValue : (int)(8L * buffer.Length)) :
 633881            (maxCollectionAllocation >= 0 ? maxCollectionAllocation :
 633882                throw new ArgumentException(
 633883                    $"The {nameof(maxCollectionAllocation)} argument must be greater than or equal to -1.",
 633884                    nameof(maxCollectionAllocation)));
 85
 633886        _activator = activator;
 633887        _classContext = default;
 633888        _currentDepth = 0;
 633889        _maxDepth = maxDepth >= 1 ? maxDepth :
 633890            throw new ArgumentException($"The {nameof(maxDepth)} argument must be greater than 0.", nameof(maxDepth));
 91
 633892        _reader = new SequenceReader<byte>(buffer);
 633893    }
 94
 95    /// <summary>Constructs a new Ice decoder over a byte buffer.</summary>
 96    /// <param name="buffer">The byte buffer.</param>
 97    /// <param name="decodingContext">The decoding context.</param>
 98    /// <param name="maxCollectionAllocation">The maximum cumulative allocation in bytes when decoding strings,
 99    /// sequences, and dictionaries from this buffer.<c>-1</c> (the default) is equivalent to 8 times the buffer
 100    /// length, clamped to <see cref="int.MaxValue" />.</param>
 101    /// <param name="activator">The activator for decoding classes and exceptions.</param>
 102    /// <param name="maxDepth">The maximum depth when decoding a class recursively. The default is <c>3</c>.</param>
 103    public IceDecoder(
 104        ReadOnlyMemory<byte> buffer,
 105        object? decodingContext = null,
 106        int maxCollectionAllocation = -1,
 107        IActivator? activator = null,
 108        int maxDepth = 3)
 173109        : this(
 173110            new ReadOnlySequence<byte>(buffer),
 173111            decodingContext,
 173112            maxCollectionAllocation,
 173113            activator,
 173114            maxDepth)
 173115    {
 173116    }
 117
 118    // Decode methods for basic types
 119
 120    /// <summary>Checks if the in memory representation of the bool value is valid according to the Ice encoding.</summa
 121    /// <param name="value">The value to check.</param>
 122    /// <exception cref="InvalidDataException">If the value is out of the bool type accepted range.</exception>
 123    public static void CheckBoolValue(bool value)
 6124    {
 6125        if (Unsafe.As<bool, byte>(ref value) > 1)
 1126        {
 1127            throw new InvalidDataException("The value is out of the bool type accepted range.");
 128        }
 5129    }
 130
 131    /// <summary>Decodes an Ice bool into a bool.</summary>
 132    /// <returns>The bool decoded by this decoder.</returns>
 133    public bool DecodeBool()
 69134    {
 69135        if (_reader.TryRead(out byte value))
 68136        {
 68137            return value switch
 68138            {
 60139                0 => false,
 7140                1 => true,
 1141                _ => throw new InvalidDataException("The value is out of the bool type accepted range.")
 68142            };
 143        }
 144        else
 1145        {
 1146            throw new InvalidDataException(EndOfBufferMessage);
 147        }
 67148    }
 149
 150    /// <summary>Decodes an Ice byte into a byte.</summary>
 151    /// <returns>The byte decoded by this decoder.</returns>
 152    public byte DecodeByte() =>
 46248153        _reader.TryRead(out byte value) ? value : throw new InvalidDataException(EndOfBufferMessage);
 154
 155    /// <summary>Decodes an Ice double into a double.</summary>
 156    /// <returns>The double decoded by this decoder.</returns>
 157    public double DecodeDouble() =>
 0158        SequenceMarshal.TryRead(ref _reader, out double value) ?
 0159            value : throw new InvalidDataException(EndOfBufferMessage);
 160
 161    /// <summary>Decodes an Ice float into a float.</summary>
 162    /// <returns>The float decoded by this decoder.</returns>
 163    public float DecodeFloat() =>
 0164        SequenceMarshal.TryRead(ref _reader, out float value) ?
 0165            value : throw new InvalidDataException(EndOfBufferMessage);
 166
 167    /// <summary>Decodes an Ice int into an int.</summary>
 168    /// <returns>The int decoded by this decoder.</returns>
 169    public int DecodeInt() =>
 11550170        SequenceMarshal.TryRead(ref _reader, out int value) ?
 11550171            value : throw new InvalidDataException(EndOfBufferMessage);
 172
 173    /// <summary>Decodes an Ice long into a long.</summary>
 174    /// <returns>The long decoded by this decoder.</returns>
 175    public long DecodeLong() =>
 167176        SequenceMarshal.TryRead(ref _reader, out long value) ?
 167177            value : throw new InvalidDataException(EndOfBufferMessage);
 178
 179    /// <summary>Decodes an Ice short into a short.</summary>
 180    /// <returns>The short decoded by this decoder.</returns>
 181    public short DecodeShort() =>
 35182        SequenceMarshal.TryRead(ref _reader, out short value) ?
 35183            value : throw new InvalidDataException(EndOfBufferMessage);
 184
 185    /// <summary>Decodes a size encoded on a variable number of bytes.</summary>
 186    /// <returns>The size decoded by this decoder.</returns>
 187    public int DecodeSize()
 16037188    {
 16037189        byte firstByte = DecodeByte();
 16037190        if (firstByte < 255)
 15999191        {
 15999192            return firstByte;
 193        }
 194        else
 38195        {
 38196            int size = DecodeInt();
 38197            if (size < 0)
 0198            {
 0199                throw new InvalidDataException($"Decoded invalid size: {size}.");
 200            }
 38201            return size;
 202        }
 16037203    }
 204
 205    /// <summary>Decodes an Ice string into a string.</summary>
 206    /// <returns>The string decoded by this decoder.</returns>
 207    public string DecodeString()
 8002208    {
 8002209        int size = DecodeSize();
 8002210        if (size == 0)
 4125211        {
 4125212            return "";
 213        }
 214        else
 3877215        {
 216            // In the worst-case scenario, each byte becomes a new character. We'll adjust this allocation increase
 217            // after decoding the string.
 3877218            IncreaseCollectionAllocation(size, Unsafe.SizeOf<char>());
 219
 220            string result;
 3874221            if (_reader.UnreadSpan.Length >= size)
 3874222            {
 223                try
 3874224                {
 3874225                    result = _utf8.GetString(_reader.UnreadSpan[0..size]);
 3873226                }
 1227                catch (Exception exception) when (exception is ArgumentException or DecoderFallbackException)
 1228                {
 229                    // The two exceptions that can be thrown by GetString are ArgumentException and
 230                    // DecoderFallbackException. Both of which are a result of malformed data. As such, we can just
 231                    // throw an InvalidDataException.
 1232                    throw new InvalidDataException("Invalid UTF-8 string.", exception);
 233                }
 3873234            }
 235            else
 0236            {
 0237                ReadOnlySequence<byte> bytes = _reader.UnreadSequence;
 0238                if (size > bytes.Length)
 0239                {
 0240                    throw new InvalidDataException(EndOfBufferMessage);
 241                }
 242                try
 0243                {
 0244                    result = _utf8.GetString(bytes.Slice(0, size));
 0245                }
 0246                catch (Exception exception) when (exception is ArgumentException or DecoderFallbackException)
 0247                {
 248                    // The two exceptions that can be thrown by GetString are ArgumentException and
 249                    // DecoderFallbackException. Both of which are a result of malformed data. As such, we can just
 250                    // throw an InvalidDataException.
 0251                    throw new InvalidDataException("Invalid UTF-8 string.", exception);
 252                }
 0253            }
 254
 3873255            _reader.Advance(size);
 256
 257            // Make the adjustment. The overall increase in allocation is result.Length * SizeOf<char>().
 3873258            DecreaseCollectionAllocation(size - result.Length, Unsafe.SizeOf<char>());
 3873259            return result;
 260        }
 7998261    }
 262
 263    // Other methods
 264
 265    /// <summary>Copy bytes from the underlying reader into the destination to fill completely destination.
 266    /// </summary>
 267    /// <param name="destination">The span to which bytes of this decoder will be copied.</param>
 268    /// <remarks>This method also moves the reader's Consumed property.</remarks>
 269    public void CopyTo(Span<byte> destination)
 53270    {
 53271        if (_reader.TryCopyTo(destination))
 53272        {
 53273            _reader.Advance(destination.Length);
 53274        }
 275        else
 0276        {
 0277            throw new InvalidDataException(EndOfBufferMessage);
 278        }
 53279    }
 280
 281    /// <summary>Decodes a tagged field.</summary>
 282    /// <typeparam name="T">The type of the decoded value.</typeparam>
 283    /// <param name="tag">The tag.</param>
 284    /// <param name="tagFormat">The expected tag format of this tag when found in the underlying buffer.</param>
 285    /// <param name="decodeFunc">A decode function that decodes the value of this tag.</param>
 286    /// <returns>The decoded value of the tagged field, or <see langword="null" /> if not found.</returns>
 287    /// <remarks>We return a T? and not a T to avoid ambiguities in the generated code with nullable reference types
 288    /// such as string?.</remarks>
 289    public T? DecodeTagged<T>(int tag, TagFormat tagFormat, DecodeFunc<T> decodeFunc)
 112290    {
 112291        if (DecodeTagHeader(tag, tagFormat))
 61292        {
 61293            if (tagFormat == TagFormat.VSize)
 10294            {
 10295                SkipSize();
 10296            }
 51297            else if (tagFormat == TagFormat.FSize)
 5298            {
 5299                Skip(4);
 5300            }
 61301            return decodeFunc(ref this);
 302        }
 303        else
 51304        {
 51305            return default!; // i.e. null
 306        }
 112307    }
 308
 309    /// <summary>Skip the given number of bytes.</summary>
 310    /// <param name="count">The number of bytes to skip.</param>
 311    public void Skip(int count)
 104312    {
 104313        if (_reader.Remaining >= count)
 104314        {
 104315            _reader.Advance(count);
 104316        }
 317        else
 0318        {
 0319            throw new InvalidDataException(EndOfBufferMessage);
 320        }
 104321    }
 322
 323    /// <summary>Skips the remaining tagged fields.</summary>
 324    public void SkipTagged()
 140325    {
 326        // True when decoding a class or exception, false when decoding parameters. Keep in mind we never decode a
 327        // class while decoding a tagged parameter.
 140328        bool useTagEndMarker = _classContext.Current.InstanceType != InstanceType.None;
 329
 181330        while (true)
 181331        {
 181332            if (!useTagEndMarker && _reader.End)
 113333            {
 334                // When we don't use an end marker, the end of the buffer indicates the end of the tagged fields.
 113335                break;
 336            }
 337
 68338            int v = DecodeByte();
 68339            if (useTagEndMarker && v == TagEndMarker)
 27340            {
 341                // When we use an end marker, the end marker (and only the end marker) indicates the end of the
 342                // tagged fields.
 27343                break;
 344            }
 345
 41346            var format = (TagFormat)(v & 0x07); // Read first 3 bits.
 41347            if ((v >> 3) == 30)
 4348            {
 4349                SkipSize();
 4350            }
 41351            SkipTaggedValue(format);
 41352        }
 140353    }
 354
 355    /// <summary>Skip Ice size.</summary>
 356    public void SkipSize()
 18357    {
 18358        byte b = DecodeByte();
 18359        if (b == 255)
 0360        {
 0361            Skip(4);
 0362        }
 18363    }
 364
 365    /// <summary>Increases the number of bytes in the decoder's collection allocation.</summary>
 366    /// <param name="count">The number of elements.</param>
 367    /// <param name="elementSize">The size of each element in bytes.</param>
 368    /// <exception cref="InvalidDataException">Thrown when the total number of bytes exceeds the max collection
 369    /// allocation.</exception>
 370    /// <seealso cref="IceDecoder(ReadOnlySequence{byte}, object?, int, IActivator?, int)" />
 371    internal void IncreaseCollectionAllocation(int count, int elementSize)
 4026372    {
 4026373        Debug.Assert(count >= 0, $"{nameof(count)} must be greater than or equal to 0.");
 4026374        Debug.Assert(elementSize > 0, $"{nameof(elementSize)} must be greater than 0.");
 375
 4026376        long byteCount = (long)count * elementSize;
 4026377        int remainingAllocation = _maxCollectionAllocation - _currentCollectionAllocation;
 4026378        if (byteCount > remainingAllocation)
 11379        {
 11380            throw new InvalidDataException(
 11381                $"The decoding exceeds the max collection allocation of '{_maxCollectionAllocation}'.");
 382        }
 4015383        _currentCollectionAllocation += (int)byteCount;
 4015384    }
 385
 386    private bool DecodeTagHeader(int tag, TagFormat expectedFormat)
 112387    {
 388        // True when decoding a class or exception, false when decoding parameters. Keep in mind we never decode a
 389        // class while decoding a tagged parameter.
 112390        bool useTagEndMarker = _classContext.Current.InstanceType != InstanceType.None;
 391
 112392        if (_classContext.Current.InstanceType != InstanceType.None)
 69393        {
 394            // tagged fields of a class or exception
 69395            if ((_classContext.Current.SliceFlags & SliceFlags.HasTaggedFields) == 0)
 21396            {
 397                // The current slice has no tagged field.
 21398                return false;
 399            }
 48400        }
 401
 91402        int requestedTag = tag;
 403
 91404        while (true)
 91405        {
 91406            if (!useTagEndMarker && _reader.End)
 14407            {
 14408                return false; // End of buffer indicates end of tagged fields.
 409            }
 410
 77411            long savedPos = _reader.Consumed;
 412
 77413            int v = DecodeByte();
 77414            if (useTagEndMarker && v == TagEndMarker)
 2415            {
 2416                _reader.Rewind(_reader.Consumed - savedPos);
 2417                return false;
 418            }
 419
 75420            var format = (TagFormat)(v & 0x07); // First 3 bits.
 75421            tag = v >> 3;
 75422            if (tag == 30)
 8423            {
 8424                tag = DecodeSize();
 8425            }
 426
 75427            if (tag > requestedTag)
 14428            {
 14429                _reader.Rewind(_reader.Consumed - savedPos);
 14430                return false; // No tagged field with the requested tag.
 431            }
 61432            else if (tag < requestedTag)
 0433            {
 0434                SkipTaggedValue(format);
 0435            }
 436            else
 61437            {
 61438                if (expectedFormat == TagFormat.OptimizedVSize)
 16439                {
 16440                    expectedFormat = TagFormat.VSize; // fix virtual tag format
 16441                }
 442
 61443                if (format != expectedFormat)
 0444                {
 0445                    throw new InvalidDataException($"Invalid tagged field '{tag}': unexpected format.");
 446                }
 61447                return true;
 448            }
 0449        }
 112450    }
 451
 452    /// <summary>Decreases the number of bytes in the decoder's collection allocation.</summary>
 453    /// <param name="count">The number of elements.</param>
 454    /// <param name="elementSize">The size of each element in bytes.</param>
 455    private void DecreaseCollectionAllocation(int count, int elementSize)
 3873456    {
 3873457        Debug.Assert(count >= 0, $"{nameof(count)} must be greater than or equal to 0.");
 3873458        Debug.Assert(elementSize > 0, $"{nameof(elementSize)} must be greater than 0.");
 459
 460        // Widen count to long to avoid overflow when multiplying by elementSize.
 3873461        long byteCount = (long)count * elementSize;
 462
 3873463        Debug.Assert(byteCount <= _currentCollectionAllocation, "Decreasing more than the current collection allocation.
 3873464        _currentCollectionAllocation -= (int)byteCount;
 3873465    }
 466
 467    private void SkipTaggedValue(TagFormat format)
 41468    {
 41469        switch (format)
 470        {
 471            case TagFormat.F1:
 4472                Skip(1);
 4473                break;
 474            case TagFormat.F2:
 2475                Skip(2);
 2476                break;
 477            case TagFormat.F4:
 7478                Skip(4);
 7479                break;
 480            case TagFormat.F8:
 6481                Skip(8);
 6482                break;
 483            case TagFormat.Size:
 4484                SkipSize();
 4485                break;
 486            case TagFormat.VSize:
 14487                Skip(DecodeSize());
 14488                break;
 489            case TagFormat.FSize:
 4490                int size = DecodeInt();
 4491                if (size < 0)
 0492                {
 0493                    throw new InvalidDataException($"Decoded invalid size: {size}.");
 494                }
 4495                Skip(size);
 4496                break;
 497            default:
 0498                throw new InvalidDataException($"Cannot skip tagged field with tag format '{format}'.");
 499        }
 41500    }
 501}