File: System\Formats\Asn1\AsnWriter.BitString.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.
 
using System.Diagnostics;
 
namespace System.Formats.Asn1
{
    public sealed partial class AsnWriter
    {
        /// <summary>
        ///   Write a Bit String value with a specified tag.
        /// </summary>
        /// <param name="value">The value to write.</param>
        /// <param name="unusedBitCount">
        ///   The number of trailing bits which are not semantic.
        /// </param>
        /// <param name="tag">The tag to write, or <see langword="null"/> for the default tag (Universal 3).</param>
        /// <exception cref="ArgumentException">
        ///   <paramref name="tag"/>.<see cref="Asn1Tag.TagClass"/> is
        ///   <see cref="TagClass.Universal"/>, but
        ///   <paramref name="tag"/>.<see cref="Asn1Tag.TagValue"/> is not correct for
        ///   the method.
        ///
        ///   -or-
        ///
        ///   <paramref name="value"/> has length 0 and <paramref name="unusedBitCount"/> is not 0
        ///
        ///   -or-
        ///
        ///   <paramref name="value"/> is not empty and any of the bits identified by
        ///   <paramref name="unusedBitCount"/> is set.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        ///   <paramref name="unusedBitCount"/> is not in the range [0,7].
        /// </exception>
        public void WriteBitString(ReadOnlySpan<byte> value, int unusedBitCount = 0, Asn1Tag? tag = null)
        {
            CheckUniversalTag(tag, UniversalTagNumber.BitString);
 
            // Primitive or constructed, doesn't matter.
            WriteBitStringCore(tag ?? Asn1Tag.PrimitiveBitString, value, unusedBitCount);
        }
 
        // T-REC-X.690-201508 sec 8.6
        private void WriteBitStringCore(Asn1Tag tag, ReadOnlySpan<byte> bitString, int unusedBitCount)
        {
            // T-REC-X.690-201508 sec 8.6.2.2
            if (unusedBitCount < 0 || unusedBitCount > 7)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(unusedBitCount),
                    unusedBitCount,
                    SR.Argument_UnusedBitCountRange);
            }
 
            // T-REC-X.690-201508 sec 8.6.2.3
            if (bitString.Length == 0 && unusedBitCount != 0)
            {
                throw new ArgumentException(SR.Argument_UnusedBitCountMustBeZero, nameof(unusedBitCount));
            }
 
            byte lastByte = bitString.IsEmpty ? (byte)0 : bitString[bitString.Length - 1];
 
            // T-REC-X.690-201508 sec 11.2
            //
            // This could be ignored for BER, but since DER is more common and
            // it likely suggests a program error on the caller, leave it enabled for
            // BER for now.
            if (!CheckValidLastByte(lastByte, unusedBitCount))
            {
                throw new ArgumentException(SR.Argument_UnusedBitWasSet, nameof(unusedBitCount));
            }
 
            if (RuleSet == AsnEncodingRules.CER)
            {
                // T-REC-X.690-201508 sec 9.2
                //
                // If it's not within a primitive segment, use the constructed encoding.
                // (>= instead of > because of the unused bit count byte)
                if (bitString.Length >= AsnReader.MaxCERSegmentSize)
                {
                    WriteConstructedCerBitString(tag, bitString, unusedBitCount);
                    return;
                }
            }
 
            // Clear the constructed flag, if present.
            WriteTag(tag.AsPrimitive());
            // The unused bits byte requires +1.
            WriteLength(bitString.Length + 1);
            _buffer[_offset] = (byte)unusedBitCount;
            _offset++;
            bitString.CopyTo(_buffer.AsSpan(_offset));
            _offset += bitString.Length;
        }
 
        private static bool CheckValidLastByte(byte lastByte, int unusedBitCount)
        {
            // If 3 bits are "unused" then build a mask for them to check for 0.
            // 1 << 3 => 0b0000_1000
            // subtract 1 => 0b000_0111
            int mask = (1 << unusedBitCount) - 1;
            return ((lastByte & mask) == 0);
        }
 
        private static int DetermineCerBitStringTotalLength(Asn1Tag tag, int contentLength)
        {
            const int MaxCERSegmentSize = AsnReader.MaxCERSegmentSize;
            // Every segment has an "unused bit count" byte.
            const int MaxCERContentSize = MaxCERSegmentSize - 1;
            Debug.Assert(contentLength > MaxCERContentSize);
 
            int fullSegments = Math.DivRem(contentLength, MaxCERContentSize, out int lastContentSize);
 
            // The tag size is 1 byte.
            // The length will always be encoded as 82 03 E8 (3 bytes)
            // And 1000 content octets (by T-REC-X.690-201508 sec 9.2)
            const int FullSegmentEncodedSize = 1004;
            Debug.Assert(
                FullSegmentEncodedSize == 1 + 1 + MaxCERSegmentSize + GetEncodedLengthSubsequentByteCount(MaxCERSegmentSize));
 
            int remainingEncodedSize;
 
            if (lastContentSize == 0)
            {
                remainingEncodedSize = 0;
            }
            else
            {
                // One byte of tag, minimum one byte of length, and one byte of unused bit count.
                remainingEncodedSize = 3 + lastContentSize + GetEncodedLengthSubsequentByteCount(lastContentSize);
            }
 
            // Reduce the number of copies by pre-calculating the size.
            // +2 for End-Of-Contents
            // +1 for 0x80 indefinite length
            // +tag length
            return fullSegments * FullSegmentEncodedSize + remainingEncodedSize + 3 + tag.CalculateEncodedSize();
        }
 
        // T-REC-X.690-201508 sec 9.2, 8.6
        private void WriteConstructedCerBitString(Asn1Tag tag, ReadOnlySpan<byte> payload, int unusedBitCount)
        {
            const int MaxCERSegmentSize = AsnReader.MaxCERSegmentSize;
            // Every segment has an "unused bit count" byte.
            const int MaxCERContentSize = MaxCERSegmentSize - 1;
            Debug.Assert(payload.Length > MaxCERContentSize);
 
            int expectedSize = DetermineCerBitStringTotalLength(tag, payload.Length);
            EnsureWriteCapacity(expectedSize);
            int savedOffset = _offset;
 
            WriteTag(tag.AsConstructed());
            // T-REC-X.690-201508 sec 9.1
            // Constructed CER uses the indefinite form.
            WriteLength(-1);
 
            byte[] ensureNoExtraCopy = _buffer;
 
            ReadOnlySpan<byte> remainingData = payload;
            Span<byte> dest;
            Asn1Tag primitiveBitString = Asn1Tag.PrimitiveBitString;
 
            while (remainingData.Length > MaxCERContentSize)
            {
                // T-REC-X.690-201508 sec 8.6.4.1
                WriteTag(primitiveBitString);
                WriteLength(MaxCERSegmentSize);
                // 0 unused bits in this segment.
                _buffer[_offset] = 0;
                _offset++;
 
                dest = _buffer.AsSpan(_offset);
                remainingData.Slice(0, MaxCERContentSize).CopyTo(dest);
 
                remainingData = remainingData.Slice(MaxCERContentSize);
                _offset += MaxCERContentSize;
            }
 
            WriteTag(primitiveBitString);
            WriteLength(remainingData.Length + 1);
 
            _buffer[_offset] = (byte)unusedBitCount;
            _offset++;
 
            dest = _buffer.AsSpan(_offset);
            remainingData.CopyTo(dest);
            _offset += remainingData.Length;
 
            WriteEndOfContents();
 
            Debug.Assert(_offset - savedOffset == expectedSize, $"expected size was {expectedSize}, actual was {_offset - savedOffset}");
            Debug.Assert(_buffer == ensureNoExtraCopy, $"_buffer was replaced during {nameof(WriteConstructedCerBitString)}");
        }
    }
}