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}