rosu_map/decode.rs
1use std::{
2 error::Error,
3 fs::File,
4 io,
5 io::{BufRead, BufReader, Cursor},
6 ops::ControlFlow,
7 path::Path,
8};
9
10use crate::{format_version, reader::Decoder, section::Section};
11
12/// Parse a type that implements [`DecodeBeatmap`] by providing a path to a
13/// `.osu` file.
14///
15/// # Example
16///
17/// ```rust,no_run
18/// use rosu_map::section::hit_objects::HitObjects;
19///
20/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
21/// let path = "/path/to/file.osu";
22/// let content: HitObjects = rosu_map::from_path(path)?;
23/// # Ok(()) }
24/// ```
25pub fn from_path<D: DecodeBeatmap>(path: impl AsRef<Path>) -> Result<D, io::Error> {
26 File::open(path).map(BufReader::new).and_then(D::decode)
27}
28
29/// Parse a type that implements [`DecodeBeatmap`] by providing the content of
30/// a `.osu` file as a slice of bytes.
31///
32/// # Example
33///
34/// ```rust
35/// use rosu_map::section::metadata::Metadata;
36///
37/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
38/// let bytes: &[u8] = b"[General]
39/// Mode: 2
40///
41/// [Metadata]
42/// Creator: pishifat";
43///
44/// let metadata: Metadata = rosu_map::from_bytes(bytes)?;
45/// assert_eq!(metadata.creator, "pishifat");
46/// # Ok(()) }
47/// ```
48pub fn from_bytes<D: DecodeBeatmap>(bytes: &[u8]) -> Result<D, io::Error> {
49 D::decode(Cursor::new(bytes))
50}
51
52/// Parse a type that implements [`DecodeBeatmap`] by providing the content of
53/// a `.osu` file as a string.
54///
55/// # Example
56///
57/// ```rust
58/// use rosu_map::section::difficulty::Difficulty;
59///
60/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
61/// let s: &str = "[Difficulty]
62/// SliderMultiplier: 3
63///
64/// [Editor]
65/// BeatDivisor: 4";
66///
67/// let difficulty: Difficulty = rosu_map::from_str(s)?;
68/// assert_eq!(difficulty.slider_multiplier, 3.0);
69/// # Ok(()) }
70/// ```
71pub fn from_str<D: DecodeBeatmap>(s: &str) -> Result<D, io::Error> {
72 D::decode(Cursor::new(s))
73}
74
75/// Intermediate state while parsing via [`DecodeBeatmap`].
76pub trait DecodeState: Sized {
77 /// Given the format version, create an instance.
78 ///
79 /// If the version is not of interest, this is basically
80 /// `Default::default()`.
81 fn create(version: i32) -> Self;
82}
83
84/// Trait to handle reading and parsing content of `.osu` files.
85///
86/// Generally, the only way to interact with this trait should be calling the
87/// [`decode`] method.
88///
89/// Each section has its own `parse_[section]` method in which, given the next
90/// line, the state should be updated. Note that the given lines will be
91/// non-empty but comments (text starting with `//`) are **not trimmed**.
92///
93/// # Example
94///
95/// [`DecodeBeatmap`] is implemented for structs like [`HitObjects`] or
96/// [`Beatmap`] so it can be used out the box.
97///
98/// ```
99/// use std::io::Cursor;
100/// use rosu_map::{Beatmap, DecodeBeatmap};
101/// use rosu_map::section::general::GameMode;
102/// use rosu_map::section::hit_objects::HitObjects;
103///
104/// let content: &str = "osu file format v14
105///
106/// [General]
107/// Mode: 1 // Some comment
108///
109/// [Metadata]
110/// Title: Some song title";
111///
112/// // Converting &str to &[u8] so that io::BufRead is satisfied
113/// let mut reader = content.as_bytes();
114/// let decoded = HitObjects::decode(&mut reader).unwrap();
115/// assert_eq!(decoded.mode, GameMode::Taiko);
116/// assert!(decoded.hit_objects.is_empty());
117///
118/// let mut reader = content.as_bytes();
119/// let decoded = Beatmap::decode(&mut reader).unwrap();
120/// assert_eq!(decoded.mode, GameMode::Taiko);
121/// assert_eq!(decoded.title, "Some song title");
122/// ```
123///
124/// Let's assume only the beatmap title and difficulty attributes are of
125/// interest. Using [`Beatmap`] will parse **everything** which will be much
126/// slower than implementing this trait on a custom type:
127///
128/// ```
129/// use rosu_map::{DecodeBeatmap, DecodeState};
130/// use rosu_map::section::difficulty::{Difficulty, DifficultyState, ParseDifficultyError};
131/// use rosu_map::section::metadata::MetadataKey;
132/// use rosu_map::util::KeyValue;
133///
134/// // Our final struct that we want to parse into.
135/// struct CustomBeatmap {
136/// title: String,
137/// ar: f32,
138/// cs: f32,
139/// hp: f32,
140/// od: f32,
141/// }
142///
143/// // The struct that will be built gradually while parsing.
144/// struct CustomBeatmapState {
145/// title: String,
146/// // Built-in way to handle difficulty parsing.
147/// difficulty: DifficultyState,
148/// }
149///
150/// // Required to implement for the `DecodeBeatmap` trait.
151/// impl DecodeState for CustomBeatmapState {
152/// fn create(version: i32) -> Self {
153/// Self {
154/// title: String::new(),
155/// difficulty: DifficultyState::create(version),
156/// }
157/// }
158/// }
159///
160/// // Also required for the `DecodeBeatmap` trait
161/// impl From<CustomBeatmapState> for CustomBeatmap {
162/// fn from(state: CustomBeatmapState) -> Self {
163/// let difficulty = Difficulty::from(state.difficulty);
164///
165/// Self {
166/// title: state.title,
167/// ar: difficulty.approach_rate,
168/// cs: difficulty.circle_size,
169/// hp: difficulty.hp_drain_rate,
170/// od: difficulty.overall_difficulty,
171/// }
172/// }
173/// }
174///
175/// impl DecodeBeatmap for CustomBeatmap {
176/// type State = CustomBeatmapState;
177///
178/// // In our case, only parsing the difficulty can fail so we can just use
179/// // its error type.
180/// type Error = ParseDifficultyError;
181///
182/// fn parse_metadata(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
183/// // Note that comments are *not* trimmed at this point.
184/// // To do that, one can use the `rosu_map::util::StrExt` trait and
185/// // its `trim_comment` method.
186/// let Ok(KeyValue { key, value }) = KeyValue::parse(line) else {
187/// // Unknown key, discard line
188/// return Ok(());
189/// };
190///
191/// match key {
192/// MetadataKey::Title => state.title = value.to_owned(),
193/// _ => {}
194/// }
195///
196/// Ok(())
197/// }
198///
199/// fn parse_difficulty(state: &mut Self::State, line: &str) -> Result<(), Self::Error> {
200/// // Let `Difficulty` and its state handle the difficulty parsing.
201/// Difficulty::parse_difficulty(&mut state.difficulty, line)
202/// }
203///
204/// // None of the other sections are of interest.
205/// fn parse_general(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
206/// Ok(())
207/// }
208/// fn parse_editor(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
209/// Ok(())
210/// }
211/// fn parse_events(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
212/// Ok(())
213/// }
214/// fn parse_timing_points(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
215/// Ok(())
216/// }
217/// fn parse_colors(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
218/// Ok(())
219/// }
220/// fn parse_hit_objects(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
221/// Ok(())
222/// }
223/// fn parse_variables(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
224/// Ok(())
225/// }
226/// fn parse_catch_the_beat(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
227/// Ok(())
228/// }
229/// fn parse_mania(_state: &mut Self::State, _line: &str) -> Result<(), Self::Error> {
230/// Ok(())
231/// }
232/// }
233/// ```
234///
235/// For more examples, check out how structs like [`TimingPoints`] or
236/// [`Beatmap`] implement the [`DecodeBeatmap`] trait.
237///
238/// [`decode`]: DecodeBeatmap::decode
239/// [`Beatmap`]: crate::beatmap::Beatmap
240/// [`HitObjects`]: crate::section::hit_objects::HitObjects
241/// [`TimingPoints`]: crate::section::timing_points::TimingPoints
242pub trait DecodeBeatmap: Sized {
243 /// Error type in case something goes wrong while parsing.
244 ///
245 /// Note that this error is not thrown by the [`decode`] method. Instead,
246 /// when a `parse_[section]` method returns such an error, it will be
247 /// handled silently. That means, if the `tracing` feature is enabled, the
248 /// error and its causes will be logged on the `ERROR` level. If `tracing`
249 /// is not enabled, the error will be ignored entirely.
250 ///
251 /// [`decode`]: DecodeBeatmap::decode
252 type Error: Error;
253
254 /// The parsing state which will be updated on each line and turned into
255 /// `Self` at the end.
256 type State: DecodeState + Into<Self>;
257
258 /// The key method to read and parse content of a `.osu` file into `Self`.
259 ///
260 /// This method should not be implemented manually.
261 fn decode<R: BufRead>(src: R) -> Result<Self, io::Error> {
262 let mut reader = Decoder::new(src)?;
263
264 let (version, use_curr_line) = parse_version(&mut reader)?;
265 let mut state =
266 Self::State::create(version.unwrap_or(format_version::LATEST_FORMAT_VERSION));
267
268 let Some(mut section) = parse_first_section(&mut reader, use_curr_line)? else {
269 return Ok(state.into());
270 };
271
272 loop {
273 let parse_fn = match section {
274 Section::General => Self::parse_general,
275 Section::Editor => Self::parse_editor,
276 Section::Metadata => Self::parse_metadata,
277 Section::Difficulty => Self::parse_difficulty,
278 Section::Events => Self::parse_events,
279 Section::TimingPoints => Self::parse_timing_points,
280 Section::Colors => Self::parse_colors,
281 Section::HitObjects => Self::parse_hit_objects,
282 Section::Variables => Self::parse_variables,
283 Section::CatchTheBeat => Self::parse_catch_the_beat,
284 Section::Mania => Self::parse_mania,
285 };
286
287 match parse_section::<_, Self>(&mut reader, &mut state, parse_fn) {
288 Ok(SectionFlow::Continue(next)) => section = next,
289 Ok(SectionFlow::Break(())) => break,
290 Err(err) => return Err(err),
291 }
292 }
293
294 Ok(state.into())
295 }
296
297 /// Whether a line should *not* be forwarded to the parsing methods.
298 fn should_skip_line(line: &str) -> bool {
299 line.is_empty() || line.trim_start().starts_with("//")
300 }
301
302 /// Update the state based on a line of the `[General]` section.
303 #[allow(unused_variables)]
304 fn parse_general(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
305
306 /// Update the state based on a line of the `[Editor]` section.
307 #[allow(unused_variables)]
308 fn parse_editor(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
309
310 /// Update the state based on a line of the `[Metadata]` section.
311 #[allow(unused_variables)]
312 fn parse_metadata(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
313
314 /// Update the state based on a line of the `[Difficulty]` section.
315 #[allow(unused_variables)]
316 fn parse_difficulty(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
317
318 /// Update the state based on a line of the `[Events]` section.
319 #[allow(unused_variables)]
320 fn parse_events(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
321
322 /// Update the state based on a line of the `[TimingPoints]` section.
323 #[allow(unused_variables)]
324 fn parse_timing_points(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
325
326 /// Update the state based on a line of the `[Colours]` section.
327 #[allow(unused_variables)]
328 fn parse_colors(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
329
330 /// Update the state based on a line of the `[HitObjects]` section.
331 #[allow(unused_variables)]
332 fn parse_hit_objects(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
333
334 /// Update the state based on a line of the `[Variables]` section.
335 #[allow(unused_variables)]
336 fn parse_variables(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
337
338 /// Update the state based on a line of the `[CatchTheBeat]` section.
339 #[allow(unused_variables)]
340 fn parse_catch_the_beat(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
341
342 /// Update the state based on a line of the `[Mania]` section.
343 #[allow(unused_variables)]
344 fn parse_mania(state: &mut Self::State, line: &str) -> Result<(), Self::Error>;
345}
346
347struct UseCurrentLine(bool);
348
349fn parse_version<R>(reader: &mut Decoder<R>) -> Result<(Option<i32>, UseCurrentLine), io::Error>
350where
351 R: BufRead,
352{
353 loop {
354 let (version, use_curr_line) = match reader.read_line() {
355 Ok(Some(line)) => match format_version::try_version_from_line(line) {
356 ControlFlow::Continue(()) => continue,
357 ControlFlow::Break(Ok(version)) => (Some(version), false),
358 // Only used when `tracing` feature is enabled
359 #[allow(unused)]
360 ControlFlow::Break(Err(err)) => {
361 #[cfg(feature = "tracing")]
362 {
363 tracing::error!("Failed to parse format version: {err}");
364 log_error_cause(&err);
365 }
366
367 (None, true)
368 }
369 },
370 Ok(None) => (None, false),
371 Err(err) => return Err(err),
372 };
373
374 return Ok((version, UseCurrentLine(use_curr_line)));
375 }
376}
377
378fn parse_first_section<R: BufRead>(
379 reader: &mut Decoder<R>,
380 UseCurrentLine(use_curr_line): UseCurrentLine,
381) -> Result<Option<Section>, io::Error> {
382 if use_curr_line {
383 if let opt @ Some(_) = Section::try_from_line(reader.curr_line()) {
384 return Ok(opt);
385 }
386 }
387
388 loop {
389 match reader.read_line() {
390 Ok(Some(line)) => {
391 if let Some(section) = Section::try_from_line(line) {
392 return Ok(Some(section));
393 }
394 }
395 Ok(None) => return Ok(None),
396 Err(err) => return Err(err),
397 }
398 }
399}
400
401type SectionFlow = ControlFlow<(), Section>;
402
403fn parse_section<R, D>(
404 reader: &mut Decoder<R>,
405 state: &mut D::State,
406 f: fn(&mut D::State, &str) -> Result<(), D::Error>,
407) -> Result<SectionFlow, io::Error>
408where
409 R: BufRead,
410 D: DecodeBeatmap,
411{
412 loop {
413 match reader.read_line() {
414 Ok(Some(line)) => {
415 if D::should_skip_line(line) {
416 continue;
417 }
418
419 if let Some(next) = Section::try_from_line(line) {
420 return Ok(SectionFlow::Continue(next));
421 }
422
423 // Only used when `tracing` feature is enabled
424 #[allow(unused)]
425 let res = f(state, line);
426
427 #[cfg(feature = "tracing")]
428 if let Err(err) = res {
429 tracing::error!("Failed to process line {line:?}: {err}");
430 log_error_cause(&err);
431 }
432 }
433 Ok(None) => return Ok(SectionFlow::Break(())),
434 Err(err) => return Err(err),
435 }
436 }
437}
438
439#[cfg(feature = "tracing")]
440fn log_error_cause(mut err: &dyn Error) {
441 while let Some(src) = err.source() {
442 tracing::error!(" - caused by: {src}");
443 err = src;
444 }
445}