File: PooledCacheRender.cs
Web Access
Project: Microsoft.NET.ProjectData.Tasks.csproj (Microsoft.NET.ProjectData.Tasks)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Buffers;
using System.Text;

namespace Microsoft.NET.ProjectData.Tasks;

/// <summary>
/// A <see cref="TextWriter"/> that renders cache content into pooled <see cref="char"/> and
/// <see cref="byte"/> buffers, normalizing line endings in place, so the writer never allocates
/// the candidate as a <see cref="string"/> or a heap <see cref="byte"/> array on the common
/// path. The candidate string is produced lazily via <see cref="GetText"/> only when a newer
/// minor version's data has to be spliced in. Always use with <c>using</c> so both pooled
/// buffers are returned.
/// </summary>
internal sealed class PooledCacheRender : TextWriter
{
	private static readonly char[] NewLineChars = { '\n' };

	private char[] _chars;
	private int _charLength;
	private byte[]? _bytes;
	private int _byteLength;

	private PooledCacheRender(int initialCapacity)
	{
		this._chars = ArrayPool<char>.Shared.Rent(initialCapacity);
		this.CoreNewLine = NewLineChars;
	}

	public override Encoding Encoding => ProjectDataWriter.Utf8NoBom;

	public byte[] Bytes => this._bytes!;

	public int ByteLength => this._byteLength;

	public static PooledCacheRender Create(Action<TextWriter> writeContent)
	{
		var render = new PooledCacheRender(initialCapacity: 4096);
		try
		{
			writeContent(render);
			render.Finish();
			return render;
		}
		catch
		{
			render.Dispose();
			throw;
		}
	}

	/// <summary>Materializes the rendered content as a string (rare splice path only).</summary>
	public string GetText() => new string(this._chars, 0, this._charLength);

	public override void Write(char value)
	{
		this.EnsureCapacity(this._charLength + 1);
		this._chars[this._charLength++] = value;
	}

	public override void Write(string? value)
	{
		if (string.IsNullOrEmpty(value))
			return;

		this.EnsureCapacity(this._charLength + value!.Length);
		value.CopyTo(0, this._chars, this._charLength, value.Length);
		this._charLength += value.Length;
	}

	public override void Write(char[] buffer, int index, int count)
	{
		if (count <= 0)
			return;

		this.EnsureCapacity(this._charLength + count);
		Array.Copy(buffer, index, this._chars, this._charLength, count);
		this._charLength += count;
	}

	/// <summary>
	/// Normalizes line endings in <paramref name="buffer"/> in place (<c>\r\n</c> and lone
	/// <c>\r</c> both collapse to <c>\n</c>), returning the new logical length. Mirrors
	/// <c>ProjectDataWriter.NormalizeLineEndings(string)</c> exactly but without allocating.
	/// </summary>
	private static int NormalizeNewLinesInPlace(char[] buffer, int length)
	{
		int firstCr = Array.IndexOf(buffer, '\r', 0, length);
		if (firstCr < 0)
			return length;

		int write = firstCr;
		for (int read = firstCr; read < length; read++)
		{
			char c = buffer[read];
			if (c == '\r')
			{
				buffer[write++] = '\n';
				if (read + 1 < length && buffer[read + 1] == '\n')
					read++; // collapse the "\n" half of a "\r\n" pair
			}
			else
			{
				buffer[write++] = c;
			}
		}

		return write;
	}

	private void Finish()
	{
		this._charLength = NormalizeNewLinesInPlace(this._chars, this._charLength);
		int byteCount = ProjectDataWriter.Utf8NoBom.GetByteCount(this._chars, 0, this._charLength);
		this._bytes = ArrayPool<byte>.Shared.Rent(byteCount);
		this._byteLength = ProjectDataWriter.Utf8NoBom.GetBytes(this._chars, 0, this._charLength, this._bytes, 0);
	}

	private void EnsureCapacity(int required)
	{
		if (required <= this._chars.Length)
			return;

		int newSize = Math.Max(required, this._chars.Length * 2);
		char[] larger = ArrayPool<char>.Shared.Rent(newSize);
		Array.Copy(this._chars, 0, larger, 0, this._charLength);
		ArrayPool<char>.Shared.Return(this._chars);
		this._chars = larger;
	}

	protected override void Dispose(bool disposing)
	{
		if (disposing)
		{
			ArrayPool<char>.Shared.Return(this._chars);
			if (this._bytes != null)
				ArrayPool<byte>.Shared.Return(this._bytes);
		}

		base.Dispose(disposing);
	}
}