osu_file_parser/osu_file/
mod.rs1pub 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#[derive(Clone, Debug, Hash, PartialEq, Eq)]
40#[non_exhaustive]
41pub struct OsuFile {
42 pub version: Version,
44 pub general: Option<General>,
47 pub editor: Option<Editor>,
50 pub osb: Option<Osb>,
52 pub metadata: Option<Metadata>,
55 pub difficulty: Option<Difficulty>,
58 pub events: Option<Events>,
61 pub timing_points: Option<TimingPoints>,
64 pub colours: Option<Colours>,
67 pub hitobjects: Option<HitObjects>,
70}
71
72impl OsuFile {
73 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 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 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 let err = if let nom::Err::Error(err) = err {
186 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(§ion_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]
324pub enum ParseError {
326 #[error("Invalid file version, expected versions from {MIN_VERSION} ~ {LATEST_VERSION}")]
328 InvalidFileVersion,
329 #[error("File version defined wrong, expected `osu file format v..` at the start")]
331 FileVersionDefinedWrong,
332 #[error("Found file version definition, but not defined at the first line")]
334 FileVersionInWrongLine,
335 #[error("Unexpected line before any section")]
337 UnexpectedLine,
338 #[error("There are multiple sections defined as the same name")]
340 DuplicateSections,
341 #[error("There is an unknown section")]
343 UnknownSection,
344 #[error("The opening bracket of the section is missing, expected `[` before {0}")]
346 SectionNameNoOpenBracket(String),
347 #[error("The closing bracket of the section is missing, expected `]` after {0}")]
349 SectionNameNoCloseBracket(String),
350 #[error(transparent)]
352 ParseGeneralError {
353 #[from]
354 source: general::ParseError,
355 },
356 #[error(transparent)]
358 ParseEditorError {
359 #[from]
360 source: editor::ParseError,
361 },
362 #[error(transparent)]
364 ParseMetadataError {
365 #[from]
366 source: metadata::ParseError,
367 },
368 #[error(transparent)]
370 ParseDifficultyError {
371 #[from]
372 source: difficulty::ParseError,
373 },
374 #[error(transparent)]
376 ParseEventsError {
377 #[from]
378 source: events::ParseError,
379 },
380 #[error(transparent)]
382 ParseTimingPointsError {
383 #[from]
384 source: timingpoints::ParseError,
385 },
386 #[error(transparent)]
388 ParseColoursError {
389 #[from]
390 source: colours::ParseError,
391 },
392 #[error(transparent)]
394 ParseHitObjectsError {
395 #[from]
396 source: hitobjects::ParseError,
397 },
398}