File: HashObjectWriter.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.ProjectModel\NuGet.ProjectModel.csproj (NuGet.ProjectModel)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using NuGet.Packaging;
using NuGet.RuntimeModel;

namespace NuGet.ProjectModel
{
    /// <summary>
    /// Generates a hash from an object graph.
    ///
    /// This is non-private only to facilitate unit testing.
    /// </summary>
    public sealed class HashObjectWriter : IObjectWriter, IDisposable
    {
        private const int DefaultBufferSize = 4096;

        private readonly byte[] _buffer;
        private readonly IHashFunction _hashFunc;
        private bool _isDisposed;
        private bool _isReadOnly;
        private int _nestLevel;
        private readonly CircularMemoryStream _stream;
        private readonly StreamWriter _streamWriter;
        private readonly JsonTextWriter _writer;

        /// <summary>
        /// Creates a new instance with the provide hash function.
        /// </summary>
        /// <param name="hashFunc">An <see cref="IHashFunction"/> instance.  Throws if <see langword="null" />.</param>
        public HashObjectWriter(IHashFunction hashFunc)
        {
            if (hashFunc == null)
            {
                throw new ArgumentNullException(nameof(hashFunc));
            }

            _buffer = new byte[DefaultBufferSize];
            _hashFunc = hashFunc;
            _stream = new CircularMemoryStream(_buffer);
            _streamWriter = new StreamWriter(_stream);
            _writer = new JsonTextWriter(_streamWriter);

            _stream.OnFlush += OnFlush;
        }

        public void Dispose()
        {
            if (!_isDisposed)
            {
                _stream.OnFlush -= OnFlush;

                _hashFunc.Dispose();
                _writer.Close();
                _streamWriter.Dispose();
                _stream.Dispose();

                _isDisposed = true;
            }
        }

        public void WriteObjectStart()
        {
            ThrowIfDisposed();
            ThrowIfReadOnly();

            _writer.WriteStartObject();

            ++_nestLevel;
        }

        public void WriteObjectStart(string name)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            ThrowIfDisposed();
            ThrowIfReadOnly();

            _writer.WritePropertyName(name);
            _writer.WriteStartObject();

            ++_nestLevel;
        }

        public void WriteObjectEnd()
        {
            ThrowIfDisposed();
            ThrowIfReadOnly();

            if (_nestLevel == 0)
            {
                throw new InvalidOperationException();
            }

            _writer.WriteEndObject();

            --_nestLevel;
        }

        public void WriteNameValue(string name, int value)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            ThrowIfDisposed();
            ThrowIfReadOnly();

            _writer.WritePropertyName(name);
            _writer.WriteValue(value);
        }

        public void WriteNameValue(string name, bool value)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            ThrowIfDisposed();
            ThrowIfReadOnly();

            _writer.WritePropertyName(name);
            _writer.WriteValue(value);
        }

        public void WriteNameValue(string name, string? value)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            ThrowIfDisposed();
            ThrowIfReadOnly();

            _writer.WritePropertyName(name);
            _writer.WriteValue(value);
        }

        public void WriteNameArray(string name, IEnumerable<string> values)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            if (values == null)
            {
                throw new ArgumentNullException(nameof(values));
            }

            ThrowIfDisposed();
            ThrowIfReadOnly();

            _writer.WritePropertyName(name);
            _writer.WriteStartArray();

            foreach (string value in values)
            {
                _writer.WriteValue(value);
            }

            _writer.WriteEndArray();
        }

        public void WriteNonEmptyNameArray(string name, IEnumerable<string> values)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            if (values == null)
            {
                throw new ArgumentNullException(nameof(values));
            }

            ThrowIfDisposed();
            ThrowIfReadOnly();

            // Manually enumerate the IEnumerable so we only write the name
            // when there are corresponding values and avoid potentially expensive
            // multiple enumeration.
            var enumerator = values.NoAllocEnumerate().GetEnumerator();
            if (!enumerator.MoveNext())
            {
                return;
            }

            _writer.WritePropertyName(name);
            _writer.WriteStartArray();
            _writer.WriteValue(enumerator.Current);
            while (enumerator.MoveNext())
            {
                _writer.WriteValue(enumerator.Current);
            }

            _writer.WriteEndArray();
        }

        /// <summary>
        /// Gets the hash for the object.
        ///
        /// Once GetHash is called, no further writing is allowed.
        /// </summary>
        /// <returns>A hash of the object.</returns>
        public string GetHash()
        {
            ThrowIfDisposed();

            if (!_isReadOnly)
            {
                _writer.Flush();

                _isReadOnly = true;
            }

            return _hashFunc.GetHash();
        }

        public void WriteArrayStart(string name)
        {
            if (name == null)
            {
                throw new ArgumentNullException(nameof(name));
            }

            ThrowIfDisposed();
            ThrowIfReadOnly();

            _writer.WritePropertyName(name);
            _writer.WriteStartArray();

            ++_nestLevel;
        }

        public void WriteArrayEnd()
        {
            ThrowIfDisposed();
            ThrowIfReadOnly();

            if (_nestLevel == 0)
            {
                throw new InvalidOperationException();
            }

            _writer.WriteEndArray();

            --_nestLevel;
        }

        private void OnFlush(object? sender, ArraySegment<byte> bytes)
        {
            if (bytes.Count > 0)
            {
                _hashFunc.Update(bytes.Array!, bytes.Offset, bytes.Count);
            }
        }

        private void ThrowIfReadOnly()
        {
            if (_isReadOnly)
            {
                throw new InvalidOperationException();
            }
        }

        private void ThrowIfDisposed()
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException(nameof(HashObjectWriter));
            }
        }
    }
}