File: System\Formats\Asn1\AsnWriter.cs
Web Access
Project: src\src\libraries\System.Formats.Asn1\src\System.Formats.Asn1.csproj (System.Formats.Asn1)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// Enable CHECK_ACCURATE_ENSURE to ensure that the AsnWriter is not ever
// abusing the normal EnsureWriteCapacity + ArrayPool behaviors of rounding up.
//#define CHECK_ACCURATE_ENSURE
 
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
 
namespace System.Formats.Asn1
{
    /// <summary>
    ///   A writer for BER-, CER-, and DER-encoded ASN.1 data.
    /// </summary>
    public sealed partial class AsnWriter
    {
        private byte[] _buffer = null!;
        private int _offset;
        private Stack<StackFrame>? _nestingStack;
#if NET9_0_OR_GREATER
        private int _encodeDepth;
#endif
 
        /// <summary>
        ///   Gets the encoding rules in use by this writer.
        /// </summary>
        /// <value>
        ///   The encoding rules in use by this writer.
        /// </value>
        public AsnEncodingRules RuleSet { get; }
 
        /// <summary>
        ///   Create a new <see cref="AsnWriter"/> with a given set of encoding rules.
        /// </summary>
        /// <param name="ruleSet">The encoding constraints for the writer.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="ruleSet"/> is not defined.
        /// </exception>
        public AsnWriter(AsnEncodingRules ruleSet)
        {
            if (ruleSet != AsnEncodingRules.BER &&
                ruleSet != AsnEncodingRules.CER &&
                ruleSet != AsnEncodingRules.DER)
            {
                throw new ArgumentOutOfRangeException(nameof(ruleSet));
            }
 
            RuleSet = ruleSet;
        }
 
        /// <summary>
        ///   Initializes a new instance of <see cref="AsnWriter" /> with a given set of encoding rules and an initial capacity.
        /// </summary>
        /// <param name="ruleSet">The encoding constraints for the writer.</param>
        /// <param name="initialCapacity">The minimum capacity with which to initialize the underlying buffer.</param>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <para><paramref name="ruleSet"/> is not defined.</para>
        ///   <para> -or- </para>
        ///   <para><paramref name="initialCapacity"/> is a negative number.</para>
        /// </exception>
        /// <remarks>
        ///   Specifying <paramref name="initialCapacity" /> with a value of zero behaves as if no initial capacity were
        ///   specified.
        /// </remarks>
        public AsnWriter(AsnEncodingRules ruleSet, int initialCapacity) : this(ruleSet)
        {
            if (initialCapacity < 0)
                throw new ArgumentOutOfRangeException(nameof(initialCapacity), SR.ArgumentOutOfRange_NeedNonNegNum);
 
            if (initialCapacity > 0)
            {
                _buffer = new byte[initialCapacity];
            }
        }
 
        /// <summary>
        ///   Reset the writer to have no data, without releasing resources.
        /// </summary>
        public void Reset()
        {
#if NET9_0_OR_GREATER
            if (_encodeDepth != 0)
            {
                throw new InvalidOperationException(SR.AsnWriter_ModifyingWhileEncoding);
            }
#endif
 
            if (_offset > 0)
            {
                Debug.Assert(_buffer != null);
                Array.Clear(_buffer, 0, _offset);
                _offset = 0;
 
                _nestingStack?.Clear();
            }
        }
 
        /// <summary>
        ///   Gets the number of bytes that would be written by <see cref="TryEncode"/>.
        /// </summary>
        /// <returns>
        ///   The number of bytes that would be written by <see cref="TryEncode"/>.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        ///   <see cref="PushSequence"/>, <see cref="PushSetOf"/>, or
        ///   <see cref="PushOctetString"/> was called without the corresponding
        ///   Pop method.
        /// </exception>
        public int GetEncodedLength()
        {
            if ((_nestingStack?.Count ?? 0) != 0)
            {
                throw new InvalidOperationException(SR.AsnWriter_EncodeUnbalancedStack);
            }
 
            return _offset;
        }
 
        /// <summary>
        ///   Attempts to write the encoded representation of the data to <paramref name="destination"/>.
        /// </summary>
        /// <param name="destination">The buffer in which to write.</param>
        /// <param name="bytesWritten">
        ///   On success, receives the number of bytes written to <paramref name="destination"/>.
        /// </param>
        /// <returns>
        ///   <see langword="true"/> if the encode succeeded,
        ///   <see langword="false"/> if <paramref name="destination"/> is too small.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        ///   A <see cref="PushSequence"/> or <see cref="PushSetOf"/> has not been closed via
        ///   <see cref="PopSequence"/> or <see cref="PopSetOf"/>.
        /// </exception>
        public bool TryEncode(Span<byte> destination, out int bytesWritten)
        {
            if ((_nestingStack?.Count ?? 0) != 0)
                throw new InvalidOperationException(SR.AsnWriter_EncodeUnbalancedStack);
 
            // If the stack is closed out then everything is a definite encoding (BER, DER) or a
            // required indefinite encoding (CER). So we're correctly sized up, and ready to copy.
            if (destination.Length < _offset)
            {
                bytesWritten = 0;
                return false;
            }
 
            if (_offset == 0)
            {
                bytesWritten = 0;
                return true;
            }
 
            bytesWritten = _offset;
            _buffer.AsSpan(0, _offset).CopyTo(destination);
            return true;
        }
 
        /// <summary>
        ///   Writes the encoded representation of the data to <paramref name="destination"/>.
        /// </summary>
        /// <param name="destination">The buffer in which to write.</param>
        /// <returns>
        ///   The number of bytes written to <paramref name="destination" />.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        ///   A <see cref="PushSequence"/> or <see cref="PushSetOf"/> has not been closed via
        ///   <see cref="PopSequence"/> or <see cref="PopSetOf"/>.
        /// </exception>
        public int Encode(Span<byte> destination)
        {
            // Since TryEncode doesn't have any side effects on the return false paths, just
            // call it from here and do argument validation late.
            if (!TryEncode(destination, out int bytesWritten))
            {
                throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination));
            }
 
            Debug.Assert(bytesWritten == _offset);
            return bytesWritten;
        }
 
        /// <summary>
        ///   Return a new array containing the encoded value.
        /// </summary>
        /// <returns>
        ///   A precisely-sized array containing the encoded value.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        ///   A <see cref="PushSequence"/> or <see cref="PushSetOf"/> has not been closed via
        ///   <see cref="PopSequence"/> or <see cref="PopSetOf"/>.
        /// </exception>
        public byte[] Encode()
        {
            if ((_nestingStack?.Count ?? 0) != 0)
            {
                throw new InvalidOperationException(SR.AsnWriter_EncodeUnbalancedStack);
            }
 
            if (_offset == 0)
            {
                return Array.Empty<byte>();
            }
 
            // If the stack is closed out then everything is a definite encoding (BER, DER) or a
            // required indefinite encoding (CER). So we're correctly sized up, and ready to copy.
            return _buffer.AsSpan(0, _offset).ToArray();
        }
 
#if NET9_0_OR_GREATER
        /// <summary>
        ///   Provides the encoded representation of the data to the specified callback.
        /// </summary>
        /// <param name="encodeCallback">
        ///   The callback that receives the encoded data.
        /// </param>
        /// <typeparam name="TReturn">
        ///   The type of the return value.
        /// </typeparam>
        /// <returns>
        ///   Returns the value returned from <paramref name="encodeCallback" />.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="encodeCallback"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        ///   A <see cref="PushSequence"/> or <see cref="PushSetOf"/> has not been closed via
        ///   <see cref="PopSequence"/> or <see cref="PopSetOf"/>.
        /// </exception>
        public TReturn Encode<TReturn>(Func<ReadOnlySpan<byte>, TReturn> encodeCallback)
        {
            if (encodeCallback is null)
                throw new ArgumentNullException(nameof(encodeCallback));
 
            try
            {
                _encodeDepth = checked(_encodeDepth + 1);
                ReadOnlySpan<byte> encoded = EncodeAsSpan();
                return encodeCallback(encoded);
            }
            finally
            {
                _encodeDepth--;
            }
        }
 
        /// <summary>
        ///   Provides the encoded representation of the data to the specified callback.
        /// </summary>
        /// <param name="encodeCallback">
        ///   The callback that receives the encoded data.
        /// </param>
        /// <param name="state">
        ///   The state to pass to <paramref name="encodeCallback" />.
        /// </param>
        /// <typeparam name="TState">
        ///   The type of the state.
        /// </typeparam>
        /// <typeparam name="TReturn">
        ///   The type of the return value.
        /// </typeparam>
        /// <returns>
        ///   Returns the value returned from <paramref name="encodeCallback" />.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="encodeCallback"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        ///   A <see cref="PushSequence"/> or <see cref="PushSetOf"/> has not been closed via
        ///   <see cref="PopSequence"/> or <see cref="PopSetOf"/>.
        /// </exception>
        public TReturn Encode<TState, TReturn>(TState state, Func<TState, ReadOnlySpan<byte>, TReturn> encodeCallback)
            where TState : allows ref struct
        {
            if (encodeCallback is null)
                throw new ArgumentNullException(nameof(encodeCallback));
 
            try
            {
                _encodeDepth = checked(_encodeDepth + 1);
                ReadOnlySpan<byte> encoded = EncodeAsSpan();
                return encodeCallback(state, encoded);
            }
            finally
            {
                _encodeDepth--;
            }
        }
#endif
 
        private ReadOnlySpan<byte> EncodeAsSpan()
        {
            if ((_nestingStack?.Count ?? 0) != 0)
            {
                throw new InvalidOperationException(SR.AsnWriter_EncodeUnbalancedStack);
            }
 
            if (_offset == 0)
            {
                return ReadOnlySpan<byte>.Empty;
            }
 
            // If the stack is closed out then everything is a definite encoding (BER, DER) or a
            // required indefinite encoding (CER). So we're correctly sized up, and ready to copy.
            return new ReadOnlySpan<byte>(_buffer, 0, _offset);
        }
 
        /// <summary>
        ///   Determines if <see cref="Encode()"/> would produce an output identical to
        ///   <paramref name="other"/>.
        /// </summary>
        /// <returns>
        ///   <see langword="true"/> if the pending encoded data is identical to <paramref name="other"/>,
        ///   <see langword="false"/> otherwise.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        ///   A <see cref="PushSequence"/> or <see cref="PushSetOf"/> has not been closed via
        ///   <see cref="PopSequence"/> or <see cref="PopSetOf"/>.
        /// </exception>
        public bool EncodedValueEquals(ReadOnlySpan<byte> other)
        {
            return EncodeAsSpan().SequenceEqual(other);
        }
 
        /// <summary>
        ///   Determines if <see cref="Encode()"/> would produce an output identical to
        ///   <paramref name="other"/>.
        /// </summary>
        /// <returns>
        ///   <see langword="true"/> if the pending encoded data is identical to <paramref name="other"/>,
        ///   <see langword="false"/> otherwise.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="other"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        ///   A <see cref="PushSequence"/> or <see cref="PushSetOf"/> has not been closed via
        ///   <see cref="PopSequence"/> or <see cref="PopSetOf"/>.
        /// </exception>
        public bool EncodedValueEquals(AsnWriter other)
        {
            if (other is null)
            {
                throw new ArgumentNullException(nameof(other));
            }
 
            return EncodeAsSpan().SequenceEqual(other.EncodeAsSpan());
        }
 
        private void EnsureWriteCapacity(int pendingCount)
        {
            if (pendingCount < 0)
            {
                throw new OverflowException();
            }
 
#if NET9_0_OR_GREATER
            if (_encodeDepth != 0)
            {
                throw new InvalidOperationException(SR.AsnWriter_ModifyingWhileEncoding);
            }
#endif
 
            if (_buffer == null || _buffer.Length - _offset < pendingCount)
            {
#if CHECK_ACCURATE_ENSURE
                // A debug paradigm to make sure that throughout the execution nothing ever writes
                // past where the buffer was "allocated".  This causes quite a number of reallocs
                // and copies, so it's a #define opt-in.
                byte[] newBytes = new byte[_offset + pendingCount];
 
                if (_buffer != null)
                {
                    Span<byte> bufferSpan = _buffer.AsSpan(0, _offset);
                    bufferSpan.CopyTo(newBytes);
                    bufferSpan.Clear();
                }
 
                _buffer = newBytes;
#else
                const int BlockSize = 1024;
                // Make sure we don't run into a lot of "grow a little" by asking in 1k steps.
                int blocks = checked(_offset + pendingCount + (BlockSize - 1)) / BlockSize;
                byte[]? oldBytes = _buffer;
                Array.Resize(ref _buffer, BlockSize * blocks);
 
                oldBytes?.AsSpan(0, _offset).Clear();
#endif
 
#if DEBUG
                // Ensure no "implicit 0" is happening, in case we move to pooling.
                _buffer.AsSpan(_offset).Fill(0xCA);
#endif
            }
        }
 
        private void WriteTag(Asn1Tag tag)
        {
            int spaceRequired = tag.CalculateEncodedSize();
            EnsureWriteCapacity(spaceRequired);
 
            if (!tag.TryEncode(_buffer.AsSpan(_offset, spaceRequired), out int written) ||
                written != spaceRequired)
            {
                Debug.Fail($"TryWrite failed or written was wrong value ({written} vs {spaceRequired})");
                throw new InvalidOperationException();
            }
 
            _offset += spaceRequired;
        }
 
        // T-REC-X.690-201508 sec 8.1.3
        private void WriteLength(int length)
        {
            const byte MultiByteMarker = 0x80;
            Debug.Assert(length >= -1);
 
            // If the indefinite form has been requested.
            // T-REC-X.690-201508 sec 8.1.3.6
            if (length == -1)
            {
                EnsureWriteCapacity(1);
                _buffer[_offset] = MultiByteMarker;
                _offset++;
                return;
            }
 
            Debug.Assert(length >= 0);
 
            // T-REC-X.690-201508 sec 8.1.3.3, 8.1.3.4
            if (length < MultiByteMarker)
            {
                // Pre-allocate the pending data since we know how much.
                EnsureWriteCapacity(1 + length);
                _buffer[_offset] = (byte)length;
                _offset++;
                return;
            }
 
            // The rest of the method implements T-REC-X.680-201508 sec 8.1.3.5
            int lengthLength = GetEncodedLengthSubsequentByteCount(length);
 
            // Pre-allocate the pending data since we know how much.
            EnsureWriteCapacity(lengthLength + 1 + length);
            _buffer[_offset] = (byte)(MultiByteMarker | lengthLength);
 
            // No minus one because offset didn't get incremented yet.
            int idx = _offset + lengthLength;
 
            int remaining = length;
 
            do
            {
                _buffer[idx] = (byte)remaining;
                remaining >>= 8;
                idx--;
            } while (remaining > 0);
 
            Debug.Assert(idx == _offset);
            _offset += lengthLength + 1;
        }
 
        // T-REC-X.690-201508 sec 8.1.3.5
        private static int GetEncodedLengthSubsequentByteCount(int length)
        {
            if (length < 0)
                throw new OverflowException();
            if (length <= 0x7F)
                return 0;
            if (length <= byte.MaxValue)
                return 1;
            if (length <= ushort.MaxValue)
                return 2;
            if (length <= 0x00FFFFFF)
                return 3;
 
            return 4;
        }
 
        /// <summary>
        ///   Copy the value of this writer into another.
        /// </summary>
        /// <param name="destination">The writer to receive the value.</param>
        /// <exception cref="ArgumentNullException">
        ///   <paramref name="destination"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        ///   A <see cref="PushSequence"/> or <see cref="PushSetOf"/> has not been closed via
        ///   <see cref="PopSequence"/> or <see cref="PopSetOf"/>.
        ///
        ///   -or-
        ///
        ///   This writer is empty.
        ///
        ///   -or-
        ///
        ///   This writer represents more than one top-level value.
        ///
        ///   -or-
        ///
        ///   This writer's value is encoded in a manner that is not compatible with the
        ///   ruleset for the destination writer.
        /// </exception>
        public void CopyTo(AsnWriter destination)
        {
            if (destination is null)
            {
                throw new ArgumentNullException(nameof(destination));
            }
 
            try
            {
                destination.WriteEncodedValue(EncodeAsSpan());
            }
            catch (ArgumentException e)
            {
                throw new InvalidOperationException(new InvalidOperationException().Message, e);
            }
        }
 
        /// <summary>
        ///   Write a single value which has already been encoded.
        /// </summary>
        /// <param name="value">The value to write.</param>
        /// <remarks>
        ///   This method only checks that the tag and length are encoded according to the current ruleset,
        ///   and that the end of the value is the end of the input. The contents are not evaluated for
        ///   semantic meaning.
        /// </remarks>
        /// <exception cref="ArgumentException">
        ///   <paramref name="value"/> could not be read under the current encoding rules.
        ///
        ///   -or-
        ///
        ///   <paramref name="value"/> has data beyond the end of the first value.
        /// </exception>
        public void WriteEncodedValue(ReadOnlySpan<byte> value)
        {
            // Is it legal under the current rules?
            bool read = AsnDecoder.TryReadEncodedValue(
                value,
                RuleSet,
                out _,
                out _,
                out _,
                out int consumed);
 
            if (!read || consumed != value.Length)
            {
                throw new ArgumentException(
                    SR.Argument_WriteEncodedValue_OneValueAtATime,
                    nameof(value));
            }
 
            EnsureWriteCapacity(value.Length);
            value.CopyTo(_buffer.AsSpan(_offset));
            _offset += value.Length;
        }
 
        // T-REC-X.690-201508 sec 8.1.5
        private void WriteEndOfContents()
        {
            EnsureWriteCapacity(2);
            _buffer[_offset++] = 0;
            _buffer[_offset++] = 0;
        }
 
        private Scope PushTag(Asn1Tag tag, UniversalTagNumber tagType)
        {
            _nestingStack ??= new Stack<StackFrame>();
 
            Debug.Assert(tag.IsConstructed);
            WriteTag(tag);
            _nestingStack.Push(new StackFrame(tag, _offset, tagType));
            // Indicate that the length is indefinite.
            // We'll come back and clean this up (as appropriate) in PopTag.
            WriteLength(-1);
            return new Scope(this);
        }
 
        private void PopTag(Asn1Tag tag, UniversalTagNumber tagType, bool sortContents = false)
        {
            if (_nestingStack == null || _nestingStack.Count == 0)
            {
                throw new InvalidOperationException(SR.AsnWriter_PopWrongTag);
            }
 
            (Asn1Tag stackTag, int lenOffset, UniversalTagNumber stackTagType) = _nestingStack.Peek();
 
            Debug.Assert(tag.IsConstructed);
            if (stackTag != tag || stackTagType != tagType)
            {
                throw new InvalidOperationException(SR.AsnWriter_PopWrongTag);
            }
 
            _nestingStack.Pop();
 
            if (sortContents)
            {
                Debug.Assert(tagType == UniversalTagNumber.SetOf);
                SortContents(_buffer, lenOffset + 1, _offset);
            }
 
            // BER could use the indefinite encoding that CER does.
            // But since the definite encoding form is easier to read (doesn't require a contextual
            // parser to find the end-of-contents marker) some ASN.1 readers (including the previous
            // incarnation of AsnReader) may choose not to support it.
            //
            // So, BER will use the DER rules here, in the interest of broader compatibility.
 
            // T-REC-X.690-201508 sec 9.1 (constructed CER => indefinite length)
            // T-REC-X.690-201508 sec 8.1.3.6
            if (RuleSet == AsnEncodingRules.CER && tagType != UniversalTagNumber.OctetString)
            {
                WriteEndOfContents();
                return;
            }
 
            int containedLength = _offset - 1 - lenOffset;
            Debug.Assert(containedLength >= 0);
 
            int start = lenOffset + 1;
 
            // T-REC-X.690-201508 sec 9.2
            // T-REC-X.690-201508 sec 10.2
            if (tagType == UniversalTagNumber.OctetString)
            {
                if (RuleSet != AsnEncodingRules.CER || containedLength <= AsnDecoder.MaxCERSegmentSize)
                {
                    // Need to replace the tag with the primitive tag.
                    // Since the P/C bit doesn't affect the length, overwrite the tag.
                    int tagLen = tag.CalculateEncodedSize();
                    tag.AsPrimitive().Encode(_buffer.AsSpan(lenOffset - tagLen, tagLen));
                    // Continue with the regular flow.
                }
                else
                {
                    int fullSegments = Math.DivRem(
                        containedLength,
                        AsnDecoder.MaxCERSegmentSize,
                        out int lastSegmentSize);
 
                    int requiredPadding =
                        // Each full segment has a header of 048203E8
                        4 * fullSegments +
                        // The last one is 04 plus the encoded length.
                        2 + GetEncodedLengthSubsequentByteCount(lastSegmentSize);
 
                    // Shift the data forward so we can use right-source-overlapped
                    // copy in the existing method.
                    // Also, ensure the space for the end-of-contents marker.
                    EnsureWriteCapacity(requiredPadding + 2);
                    ReadOnlySpan<byte> src = _buffer.AsSpan(start, containedLength);
                    Span<byte> dest = _buffer.AsSpan(start + requiredPadding, containedLength);
                    src.CopyTo(dest);
 
                    int expectedEnd = start + containedLength + requiredPadding + 2;
                    _offset = lenOffset - tag.CalculateEncodedSize();
                    WriteConstructedCerOctetString(tag, dest);
                    Debug.Assert(_offset == expectedEnd);
                    return;
                }
            }
 
            int shiftSize = GetEncodedLengthSubsequentByteCount(containedLength);
 
            // Best case, length fits in the compact byte
            if (shiftSize == 0)
            {
                _buffer[lenOffset] = (byte)containedLength;
                return;
            }
 
            // We're currently at the end, so ensure we have room for N more bytes.
            EnsureWriteCapacity(shiftSize);
 
            // Buffer.BlockCopy correctly does forward-overlapped, so use it.
            Buffer.BlockCopy(_buffer, start, _buffer, start + shiftSize, containedLength);
 
            int tmp = _offset;
            _offset = lenOffset;
            WriteLength(containedLength);
            Debug.Assert(_offset - lenOffset - 1 == shiftSize);
            _offset = tmp + shiftSize;
        }
 
        private static void SortContents(byte[] buffer, int start, int end)
        {
            Debug.Assert(buffer != null);
            Debug.Assert(end >= start);
 
            int len = end - start;
 
            if (len == 0)
            {
                return;
            }
 
            // Since BER can read everything and the reader does not mutate data
            // just use a BER reader for identifying the positions of the values
            // within this memory segment.
            //
            // Since it's not mutating, any restrictions imposed by CER or DER will
            // still be maintained.
            var reader = new AsnReader(new ReadOnlyMemory<byte>(buffer, start, len), AsnEncodingRules.BER);
            int pos = start;
            ReadOnlyMemory<byte> encoded = reader.ReadEncodedValue();
 
            if (!reader.HasData)
            {
                // If there is no more data, then there was only one value, so we don't need to sort anything.
                return;
            }
 
            List<(int, int)> positions = new List<(int, int)>();
            positions.Add((pos, encoded.Length));
            pos += encoded.Length;
 
            do
            {
                encoded = reader.ReadEncodedValue();
                positions.Add((pos, encoded.Length));
                pos += encoded.Length;
            }
            while (reader.HasData);
 
            Debug.Assert(pos == end);
 
            var comparer = new ArrayIndexSetOfValueComparer(buffer);
            positions.Sort(comparer);
 
            byte[] tmp = CryptoPool.Rent(len);
 
            pos = 0;
 
            foreach ((int offset, int length) in positions)
            {
                Buffer.BlockCopy(buffer, offset, tmp, pos, length);
                pos += length;
            }
 
            Debug.Assert(pos == len);
 
            Buffer.BlockCopy(tmp, 0, buffer, start, len);
            CryptoPool.Return(tmp, len);
        }
 
        private static void CheckUniversalTag(Asn1Tag? tag, UniversalTagNumber universalTagNumber)
        {
            if (tag != null)
            {
                Asn1Tag value = tag.Value;
 
                if (value.TagClass == TagClass.Universal && value.TagValue != (int)universalTagNumber)
                {
                    throw new ArgumentException(
                        SR.Argument_UniversalValueIsFixed,
                        nameof(tag));
                }
            }
        }
 
        private sealed class ArrayIndexSetOfValueComparer : IComparer<(int, int)>
        {
            private readonly byte[] _data;
 
            public ArrayIndexSetOfValueComparer(byte[] data)
            {
                _data = data;
            }
 
            public int Compare((int, int) x, (int, int) y)
            {
                (int xOffset, int xLength) = x;
                (int yOffset, int yLength) = y;
 
                int value =
                    SetOfValueComparer.Instance.Compare(
                        new ReadOnlyMemory<byte>(_data, xOffset, xLength),
                        new ReadOnlyMemory<byte>(_data, yOffset, yLength));
 
                if (value == 0)
                {
                    // Whichever had the lowest index wins (once sorted, stay sorted)
                    return xOffset - yOffset;
                }
 
                return value;
            }
        }
 
        private readonly struct StackFrame : IEquatable<StackFrame>
        {
            public Asn1Tag Tag { get; }
            public int Offset { get; }
            public UniversalTagNumber ItemType { get; }
 
            internal StackFrame(Asn1Tag tag, int offset, UniversalTagNumber itemType)
            {
                Tag = tag;
                Offset = offset;
                ItemType = itemType;
            }
 
            public void Deconstruct(out Asn1Tag tag, out int offset, out UniversalTagNumber itemType)
            {
                tag = Tag;
                offset = Offset;
                itemType = ItemType;
            }
 
            public bool Equals(StackFrame other)
            {
                return Tag.Equals(other.Tag) && Offset == other.Offset && ItemType == other.ItemType;
            }
 
            public override bool Equals([NotNullWhen(true)] object? obj) => obj is StackFrame other && Equals(other);
 
            public override int GetHashCode()
            {
                return (Tag, Offset, ItemType).GetHashCode();
            }
 
            public static bool operator ==(StackFrame left, StackFrame right) => left.Equals(right);
 
            public static bool operator !=(StackFrame left, StackFrame right) => !left.Equals(right);
        }
 
        /// <summary>
        ///   Represents a pushed ASN.1 scope.
        /// </summary>
        /// <remarks>
        ///   Instances of this type are expected to be created from a <c>Push</c> member on <see cref="AsnWriter"/>,
        ///   not instantiated directly.
        ///   Calling <see cref="Dispose" /> will call the corresponding <c>Pop</c> associated with the <c>Push</c>.
        /// </remarks>
        public readonly struct Scope : IDisposable
        {
            private readonly AsnWriter _writer;
            private readonly StackFrame _frame;
            private readonly int _depth;
 
            internal Scope(AsnWriter writer)
            {
                Debug.Assert(writer._nestingStack != null);
 
                _writer = writer;
                _frame = _writer._nestingStack.Peek();
                _depth = _writer._nestingStack.Count;
            }
 
            /// <summary>
            ///   Pops the ASN.1 scope.
            /// </summary>
            /// <exception cref="InvalidOperationException">
            ///   A scope was pushed within this scope, but has yet to be popped.
            /// </exception>
            public void Dispose()
            {
                Debug.Assert(_writer == null || _writer._nestingStack != null);
 
                if (_writer == null || _writer._nestingStack!.Count == 0)
                {
                    return;
                }
 
                if (_writer._nestingStack.Peek() == _frame)
                {
                    switch (_frame.ItemType)
                    {
                        case UniversalTagNumber.SetOf:
                            _writer.PopSetOf(_frame.Tag);
                            break;
                        case UniversalTagNumber.Sequence:
                            _writer.PopSequence(_frame.Tag);
                            break;
                        case UniversalTagNumber.OctetString:
                            _writer.PopOctetString(_frame.Tag);
                            break;
                        default:
                            Debug.Fail($"No handler for {_frame.ItemType}");
                            throw new InvalidOperationException();
                    }
                }
                else if (_writer._nestingStack.Count > _depth &&
                    _writer._nestingStack.Contains(_frame))
                {
                    // Another frame was pushed when we got disposed.
                    // Report the imbalance.
                    throw new InvalidOperationException(SR.AsnWriter_PopWrongTag);
                }
            }
        }
    }
}