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::{
|
use std::{
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
fmt,
|
fmt,
|
||||||
@@ -41,7 +39,7 @@ impl fmt::Display for ParseError {
|
|||||||
match self {
|
match self {
|
||||||
Self::IOError(_) => f.write_str("IO error"),
|
Self::IOError(_) => f.write_str("IO error"),
|
||||||
Self::IncorrectFileHeader => {
|
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::BadLine => f.write_str("line not in `Key:Value` pattern"),
|
||||||
Self::InvalidCurvePoints => f.write_str("invalid curve point"),
|
Self::InvalidCurvePoints => f.write_str("invalid curve point"),
|
||||||
|
|||||||
+103
-185
@@ -4,6 +4,7 @@ mod error;
|
|||||||
mod hitobject;
|
mod hitobject;
|
||||||
mod hitsound;
|
mod hitsound;
|
||||||
mod pos2;
|
mod pos2;
|
||||||
|
mod reader;
|
||||||
mod sort;
|
mod sort;
|
||||||
|
|
||||||
pub use attributes::BeatmapAttributes;
|
pub use attributes::BeatmapAttributes;
|
||||||
@@ -12,33 +13,24 @@ pub use error::{ParseError, ParseResult};
|
|||||||
pub use hitobject::{HitObject, HitObjectKind};
|
pub use hitobject::{HitObject, HitObjectKind};
|
||||||
pub use hitsound::HitSound;
|
pub use hitsound::HitSound;
|
||||||
pub use pos2::Pos2;
|
pub use pos2::Pos2;
|
||||||
|
pub use slider_parsing::*;
|
||||||
|
|
||||||
|
use reader::FileReader;
|
||||||
use sort::legacy_sort;
|
use sort::legacy_sort;
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
use std::{
|
use std::{fs::File, io::Read};
|
||||||
fs::File,
|
|
||||||
io::{BufRead, BufReader, Read},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "async_tokio")]
|
#[cfg(feature = "async_tokio")]
|
||||||
use tokio::{
|
use tokio::{fs::File, io::AsyncRead};
|
||||||
fs::File,
|
|
||||||
io::{AsyncBufReadExt, AsyncRead, BufReader},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(feature = "async_std"))]
|
#[cfg(not(feature = "async_std"))]
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
#[cfg(feature = "async_std")]
|
#[cfg(feature = "async_std")]
|
||||||
use async_std::{
|
use async_std::{fs::File, io::Read as AsyncRead, path::Path};
|
||||||
fs::File,
|
|
||||||
io::{prelude::BufReadExt, BufReader as AsyncBufReader, Read as AsyncRead},
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use slider_parsing::*;
|
|
||||||
|
|
||||||
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
fn sort_unstable<T: PartialOrd>(slice: &mut [T]) {
|
||||||
slice.sort_unstable_by(|p1, p2| p1.partial_cmp(p2).unwrap_or(Ordering::Equal));
|
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 {
|
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")))]
|
#[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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "async_std", feature = "async_tokio"))]
|
#[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;
|
break;
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! read_line {
|
macro_rules! next_line {
|
||||||
($reader:ident, $buf:expr) => {{
|
($reader:ident) => {{
|
||||||
#[cfg(any(feature = "async_std", feature = "async_tokio"))]
|
#[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")))]
|
#[cfg(not(any(feature = "async_std", feature = "async_tokio")))]
|
||||||
{
|
{
|
||||||
$reader.read_line($buf)
|
$reader.next_line()
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_general_body {
|
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 mode = None;
|
||||||
let mut empty = true;
|
let mut empty = true;
|
||||||
let mut stack_leniency = None;
|
let mut stack_leniency = None;
|
||||||
|
|
||||||
while read_line!($reader, $buf)? != 0 {
|
while next_line!($reader)? != 0 {
|
||||||
let line = line_prepare!($buf);
|
if let Some(bytes) = $reader.get_section() {
|
||||||
|
*$section = Section::from_bytes(bytes);
|
||||||
if line.starts_with('[') && line.ends_with(']') {
|
|
||||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
|
||||||
empty = false;
|
empty = false;
|
||||||
$buf.clear();
|
|
||||||
break;
|
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 {
|
mode = match value {
|
||||||
"0" => Some(GameMode::STD),
|
"0" => Some(GameMode::STD),
|
||||||
"1" => Some(GameMode::TKO),
|
"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()?);
|
stack_leniency = Some(value.parse()?);
|
||||||
}
|
}
|
||||||
|
|
||||||
$buf.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$self.mode = mode.unwrap_or(GameMode::STD);
|
$self.mode = mode.unwrap_or(GameMode::STD);
|
||||||
@@ -157,28 +127,26 @@ macro_rules! parse_general {
|
|||||||
() => {
|
() => {
|
||||||
fn parse_general<R: Read>(
|
fn parse_general<R: Read>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut BufReader<R>,
|
reader: &mut FileReader<R>,
|
||||||
buf: &mut String,
|
|
||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
parse_general_body!(self, reader, buf, section)
|
parse_general_body!(self, reader, section)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(async $reader:ident<$inner:ident>) => {
|
(async) => {
|
||||||
async fn parse_general<R: $inner + Unpin>(
|
async fn parse_general<R: AsyncRead + Unpin>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut $reader<R>,
|
reader: &mut FileReader<R>,
|
||||||
buf: &mut String,
|
|
||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
parse_general_body!(self, reader, buf, section)
|
parse_general_body!(self, reader, section)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_difficulty_body {
|
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 ar = None;
|
||||||
let mut od = None;
|
let mut od = None;
|
||||||
let mut cs = None;
|
let mut cs = None;
|
||||||
@@ -188,29 +156,24 @@ macro_rules! parse_difficulty_body {
|
|||||||
|
|
||||||
let mut empty = true;
|
let mut empty = true;
|
||||||
|
|
||||||
while read_line!($reader, $buf)? != 0 {
|
while next_line!($reader)? != 0 {
|
||||||
let line = line_prepare!($buf);
|
if let Some(bytes) = $reader.get_section() {
|
||||||
|
*$section = Section::from_bytes(bytes);
|
||||||
if line.starts_with('[') && line.ends_with(']') {
|
|
||||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
|
||||||
empty = false;
|
empty = false;
|
||||||
$buf.clear();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (key, value) = split_colon(&line).ok_or(ParseError::BadLine)?;
|
let (key, value) = $reader.split_colon().ok_or(ParseError::BadLine)?;
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
"ApproachRate" => ar = Some(value.parse()?),
|
b"ApproachRate" => ar = Some(value.parse()?),
|
||||||
"OverallDifficulty" => od = Some(value.parse()?),
|
b"OverallDifficulty" => od = Some(value.parse()?),
|
||||||
"CircleSize" => cs = Some(value.parse()?),
|
b"CircleSize" => cs = Some(value.parse()?),
|
||||||
"HPDrainRate" => hp = Some(value.parse()?),
|
b"HPDrainRate" => hp = Some(value.parse()?),
|
||||||
"SliderTickRate" => tick_rate = Some(value.parse()?),
|
b"SliderTickRate" => tick_rate = Some(value.parse()?),
|
||||||
"SliderMultiplier" => sv = Some(value.parse()?),
|
b"SliderMultiplier" => sv = Some(value.parse()?),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
$buf.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_DIFFICULTY: f32 = 5.0;
|
const DEFAULT_DIFFICULTY: f32 = 5.0;
|
||||||
@@ -230,28 +193,26 @@ macro_rules! parse_difficulty {
|
|||||||
() => {
|
() => {
|
||||||
fn parse_difficulty<R: Read>(
|
fn parse_difficulty<R: Read>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut BufReader<R>,
|
reader: &mut FileReader<R>,
|
||||||
buf: &mut String,
|
|
||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
parse_difficulty_body!(self, reader, buf, section)
|
parse_difficulty_body!(self, reader, section)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(async $reader:ident<$inner:ident>) => {
|
(async) => {
|
||||||
async fn parse_difficulty<R: $inner + Unpin>(
|
async fn parse_difficulty<R: AsyncRead + Unpin>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut $reader<R>,
|
reader: &mut FileReader<R>,
|
||||||
buf: &mut String,
|
|
||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
parse_difficulty_body!(self, reader, buf, section)
|
parse_difficulty_body!(self, reader, section)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_timingpoints_body {
|
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_timings = false;
|
||||||
let mut unsorted_difficulties = false;
|
let mut unsorted_difficulties = false;
|
||||||
|
|
||||||
@@ -260,16 +221,14 @@ macro_rules! parse_timingpoints_body {
|
|||||||
|
|
||||||
let mut empty = true;
|
let mut empty = true;
|
||||||
|
|
||||||
while read_line!($reader, $buf)? != 0 {
|
while next_line!($reader)? != 0 {
|
||||||
let line = line_prepare!($buf);
|
if let Some(bytes) = $reader.get_section() {
|
||||||
|
*$section = Section::from_bytes(bytes);
|
||||||
if line.starts_with('[') && line.ends_with(']') {
|
|
||||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
|
||||||
empty = false;
|
empty = false;
|
||||||
$buf.clear();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let line = $reader.get_line()?;
|
||||||
let mut split = line.split(',');
|
let mut split = line.split(',');
|
||||||
|
|
||||||
let time = split
|
let time = split
|
||||||
@@ -311,8 +270,6 @@ macro_rules! parse_timingpoints_body {
|
|||||||
prev_diff = time;
|
prev_diff = time;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$buf.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if unsorted_timings {
|
if unsorted_timings {
|
||||||
@@ -331,28 +288,26 @@ macro_rules! parse_timingpoints {
|
|||||||
() => {
|
() => {
|
||||||
fn parse_timingpoints<R: Read>(
|
fn parse_timingpoints<R: Read>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut BufReader<R>,
|
reader: &mut FileReader<R>,
|
||||||
buf: &mut String,
|
|
||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
parse_timingpoints_body!(self, reader, buf, section)
|
parse_timingpoints_body!(self, reader, section)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(async $reader:ident<$inner:ident>) => {
|
(async) => {
|
||||||
async fn parse_timingpoints<R: $inner + Unpin>(
|
async fn parse_timingpoints<R: AsyncRead + Unpin>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut $reader<R>,
|
reader: &mut FileReader<R>,
|
||||||
buf: &mut String,
|
|
||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
parse_timingpoints_body!(self, reader, buf, section)
|
parse_timingpoints_body!(self, reader, section)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_hitobjects_body {
|
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 unsorted = false;
|
||||||
let mut prev_time = 0.0;
|
let mut prev_time = 0.0;
|
||||||
let mut empty = true;
|
let mut empty = true;
|
||||||
@@ -366,16 +321,14 @@ macro_rules! parse_hitobjects_body {
|
|||||||
// Buffer to re-use for all sliders
|
// Buffer to re-use for all sliders
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
|
|
||||||
while read_line!($reader, $buf)? != 0 {
|
while next_line!($reader)? != 0 {
|
||||||
let line = line_prepare!($buf);
|
if let Some(bytes) = $reader.get_section() {
|
||||||
|
*$section = Section::from_bytes(bytes);
|
||||||
if line.starts_with('[') && line.ends_with(']') {
|
|
||||||
*$section = Section::from_str(&line[1..line.len() - 1]);
|
|
||||||
empty = false;
|
empty = false;
|
||||||
$buf.clear();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let line = $reader.get_line()?;
|
||||||
let mut split = line.split(',');
|
let mut split = line.split(',');
|
||||||
|
|
||||||
let pos = Pos2 {
|
let pos = Pos2 {
|
||||||
@@ -511,7 +464,6 @@ macro_rules! parse_hitobjects_body {
|
|||||||
$self.sounds.push(sound);
|
$self.sounds.push(sound);
|
||||||
|
|
||||||
prev_time = time;
|
prev_time = time;
|
||||||
$buf.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BUG: If [General] section comes after [HitObjects] then the mode
|
// BUG: If [General] section comes after [HitObjects] then the mode
|
||||||
@@ -536,52 +488,35 @@ macro_rules! parse_hitobjects {
|
|||||||
() => {
|
() => {
|
||||||
fn parse_hitobjects<R: Read>(
|
fn parse_hitobjects<R: Read>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut BufReader<R>,
|
reader: &mut FileReader<R>,
|
||||||
buf: &mut String,
|
|
||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
parse_hitobjects_body!(self, reader, buf, section)
|
parse_hitobjects_body!(self, reader, section)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(async $reader:ident<$inner:ident>) => {
|
(async) => {
|
||||||
async fn parse_hitobjects<R: $inner + Unpin>(
|
async fn parse_hitobjects<R: AsyncRead + Unpin>(
|
||||||
&mut self,
|
&mut self,
|
||||||
reader: &mut $reader<R>,
|
reader: &mut FileReader<R>,
|
||||||
buf: &mut String,
|
|
||||||
section: &mut Section,
|
section: &mut Section,
|
||||||
) -> ParseResult<bool> {
|
) -> ParseResult<bool> {
|
||||||
parse_hitobjects_body!(self, reader, buf, section)
|
parse_hitobjects_body!(self, reader, section)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! parse_body {
|
macro_rules! parse_body {
|
||||||
($reader:ident<$inner:ident>: $input:ident) => {{
|
($input:ident) => {{
|
||||||
let mut reader = $reader::new($input);
|
let mut reader = FileReader::new($input);
|
||||||
let mut buf = String::new();
|
next_line!(reader)?;
|
||||||
|
|
||||||
while read_line!(reader, &mut buf)? != 0 {
|
if reader.is_initial_empty_line() {
|
||||||
// Check for character U+FEFF specifically thanks to map id 797130
|
next_line!(reader)?;
|
||||||
if !buf
|
|
||||||
.trim_matches(|c: char| c.is_whitespace() || c == '')
|
|
||||||
.is_empty()
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
let mut map = Beatmap {
|
||||||
version,
|
version: reader.version()?,
|
||||||
hit_objects: Vec::with_capacity(256),
|
hit_objects: Vec::with_capacity(256),
|
||||||
sounds: Vec::with_capacity(256),
|
sounds: Vec::with_capacity(256),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -591,22 +526,18 @@ macro_rules! parse_body {
|
|||||||
|
|
||||||
loop {
|
loop {
|
||||||
match section {
|
match section {
|
||||||
Section::General => section!(map, parse_general, reader, buf, section),
|
Section::General => section!(map, parse_general, reader, section),
|
||||||
Section::Difficulty => section!(map, parse_difficulty, reader, buf, section),
|
Section::Difficulty => section!(map, parse_difficulty, reader, section),
|
||||||
Section::TimingPoints => section!(map, parse_timingpoints, reader, buf, section),
|
Section::TimingPoints => section!(map, parse_timingpoints, reader, section),
|
||||||
Section::HitObjects => section!(map, parse_hitobjects, reader, buf, section),
|
Section::HitObjects => section!(map, parse_hitobjects, reader, section),
|
||||||
Section::None => {
|
Section::None => {
|
||||||
if read_line!(reader, &mut buf)? == 0 {
|
if next_line!(reader)? == 0 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let line = line_prepare!(buf);
|
if let Some(bytes) = reader.get_section() {
|
||||||
|
section = Section::from_bytes(bytes);
|
||||||
if line.starts_with('[') && line.ends_with(']') {
|
|
||||||
section = Section::from_str(&line[1..line.len() - 1]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -623,19 +554,19 @@ macro_rules! parse {
|
|||||||
/// You'll likely want to pass (a reference of) a [`File`](std::fs::File)
|
/// 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]`).
|
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||||
pub fn parse<R: Read>(input: R) -> ParseResult<Self> {
|
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.
|
/// Parse a beatmap from a `.osu` file.
|
||||||
///
|
///
|
||||||
/// As argument you can give anything that implements `tokio::io::AsyncRead`
|
/// As argument you can give anything that implements `tokio::io::AsyncRead`
|
||||||
/// or `async_std::io::Read`, depending which feature you chose.
|
/// or `async_std::io::Read`, depending which feature you chose.
|
||||||
/// You'll likely want to pass a `File`
|
/// You'll likely want to pass a `File`
|
||||||
/// or the file's content as a slice of bytes (`&[u8]`).
|
/// or the file's content as a slice of bytes (`&[u8]`).
|
||||||
pub async fn parse<R: $inner + Unpin>(input: R) -> ParseResult<Self> {
|
pub async fn parse<R: AsyncRead + Unpin>(input: R) -> ParseResult<Self> {
|
||||||
parse_body!($reader<$inner>: input)
|
parse_body!(input)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -652,11 +583,11 @@ macro_rules! from_path {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
(async $path:ident) => {
|
(async) => {
|
||||||
/// Pass the path to a `.osu` file.
|
/// Pass the path to a `.osu` file.
|
||||||
///
|
///
|
||||||
/// Useful when you don't want to create the file manually.
|
/// 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
|
Self::parse(File::open(path).await?).await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -728,8 +659,6 @@ pub struct Beatmap {
|
|||||||
pub stack_leniency: f32,
|
pub stack_leniency: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) const OSU_FILE_HEADER: &str = "osu file format v";
|
|
||||||
|
|
||||||
impl Beatmap {
|
impl Beatmap {
|
||||||
const CIRCLE_FLAG: u8 = 1 << 0;
|
const CIRCLE_FLAG: u8 = 1 << 0;
|
||||||
const SLIDER_FLAG: u8 = 1 << 1;
|
const SLIDER_FLAG: u8 = 1 << 1;
|
||||||
@@ -916,34 +845,24 @@ impl Beatmap {
|
|||||||
|
|
||||||
#[cfg(feature = "async_tokio")]
|
#[cfg(feature = "async_tokio")]
|
||||||
impl Beatmap {
|
impl Beatmap {
|
||||||
parse!(async BufReader<AsyncRead>);
|
parse!(async);
|
||||||
parse_general!(async BufReader<AsyncRead>);
|
parse_general!(async);
|
||||||
parse_difficulty!(async BufReader<AsyncRead>);
|
parse_difficulty!(async);
|
||||||
parse_timingpoints!(async BufReader<AsyncRead>);
|
parse_timingpoints!(async);
|
||||||
parse_hitobjects!(async BufReader<AsyncRead>);
|
parse_hitobjects!(async);
|
||||||
|
|
||||||
from_path!(async Path);
|
from_path!(async);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "async_std")]
|
#[cfg(feature = "async_std")]
|
||||||
impl Beatmap {
|
impl Beatmap {
|
||||||
parse!(async AsyncBufReader<AsyncRead>);
|
parse!(async);
|
||||||
parse_general!(async AsyncBufReader<AsyncRead>);
|
parse_general!(async);
|
||||||
parse_difficulty!(async AsyncBufReader<AsyncRead>);
|
parse_difficulty!(async);
|
||||||
parse_timingpoints!(async AsyncBufReader<AsyncRead>);
|
parse_timingpoints!(async);
|
||||||
parse_hitobjects!(async AsyncBufReader<AsyncRead>);
|
parse_hitobjects!(async);
|
||||||
|
|
||||||
from_path!(async Path);
|
from_path!(async);
|
||||||
}
|
|
||||||
|
|
||||||
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()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug)]
|
||||||
@@ -956,13 +875,12 @@ enum Section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Section {
|
impl Section {
|
||||||
#[inline]
|
fn from_bytes(bytes: &[u8]) -> Self {
|
||||||
fn from_str(s: &str) -> Self {
|
match bytes {
|
||||||
match s {
|
b"General" => Self::General,
|
||||||
"General" => Self::General,
|
b"Difficulty" => Self::Difficulty,
|
||||||
"Difficulty" => Self::Difficulty,
|
b"TimingPoints" => Self::TimingPoints,
|
||||||
"TimingPoints" => Self::TimingPoints,
|
b"HitObjects" => Self::HitObjects,
|
||||||
"HitObjects" => Self::HitObjects,
|
|
||||||
_ => Self::None,
|
_ => 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