don't read file into a string buffer to parse (#9)
* perform utf8 validation manually while parsing * perform manual utf8 validation in async * cleaned up reader * fixed parsing bugs * handle utf-16 * skip whitespace before file format * dont introduce new error variant * removed new error variant * simplify macros
This commit is contained in:
+1
-3
@@ -1,5 +1,3 @@
|
||||
use super::OSU_FILE_HEADER;
|
||||
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
@@ -41,7 +39,7 @@ impl fmt::Display for ParseError {
|
||||
match self {
|
||||
Self::IOError(_) => f.write_str("IO error"),
|
||||
Self::IncorrectFileHeader => {
|
||||
write!(f, "expected `{}` at file begin", OSU_FILE_HEADER)
|
||||
write!(f, "expected `osu file format v` at file begin")
|
||||
}
|
||||
Self::BadLine => f.write_str("line not in `Key:Value` pattern"),
|
||||
Self::InvalidCurvePoints => f.write_str("invalid curve point"),
|
||||
|
||||
+103
-185
@@ -4,6 +4,7 @@ mod error;
|
||||
mod hitobject;
|
||||
mod hitsound;
|
||||
mod pos2;
|
||||
mod reader;
|
||||
mod sort;
|
||||
|
||||
pub use attributes::BeatmapAttributes;
|
||||
@@ -12,33 +13,24 @@ pub use error::{ParseError, ParseResult};
|
||||
pub use hitobject::{HitObject, HitObjectKind};
|
||||
pub use hitsound::HitSound;
|
||||
pub use pos2::Pos2;
|
||||
pub use slider_parsing::*;
|
||||
|
||||
use reader::FileReader;
|
||||
use sort::legacy_sort;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Read},
|
||||
};
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
#[cfg(feature = "async_tokio")]
|
||||
use tokio::{
|
||||
fs::File,
|
||||
io::{AsyncBufReadExt, AsyncRead, BufReader},
|
||||
};
|
||||
use tokio::{fs::File, io::AsyncRead};
|
||||
|
||||
#[cfg(not(feature = "async_std"))]
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(feature = "async_std")]
|
||||
use async_std::{
|
||||
fs::File,
|
||||
io::{prelude::BufReadExt, BufReader as AsyncBufReader, Read as AsyncRead},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
pub use slider_parsing::*;
|
||||
use async_std::{fs::File, io::Read as AsyncRead, path::Path};
|
||||
|
||||
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
||||
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
||||
@@ -66,70 +58,50 @@ impl FloatExt for f64 {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! line_prepare {
|
||||
($buf:ident) => {{
|
||||
let mut line = $buf.trim_end();
|
||||
|
||||
if skip_line(line) {
|
||||
$buf.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(idx) = line.find("//") {
|
||||
line = &line[..idx];
|
||||
}
|
||||
|
||||
line
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! section {
|
||||
($map:ident, $func:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
||||
($map:ident, $func:ident, $reader:ident, $section:ident) => {{
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
if $map.$func(&mut $reader, &mut $buf, &mut $section)? {
|
||||
if $map.$func(&mut $reader, &mut $section)? {
|
||||
break;
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async_std", feature = "async_tokio"))]
|
||||
if $map.$func(&mut $reader, &mut $buf, &mut $section).await? {
|
||||
if $map.$func(&mut $reader, &mut $section).await? {
|
||||
break;
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! read_line {
|
||||
($reader:ident, $buf:expr) => {{
|
||||
macro_rules! next_line {
|
||||
($reader:ident) => {{
|
||||
#[cfg(any(feature = "async_std", feature = "async_tokio"))]
|
||||
{
|
||||
$reader.read_line($buf).await
|
||||
$reader.next_line().await
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
{
|
||||
$reader.read_line($buf)
|
||||
$reader.next_line()
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! parse_general_body {
|
||||
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
||||
($self:ident, $reader:ident, $section:ident) => {{
|
||||
let mut mode = None;
|
||||
let mut empty = true;
|
||||
let mut stack_leniency = None;
|
||||
|
||||
while read_line!($reader, $buf)? != 0 {
|
||||
let line = line_prepare!($buf);
|
||||
|
||||
if line.starts_with('[') && line.ends_with(']') {
|
||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
||||
while next_line!($reader)? != 0 {
|
||||
if let Some(bytes) = $reader.get_section() {
|
||||
*$section = Section::from_bytes(bytes);
|
||||
empty = false;
|
||||
$buf.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
|
||||
let (key, value) = $reader.split_colon().ok_or(ParseError::BadLine)?;
|
||||
|
||||
if key == "Mode" {
|
||||
if key == b"Mode" {
|
||||
mode = match value {
|
||||
"0" => Some(GameMode::STD),
|
||||
"1" => Some(GameMode::TKO),
|
||||
@@ -139,11 +111,9 @@ macro_rules! parse_general_body {
|
||||
};
|
||||
}
|
||||
|
||||
if key == "StackLeniency" {
|
||||
if key == b"StackLeniency" {
|
||||
stack_leniency = Some(value.parse()?);
|
||||
}
|
||||
|
||||
$buf.clear();
|
||||
}
|
||||
|
||||
$self.mode = mode.unwrap_or(GameMode::STD);
|
||||
@@ -157,28 +127,26 @@ macro_rules! parse_general {
|
||||
() => {
|
||||
fn parse_general<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut BufReader<R>,
|
||||
buf: &mut String,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_general_body!(self, reader, buf, section)
|
||||
parse_general_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
|
||||
(async $reader:ident<$inner:ident>) => {
|
||||
async fn parse_general<R: $inner + Unpin>(
|
||||
(async) => {
|
||||
async fn parse_general<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut $reader<R>,
|
||||
buf: &mut String,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_general_body!(self, reader, buf, section)
|
||||
parse_general_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_difficulty_body {
|
||||
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
||||
($self:ident, $reader:ident, $section:ident) => {{
|
||||
let mut ar = None;
|
||||
let mut od = None;
|
||||
let mut cs = None;
|
||||
@@ -188,29 +156,24 @@ macro_rules! parse_difficulty_body {
|
||||
|
||||
let mut empty = true;
|
||||
|
||||
while read_line!($reader, $buf)? != 0 {
|
||||
let line = line_prepare!($buf);
|
||||
|
||||
if line.starts_with('[') && line.ends_with(']') {
|
||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
||||
while next_line!($reader)? != 0 {
|
||||
if let Some(bytes) = $reader.get_section() {
|
||||
*$section = Section::from_bytes(bytes);
|
||||
empty = false;
|
||||
$buf.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
|
||||
let (key, value) = $reader.split_colon().ok_or(ParseError::BadLine)?;
|
||||
|
||||
match key {
|
||||
"ApproachRate" => ar = Some(value.parse()?),
|
||||
"OverallDifficulty" => od = Some(value.parse()?),
|
||||
"CircleSize" => cs = Some(value.parse()?),
|
||||
"HPDrainRate" => hp = Some(value.parse()?),
|
||||
"SliderTickRate" => tick_rate = Some(value.parse()?),
|
||||
"SliderMultiplier" => sv = Some(value.parse()?),
|
||||
b"ApproachRate" => ar = Some(value.parse()?),
|
||||
b"OverallDifficulty" => od = Some(value.parse()?),
|
||||
b"CircleSize" => cs = Some(value.parse()?),
|
||||
b"HPDrainRate" => hp = Some(value.parse()?),
|
||||
b"SliderTickRate" => tick_rate = Some(value.parse()?),
|
||||
b"SliderMultiplier" => sv = Some(value.parse()?),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
$buf.clear();
|
||||
}
|
||||
|
||||
const DEFAULT_DIFFICULTY: f32 = 5.0;
|
||||
@@ -230,28 +193,26 @@ macro_rules! parse_difficulty {
|
||||
() => {
|
||||
fn parse_difficulty<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut BufReader<R>,
|
||||
buf: &mut String,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_difficulty_body!(self, reader, buf, section)
|
||||
parse_difficulty_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
|
||||
(async $reader:ident<$inner:ident>) => {
|
||||
async fn parse_difficulty<R: $inner + Unpin>(
|
||||
(async) => {
|
||||
async fn parse_difficulty<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut $reader<R>,
|
||||
buf: &mut String,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_difficulty_body!(self, reader, buf, section)
|
||||
parse_difficulty_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_timingpoints_body {
|
||||
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
||||
($self:ident, $reader:ident, $section:ident) => {{
|
||||
let mut unsorted_timings = false;
|
||||
let mut unsorted_difficulties = false;
|
||||
|
||||
@@ -260,16 +221,14 @@ macro_rules! parse_timingpoints_body {
|
||||
|
||||
let mut empty = true;
|
||||
|
||||
while read_line!($reader, $buf)? != 0 {
|
||||
let line = line_prepare!($buf);
|
||||
|
||||
if line.starts_with('[') && line.ends_with(']') {
|
||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
||||
while next_line!($reader)? != 0 {
|
||||
if let Some(bytes) = $reader.get_section() {
|
||||
*$section = Section::from_bytes(bytes);
|
||||
empty = false;
|
||||
$buf.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
let line = $reader.get_line()?;
|
||||
let mut split = line.split(',');
|
||||
|
||||
let time = split
|
||||
@@ -311,8 +270,6 @@ macro_rules! parse_timingpoints_body {
|
||||
prev_diff = time;
|
||||
}
|
||||
}
|
||||
|
||||
$buf.clear();
|
||||
}
|
||||
|
||||
if unsorted_timings {
|
||||
@@ -331,28 +288,26 @@ macro_rules! parse_timingpoints {
|
||||
() => {
|
||||
fn parse_timingpoints<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut BufReader<R>,
|
||||
buf: &mut String,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_timingpoints_body!(self, reader, buf, section)
|
||||
parse_timingpoints_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
|
||||
(async $reader:ident<$inner:ident>) => {
|
||||
async fn parse_timingpoints<R: $inner + Unpin>(
|
||||
(async) => {
|
||||
async fn parse_timingpoints<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut $reader<R>,
|
||||
buf: &mut String,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_timingpoints_body!(self, reader, buf, section)
|
||||
parse_timingpoints_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_hitobjects_body {
|
||||
($self:ident, $reader:ident, $buf:ident, $section:ident) => {{
|
||||
($self:ident, $reader:ident, $section:ident) => {{
|
||||
let mut unsorted = false;
|
||||
let mut prev_time = 0.0;
|
||||
let mut empty = true;
|
||||
@@ -366,16 +321,14 @@ macro_rules! parse_hitobjects_body {
|
||||
// Buffer to re-use for all sliders
|
||||
let mut vertices = Vec::new();
|
||||
|
||||
while read_line!($reader, $buf)? != 0 {
|
||||
let line = line_prepare!($buf);
|
||||
|
||||
if line.starts_with('[') && line.ends_with(']') {
|
||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
||||
while next_line!($reader)? != 0 {
|
||||
if let Some(bytes) = $reader.get_section() {
|
||||
*$section = Section::from_bytes(bytes);
|
||||
empty = false;
|
||||
$buf.clear();
|
||||
break;
|
||||
}
|
||||
|
||||
let line = $reader.get_line()?;
|
||||
let mut split = line.split(',');
|
||||
|
||||
let pos = Pos2 {
|
||||
@@ -511,7 +464,6 @@ macro_rules! parse_hitobjects_body {
|
||||
$self.sounds.push(sound);
|
||||
|
||||
prev_time = time;
|
||||
$buf.clear();
|
||||
}
|
||||
|
||||
// BUG: If [General] section comes after [HitObjects] then the mode
|
||||
@@ -536,52 +488,35 @@ macro_rules! parse_hitobjects {
|
||||
() => {
|
||||
fn parse_hitobjects<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut BufReader<R>,
|
||||
buf: &mut String,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_hitobjects_body!(self, reader, buf, section)
|
||||
parse_hitobjects_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
|
||||
(async $reader:ident<$inner:ident>) => {
|
||||
async fn parse_hitobjects<R: $inner + Unpin>(
|
||||
(async) => {
|
||||
async fn parse_hitobjects<R: AsyncRead + Unpin>(
|
||||
&mut self,
|
||||
reader: &mut $reader<R>,
|
||||
buf: &mut String,
|
||||
reader: &mut FileReader<R>,
|
||||
section: &mut Section,
|
||||
) -> ParseResult<bool> {
|
||||
parse_hitobjects_body!(self, reader, buf, section)
|
||||
parse_hitobjects_body!(self, reader, section)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_body {
|
||||
($reader:ident<$inner:ident>: $input:ident) => {{
|
||||
let mut reader = $reader::new($input);
|
||||
let mut buf = String::new();
|
||||
($input:ident) => {{
|
||||
let mut reader = FileReader::new($input);
|
||||
next_line!(reader)?;
|
||||
|
||||
while read_line!(reader, &mut buf)? != 0 {
|
||||
// Check for character U+FEFF specifically thanks to map id 797130
|
||||
if !buf
|
||||
.trim_matches(|c: char| c.is_whitespace() || c == '')
|
||||
.is_empty()
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
if reader.is_initial_empty_line() {
|
||||
next_line!(reader)?;
|
||||
}
|
||||
|
||||
let version = match buf.find(OSU_FILE_HEADER) {
|
||||
Some(idx) => buf[idx + OSU_FILE_HEADER.len()..].trim_end().parse()?,
|
||||
None => return Err(ParseError::IncorrectFileHeader),
|
||||
};
|
||||
|
||||
buf.clear();
|
||||
|
||||
let mut map = Beatmap {
|
||||
version,
|
||||
version: reader.version()?,
|
||||
hit_objects: Vec::with_capacity(256),
|
||||
sounds: Vec::with_capacity(256),
|
||||
..Default::default()
|
||||
@@ -591,22 +526,18 @@ macro_rules! parse_body {
|
||||
|
||||
loop {
|
||||
match section {
|
||||
Section::General => section!(map, parse_general, reader, buf, section),
|
||||
Section::Difficulty => section!(map, parse_difficulty, reader, buf, section),
|
||||
Section::TimingPoints => section!(map, parse_timingpoints, reader, buf, section),
|
||||
Section::HitObjects => section!(map, parse_hitobjects, reader, buf, section),
|
||||
Section::General => section!(map, parse_general, reader, section),
|
||||
Section::Difficulty => section!(map, parse_difficulty, reader, section),
|
||||
Section::TimingPoints => section!(map, parse_timingpoints, reader, section),
|
||||
Section::HitObjects => section!(map, parse_hitobjects, reader, section),
|
||||
Section::None => {
|
||||
if read_line!(reader, &mut buf)? == 0 {
|
||||
if next_line!(reader)? == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let line = line_prepare!(buf);
|
||||
|
||||
if line.starts_with('[') && line.ends_with(']') {
|
||||
section = Section::from_str(&line[1..line.len() - 1]);
|
||||
if let Some(bytes) = reader.get_section() {
|
||||
section = Section::from_bytes(bytes);
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -623,19 +554,19 @@ macro_rules! parse {
|
||||
/// You'll likely want to pass (a reference of) a [`File`](std::fs::File)
|
||||
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
|
||||
parse_body!(BufReader<Read>: input)
|
||||
parse_body!(input)
|
||||
}
|
||||
};
|
||||
|
||||
(async $reader:ident<$inner:ident>) => {
|
||||
(async) => {
|
||||
/// Parse a beatmap from a `.osu` file.
|
||||
///
|
||||
/// As argument you can give anything that implements `tokio::io::AsyncRead`
|
||||
/// or `async_std::io::Read`, depending which feature you chose.
|
||||
/// You'll likely want to pass a `File`
|
||||
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||
pub async fn parse<R: $inner + Unpin>(input: R) -> ParseResult<Self> {
|
||||
parse_body!($reader<$inner>: input)
|
||||
pub async fn parse<R: AsyncRead + Unpin>(input: R) -> ParseResult<Self> {
|
||||
parse_body!(input)
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -652,11 +583,11 @@ macro_rules! from_path {
|
||||
}
|
||||
};
|
||||
|
||||
(async $path:ident) => {
|
||||
(async) => {
|
||||
/// Pass the path to a `.osu` file.
|
||||
///
|
||||
/// Useful when you don't want to create the file manually.
|
||||
pub async fn from_path<P: AsRef<$path>>(path: P) -> ParseResult<Self> {
|
||||
pub async fn from_path<P: AsRef<Path>>(path: P) -> ParseResult<Self> {
|
||||
Self::parse(File::open(path).await?).await
|
||||
}
|
||||
};
|
||||
@@ -728,8 +659,6 @@ pub struct Beatmap {
|
||||
pub stack_leniency: f32,
|
||||
}
|
||||
|
||||
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
||||
|
||||
impl Beatmap {
|
||||
const CIRCLE_FLAG: u8 = 1 << 0;
|
||||
const SLIDER_FLAG: u8 = 1 << 1;
|
||||
@@ -916,34 +845,24 @@ impl Beatmap {
|
||||
|
||||
#[cfg(feature = "async_tokio")]
|
||||
impl Beatmap {
|
||||
parse!(async BufReader<AsyncRead>);
|
||||
parse_general!(async BufReader<AsyncRead>);
|
||||
parse_difficulty!(async BufReader<AsyncRead>);
|
||||
parse_timingpoints!(async BufReader<AsyncRead>);
|
||||
parse_hitobjects!(async BufReader<AsyncRead>);
|
||||
parse!(async);
|
||||
parse_general!(async);
|
||||
parse_difficulty!(async);
|
||||
parse_timingpoints!(async);
|
||||
parse_hitobjects!(async);
|
||||
|
||||
from_path!(async Path);
|
||||
from_path!(async);
|
||||
}
|
||||
|
||||
#[cfg(feature = "async_std")]
|
||||
impl Beatmap {
|
||||
parse!(async AsyncBufReader<AsyncRead>);
|
||||
parse_general!(async AsyncBufReader<AsyncRead>);
|
||||
parse_difficulty!(async AsyncBufReader<AsyncRead>);
|
||||
parse_timingpoints!(async AsyncBufReader<AsyncRead>);
|
||||
parse_hitobjects!(async AsyncBufReader<AsyncRead>);
|
||||
parse!(async);
|
||||
parse_general!(async);
|
||||
parse_difficulty!(async);
|
||||
parse_timingpoints!(async);
|
||||
parse_hitobjects!(async);
|
||||
|
||||
from_path!(async Path);
|
||||
}
|
||||
|
||||
fn skip_line(line: &str) -> bool {
|
||||
line.is_empty() || line.starts_with("//") || line.starts_with(' ') || line.starts_with('_')
|
||||
}
|
||||
|
||||
fn split_colon(line: &str) -> Option<(&str, &str)> {
|
||||
let (front, back) = line.split_once(':')?;
|
||||
|
||||
Some((front, back.trim()))
|
||||
from_path!(async);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -956,13 +875,12 @@ enum Section {
|
||||
}
|
||||
|
||||
impl Section {
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> Self {
|
||||
match s {
|
||||
"General" => Self::General,
|
||||
"Difficulty" => Self::Difficulty,
|
||||
"TimingPoints" => Self::TimingPoints,
|
||||
"HitObjects" => Self::HitObjects,
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
match bytes {
|
||||
b"General" => Self::General,
|
||||
b"Difficulty" => Self::Difficulty,
|
||||
b"TimingPoints" => Self::TimingPoints,
|
||||
b"HitObjects" => Self::HitObjects,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
use std::io::{Error as IoError, ErrorKind as IoErrorKind};
|
||||
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
use std::io::{BufRead, BufReader, Read};
|
||||
|
||||
#[cfg(feature = "async_tokio")]
|
||||
use tokio::io::{AsyncBufReadExt, AsyncRead as Read, BufReader};
|
||||
|
||||
#[cfg(feature = "async_std")]
|
||||
use async_std::io::{prelude::BufReadExt, BufReader, Read};
|
||||
|
||||
use crate::ParseError;
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum Encoding {
|
||||
Utf8,
|
||||
Utf16,
|
||||
}
|
||||
|
||||
pub(crate) struct FileReader<R> {
|
||||
buf: Vec<u8>,
|
||||
encoding: Encoding,
|
||||
|
||||
#[cfg(feature = "async_std")]
|
||||
inner: BufReader<R>,
|
||||
#[cfg(feature = "async_tokio")]
|
||||
inner: BufReader<R>,
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
inner: BufReader<R>,
|
||||
}
|
||||
|
||||
macro_rules! read_until {
|
||||
($self:expr) => {{
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
{
|
||||
$self.inner.read_until(b'\n', &mut $self.buf)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async_std", feature = "async_tokio"))]
|
||||
{
|
||||
$self.inner.read_until(b'\n', &mut $self.buf).await
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! impl_reader {
|
||||
() => {
|
||||
impl<R: Read> FileReader<R> {
|
||||
impl_reader!(@NEW);
|
||||
impl_reader!(@NEXT_LINE);
|
||||
}
|
||||
};
|
||||
(async) => {
|
||||
impl<R: Read + Unpin> FileReader<R> {
|
||||
impl_reader!(@NEW);
|
||||
impl_reader!(@ASYNC NEXT_LINE);
|
||||
}
|
||||
};
|
||||
(@NEW) => {
|
||||
pub(crate) fn new(src: R) -> Self {
|
||||
Self {
|
||||
buf: Vec::with_capacity(32),
|
||||
encoding: Encoding::Utf8,
|
||||
inner: BufReader::new(src),
|
||||
}
|
||||
}
|
||||
};
|
||||
(@NEXT_LINE) => {
|
||||
pub(crate) fn next_line(&mut self) -> Result<usize, IoError> {
|
||||
impl_reader!(@NEXT_LINE_BODY, self);
|
||||
}
|
||||
};
|
||||
(@ASYNC NEXT_LINE) => {
|
||||
pub(crate) async fn next_line(&mut self) -> Result<usize, IoError> {
|
||||
impl_reader!(@NEXT_LINE_BODY, self);
|
||||
}
|
||||
};
|
||||
(@NEXT_LINE_BODY, $self:ident) => {
|
||||
loop {
|
||||
$self.buf.clear();
|
||||
let bytes = read_until!($self)?;
|
||||
|
||||
if bytes == 0 {
|
||||
return Ok(bytes);
|
||||
}
|
||||
|
||||
$self.truncate();
|
||||
|
||||
if !$self.buf.is_empty() {
|
||||
return Ok(bytes);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||
impl_reader!();
|
||||
|
||||
#[cfg(feature = "async_tokio")]
|
||||
impl_reader!(async);
|
||||
|
||||
#[cfg(feature = "async_std")]
|
||||
impl_reader!(async);
|
||||
|
||||
impl<R> FileReader<R> {
|
||||
pub(crate) fn is_initial_empty_line(&mut self) -> bool {
|
||||
if self.buf.starts_with(&[239, 187, 191]) {
|
||||
// UTF-8
|
||||
self.buf.rotate_left(3);
|
||||
|
||||
self.buf.len() == 3
|
||||
} else {
|
||||
if self.buf.starts_with(&[255, 254]) {
|
||||
// UTF-16
|
||||
self.encoding = Encoding::Utf16;
|
||||
// will rotate by 1 so this truncates `255`
|
||||
let mut sub = 1;
|
||||
|
||||
if self.buf.len() >= 3 {
|
||||
// truncate one `0` that was left after truncating `\n` when reading the line.
|
||||
// additionally truncate `0\r` if possible.
|
||||
sub += 1 + 2
|
||||
* (self.buf.len() >= 4 && self.buf[self.buf.len() - 2] == b'\r') as usize;
|
||||
}
|
||||
|
||||
self.buf.rotate_left(1);
|
||||
self.buf.truncate(self.buf.len() - sub);
|
||||
self.decode_utf16();
|
||||
}
|
||||
|
||||
self.buf.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn version(&self) -> Result<u8, ParseError> {
|
||||
self.buf
|
||||
.iter()
|
||||
.position(|&byte| byte == b'o')
|
||||
.and_then(|idx| {
|
||||
self.buf[idx..]
|
||||
.starts_with(b"osu file format v")
|
||||
.then(|| idx + 17)
|
||||
})
|
||||
.map(|idx| {
|
||||
let mut n = 0;
|
||||
|
||||
for byte in &self.buf[idx..] {
|
||||
if !(b'0'..=b'9').contains(byte) {
|
||||
break;
|
||||
}
|
||||
|
||||
n = 10 * n + (*byte & 0xF);
|
||||
}
|
||||
|
||||
n
|
||||
})
|
||||
.ok_or(ParseError::IncorrectFileHeader)
|
||||
}
|
||||
|
||||
/// Returns the bytes inbetween '[' and ']'.
|
||||
pub(crate) fn get_section(&self) -> Option<&[u8]> {
|
||||
if self.buf[0] == b'[' {
|
||||
if let Some(end) = self.buf[1..].iter().position(|&byte| byte == b']') {
|
||||
return Some(&self.buf[1..=end]);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse the buffer into a string, returning `None` if the UTF-8 validation fails.
|
||||
pub(crate) fn get_line(&self) -> Result<&str, ParseError> {
|
||||
std::str::from_utf8(&self.buf)
|
||||
.map_err(|e| ParseError::IOError(IoError::new(IoErrorKind::InvalidData, Box::new(e))))
|
||||
}
|
||||
|
||||
/// Split the buffer at the first ':', then parse the second half into a string.
|
||||
///
|
||||
/// Returns `None` if there is no ':' or if the second half is invalid UTF-8.
|
||||
pub(crate) fn split_colon(&self) -> Option<(&[u8], &str)> {
|
||||
let idx = self.buf.iter().position(|&byte| byte == b':')?;
|
||||
let front = &self.buf[..idx];
|
||||
let back = std::str::from_utf8(&self.buf[idx + 1..]).ok()?;
|
||||
|
||||
Some((front, back.trim_start()))
|
||||
}
|
||||
|
||||
/// Truncate away trailing `\r\n`, `\n`, content after `//` and whitespace
|
||||
fn truncate(&mut self) {
|
||||
if self.encoding == Encoding::Utf16 {
|
||||
self.decode_utf16();
|
||||
}
|
||||
|
||||
// necessary check for the edge case `//\r\n`
|
||||
if self.buf.starts_with(&[b'/', b'/']) {
|
||||
return self.buf.clear();
|
||||
}
|
||||
|
||||
// len without "//" or alternatively len without trailing "(\r)\n"
|
||||
let len = self
|
||||
.buf
|
||||
.windows(3)
|
||||
.rev()
|
||||
.step_by(2)
|
||||
.zip(1..)
|
||||
.find_map(|(window, i)| {
|
||||
if window[1] == b'/' {
|
||||
if window[0] == b'/' {
|
||||
return Some(self.buf.len() - 2 * i - 1);
|
||||
} else if window[2] == b'/' {
|
||||
return Some(self.buf.len() - 2 * i);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
})
|
||||
.unwrap_or_else(|| match &self.buf[..] {
|
||||
[.., b'\r', b'\n'] => self.buf.len() - 2,
|
||||
[.., b'\n'] => self.buf.len() - 1,
|
||||
_ => self.buf.len(),
|
||||
});
|
||||
|
||||
// trim whitespace
|
||||
let len = self.buf[..len]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find_map(|(i, byte)| (!matches!(byte, b' ' | b'\t')).then(|| i + 1))
|
||||
.unwrap_or(0);
|
||||
|
||||
self.buf.truncate(len);
|
||||
}
|
||||
|
||||
/// Assumes the buffer is of the form `[_, a, 0, b, 0, c, ...]` so it removes
|
||||
/// the first element and all the 0's, turning it into `[a, b, c, ...]`.
|
||||
fn decode_utf16(&mut self) {
|
||||
// remove the 0's
|
||||
let limit = self.buf.len() / 2 + 1 + (self.buf.len() % 2);
|
||||
|
||||
for i in 2..limit {
|
||||
self.buf.swap(i, i * 2 - 1);
|
||||
}
|
||||
|
||||
self.buf.truncate(limit);
|
||||
|
||||
// remove the first element
|
||||
// panics if buffer is empty
|
||||
self.buf.rotate_left(1);
|
||||
self.buf.truncate(self.buf.len() - 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user