osu/osu.Game/IO/LineBufferedReader.cs
Bartłomiej Dach 86588778b1 Implement fallback decoder registration
After the preparatory introduction of LineBufferedReader, it is now
possible to introduce registration of fallback decoders that won't drop
input supplied in the first line of the file.

A fallback decoder is used when the magic in the first line of the file
does not match any of the other known decoders. In such a case,
the fallback decoder is constructed and provided a LineBufferedReader
instance. The process of matching magic only peeks the first non-empty
line, so it is available for re-reading in Decode() using ReadLine().

There can be only one fallback decoder per type; a second attempt of
registering a fallback will result in an exception to avoid bugs.

To address the issue of parsing failing on badly or non-headered files,
set the legacy decoders for Beatmaps and Storyboards as the fallbacks.

Due to non-trivial logic, several new, passing unit tests with possible
edge cases also included.
2019-09-15 01:28:07 +02:00

73 lines
2.3 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.
/// Subsequent calls to <see cref="PeekLine"/> without a <see cref="ReadLine"/> will return the same string.
/// </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.
/// If a line was peeked, that same line will then be consumed and returned.
/// </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();
}
}
}