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}