osu_file_parser/osu_file/
mod.rs

1pub mod colours;
2pub mod difficulty;
3pub mod editor;
4pub mod events;
5pub mod general;
6pub mod hitobjects;
7pub mod metadata;
8pub mod osb;
9pub mod timingpoints;
10pub mod types;
11
12use std::fmt::{Debug, Display};
13use std::hash::Hash;
14use std::str::FromStr;
15
16use nom::branch::alt;
17use nom::bytes::complete::{tag, take_till};
18use nom::character::complete::multispace0;
19use nom::combinator::{map_res, success};
20use nom::multi::many0;
21use nom::sequence::{preceded, tuple};
22use thiserror::Error;
23
24use crate::parsers::square_section;
25
26pub use colours::Colours;
27pub use difficulty::Difficulty;
28pub use editor::Editor;
29pub use events::Events;
30pub use general::General;
31pub use hitobjects::HitObjects;
32pub use metadata::Metadata;
33pub use osb::Osb;
34pub use timingpoints::TimingPoints;
35
36pub use types::*;
37
38/// An .osu file represented as a struct.
39#[derive(Clone, Debug, Hash, PartialEq, Eq)]
40#[non_exhaustive]
41pub struct OsuFile {
42    /// Version of the file format.
43    pub version: Version,
44    /// General information about the beatmap.
45    /// - `key`: `value` pairs.
46    pub general: Option<General>,
47    /// Saved settings for the beatmap editor.
48    /// - `key`: `value` pairs.
49    pub editor: Option<Editor>,
50    /// Contents of an .osb storyboard file.
51    pub osb: Option<Osb>,
52    /// Information used to identify the beatmap.
53    /// - `key`:`value` pairs.
54    pub metadata: Option<Metadata>,
55    /// Difficulty settings.
56    /// - `key`:`value` pairs.
57    pub difficulty: Option<Difficulty>,
58    /// Beatmap and storyboard graphic events.
59    /// Comma-separated lists.
60    pub events: Option<Events>,
61    /// Timing and control points.
62    /// Comma-separated lists.
63    pub timing_points: Option<TimingPoints>,
64    /// Combo and skin colours.
65    /// `key` : `value` pairs.
66    pub colours: Option<Colours>,
67    /// Hit objects.
68    /// Comma-separated lists.
69    pub hitobjects: Option<HitObjects>,
70}
71
72impl OsuFile {
73    /// New `OsuFile` with no data.
74    pub fn new(version: Version) -> Self {
75        Self {
76            version,
77            general: None,
78            editor: None,
79            metadata: None,
80            difficulty: None,
81            events: None,
82            timing_points: None,
83            colours: None,
84            hitobjects: None,
85            osb: None,
86        }
87    }
88
89    /// Appends .osb file.
90    pub fn append_osb(&mut self, s: &str) -> Result<(), Error<osb::ParseError>> {
91        self.osb = Osb::from_str(s, self.version)?;
92
93        Ok(())
94    }
95
96    /// Generates .osb file contents.
97    pub fn osb_to_string(&self) -> Option<String> {
98        match &self.osb {
99            Some(osb) => osb.to_string(self.version),
100            None => None,
101        }
102    }
103
104    pub fn default(version: Version) -> OsuFile {
105        OsuFile::new(version)
106    }
107}
108
109impl Display for OsuFile {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        let mut sections = Vec::new();
112
113        if let Some(general) = &self.general {
114            if let Some(general) = general.to_string(self.version) {
115                sections.push(("General", general));
116            }
117        }
118        if let Some(editor) = &self.editor {
119            if let Some(editor) = editor.to_string(self.version) {
120                sections.push(("Editor", editor));
121            }
122        }
123        if let Some(metadata) = &self.metadata {
124            if let Some(metadata) = metadata.to_string(self.version) {
125                sections.push(("Metadata", metadata));
126            }
127        }
128        if let Some(difficulty) = &self.difficulty {
129            if let Some(difficulty) = difficulty.to_string(self.version) {
130                sections.push(("Difficulty", difficulty));
131            }
132        }
133        if let Some(events) = &self.events {
134            if let Some(events) = events.to_string(self.version) {
135                sections.push(("Events", events));
136            }
137        }
138        if let Some(timing_points) = &self.timing_points {
139            if let Some(timing_points) = timing_points.to_string(self.version) {
140                sections.push(("TimingPoints", timing_points));
141            }
142        }
143        if let Some(colours) = &self.colours {
144            if let Some(colours) = colours.to_string(self.version) {
145                sections.push(("Colours", colours));
146            }
147        }
148        if let Some(hitobjects) = &self.hitobjects {
149            if let Some(hitobjects) = hitobjects.to_string(self.version) {
150                sections.push(("HitObjects", hitobjects));
151            }
152        }
153
154        write!(
155            f,
156            "osu file format v{}\n\n{}",
157            self.version,
158            sections
159                .iter()
160                .map(|(name, content)| format!("[{name}]\n{content}"))
161                .collect::<Vec<_>>()
162                .join("\n\n")
163        )
164    }
165}
166
167impl FromStr for OsuFile {
168    type Err = Error<ParseError>;
169
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        let version_text = preceded(
172            alt((tag("\u{feff}"), success(""))),
173            tag::<_, _, nom::error::Error<_>>("osu file format v"),
174        );
175        let version_number = map_res(take_till(|c| c == '\r' || c == '\n'), |s: &str| s.parse());
176
177        let (s, (trailing_ws, version)) = match tuple((
178            multispace0,
179            preceded(version_text, version_number),
180        ))(s)
181        {
182            Ok(ok) => ok,
183            Err(err) => {
184                // wrong line?
185                let err = if let nom::Err::Error(err) = err {
186                    // can find out error by checking the error type
187                    match err.code {
188                        nom::error::ErrorKind::Tag => ParseError::FileVersionDefinedWrong,
189                        nom::error::ErrorKind::MapRes => ParseError::InvalidFileVersion,
190                        _ => {
191                            unreachable!("Not possible to have the error kind {:#?}", err.code)
192                        }
193                    }
194                } else {
195                    unreachable!("Not possible to reach when the errors are already handled, error type is {:#?}", err)
196                };
197
198                return Err(err.into());
199            }
200        };
201
202        if !(MIN_VERSION..=LATEST_VERSION).contains(&version) {
203            return Err(ParseError::InvalidFileVersion.into());
204        }
205
206        let pre_section_count = s
207            .lines()
208            .take_while(|s| {
209                let s = s.trim();
210                !s.trim().starts_with('[') && !s.trim().ends_with(']')
211            })
212            .count();
213
214        for (i, line) in s.lines().take(pre_section_count).enumerate() {
215            let line = line.trim();
216
217            if line.is_empty() {
218                continue;
219            }
220
221            if line.starts_with("//") {
222                continue;
223            }
224
225            return Err(Error::new(ParseError::UnexpectedLine, i));
226        }
227
228        let s = s
229            .lines()
230            .skip(pre_section_count)
231            .collect::<Vec<_>>()
232            .join("\n");
233
234        let (_, sections) = many0(square_section())(&s).unwrap();
235
236        let mut section_parsed = Vec::with_capacity(8);
237
238        let (
239            mut general,
240            mut editor,
241            mut metadata,
242            mut difficulty,
243            mut events,
244            mut timing_points,
245            mut colours,
246            mut hitobjects,
247        ) = (None, None, None, None, None, None, None, None);
248
249        let mut line_number = trailing_ws.lines().count() + pre_section_count;
250
251        for (ws, section_name, ws2, section) in sections {
252            line_number += ws.lines().count();
253
254            if section_parsed.contains(&section_name) {
255                return Err(Error::new(ParseError::DuplicateSections, line_number));
256            }
257
258            let section_name_line = line_number;
259            line_number += ws2.lines().count();
260
261            match section_name {
262                "General" => {
263                    general =
264                        Error::processing_line(General::from_str(section, version), line_number)?;
265                }
266                "Editor" => {
267                    editor =
268                        Error::processing_line(Editor::from_str(section, version), line_number)?;
269                }
270                "Metadata" => {
271                    metadata =
272                        Error::processing_line(Metadata::from_str(section, version), line_number)?;
273                }
274                "Difficulty" => {
275                    difficulty = Error::processing_line(
276                        Difficulty::from_str(section, version),
277                        line_number,
278                    )?;
279                }
280                "Events" => {
281                    events =
282                        Error::processing_line(Events::from_str(section, version), line_number)?;
283                }
284                "TimingPoints" => {
285                    timing_points = Error::processing_line(
286                        TimingPoints::from_str(section, version),
287                        line_number,
288                    )?;
289                }
290                "Colours" => {
291                    colours =
292                        Error::processing_line(Colours::from_str(section, version), line_number)?;
293                }
294                "HitObjects" => {
295                    hitobjects = Error::processing_line(
296                        HitObjects::from_str(section, version),
297                        line_number,
298                    )?;
299                }
300                _ => return Err(Error::new(ParseError::UnknownSection, section_name_line)),
301            }
302
303            section_parsed.push(section_name);
304            line_number += section.lines().count() - 1;
305        }
306
307        Ok(OsuFile {
308            version,
309            general,
310            editor,
311            metadata,
312            difficulty,
313            events,
314            timing_points,
315            colours,
316            hitobjects,
317            osb: None,
318        })
319    }
320}
321
322#[derive(Debug, Error)]
323#[non_exhaustive]
324/// Error for when there's a problem parsing an .osu file.
325pub enum ParseError {
326    /// File version is invalid.
327    #[error("Invalid file version, expected versions from {MIN_VERSION} ~ {LATEST_VERSION}")]
328    InvalidFileVersion,
329    /// File version is defined wrong.
330    #[error("File version defined wrong, expected `osu file format v..` at the start")]
331    FileVersionDefinedWrong,
332    /// File version not defined in line 1.
333    #[error("Found file version definition, but not defined at the first line")]
334    FileVersionInWrongLine,
335    /// Unexpected line before any section.
336    #[error("Unexpected line before any section")]
337    UnexpectedLine,
338    /// Duplicate section names defined.
339    #[error("There are multiple sections defined as the same name")]
340    DuplicateSections,
341    /// Unknown section name defined.
342    #[error("There is an unknown section")]
343    UnknownSection,
344    /// Error used when the opening bracket for the section is missing.
345    #[error("The opening bracket of the section is missing, expected `[` before {0}")]
346    SectionNameNoOpenBracket(String),
347    /// Error used when the closing bracket for the section is missing.
348    #[error("The closing bracket of the section is missing, expected `]` after {0}")]
349    SectionNameNoCloseBracket(String),
350    /// Error parsing the general section.
351    #[error(transparent)]
352    ParseGeneralError {
353        #[from]
354        source: general::ParseError,
355    },
356    /// Error parsing the editor section.
357    #[error(transparent)]
358    ParseEditorError {
359        #[from]
360        source: editor::ParseError,
361    },
362    /// Error parsing the metadata section.
363    #[error(transparent)]
364    ParseMetadataError {
365        #[from]
366        source: metadata::ParseError,
367    },
368    /// Error parsing the difficulty section.
369    #[error(transparent)]
370    ParseDifficultyError {
371        #[from]
372        source: difficulty::ParseError,
373    },
374    /// Error parsing the events section.
375    #[error(transparent)]
376    ParseEventsError {
377        #[from]
378        source: events::ParseError,
379    },
380    /// Error parsing the timingpoints section.
381    #[error(transparent)]
382    ParseTimingPointsError {
383        #[from]
384        source: timingpoints::ParseError,
385    },
386    /// Error parsing the colours section.
387    #[error(transparent)]
388    ParseColoursError {
389        #[from]
390        source: colours::ParseError,
391    },
392    /// Error parsing the hitobjects section.
393    #[error(transparent)]
394    ParseHitObjectsError {
395        #[from]
396        source: hitobjects::ParseError,
397    },
398}