osu_file_parser/
lib.rs

1//! A crate to parse an osu! beatmap file.
2//!
3//! # How to use
4//!
5//! ```
6//! use osu_file_parser::*;
7//!
8//! let osu_file_str = include_str!("./tests/osu_files/files/acid_rain.osu");
9//! // parse the .osu file
10//! let mut osu_file = osu_file_str.parse::<OsuFile>().unwrap();
11//!
12//! let osb_str = include_str!("./tests/osu_files/files/aspire_osb1.osb");
13//! // .osb file can also be parsed and appended to the `OsuFile` instance
14//! osu_file.append_osb(osb_str).unwrap();
15//!
16//! // you can use `assert_eq_osu_str` to assert that the parsed .osu file is equal to the original .osu file
17//! assert_eq_osu_str(&osu_file.to_string(), osu_file_str);
18//! assert_eq_osu_str(&osu_file.osb_to_string().unwrap(), osb_str);
19//! ```
20//!
21//! # General information
22//!
23//! ## Alternative traits
24//! - Most of the types in the crate uses the `VersionedToString`, `VersionedFromStr` and `VersionedDefault` traits as replacements for the `Display`, `FromStr` and `Default` traits.
25//! - Those traits take an extra `version` parameter to choose what version output to use.
26//! - If the type doesn't exist in certain versions, the output will be `None`.
27//!
28//! ## Errors
29//! - Structs that takes lines of string as input can return errors containing information of where the error occurred and what the error was.
30//! - The error type is wrapped in [`Error`] in those cases.
31//! - [`Error`] has methods that tells you where the error happened in the input string and what the error was.
32
33#[cfg(test)]
34mod tests;
35
36mod helper;
37pub mod osu_file;
38pub use osu_file::*;
39mod parsers;
40
41/// Trims the given osu file string into something that can be tested for equality.
42/// - Ignores all empty lines and key value pair's spacing between the key and comma.
43/// - Deletes `\u{feff}` characters.
44pub fn osu_str_trimmer(s: &str) -> String {
45    let mut builder = Vec::new();
46    let mut section_values_inner = Vec::new();
47    let mut in_sections = false;
48    let mut first_line = true;
49    let mut section_coloned = true;
50
51    let section_values_sort_and_push =
52        |section_values_inner: &mut Vec<String>, builder: &mut Vec<String>| {
53            section_values_inner.sort();
54            builder.append(section_values_inner);
55        };
56
57    for line in s.lines() {
58        let line = line.trim().replace('\u{feff}', "");
59
60        if line.is_empty() {
61            continue;
62        }
63
64        if !in_sections {
65            if line.starts_with('[') && line.ends_with(']') {
66                in_sections = true;
67            } else {
68                // data before the sections are irrelevant after first line with file format specifier
69                if first_line && line.starts_with("osu file format v") {
70                    builder.push(line);
71                }
72
73                first_line = false;
74
75                continue;
76            }
77
78            builder.push(line);
79
80            continue;
81        } else {
82            if line.starts_with('[') && line.ends_with(']') {
83                section_values_sort_and_push(&mut section_values_inner, &mut builder);
84                builder.push(line);
85                section_coloned = true;
86
87                continue;
88            }
89
90            if line.contains(':') {
91                let mut header = true;
92                let mut value = false;
93
94                let line = line
95                    .chars()
96                    .filter(|c| {
97                        if !header && !value && *c == ':' {
98                            header = false;
99                            value = true;
100                            return true;
101                        }
102
103                        if header && c.is_whitespace() {
104                            return false;
105                        }
106
107                        true
108                    })
109                    .collect::<String>();
110
111                if section_coloned {
112                    section_values_inner.push(line);
113                } else {
114                    builder.push(line);
115                }
116            } else {
117                section_coloned = false;
118                builder.append(&mut section_values_inner);
119                builder.push(line);
120            }
121        }
122    }
123
124    section_values_sort_and_push(&mut section_values_inner, &mut builder);
125
126    builder.join("\n")
127}
128
129/// Tests equality of two osu file strings.
130pub fn osu_str_eq<L: AsRef<str>, R: AsRef<str>>(left: L, right: R) -> bool {
131    let left = osu_str_trimmer(left.as_ref());
132    let right = osu_str_trimmer(right.as_ref());
133
134    left == right
135}
136
137/// Asserts that two osu file strings are equal.
138pub fn assert_eq_osu_str<L: AsRef<str>, R: AsRef<str>>(left: L, right: R) {
139    let left = osu_str_trimmer(left.as_ref());
140    let right = osu_str_trimmer(right.as_ref());
141
142    pretty_assertions::assert_eq!(left, right)
143}