File: System\BinaryData.cs
Web Access
Project: src\src\libraries\System.Memory.Data\src\System.Memory.Data.csproj (System.Memory.Data)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;
 
namespace System
{
    /// <summary>
    /// A lightweight abstraction for a payload of bytes that supports converting between string, stream, JSON, and bytes.
    /// </summary>
    [JsonConverter(typeof(BinaryDataJsonConverter))]
    public class BinaryData
    {
        private const string JsonSerializerRequiresDynamicCode = "JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation.";
        private const string JsonSerializerRequiresUnreferencedCode = "JSON serialization and deserialization might require types that cannot be statically analyzed.";
        private const string MediaTypeApplicationJson = "application/json";
 
        /// <summary>
        /// The backing store for the <see cref="BinaryData"/> instance.
        /// </summary>
        private readonly ReadOnlyMemory<byte> _bytes;
 
        /// <summary>
        /// Returns an empty BinaryData.
        /// </summary>
        public static BinaryData Empty { get; } = new BinaryData(ReadOnlyMemory<byte>.Empty);
 
        /// <summary>
        /// Gets the number of bytes of this data.
        /// </summary>
        /// <returns>The number of bytes of this data.</returns>
        public int Length => _bytes.Length;
 
        /// <summary>
        /// Gets a value that indicates whether this data is empty.
        /// </summary>
        /// <returns><see langword="true" /> if the data is empty (that is, its <see cref="Length" /> is 0); otherwise, <see langword="false" />.</returns>
        public bool IsEmpty => _bytes.IsEmpty;
 
        /// <summary>
        /// Gets the MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.
        /// </summary>
        /// <seealso cref="MediaTypeNames"/>
        public string? MediaType { get; }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the
        /// provided byte array.
        /// </summary>
        /// <param name="data">The array to wrap.</param>
        public BinaryData(byte[] data)
        {
            _bytes = data ?? throw new ArgumentNullException(nameof(data));
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the provided byte array
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <param name="data">The array to wrap.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <seealso cref="MediaTypeNames"/>
        public BinaryData(byte[] data, string? mediaType) : this(data)
        {
            MediaType = mediaType;
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by serializing the provided object to JSON
        /// using <see cref="JsonSerializer"/>.
        /// and sets <see cref="MediaType"/> to "application/json".
        /// </summary>
        /// <param name="jsonSerializable">The object that will be serialized to JSON using
        /// <see cref="JsonSerializer"/>.</param>
        /// <param name="options">The options to use when serializing to JSON.</param>
        /// <param name="type">The type to use when serializing the data. If not specified, <see cref="object.GetType"/> will
        /// be used to determine the type.</param>
        /// <seealso cref="MediaTypeNames"/>
        [RequiresDynamicCode(JsonSerializerRequiresDynamicCode)]
        [RequiresUnreferencedCode(JsonSerializerRequiresUnreferencedCode)]
        public BinaryData(object? jsonSerializable, JsonSerializerOptions? options = default, Type? type = default) : this(
            JsonSerializer.SerializeToUtf8Bytes(jsonSerializable, type ?? jsonSerializable?.GetType() ?? typeof(object), options),
            MediaTypeApplicationJson)
        {
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by serializing the provided object to JSON
        /// using <see cref="JsonSerializer"/>
        /// and sets <see cref="MediaType"/> to "application/json".
        /// </summary>
        /// <param name="jsonSerializable">The object that will be serialized to JSON using
        /// <see cref="JsonSerializer"/>.</param>
        /// <param name="context">The <see cref="JsonSerializerContext" /> to use when serializing to JSON.</param>
        /// <param name="type">The type to use when serializing the data. If not specified, <see cref="object.GetType"/> will
        /// be used to determine the type.</param>
        /// <seealso cref="MediaTypeNames"/>
        public BinaryData(object? jsonSerializable, JsonSerializerContext context, Type? type = default) : this(
            JsonSerializer.SerializeToUtf8Bytes(jsonSerializable, type ?? jsonSerializable?.GetType() ?? typeof(object), context),
            MediaTypeApplicationJson)
        {
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the
        /// provided bytes.
        /// </summary>
        /// <param name="data">Byte data to wrap.</param>
        public BinaryData(ReadOnlyMemory<byte> data)
        {
            _bytes = data;
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the provided bytes
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <param name="data">Byte data to wrap.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <seealso cref="MediaTypeNames"/>
        public BinaryData(ReadOnlyMemory<byte> data, string? mediaType) : this(data)
        {
            MediaType = mediaType;
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from a string by converting
        /// the string to bytes using the UTF-8 encoding.
        /// </summary>
        /// <param name="data">The string data.</param>
        public BinaryData(string data)
        {
            if (data is null)
            {
                throw new ArgumentNullException(nameof(data));
            }
 
            _bytes = Encoding.UTF8.GetBytes(data);
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from a string by converting
        /// the string to bytes using the UTF-8 encoding
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <param name="data">The string data.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <seealso cref="MediaTypeNames"/>
        public BinaryData(string data, string? mediaType) : this(data)
        {
            MediaType = mediaType;
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the provided
        /// <see cref="ReadOnlyMemory{Byte}"/>.
        /// </summary>
        /// <param name="data">Byte data to wrap.</param>
        /// <returns>A wrapper over <paramref name="data"/>.</returns>
        public static BinaryData FromBytes(ReadOnlyMemory<byte> data) => new BinaryData(data);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the provided
        /// <see cref="ReadOnlyMemory{Byte}"/>
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <param name="data">Byte data to wrap.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <returns>A wrapper over <paramref name="data"/>.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public static BinaryData FromBytes(ReadOnlyMemory<byte> data, string? mediaType)
            => new BinaryData(data, mediaType);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the provided
        /// byte array.
        /// </summary>
        /// <param name="data">The array to wrap.</param>
        /// <returns>A wrapper over <paramref name="data"/>.</returns>
        public static BinaryData FromBytes(byte[] data) => new BinaryData(data);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the provided byte array
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <param name="data">The array to wrap.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <returns>A wrapper over <paramref name="data"/>.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public static BinaryData FromBytes(byte[] data, string? mediaType)
            => new BinaryData(data, mediaType);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from a string by converting
        /// the string to bytes using the UTF-8 encoding.
        /// </summary>
        /// <param name="data">The string data.</param>
        /// <returns>A value representing the UTF-8 encoding of <paramref name="data"/>.</returns>
        public static BinaryData FromString(string data) => new BinaryData(data);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from a string by converting
        /// the string to bytes using the UTF-8 encoding
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <param name="data">The string data.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Text.Plain"/>.</param>
        /// <returns>A value representing the UTF-8 encoding of <paramref name="data"/>.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public static BinaryData FromString(string data, string? mediaType)
            => new BinaryData(data, mediaType);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from the specified stream.
        /// The stream is not disposed by this method.
        /// </summary>
        /// <param name="stream">Stream containing the data.</param>
        /// <returns>A value representing all of the data remaining in <paramref name="stream"/>.</returns>
        public static BinaryData FromStream(Stream stream) => FromStream(stream, mediaType: null);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from the specified stream
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// The stream is not disposed by this method.
        /// </summary>
        /// <param name="stream">Stream containing the data.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <returns>A value representing all of the data remaining in <paramref name="stream"/>.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public static BinaryData FromStream(Stream stream, string? mediaType)
        {
            if (stream is null)
            {
                throw new ArgumentNullException(nameof(stream));
            }
 
            return FromStreamAsync(stream, useAsync: false, mediaType).GetAwaiter().GetResult();
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from the specified stream.
        /// The stream is not disposed by this method.
        /// </summary>
        /// <param name="stream">Stream containing the data.</param>
        /// <param name="cancellationToken">A token that may be used to cancel the operation.</param>
        /// <returns>A value representing all of the data remaining in <paramref name="stream"/>.</returns>
        public static Task<BinaryData> FromStreamAsync(Stream stream, CancellationToken cancellationToken = default)
            => FromStreamAsync(stream, mediaType: null, cancellationToken);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from the specified stream
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// The stream is not disposed by this method.
        /// </summary>
        /// <param name="stream">Stream containing the data.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <param name="cancellationToken">A token that may be used to cancel the operation.</param>
        /// <returns>A value representing all of the data remaining in <paramref name="stream"/>.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public static Task<BinaryData> FromStreamAsync(Stream stream, string? mediaType,
            CancellationToken cancellationToken = default)
        {
            if (stream is null)
            {
                throw new ArgumentNullException(nameof(stream));
            }
 
            return FromStreamAsync(stream, useAsync: true, mediaType, cancellationToken);
        }
 
        private static async Task<BinaryData> FromStreamAsync(Stream stream, bool useAsync,
            string? mediaType = default, CancellationToken cancellationToken = default)
        {
            const int CopyToBufferSize = 81920;  // the default used by Stream.CopyToAsync
            int bufferSize = CopyToBufferSize;
            MemoryStream memoryStream;
 
            if (stream.CanSeek)
            {
                long longLength = stream.Length - stream.Position;
                if (longLength > int.MaxValue || longLength < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(stream), SR.ArgumentOutOfRange_StreamLengthMustBeNonNegativeInt32);
                }
 
                // choose a minimum valid (non-zero) buffer size.
                bufferSize = longLength == 0 ? 1 : Math.Min((int)longLength, CopyToBufferSize);
                memoryStream = new MemoryStream((int)longLength);
            }
            else
            {
                memoryStream = new MemoryStream();
            }
 
            using (memoryStream)
            {
                if (useAsync)
                {
                    await stream.CopyToAsync(memoryStream, bufferSize, cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    stream.CopyTo(memoryStream, bufferSize);
                }
                return new BinaryData(memoryStream.GetBuffer().AsMemory(0, (int)memoryStream.Position), mediaType);
            }
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from the specified file.
        /// </summary>
        /// <param name="path">The path to the file.</param>
        /// <returns>A value representing all of the data from the file.</returns>
        public static BinaryData FromFile(string path) => FromFile(path, mediaType: null);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from the specified file
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <param name="path">The path to the file.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <returns>A value representing all of the data from the file.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public static BinaryData FromFile(string path, string? mediaType)
        {
            if (path is null)
            {
                throw new ArgumentNullException(nameof(path));
            }
 
            return new BinaryData(File.ReadAllBytes(path), mediaType);
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from the specified file.
        /// </summary>
        /// <param name="path">The path to the file.</param>
        /// <param name="cancellationToken">A token that may be used to cancel the operation.</param>
        /// <returns>A value representing all of the data from the file.</returns>
        public static Task<BinaryData> FromFileAsync(string path, CancellationToken cancellationToken = default)
            => FromFileAsync(path, mediaType: null, cancellationToken);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance from the specified file
        /// and sets <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <param name="path">The path to the file.</param>
        /// <param name="mediaType">MIME type of this data, e.g. <see cref="MediaTypeNames.Application.Octet"/>.</param>
        /// <param name="cancellationToken">A token that may be used to cancel the operation.</param>
        /// <returns>A value representing all of the data from the file.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public static Task<BinaryData> FromFileAsync(string path, string? mediaType,
            CancellationToken cancellationToken = default)
        {
            if (path is null)
            {
                throw new ArgumentNullException(nameof(path));
            }
 
            return Core();
 
            async Task<BinaryData> Core()
            {
#if NET
                return new BinaryData(
                    await File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false),
                    mediaType);
#else
                using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 1, useAsync: true);
                return await FromStreamAsync(fileStream, mediaType, cancellationToken).ConfigureAwait(false);
#endif
            }
        }
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by serializing the provided object using
        /// the <see cref="JsonSerializer"/>
        /// and sets <see cref="MediaType"/> to "application/json".
        /// </summary>
        /// <typeparam name="T">The type to use when serializing the data.</typeparam>
        /// <param name="jsonSerializable">The data to use.</param>
        /// <param name="options">The options to use when serializing to JSON.</param>
        /// <returns>A value representing the UTF-8 encoding of the JSON representation of <paramref name="jsonSerializable" />.</returns>
        /// <seealso cref="MediaTypeNames"/>
        [RequiresDynamicCode(JsonSerializerRequiresDynamicCode)]
        [RequiresUnreferencedCode(JsonSerializerRequiresUnreferencedCode)]
        public static BinaryData FromObjectAsJson<T>(T jsonSerializable, JsonSerializerOptions? options = default)
            => new BinaryData(JsonSerializer.SerializeToUtf8Bytes(jsonSerializable, options), MediaTypeApplicationJson);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by serializing the provided object using
        /// the <see cref="JsonSerializer"/>
        /// and sets <see cref="MediaType"/> to "application/json".
        /// </summary>
        /// <typeparam name="T">The type to use when serializing the data.</typeparam>
        /// <param name="jsonSerializable">The data to use.</param>
        /// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo"/> to use when serializing to JSON.</param>
        /// <returns>A value representing the UTF-8 encoding of the JSON representation of <paramref name="jsonSerializable" />.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public static BinaryData FromObjectAsJson<T>(T jsonSerializable, JsonTypeInfo<T> jsonTypeInfo)
            => new BinaryData(JsonSerializer.SerializeToUtf8Bytes(jsonSerializable, jsonTypeInfo), MediaTypeApplicationJson);
 
        /// <summary>
        /// Creates a <see cref="BinaryData"/> instance by wrapping the same data
        /// and changed <see cref="MediaType"/> to <see pref="mediaType"/> value.
        /// </summary>
        /// <returns>A wrapper over the same data with specified <see cref="MediaType"/>.</returns>
        /// <seealso cref="MediaTypeNames"/>
        public BinaryData WithMediaType(string? mediaType)
            => new BinaryData(_bytes, mediaType);
 
        /// <summary>
        /// Converts the value of this instance to a string using UTF-8.
        /// </summary>
        /// <remarks>
        /// No special treatment is given to the contents of the data, it is merely decoded as a UTF-8 string.
        /// For a JPEG or other binary file format the string will largely be nonsense with many embedded NUL characters,
        /// and UTF-8 JSON values will look like their file/network representation,
        /// including starting and stopping quotes on a string.
        /// </remarks>
        /// <returns>
        /// A string from the value of this instance, using UTF-8 to decode the bytes.
        /// </returns>
        /// <seealso cref="ToObjectFromJson{String}(JsonSerializerOptions)" />
        public override unsafe string ToString()
        {
            ReadOnlySpan<byte> span = _bytes.Span;
 
            if (span.IsEmpty)
            {
                return string.Empty;
            }
 
            fixed (byte* ptr = span)
            {
                return Encoding.UTF8.GetString(ptr, span.Length);
            }
        }
 
        /// <summary>
        /// Converts the <see cref="BinaryData"/> to a read-only stream.
        /// </summary>
        /// <returns>A stream representing the data.</returns>
        public Stream ToStream() => new ReadOnlyMemoryStream(_bytes);
 
        /// <summary>
        /// Gets the value of this instance as bytes without any further interpretation.
        /// </summary>
        /// <returns>The value of this instance as bytes without any further interpretation.</returns>
        public ReadOnlyMemory<byte> ToMemory() => _bytes;
 
        /// <summary>
        /// Converts the <see cref="BinaryData"/> to a byte array.
        /// </summary>
        /// <returns>A byte array representing the data.</returns>
        public byte[] ToArray() => _bytes.ToArray();
 
        /// <summary>
        /// Converts the <see cref="BinaryData"/> to the specified type using
        /// <see cref="JsonSerializer"/>.
        /// </summary>
        /// <typeparam name="T">The type that the data should be
        /// converted to.</typeparam>
        /// <param name="options">The <see cref="JsonSerializerOptions"/> to use when serializing to JSON.</param>
        /// <returns>The data converted to the specified type.</returns>
        [RequiresDynamicCode(JsonSerializerRequiresDynamicCode)]
        [RequiresUnreferencedCode(JsonSerializerRequiresUnreferencedCode)]
        public T? ToObjectFromJson<T>(JsonSerializerOptions? options = default)
            => JsonSerializer.Deserialize<T>(GetBytesWithTrimmedBom(), options);
 
        /// <summary>
        /// Converts the <see cref="BinaryData"/> to the specified type using
        /// <see cref="JsonSerializer"/>.
        /// </summary>
        /// <typeparam name="T">The type that the data should be
        /// converted to.</typeparam>
        /// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo"/> to use when serializing to JSON.</param>
        /// <returns>The data converted to the specified type.</returns>
        public T? ToObjectFromJson<T>(JsonTypeInfo<T> jsonTypeInfo)
            => JsonSerializer.Deserialize(GetBytesWithTrimmedBom(), jsonTypeInfo);
 
        private ReadOnlySpan<byte> GetBytesWithTrimmedBom()
        {
            ReadOnlySpan<byte> span = _bytes.Span;
 
            // Check for the UTF-8 byte order mark (BOM) EF BB BF
            if (span.Length > 2 && span[0] == 0xEF && span[1] == 0xBB && span[2] == 0xBF)
                span = span.Slice(3);
 
            return span;
        }
 
        /// <summary>
        /// Defines an implicit conversion from a <see cref="BinaryData" /> to a <see cref="ReadOnlyMemory{Byte}"/>.
        /// </summary>
        /// <param name="data">The value to be converted.</param>
        public static implicit operator ReadOnlyMemory<byte>(BinaryData? data) => data?._bytes ?? default;
 
        /// <summary>
        /// Defines an implicit conversion from a <see cref="BinaryData" /> to a <see cref="ReadOnlySpan{Byte}"/>.
        /// </summary>
        /// <param name="data">The value to be converted.</param>
        public static implicit operator ReadOnlySpan<byte>(BinaryData? data)
        {
            if (data == null)
            {
                return default;
            }
            return data._bytes.Span;
        }
 
        /// <summary>
        /// Determines whether the specified object is equal to the current object.
        /// </summary>
        /// <param name="obj">The object to compare with the current object.</param>
        /// <returns>
        /// <see langword="true" /> if the specified object is equal to the current object; otherwise, <see langword="false" />.
        /// </returns>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public override bool Equals([NotNullWhen(true)] object? obj) => ReferenceEquals(this, obj);
 
        /// <inheritdoc />
        [EditorBrowsable(EditorBrowsableState.Never)]
        public override int GetHashCode() => base.GetHashCode();
    }
}