osu/osu.Game/IO/LineBufferedReader.cs
Bartłomiej Dach 7b1ff38df7 Implement line-buffered reader
Add a line-buffered reader decorator operating on StreamReader
instances. The decorator has two main operations - PeekLine(), which
allows to see the next line in the stream without consuming it,
ReadLine(), which consumes and returns the next line in the stream, and
ReadToEnd() which reads all the remaining text in the stream (including
the unconsumed peeked line). Peeking line-per-line uses an internal
queue of lines that have been read ahead from the underlying stream.

The addition of the line-buffered reader is a workaround solution to
a problem with decoding. At current selecting a decoder works by
irreversibly reading the first line from the stream and looking for
a magic string that indicates the type of decoder to use.

It might however be possible for a file to be valid in format, just
missing a header. In such a case a lack of a line-buffered reader makes
it impossible to reparse the content of that first line. Introducing it
will however allow to peek the first line for magic first.

 - If magic is found in the first line, GetDecoder() will peek it and
   use it to return the correct Decoder instance. Note that in the case
   of JsonBeatmapDecoder the magic is the opening JSON object brace,
   and therefore must not be consumed.

 - If magic is not found, the fallback decoder will be able to consume
   it using ReadLine() in Decode().

This commit additionally contains basic unit tests for the reader.

Suggested-by: Aergwyn <aergwyn@t-online.de>
2019-09-15 01:26:15 +02:00

71 lines
2.1 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace osu.Game.IO
{
/// <summary>
/// A <see cref="StreamReader"/>-like decorator (with more limited API) for <see cref="Stream"/>s
/// that allows lines to be peeked without consuming.
/// </summary>
public class LineBufferedReader : IDisposable
{
private readonly StreamReader streamReader;
private readonly Queue<string> lineBuffer;
public LineBufferedReader(Stream stream)
{
streamReader = new StreamReader(stream);
lineBuffer = new Queue<string>();
}
/// <summary>
/// Reads the next line from the stream without consuming it.
/// </summary>
public string PeekLine()
{
if (lineBuffer.Count > 0)
return lineBuffer.Peek();
var line = streamReader.ReadLine();
if (line != null)
lineBuffer.Enqueue(line);
return line;
}
/// <summary>
/// Reads the next line from the stream and consumes it.
/// </summary>
public string ReadLine() => lineBuffer.Count > 0 ? lineBuffer.Dequeue() : streamReader.ReadLine();
/// <summary>
/// Reads the stream to its end and returns the text read.
/// This includes any peeked but unconsumed lines.
/// </summary>
public string ReadToEnd()
{
var remainingText = streamReader.ReadToEnd();
if (lineBuffer.Count == 0)
return remainingText;
var builder = new StringBuilder();
// this might not be completely correct due to varying platform line endings
while (lineBuffer.Count > 0)
builder.AppendLine(lineBuffer.Dequeue());
builder.Append(remainingText);
return builder.ToString();
}
public void Dispose()
{
streamReader?.Dispose();
}
}
}