use std::io::{Cursor, Read}; use anyhow::{Result, anyhow}; use byteorder::{LittleEndian, ReadBytesExt}; use std::collections::HashMap; pub struct OsuReader { cursor: Cursor>, } impl OsuReader { pub fn new(data: Vec) -> Self { Self { cursor: Cursor::new(data), } } fn read_i16(&mut self) -> Result { Ok(self.cursor.read_i16::()?) } fn read_i32(&mut self) -> Result { Ok(self.cursor.read_i32::()?) } fn read_i64(&mut self) -> Result { Ok(self.cursor.read_i64::()?) } fn read_u8(&mut self) -> Result { Ok(self.cursor.read_u8()?) } fn read_f32(&mut self) -> Result { Ok(self.cursor.read_f32::()?) } fn read_f64(&mut self) -> Result { Ok(self.cursor.read_f64::()?) } fn read_bool(&mut self) -> Result { Ok(self.read_u8()? != 0) } fn read_string(&mut self) -> Result> { let indicator = self.read_u8()?; if indicator == 0 { return Ok(None); } if indicator != 0x0B { return Err(anyhow!("Invalid osu string indicator")); } let len = self.read_uleb128()? as usize; let mut buf = vec![0; len]; self.cursor.read_exact(&mut buf)?; Ok(Some(String::from_utf8(buf)?)) } fn read_uleb128(&mut self) -> Result { let mut result = 0; let mut shift = 0; loop { let byte = self.read_u8()?; result |= ((byte & 0x7F) as u64) << shift; if (byte & 0x80) == 0 { break; } shift += 7; } Ok(result) } } #[derive(Debug)] pub struct TimingPoint { pub bpm: f64, pub offset: f64, pub inherited: bool, } #[derive(Debug)] pub struct Beatmap { pub artist_name: Option, pub artist_unicode: Option, pub song_title: Option, pub song_title_unicode: Option, pub creator: Option, pub difficulty: Option, pub audio: Option, pub md5: Option, pub osu_file_name: Option, pub ranked_status: u8, pub hitcircles: i16, pub slider_count: i16, pub spinner_count: i16, pub last_modification: i64, pub approach_rate: f32, pub circle_size: f32, pub hp_drain: f32, pub overall_difficulty: f32, pub slider_velocity: f64, pub difficulties: HashMap, pub drain_time: i32, pub total_time: i32, pub preview_offset: i32, pub beatmap_id: i32, pub beatmapset_id: i32, pub thread_id: i32, pub grade_standard: u8, pub grade_taiko: u8, pub grade_catch: u8, pub grade_mania: u8, pub local_beatmap_offset: i16, pub stack_leniency: f32, pub mode: u8, pub song_source: Option, pub song_tags: Option, pub online_beatmap_offset: i16, pub title_font: Option, pub unplayed: bool, pub last_played: i64, pub osz2: bool, pub folder_name: Option, pub last_checked_against_repository: i64, pub ignore_sound: bool, pub ignore_skin: bool, pub disable_storyboard: bool, pub disable_video: bool, pub visual_override: bool, pub last_modification_time: i32, pub mania_scroll_speed: u8, pub timing_points: Vec, } #[derive(Debug)] pub struct OsuDbData { pub osuver: i32, pub folder_count: i32, pub is_unlocked: bool, pub date_unlock_ticks: i64, pub username: Option, pub beatmaps: Vec, pub userperms: i32, } // // ========================= // PARSER // ========================= // pub fn parse_osudb(bytes: Vec) -> Result { let mut reader = OsuReader::new(bytes); let osuver = reader.read_i32()?; let folder_count = reader.read_i32()?; let is_unlocked = reader.read_bool()?; let date_unlock_ticks = reader.read_i64()?; let username = reader.read_string()?; let beatmaps_count = reader.read_i32()?; let mut beatmaps = Vec::new(); for _ in 0..beatmaps_count { if osuver < 20191107 { reader.read_i32()?; // entry size } let artist_name = reader.read_string()?; let artist_unicode = reader.read_string()?; let song_title = reader.read_string()?; let song_title_unicode = reader.read_string()?; let creator = reader.read_string()?; let difficulty = reader.read_string()?; let audio = reader.read_string()?; let md5 = reader.read_string()?; let osu_file_name = reader.read_string()?; let ranked_status = reader.read_u8()?; let hitcircles = reader.read_i16()?; let slider_count = reader.read_i16()?; let spinner_count = reader.read_i16()?; let last_modification = reader.read_i64()?; let approach_rate = if osuver < 20140609 { reader.read_u8()? as f32 } else { reader.read_f32()? }; let circle_size = if osuver < 20140609 { reader.read_u8()? as f32 } else { reader.read_f32()? }; let hp_drain = if osuver < 20140609 { reader.read_u8()? as f32 } else { reader.read_f32()? }; let overall_difficulty = if osuver < 20140609 { reader.read_u8()? as f32 } else { reader.read_f32()? }; let slider_velocity = reader.read_f64()?; let mut difficulties: HashMap = HashMap::new(); if osuver >= 20140609 { for _ in 0..4 { let len = reader.read_i32()?; for _ in 0..len { reader.read_u8()?; let mode = reader.read_i32()?; reader.read_u8()?; let diff = if osuver > 20250107 { reader.read_f32()? as f64 } else { reader.read_f64()? }; difficulties.insert(mode, diff); } } } let drain_time = reader.read_i32()?; let total_time = reader.read_i32()?; let preview_offset = reader.read_i32()?; let timing_len = reader.read_i32()?; let mut timing_points = Vec::new(); for _ in 0..timing_len { timing_points.push(TimingPoint { bpm: reader.read_f64()?, offset: reader.read_f64()?, inherited: reader.read_bool()?, }); } let beatmap_id = reader.read_i32()?; let beatmapset_id = reader.read_i32()?; let thread_id = reader.read_i32()?; let grade_standard = reader.read_u8()?; let grade_taiko = reader.read_u8()?; let grade_catch = reader.read_u8()?; let grade_mania = reader.read_u8()?; let local_beatmap_offset = reader.read_i16()?; let stack_leniency = reader.read_f32()?; let mode = reader.read_u8()?; let song_source = reader.read_string()?; let song_tags = reader.read_string()?; let online_beatmap_offset = reader.read_i16()?; let title_font = reader.read_string()?; let unplayed = reader.read_bool()?; let last_played = reader.read_i64()?; let osz2 = reader.read_bool()?; let folder_name = reader.read_string()?; let last_checked_against_repository = reader.read_i64()?; let ignore_sound = reader.read_bool()?; let ignore_skin = reader.read_bool()?; let disable_storyboard = reader.read_bool()?; let disable_video = reader.read_bool()?; let visual_override = reader.read_bool()?; if osuver < 20140609 { reader.read_i16()?; } let last_modification_time = reader.read_i32()?; let mania_scroll_speed = reader.read_u8()?; beatmaps.push(Beatmap { artist_name, artist_unicode, song_title, song_title_unicode, creator, difficulty, audio, md5, osu_file_name, ranked_status, hitcircles, slider_count, spinner_count, last_modification, approach_rate, circle_size, hp_drain, overall_difficulty, slider_velocity, difficulties, drain_time, total_time, preview_offset, beatmap_id, beatmapset_id, thread_id, grade_standard, grade_taiko, grade_catch, grade_mania, local_beatmap_offset, stack_leniency, mode, song_source, song_tags, online_beatmap_offset, title_font, unplayed, last_played, osz2, folder_name, last_checked_against_repository, ignore_sound, ignore_skin, disable_storyboard, disable_video, visual_override, last_modification_time, mania_scroll_speed, timing_points, }); } let userperms = reader.read_i32()?; Ok(OsuDbData { osuver, folder_count, is_unlocked, date_unlock_ticks, username, beatmaps, userperms, }) }