< Summary

Line coverage
88%
Covered lines: 505
Uncovered lines: 67
Coverable lines: 572
Total lines: 1101
Line coverage: 88.2%
Branch coverage
79%
Covered branches: 194
Total branches: 244
Branch coverage: 79.5%
Method coverage
94%
Covered methods: 37
Fully covered methods: 19
Total methods: 39
Method coverage: 94.8%
Full method coverage: 48.7%

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
 30020    {
 30021        IceClass? obj = DecodeClass();
 22
 19723        if (obj is T result)
 7124        {
 7125            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)}'.");
 19733    }
 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            long count = _classContext.Current.PosAfterIndirectionTable.Value - _reader.Consumed;
 1897            if (count < 0)
 098            {
 099                throw new InvalidDataException("The slice fields extend beyond the declared slice size.");
 100            }
 18101            _reader.Advance(count);
 18102            _classContext.Current.PosAfterIndirectionTable = null;
 18103            _classContext.Current.IndirectionTable = null;
 18104        }
 185105    }
 106
 107    /// <summary>Marks the start of the decoding of a class or remote exception slice.</summary>
 108    [EditorBrowsable(EditorBrowsableState.Never)]
 109    public void StartSlice()
 285110    {
 285111        Debug.Assert(_classContext.Current.InstanceType != InstanceType.None);
 285112        if (_classContext.Current.FirstSlice)
 185113        {
 185114            _classContext.Current.FirstSlice = false;
 185115        }
 116        else
 100117        {
 100118            _ = DecodeSliceHeaderIntoCurrent();
 100119            DecodeIndirectionTableIntoCurrent();
 100120        }
 285121    }
 122
 123    /// <summary>Decodes a class instance.</summary>
 124    /// <returns>The class instance. Can be <see langword="null" />.</returns>
 125    private IceClass? DecodeClass()
 300126    {
 300127        int index = DecodeSize();
 300128        if (index < 0)
 0129        {
 0130            throw new InvalidDataException($"Found invalid index {index} while decoding a class.");
 131        }
 300132        else if (index == 0)
 126133        {
 126134            return null;
 135        }
 174136        else if (_classContext.Current.InstanceType != InstanceType.None &&
 174137            (_classContext.Current.SliceFlags & SliceFlags.HasIndirectionTable) != 0)
 21138        {
 139            // When decoding an instance within a slice and there is an indirection table, we have an index within
 140            // this indirection table.
 141            // We need to decrement index since position 0 in the indirection table corresponds to index 1.
 21142            index--;
 143
 144            // If the right-hand side is null, the comparison simply evaluates to false.
 21145            if (index < _classContext.Current.IndirectionTable?.Length)
 21146            {
 21147                return _classContext.Current.IndirectionTable[index];
 148            }
 149            else
 0150            {
 0151                throw new InvalidDataException("The index is too big for the indirection table.");
 152            }
 153        }
 154        else
 153155        {
 153156            return DecodeInstance(index);
 157        }
 197158    }
 159
 160    /// <summary>Decodes an indirection table without updating _current.</summary>
 161    /// <returns>The indirection table.</returns>
 162    private IceClass[] DecodeIndirectionTable()
 28163    {
 28164        int size = DecodeSize();
 28165        if (size == 0)
 0166        {
 0167            throw new InvalidDataException("Invalid empty indirection table.");
 168        }
 28169        IncreaseCollectionAllocation(size, Unsafe.SizeOf<IceClass>());
 28170        var indirectionTable = new IceClass[size];
 116171        for (int i = 0; i < indirectionTable.Length; ++i)
 30172        {
 30173            int index = DecodeSize();
 30174            if (index < 1)
 0175            {
 0176                throw new InvalidDataException($"Found invalid index {index} decoding the indirection table.");
 177            }
 30178            indirectionTable[i] = DecodeInstance(index);
 30179        }
 28180        return indirectionTable;
 28181    }
 182
 183    /// <summary>Decodes the indirection table into _current's fields if there is an indirection table.
 184    /// Precondition: called after decoding the header of the current slice. This method does not change _pos.
 185    /// </summary>
 186    private void DecodeIndirectionTableIntoCurrent()
 289187    {
 289188        Debug.Assert(_classContext.Current.IndirectionTable is null);
 289189        if ((_classContext.Current.SliceFlags & SliceFlags.HasIndirectionTable) != 0)
 20190        {
 20191            if ((_classContext.Current.SliceFlags & SliceFlags.HasSliceSize) == 0)
 0192            {
 0193                throw new InvalidDataException("The Ice has indirection table flag but has not size flag.");
 194            }
 195
 20196            long savedPos = _reader.Consumed;
 20197            _reader.Advance(_classContext.Current.SliceSize);
 20198            _classContext.Current.IndirectionTable = DecodeIndirectionTable();
 20199            _classContext.Current.PosAfterIndirectionTable = _reader.Consumed;
 20200            _reader.Rewind(_reader.Consumed - savedPos);
 20201        }
 289202    }
 203
 204    /// <summary>Decodes a class instance.</summary>
 205    /// <param name="index">The index of the class instance. If greater than 1, it's a reference to a previously
 206    /// seen class; if 1, the class instance's bytes are next. Cannot be 0 or less.</param>
 207    private IceClass DecodeInstance(int index)
 183208    {
 183209        Debug.Assert(index > 0);
 210
 183211        if (index > 1)
 17212        {
 17213            if (_classContext.InstanceMap is not null && _classContext.InstanceMap.Count > index - 2)
 17214            {
 17215                return _classContext.InstanceMap[index - 2];
 216            }
 0217            throw new InvalidDataException($"Cannot find instance index {index} in the instance map.");
 218        }
 219
 166220        if (++_currentDepth > _maxDepth)
 1221        {
 1222            throw new InvalidDataException("The maximum decoder depth was reached while decoding a class.");
 223        }
 224
 225        // Save current in case we're decoding a nested instance.
 165226        InstanceData previousCurrent = _classContext.Current;
 165227        _classContext.Current = default;
 165228        _classContext.Current.InstanceType = InstanceType.Class;
 229
 165230        IceClass? instance = null;
 165231        _classContext.InstanceMap ??= new List<IceClass>();
 232
 165233        bool decodeIndirectionTable = true;
 165234        IActivator activator = _activator ?? _defaultActivator;
 235        do
 185236        {
 237            // Decode the slice header.
 185238            string? typeId = DecodeSliceHeaderIntoCurrent();
 239
 240            // We cannot decode the indirection table at this point as it may reference the new instance that is
 241            // not created yet.
 184242            if (typeId is not null)
 184243            {
 184244                instance = activator.CreateInstance(typeId) as IceClass;
 184245            }
 246
 184247            if (instance is null && SkipSlice(typeId))
 8248            {
 249                // Ice off what we don't understand.
 8250                instance = new UnknownIceClass();
 251                // Don't decode the indirection table as it's the last entry in DeferredIndirectionTableList.
 8252                decodeIndirectionTable = false;
 8253            }
 183254        }
 183255        while (instance is null);
 256
 257        // Add the instance to the map/list of instances. This must be done before decoding the instances (for
 258        // circular references).
 163259        _classContext.InstanceMap!.Add(instance);
 260
 261        // Decode all the deferred indirection tables now that the instance is inserted in _instanceMap.
 163262        if (_classContext.Current.DeferredIndirectionTableList?.Count > 0)
 14263        {
 14264            long savedPos = _reader.Consumed;
 265
 14266            Debug.Assert(_classContext.Current.Slices?.Count ==
 14267                _classContext.Current.DeferredIndirectionTableList.Count);
 84268            for (int i = 0; i < _classContext.Current.DeferredIndirectionTableList.Count; ++i)
 28269            {
 28270                long pos = _classContext.Current.DeferredIndirectionTableList[i];
 28271                if (pos > 0)
 8272                {
 8273                    long distance = pos - _reader.Consumed;
 8274                    if (distance > 0)
 2275                    {
 2276                        _reader.Advance(distance);
 2277                    }
 278                    else
 6279                    {
 6280                        _reader.Rewind(-distance);
 6281                    }
 8282                    _classContext.Current.Slices[i].Instances = DecodeIndirectionTable();
 8283                }
 284                // else remains empty
 28285            }
 286
 14287            long count = savedPos - _reader.Consumed;
 14288            if (count < 0)
 0289            {
 0290                throw new InvalidDataException("An indirection table extends beyond the end of its slice.");
 291            }
 14292            _reader.Advance(count);
 14293        }
 294
 163295        if (decodeIndirectionTable)
 155296        {
 155297            DecodeIndirectionTableIntoCurrent();
 155298        }
 299
 163300        instance.UnknownSlices = _classContext.Current.Slices?.ToImmutableList() ?? ImmutableList<SliceInfo>.Empty;
 163301        _classContext.Current.FirstSlice = true;
 163302        instance.Decode(ref this);
 303
 63304        _classContext.Current = previousCurrent;
 63305        --_currentDepth;
 63306        return instance;
 80307    }
 308
 309    /// <summary>Decodes the header of the current slice into _current.</summary>
 310    /// <returns>The type ID or the compact ID of the current slice.</returns>
 311    private string? DecodeSliceHeaderIntoCurrent()
 319312    {
 319313        _classContext.Current.SliceFlags = (SliceFlags)DecodeByte();
 314
 315        string? typeId;
 316        // Decode the type ID. For class slices, the type ID is encoded as a string or as an index or as a compact
 317        // ID, for exceptions it's always encoded as a string.
 319318        if (_classContext.Current.InstanceType == InstanceType.Class)
 280319        {
 280320            typeId = DecodeTypeId(_classContext.Current.SliceFlags.GetTypeIdKind());
 321
 279322            if (typeId is null)
 62323            {
 62324                if ((_classContext.Current.SliceFlags & SliceFlags.HasSliceSize) != 0)
 0325                {
 0326                    throw new InvalidDataException(
 0327                        "Invalid Ice flags; an Ice in compact format cannot carry a size.");
 328                }
 62329            }
 279330        }
 331        else
 39332        {
 333            // Exception slices always include the type ID, even when using the compact format.
 39334            typeId = DecodeString();
 39335        }
 336
 337        // Decode the slice size if available.
 318338        if ((_classContext.Current.SliceFlags & SliceFlags.HasSliceSize) != 0)
 132339        {
 132340            _classContext.Current.SliceSize =
 132341                DecodeSliceSize((_classContext.Current.SliceFlags & SliceFlags.HasTaggedFields) != 0);
 132342        }
 343        else
 186344        {
 186345            _classContext.Current.SliceSize = 0;
 186346        }
 347
 348        // Clear other per-slice fields:
 318349        _classContext.Current.IndirectionTable = null;
 318350        _classContext.Current.PosAfterIndirectionTable = null;
 351
 318352        return typeId;
 318353    }
 354
 355    /// <summary>Decodes the size of the current slice.</summary>
 356    /// <param name="sliceHasTaggedFields">Indicates whether the slice has tagged fields.</param>
 357    /// <returns>The size of the current slice, not including the size length.</returns>
 358    private int DecodeSliceSize(bool sliceHasTaggedFields)
 152359    {
 152360        int minSize = sliceHasTaggedFields ? 5 : 4; // extra byte for the tag end marker
 361
 152362        int size = DecodeInt();
 152363        if (size < minSize || size - 4 > _reader.Remaining)
 0364        {
 0365            throw new InvalidDataException($"Received invalid slice size: {size}.");
 366        }
 367
 368        // The encoded size includes the size length.
 152369        return size - 4;
 152370    }
 371
 372    /// <summary>Decodes the type ID of a class instance.</summary>
 373    /// <param name="typeIdKind">The kind of type ID to decode.</param>
 374    /// <returns>The type ID or the compact ID, if any.</returns>
 375    private string? DecodeTypeId(TypeIdKind typeIdKind)
 300376    {
 300377        _classContext.TypeIdMap ??= new List<string>();
 378
 300379        switch (typeIdKind)
 380        {
 381            case TypeIdKind.Index:
 131382                int index = DecodeSize();
 131383                if (index > 0 && index - 1 < _classContext.TypeIdMap.Count)
 131384                {
 385                    // The encoded type-id indexes start at 1, not 0.
 131386                    return _classContext.TypeIdMap[index - 1];
 387                }
 0388                throw new InvalidDataException($"Decoded invalid type ID index {index}.");
 389
 390            case TypeIdKind.String:
 74391                string typeId = DecodeString();
 392
 393                // A valid type ID always starts with "::" (see SliceInfo.TypeId). Without this check, the
 394                // re-encoding of a preserved slice would parse this type ID as a compact ID.
 74395                if (!typeId.StartsWith("::", StringComparison.Ordinal))
 1396                {
 1397                    throw new InvalidDataException($"Received invalid type ID {typeId}.");
 398                }
 399
 400                // The typeIds of slices in indirection tables can be decoded several times: when we skip the
 401                // indirection table and later on when we decode it. We only want to add this type ID to the list and
 402                // assign it an index when it's the first time we decode it, so we save the largest position we
 403                // decode to figure out when to add to the list.
 73404                if (_reader.Consumed > _classContext.PosAfterLatestInsertedTypeId)
 65405                {
 65406                    _classContext.PosAfterLatestInsertedTypeId = _reader.Consumed;
 65407                    _classContext.TypeIdMap.Add(typeId);
 65408                }
 73409                return typeId;
 410
 411            case TypeIdKind.CompactId:
 33412                return DecodeSize().ToString(CultureInfo.InvariantCulture);
 413
 414            default:
 415                // TypeIdKind has only 4 possible values.
 62416                Debug.Assert(typeIdKind == TypeIdKind.None);
 62417                return null;
 418        }
 299419    }
 420
 421    /// <summary>Skips the indirection table. The caller must save the current position before calling
 422    /// SkipIndirectionTable (to decode the indirection table at a later point) except when the caller is
 423    /// SkipIndirectionTable itself.</summary>
 424    private void SkipIndirectionTable()
 11425    {
 426        // We never skip an exception's indirection table since we don't preserve exception slices.
 11427        Debug.Assert(_classContext.Current.InstanceType == InstanceType.Class);
 428
 11429        int tableSize = DecodeSize();
 38430        for (int i = 0; i < tableSize; ++i)
 11431        {
 11432            int index = DecodeSize();
 11433            if (index <= 0)
 0434            {
 0435                throw new InvalidDataException($"Decoded invalid index {index} in indirection table.");
 436            }
 11437            if (index == 1)
 9438            {
 9439                if (++_currentDepth > _maxDepth)
 1440                {
 1441                    throw new InvalidDataException("Maximum decoder depth reached while decoding a class.");
 442                }
 443
 444                // Decode/skip this instance
 445                SliceFlags sliceFlags;
 446                do
 20447                {
 20448                    sliceFlags = (SliceFlags)DecodeByte();
 449
 450                    // Skip type ID - can update _typeIdMap
 20451                    _ = DecodeTypeId(sliceFlags.GetTypeIdKind());
 452
 453                    // Decode the slice size, then skip the slice
 20454                    if ((sliceFlags & SliceFlags.HasSliceSize) == 0)
 0455                    {
 0456                        throw new InvalidDataException("The Ice size flag is missing.");
 457                    }
 20458                    _reader.Advance(DecodeSliceSize((sliceFlags & SliceFlags.HasTaggedFields) != 0));
 459
 460                    // If this slice has an indirection table, skip it too.
 20461                    if ((sliceFlags & SliceFlags.HasIndirectionTable) != 0)
 2462                    {
 2463                        SkipIndirectionTable();
 0464                    }
 18465                }
 18466                while ((sliceFlags & SliceFlags.IsLastSlice) == 0);
 6467                _currentDepth--;
 6468            }
 8469        }
 8470    }
 471
 472    /// <summary>Skips and saves the body of the current slice (save only for classes); also skips and save the
 473    /// indirection table (if any).</summary>
 474    /// <param name="typeId">The type ID or compact ID of the current slice.</param>
 475    /// <returns><see langword="true" /> when the current slice is the last slice; otherwise, <see langword="false" />.
 476    /// </returns>
 477    private bool SkipSlice(string? typeId)
 33478    {
 33479        if (typeId is null)
 0480        {
 0481            throw new InvalidDataException("Cannot skip a class slice with no type ID.");
 482        }
 483
 33484        if ((_classContext.Current.SliceFlags & SliceFlags.HasSliceSize) == 0)
 0485        {
 0486            throw new InvalidDataException(
 0487                $"The configured activator cannot find a class for type ID '{typeId}' and the compact format prevents sl
 488        }
 489
 33490        bool hasTaggedFields = (_classContext.Current.SliceFlags & SliceFlags.HasTaggedFields) != 0;
 491        byte[] bytes;
 33492        if (hasTaggedFields)
 4493        {
 494            // Don't include the tag end marker. It will be re-written by IceEncoder.EndSlice when the sliced data
 495            // is re-written.
 4496            bytes = new byte[_classContext.Current.SliceSize - 1];
 4497            CopyTo(bytes.AsSpan());
 4498            Skip(1);
 4499        }
 500        else
 29501        {
 29502            bytes = new byte[_classContext.Current.SliceSize];
 29503            CopyTo(bytes.AsSpan());
 29504        }
 505
 33506        bool hasIndirectionTable = (_classContext.Current.SliceFlags & SliceFlags.HasIndirectionTable) != 0;
 507
 508        // SkipSlice for a class skips the indirection table and preserves its position in
 509        // _current.DeferredIndirectionTableList for later decoding.
 33510        if (_classContext.Current.InstanceType == InstanceType.Class)
 29511        {
 29512            _classContext.Current.DeferredIndirectionTableList ??= new List<long>();
 29513            if (hasIndirectionTable)
 9514            {
 9515                long savedPos = _reader.Consumed;
 9516                SkipIndirectionTable();
 517
 518                // we want to later read the deepest first
 8519                _classContext.Current.DeferredIndirectionTableList.Add(savedPos);
 8520            }
 521            else
 20522            {
 20523                _classContext.Current.DeferredIndirectionTableList.Add(0); // keep a slot for each slice
 20524            }
 525
 28526            var info = new SliceInfo(
 28527                typeId,
 28528                new ReadOnlyMemory<byte>(bytes),
 28529                _classContext.Current.IndirectionTable ?? Array.Empty<IceClass>(),
 28530                hasTaggedFields);
 531
 28532            _classContext.Current.Slices ??= new List<SliceInfo>();
 28533            _classContext.Current.Slices.Add(info);
 28534        }
 4535        else if (hasIndirectionTable)
 2536        {
 2537            Debug.Assert(_classContext.Current.PosAfterIndirectionTable is not null);
 538
 539            // Move past indirection table
 2540            long count = _classContext.Current.PosAfterIndirectionTable.Value - _reader.Consumed;
 2541            Debug.Assert(count > 0);
 2542            _reader.Advance(count);
 2543            _classContext.Current.PosAfterIndirectionTable = null;
 2544        }
 545
 546        // If we decoded the indirection table previously, we don't need it anymore since we're skipping this slice.
 32547        _classContext.Current.IndirectionTable = null;
 548
 32549        return (_classContext.Current.SliceFlags & SliceFlags.IsLastSlice) != 0;
 32550    }
 551
 552    /// <summary>Holds various fields used for class and exception decoding.</summary>
 553    private struct ClassContext
 554    {
 555        // Data for the class or exception instance that is currently getting decoded.
 556        internal InstanceData Current;
 557
 558        // Map of class instance ID to class instance.
 559        // When decoding a buffer:
 560        //  - Instance ID = 0 means null
 561        //  - Instance ID = 1 means the instance is encoded inline afterwards
 562        //  - Instance ID > 1 means a reference to a previously decoded instance, found in this map.
 563        // Since the map is actually a list, we use instance ID - 2 to lookup an instance.
 564        internal List<IceClass>? InstanceMap;
 565
 566        // See DecodeTypeId.
 567        internal long PosAfterLatestInsertedTypeId;
 568
 569        // Map of type ID index to type ID sequence, used only for classes.
 570        // We assign a type ID index (starting with 1) to each type ID (type ID sequence) we decode, in order.
 571        // Since this map is a list, we lookup a previously assigned type ID (type ID sequence) with
 572        // _typeIdMap[index - 1].
 573        internal List<string>? TypeIdMap;
 574    }
 575
 576    private struct InstanceData
 577    {
 578        // Instance fields
 579
 580        internal List<long>? DeferredIndirectionTableList;
 581        internal InstanceType InstanceType;
 582        internal List<SliceInfo>? Slices; // Preserved slices.
 583
 584        // Slice fields
 585
 586        internal bool FirstSlice;
 587        internal IceClass[]? IndirectionTable; // Indirection table of the current slice
 588        internal long? PosAfterIndirectionTable;
 589
 590        internal SliceFlags SliceFlags;
 591        internal int SliceSize;
 592    }
 593
 594    private enum InstanceType : byte
 595    {
 596        None = 0,
 597        Class,
 598        Exception
 599    }
 600}

/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>
 547417    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>
 4922    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>
 470727    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>
 4231    public readonly long Remaining => _reader.Remaining;
 32
 33    private const string EndOfBufferMessage = "Attempting to decode past the end of the Ice decoder buffer.";
 34
 835    private static readonly IActivator _defaultActivator =
 836        ActivatorFactory.Instance.Get(typeof(IceDecoder).Assembly);
 37
 838    private static readonly UTF8Encoding _utf8 =
 839        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)
 638874    {
 638875        DecodingContext = decodingContext;
 76
 638877        _currentCollectionAllocation = 0;
 78
 638879        _maxCollectionAllocation = maxCollectionAllocation == -1 ?
 638880            (buffer.Length > int.MaxValue / 8 ? int.MaxValue : (int)(8L * buffer.Length)) :
 638881            (maxCollectionAllocation >= 0 ? maxCollectionAllocation :
 638882                throw new ArgumentException(
 638883                    $"The {nameof(maxCollectionAllocation)} argument must be greater than or equal to -1.",
 638884                    nameof(maxCollectionAllocation)));
 85
 638886        _activator = activator;
 638887        _classContext = default;
 638888        _currentDepth = 0;
 638889        _maxDepth = maxDepth >= 1 ? maxDepth :
 638890            throw new ArgumentException($"The {nameof(maxDepth)} argument must be greater than 0.", nameof(maxDepth));
 91
 638892        _reader = new SequenceReader<byte>(buffer);
 638893    }
 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)
 184109        : this(
 184110            new ReadOnlySequence<byte>(buffer),
 184111            decodingContext,
 184112            maxCollectionAllocation,
 184113            activator,
 184114            maxDepth)
 184115    {
 184116    }
 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()
 79134    {
 79135        if (_reader.TryRead(out byte value))
 78136        {
 78137            return value switch
 78138            {
 69139                0 => false,
 8140                1 => true,
 1141                _ => throw new InvalidDataException("The value is out of the bool type accepted range.")
 78142            };
 143        }
 144        else
 1145        {
 1146            throw new InvalidDataException(EndOfBufferMessage);
 147        }
 77148    }
 149
 150    /// <summary>Decodes an Ice byte into a byte.</summary>
 151    /// <returns>The byte decoded by this decoder.</returns>
 152    public byte DecodeByte() =>
 46590153        _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() =>
 11600170        SequenceMarshal.TryRead(ref _reader, out int value) ?
 11600171            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() =>
 39182        SequenceMarshal.TryRead(ref _reader, out short value) ?
 39183            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()
 16153188    {
 16153189        byte firstByte = DecodeByte();
 16153190        if (firstByte < 255)
 16113191        {
 16113192            return firstByte;
 193        }
 194        else
 40195        {
 40196            int size = DecodeInt();
 40197            if (size < 0)
 0198            {
 0199                throw new InvalidDataException($"Decoded invalid size: {size}.");
 200            }
 40201            return size;
 202        }
 16153203    }
 204
 205    /// <summary>Decodes an Ice string into a string.</summary>
 206    /// <returns>The string decoded by this decoder.</returns>
 207    public string DecodeString()
 8054208    {
 8054209        int size = DecodeSize();
 8054210        if (size == 0)
 4147211        {
 4147212            return "";
 213        }
 214        else
 3907215        {
 216            // In the worst-case scenario, each byte becomes a new character. We'll adjust this allocation increase
 217            // after decoding the string.
 3907218            IncreaseCollectionAllocation(size, Unsafe.SizeOf<char>());
 219
 220            string result;
 3904221            if (_reader.UnreadSpan.Length >= size)
 3904222            {
 223                try
 3904224                {
 3904225                    result = _utf8.GetString(_reader.UnreadSpan[0..size]);
 3903226                }
 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                }
 3903234            }
 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
 3903255            _reader.Advance(size);
 256
 257            // Make the adjustment. The overall increase in allocation is result.Length * SizeOf<char>().
 3903258            DecreaseCollectionAllocation(size - result.Length, Unsafe.SizeOf<char>());
 3903259            return result;
 260        }
 8050261    }
 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)
 56270    {
 56271        if (_reader.TryCopyTo(destination))
 56272        {
 56273            _reader.Advance(destination.Length);
 56274        }
 275        else
 0276        {
 0277            throw new InvalidDataException(EndOfBufferMessage);
 278        }
 56279    }
 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)
 116290    {
 116291        if (DecodeTagHeader(tag, tagFormat))
 65292        {
 65293            if (tagFormat == TagFormat.VSize)
 10294            {
 10295                SkipSize();
 10296            }
 55297            else if (tagFormat == TagFormat.FSize)
 5298            {
 5299                Skip(4);
 5300            }
 65301            return decodeFunc(ref this);
 302        }
 303        else
 51304        {
 51305            return default!; // i.e. null
 306        }
 116307    }
 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)
 113312    {
 113313        if (_reader.Remaining >= count)
 113314        {
 113315            _reader.Advance(count);
 113316        }
 317        else
 0318        {
 0319            throw new InvalidDataException(EndOfBufferMessage);
 320        }
 113321    }
 322
 323    /// <summary>Skips the remaining tagged fields.</summary>
 324    public void SkipTagged()
 144325    {
 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.
 144328        bool useTagEndMarker = _classContext.Current.InstanceType != InstanceType.None;
 329
 185330        while (true)
 185331        {
 185332            if (!useTagEndMarker && _reader.End)
 117333            {
 334                // When we don't use an end marker, the end of the buffer indicates the end of the tagged fields.
 117335                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 the low-order 3 bits.
 41347            if ((v >> 3) == 30)
 4348            {
 4349                SkipSize();
 4350            }
 41351            SkipTaggedValue(format);
 41352        }
 144353    }
 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)
 4056372    {
 4056373        Debug.Assert(count >= 0, $"{nameof(count)} must be greater than or equal to 0.");
 4056374        Debug.Assert(elementSize > 0, $"{nameof(elementSize)} must be greater than 0.");
 375
 4056376        long byteCount = (long)count * elementSize;
 4056377        int remainingAllocation = _maxCollectionAllocation - _currentCollectionAllocation;
 4056378        if (byteCount > remainingAllocation)
 11379        {
 11380            throw new InvalidDataException(
 11381                $"The decoding exceeds the max collection allocation of '{_maxCollectionAllocation}'.");
 382        }
 4045383        _currentCollectionAllocation += (int)byteCount;
 4045384    }
 385
 386    private bool DecodeTagHeader(int tag, TagFormat expectedFormat)
 116387    {
 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.
 116390        bool useTagEndMarker = _classContext.Current.InstanceType != InstanceType.None;
 391
 116392        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
 95402        int requestedTag = tag;
 403
 95404        while (true)
 95405        {
 95406            if (!useTagEndMarker && _reader.End)
 14407            {
 14408                return false; // End of buffer indicates end of tagged fields.
 409            }
 410
 81411            long savedPos = _reader.Consumed;
 412
 81413            int v = DecodeByte();
 81414            if (useTagEndMarker && v == TagEndMarker)
 2415            {
 2416                _reader.Rewind(_reader.Consumed - savedPos);
 2417                return false;
 418            }
 419
 79420            var format = (TagFormat)(v & 0x07); // The low-order 3 bits.
 79421            tag = v >> 3;
 79422            if (tag == 30)
 8423            {
 8424                tag = DecodeSize();
 8425            }
 426
 79427            if (tag > requestedTag)
 14428            {
 14429                _reader.Rewind(_reader.Consumed - savedPos);
 14430                return false; // No tagged field with the requested tag.
 431            }
 65432            else if (tag < requestedTag)
 0433            {
 0434                SkipTaggedValue(format);
 0435            }
 436            else
 65437            {
 65438                if (expectedFormat == TagFormat.OptimizedVSize)
 17439                {
 17440                    expectedFormat = TagFormat.VSize; // fix virtual tag format
 17441                }
 442
 65443                if (format != expectedFormat)
 0444                {
 0445                    throw new InvalidDataException($"Invalid tagged field '{tag}': unexpected format.");
 446                }
 65447                return true;
 448            }
 0449        }
 116450    }
 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)
 3903456    {
 3903457        Debug.Assert(count >= 0, $"{nameof(count)} must be greater than or equal to 0.");
 3903458        Debug.Assert(elementSize > 0, $"{nameof(elementSize)} must be greater than 0.");
 459
 460        // Widen count to long to avoid overflow when multiplying by elementSize.
 3903461        long byteCount = (long)count * elementSize;
 462
 3903463        Debug.Assert(byteCount <= _currentCollectionAllocation, "Decreasing more than the current collection allocation.
 3903464        _currentCollectionAllocation -= (int)byteCount;
 3903465    }
 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}