From dbdd0760b6ecfd9ce417cd74494ac58d08a70ccd Mon Sep 17 00:00:00 2001 From: Alan Panayotov Date: Sun, 23 Feb 2025 22:31:36 +0000 Subject: [PATCH 1/6] chore: use b" literal instead of .as_bytes for static mesh magic --- crates/league-toolkit/src/core/mesh/static/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/league-toolkit/src/core/mesh/static/mod.rs b/crates/league-toolkit/src/core/mesh/static/mod.rs index 2db2b98c..98bcab5f 100644 --- a/crates/league-toolkit/src/core/mesh/static/mod.rs +++ b/crates/league-toolkit/src/core/mesh/static/mod.rs @@ -6,7 +6,7 @@ use league_primitives::Color; mod face; mod read; -const MAGIC: &[u8] = "r3d2Mesh".as_bytes(); +pub const MAGIC: &[u8] = b"r3d2Mesh"; #[derive(Clone, Debug)] pub struct StaticMesh { From 979e5d7b44dfedbb461f72c44acb19b0817a03d1 Mon Sep 17 00:00:00 2001 From: Alan Panayotov Date: Sun, 23 Feb 2025 22:31:54 +0000 Subject: [PATCH 2/6] feat: league_file --- crates/league-toolkit/Cargo.toml | 2 +- crates/league-toolkit/src/league_file/kind.rs | 91 ++++++++++++++++++ crates/league-toolkit/src/league_file/mod.rs | 15 +++ .../league-toolkit/src/league_file/pattern.rs | 92 +++++++++++++++++++ crates/league-toolkit/src/lib.rs | 1 + 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 crates/league-toolkit/src/league_file/kind.rs create mode 100644 crates/league-toolkit/src/league_file/mod.rs create mode 100644 crates/league-toolkit/src/league_file/pattern.rs diff --git a/crates/league-toolkit/Cargo.toml b/crates/league-toolkit/Cargo.toml index f63e90bb..aeac964e 100644 --- a/crates/league-toolkit/Cargo.toml +++ b/crates/league-toolkit/Cargo.toml @@ -20,7 +20,7 @@ bitflags = "2.5.0" byteorder = "1.5.0" flate2 = "1.0.30" glam = { version = "0.27.0", features = ["glam-assert"] } -lazy_static = "1.4.0" +lazy_static = "1.5.0" log = "0.4.21" memchr = "2.7.2" num_enum = "0.7.2" diff --git a/crates/league-toolkit/src/league_file/kind.rs b/crates/league-toolkit/src/league_file/kind.rs new file mode 100644 index 00000000..bda7986e --- /dev/null +++ b/crates/league-toolkit/src/league_file/kind.rs @@ -0,0 +1,91 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +pub enum LeagueFileKind { + Animation, + Jpeg, + LightGrid, + LuaObj, + MapGeometry, + Png, + Preload, + PropertyBin, + PropertyBinOverride, + RiotStringTable, + SimpleSkin, + Skeleton, + StaticMeshAscii, + StaticMeshBinary, + Svg, + Texture, + TextureDds, + Unknown, + WorldGeometry, + WwiseBank, + WwisePackage, +} + +impl LeagueFileKind { + #[inline] + #[must_use] + pub fn extension(&self) -> &'static str { + match self { + Self::Animation => "anm", + Self::Jpeg => "jpg", + Self::LightGrid => "lightgrid", + Self::LuaObj => "luaobj", + Self::MapGeometry => "mapgeo", + Self::Png => "png", + Self::Preload => "preload", + Self::PropertyBin => "bin", + Self::PropertyBinOverride => "bin", + Self::RiotStringTable => "stringtable", + Self::SimpleSkin => "skn", + Self::Skeleton => "skl", + Self::StaticMeshAscii => "sco", + Self::StaticMeshBinary => "scb", + Self::Texture => "tex", + Self::TextureDds => "dds", + Self::Unknown => "", + Self::WorldGeometry => "wgeo", + Self::WwiseBank => "bnk", + Self::WwisePackage => "wpk", + Self::Svg => "svg", + } + } + + #[must_use] + pub fn from_extension(extension: impl AsRef) -> LeagueFileKind { + let extension = extension.as_ref(); + if extension.is_empty() { + return LeagueFileKind::Unknown; + } + + let extension = match extension.starts_with('.') { + true => &extension[1..], + false => extension, + }; + + match extension { + "anm" => Self::Animation, + "bin" => Self::PropertyBin, + "bnk" => Self::WwiseBank, + "dds" => Self::TextureDds, + "jpg" => Self::Jpeg, + "luaobj" => Self::LuaObj, + "mapgeo" => Self::MapGeometry, + "png" => Self::Png, + "preload" => Self::Preload, + "scb" => Self::StaticMeshBinary, + "sco" => Self::StaticMeshAscii, + "skl" => Self::Skeleton, + "skn" => Self::SimpleSkin, + "stringtable" => Self::RiotStringTable, + "svg" => Self::Svg, + "tex" => Self::Texture, + "wgeo" => Self::WorldGeometry, + "wpk" => Self::WwisePackage, + _ => Self::Unknown, + } + } +} diff --git a/crates/league-toolkit/src/league_file/mod.rs b/crates/league-toolkit/src/league_file/mod.rs new file mode 100644 index 00000000..1db9e595 --- /dev/null +++ b/crates/league-toolkit/src/league_file/mod.rs @@ -0,0 +1,15 @@ +mod kind; +mod pattern; + +pub use kind::*; +use pattern::LEAGUE_FILE_MAGIC_BYTES; + +pub fn identify_league_file(data: &[u8]) -> LeagueFileKind { + for magic_byte in LEAGUE_FILE_MAGIC_BYTES.iter() { + if magic_byte.matches(data) { + return magic_byte.kind; + } + } + + LeagueFileKind::Unknown +} diff --git a/crates/league-toolkit/src/league_file/pattern.rs b/crates/league-toolkit/src/league_file/pattern.rs new file mode 100644 index 00000000..e7331acb --- /dev/null +++ b/crates/league-toolkit/src/league_file/pattern.rs @@ -0,0 +1,92 @@ +use lazy_static::lazy_static; + +use super::LeagueFileKind; + +pub static LEAGUE_FILE_MAGIC_BYTES: &[LeagueFilePattern] = &[ + LeagueFilePattern::from_bytes(crate::core::mesh::MAGIC, LeagueFileKind::StaticMeshBinary), + LeagueFilePattern::from_bytes(b"r3d2sklt", LeagueFileKind::Skeleton), + LeagueFilePattern::from_bytes(b"r3d2ammd", LeagueFileKind::Animation), + LeagueFilePattern::from_bytes(b"r3d2canm", LeagueFileKind::Animation), + LeagueFilePattern::from_fn( + |data| u32::from_le_bytes(data[4..8].try_into().unwrap()) == 1, + 8, + LeagueFileKind::WwisePackage, + ), + LeagueFilePattern::from_fn(|data| &data[1..4] == b"PNG", 4, LeagueFileKind::Png), + LeagueFilePattern::from_bytes(b"DDS ", LeagueFileKind::TextureDds), + LeagueFilePattern::from_bytes(&[0x33, 0x22, 0x11, 0x00], LeagueFileKind::SimpleSkin), + LeagueFilePattern::from_bytes(b"PROP", LeagueFileKind::PropertyBin), + LeagueFilePattern::from_bytes(b"BKHD", LeagueFileKind::WwiseBank), + LeagueFilePattern::from_bytes(b"WGEO", LeagueFileKind::WorldGeometry), + LeagueFilePattern::from_bytes(b"OEGM", LeagueFileKind::MapGeometry), + LeagueFilePattern::from_bytes(b"[Obj", LeagueFileKind::StaticMeshAscii), + LeagueFilePattern::from_fn(|data| &data[1..5] == b"LuaQ", 5, LeagueFileKind::LuaObj), + LeagueFilePattern::from_bytes(b"PreLoad", LeagueFileKind::Preload), + LeagueFilePattern::from_fn( + |data| u32::from_le_bytes(data[..4].try_into().unwrap()) == 3, + 4, + LeagueFileKind::LightGrid, + ), + LeagueFilePattern::from_bytes(b"RST", LeagueFileKind::RiotStringTable), + LeagueFilePattern::from_bytes(b"PTCH", LeagueFileKind::PropertyBinOverride), + LeagueFilePattern::from_fn( + |data| ((u32::from_le_bytes(data[..4].try_into().unwrap()) & 0x00FFFFFF) == 0x00FFD8FF), + 3, + LeagueFileKind::Jpeg, + ), + LeagueFilePattern::from_fn( + |data| u32::from_le_bytes(data[4..8].try_into().unwrap()) == 0x22FD4FC3, + 8, + LeagueFileKind::Skeleton, + ), + LeagueFilePattern::from_bytes(b"TEX\0", LeagueFileKind::Texture), + LeagueFilePattern::from_bytes(b" bool), +} + +pub struct LeagueFilePattern { + pub pattern: LeagueFilePatternKind, + pub min_length: usize, + pub kind: LeagueFileKind, +} + +impl LeagueFilePattern { + const fn from_bytes(bytes: &'static [u8], kind: LeagueFileKind) -> Self { + Self { + pattern: LeagueFilePatternKind::Bytes(bytes), + min_length: bytes.len(), + kind, + } + } + + const fn from_fn(f: fn(&[u8]) -> bool, min_length: usize, kind: LeagueFileKind) -> Self { + Self { + pattern: LeagueFilePatternKind::Fn(f), + min_length, + kind, + } + } + + pub fn matches(&self, data: &[u8]) -> bool { + data.len() >= self.min_length + && match self.pattern { + LeagueFilePatternKind::Bytes(bytes) => &data[..bytes.len()] == bytes, + LeagueFilePatternKind::Fn(f) => f(data), + } + } +} diff --git a/crates/league-toolkit/src/lib.rs b/crates/league-toolkit/src/lib.rs index 90e9a1cf..06484cb4 100644 --- a/crates/league-toolkit/src/lib.rs +++ b/crates/league-toolkit/src/lib.rs @@ -1,2 +1,3 @@ pub mod core; +pub mod league_file; pub mod util; From 3e657a5584ad7c433121ce667815216de9adf86b Mon Sep 17 00:00:00 2001 From: Alan Panayotov Date: Sun, 23 Feb 2025 22:48:02 +0000 Subject: [PATCH 3/6] chore: add more docs --- crates/league-toolkit/src/league_file/kind.rs | 3 +++ crates/league-toolkit/src/league_file/mod.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crates/league-toolkit/src/league_file/kind.rs b/crates/league-toolkit/src/league_file/kind.rs index bda7986e..17fad324 100644 --- a/crates/league-toolkit/src/league_file/kind.rs +++ b/crates/league-toolkit/src/league_file/kind.rs @@ -1,6 +1,7 @@ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +/// The kind of league file (animation, mapgeo, bin, etc) pub enum LeagueFileKind { Animation, Jpeg, @@ -28,6 +29,7 @@ pub enum LeagueFileKind { impl LeagueFileKind { #[inline] #[must_use] + /// The extension for this file type (.anm, .mapgeo, .bin, etc) pub fn extension(&self) -> &'static str { match self { Self::Animation => "anm", @@ -55,6 +57,7 @@ impl LeagueFileKind { } #[must_use] + /// Infer the file type from the extension. Works with or without a preceding '.'. pub fn from_extension(extension: impl AsRef) -> LeagueFileKind { let extension = extension.as_ref(); if extension.is_empty() { diff --git a/crates/league-toolkit/src/league_file/mod.rs b/crates/league-toolkit/src/league_file/mod.rs index 1db9e595..0bf1fd87 100644 --- a/crates/league-toolkit/src/league_file/mod.rs +++ b/crates/league-toolkit/src/league_file/mod.rs @@ -3,7 +3,10 @@ mod pattern; pub use kind::*; use pattern::LEAGUE_FILE_MAGIC_BYTES; +pub use pattern::MAX_MAGIC_SIZE; +/// Identify the type of league file from the magic at the start of the file. You must provide at +/// least [`struct@MAX_MAGIC_SIZE`] bytes of data to be able to detect all file types. pub fn identify_league_file(data: &[u8]) -> LeagueFileKind { for magic_byte in LEAGUE_FILE_MAGIC_BYTES.iter() { if magic_byte.matches(data) { From 06b3b6aa27f718430e0a2c074fd7d16889a9dd87 Mon Sep 17 00:00:00 2001 From: Alan Panayotov Date: Sun, 23 Feb 2025 23:57:29 +0000 Subject: [PATCH 4/6] fix: don't use lazy_static for MAX_MAGIC_SIZE manually keeping it updated is fine, and there's a test to stop accidental breaking --- .../league-toolkit/src/league_file/pattern.rs | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/crates/league-toolkit/src/league_file/pattern.rs b/crates/league-toolkit/src/league_file/pattern.rs index e7331acb..211a294b 100644 --- a/crates/league-toolkit/src/league_file/pattern.rs +++ b/crates/league-toolkit/src/league_file/pattern.rs @@ -43,16 +43,8 @@ pub static LEAGUE_FILE_MAGIC_BYTES: &[LeagueFilePattern] = &[ LeagueFilePattern::from_bytes(b" Date: Sun, 23 Feb 2025 23:57:41 +0000 Subject: [PATCH 5/6] feat: proper rustdoc for identify_league_file --- crates/league-toolkit/src/league_file/mod.rs | 27 +++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/crates/league-toolkit/src/league_file/mod.rs b/crates/league-toolkit/src/league_file/mod.rs index 0bf1fd87..81421938 100644 --- a/crates/league-toolkit/src/league_file/mod.rs +++ b/crates/league-toolkit/src/league_file/mod.rs @@ -6,7 +6,32 @@ use pattern::LEAGUE_FILE_MAGIC_BYTES; pub use pattern::MAX_MAGIC_SIZE; /// Identify the type of league file from the magic at the start of the file. You must provide at -/// least [`struct@MAX_MAGIC_SIZE`] bytes of data to be able to detect all file types. +/// least [`MAX_MAGIC_SIZE`] bytes of data to be able to detect all file types. +/// +/// # Examples +/// ``` +/// # use league_toolkit::league_file::*; +/// # +/// let data = b"r3d2skltblahblahblahblah"; +/// let kind = identify_league_file(data); +/// assert_eq!(kind, LeagueFileKind::Skeleton); +/// ``` +/// +/// +/// ## Identifying from a reader +/// ``` +/// # use std::fs::File; +/// # use std::io::{self, Cursor, Read}; +/// # use league_toolkit::league_file::*; +/// # +/// let mut reader = Cursor::new([0x33, 0x22, 0x11, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]); +/// let mut buffer = [0; MAX_MAGIC_SIZE]; +/// reader.read(&mut buffer)?; +/// +/// let kind = identify_league_file(&buffer); +/// assert_eq!(kind, LeagueFileKind::SimpleSkin); +/// # Ok::<(), io::Error>(()) +/// ``` pub fn identify_league_file(data: &[u8]) -> LeagueFileKind { for magic_byte in LEAGUE_FILE_MAGIC_BYTES.iter() { if magic_byte.matches(data) { From 67e46de7ca4005a50ed642c317254baee526840c Mon Sep 17 00:00:00 2001 From: Alan Panayotov Date: Mon, 24 Feb 2025 00:14:03 +0000 Subject: [PATCH 6/6] feat: identify_league_file -> LeagueFileKind::identify_from_bytes --- crates/league-toolkit/src/league_file/kind.rs | 63 +++++++++++++++++-- crates/league-toolkit/src/league_file/mod.rs | 41 +----------- 2 files changed, 60 insertions(+), 44 deletions(-) diff --git a/crates/league-toolkit/src/league_file/kind.rs b/crates/league-toolkit/src/league_file/kind.rs index 17fad324..66271c4b 100644 --- a/crates/league-toolkit/src/league_file/kind.rs +++ b/crates/league-toolkit/src/league_file/kind.rs @@ -1,3 +1,5 @@ +use super::pattern::LEAGUE_FILE_MAGIC_BYTES; + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] @@ -29,9 +31,16 @@ pub enum LeagueFileKind { impl LeagueFileKind { #[inline] #[must_use] - /// The extension for this file type (.anm, .mapgeo, .bin, etc) - pub fn extension(&self) -> &'static str { - match self { + /// The extension for this file type (anm, mapgeo, bin, etc) + /// ``` + /// # use league_toolkit::league_file::LeagueFileKind; + /// assert_eq!(LeagueFileKind::Animation.extension(), Some("anm")); + /// assert_eq!(LeagueFileKind::StaticMeshAscii.extension(), Some("sco")); + /// assert_eq!(LeagueFileKind::Unknown.extension(), None); + /// + pub fn extension(&self) -> Option<&'static str> { + Some(match self { + Self::Unknown => return None, Self::Animation => "anm", Self::Jpeg => "jpg", Self::LightGrid => "lightgrid", @@ -48,16 +57,21 @@ impl LeagueFileKind { Self::StaticMeshBinary => "scb", Self::Texture => "tex", Self::TextureDds => "dds", - Self::Unknown => "", Self::WorldGeometry => "wgeo", Self::WwiseBank => "bnk", Self::WwisePackage => "wpk", Self::Svg => "svg", - } + }) } #[must_use] - /// Infer the file type from the extension. Works with or without a preceding '.'. + /// Infer the file type from the extension. Works with or without a preceding `'.'`. + /// ``` + /// # use league_toolkit::league_file::LeagueFileKind; + /// # + /// assert_eq!(LeagueFileKind::from_extension("png"), LeagueFileKind::Png); + /// assert_eq!(LeagueFileKind::from_extension(".png"), LeagueFileKind::Png); + /// ``` pub fn from_extension(extension: impl AsRef) -> LeagueFileKind { let extension = extension.as_ref(); if extension.is_empty() { @@ -91,4 +105,41 @@ impl LeagueFileKind { _ => Self::Unknown, } } + + /// Identify the type of league file from the magic at the start of the file. You must provide at + /// least [`MAX_MAGIC_SIZE`] bytes of data to be able to detect all file types. + /// + /// # Examples + /// ``` + /// # use league_toolkit::league_file::*; + /// # + /// let data = b"r3d2skltblahblahblahblah"; + /// let kind = LeagueFileKind::identify_from_bytes(data); + /// assert_eq!(kind, LeagueFileKind::Skeleton); + /// ``` + /// + /// + /// ## Identifying from a reader + /// ``` + /// # use std::fs::File; + /// # use std::io::{self, Cursor, Read}; + /// # use league_toolkit::league_file::*; + /// # + /// let mut reader = Cursor::new([0x33, 0x22, 0x11, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]); + /// let mut buffer = [0; MAX_MAGIC_SIZE]; + /// reader.read(&mut buffer)?; + /// + /// let kind = LeagueFileKind::identify_from_bytes(&buffer); + /// assert_eq!(kind, LeagueFileKind::SimpleSkin); + /// # Ok::<(), io::Error>(()) + /// ``` + pub fn identify_from_bytes(data: &[u8]) -> LeagueFileKind { + for magic_byte in LEAGUE_FILE_MAGIC_BYTES.iter() { + if magic_byte.matches(data) { + return magic_byte.kind; + } + } + + LeagueFileKind::Unknown + } } diff --git a/crates/league-toolkit/src/league_file/mod.rs b/crates/league-toolkit/src/league_file/mod.rs index 81421938..9de3532f 100644 --- a/crates/league-toolkit/src/league_file/mod.rs +++ b/crates/league-toolkit/src/league_file/mod.rs @@ -1,43 +1,8 @@ +//! Utility module for identifying/working with League of Legends file types. +//! +//! See [`LeagueFileKind`] for more information. mod kind; mod pattern; pub use kind::*; -use pattern::LEAGUE_FILE_MAGIC_BYTES; pub use pattern::MAX_MAGIC_SIZE; - -/// Identify the type of league file from the magic at the start of the file. You must provide at -/// least [`MAX_MAGIC_SIZE`] bytes of data to be able to detect all file types. -/// -/// # Examples -/// ``` -/// # use league_toolkit::league_file::*; -/// # -/// let data = b"r3d2skltblahblahblahblah"; -/// let kind = identify_league_file(data); -/// assert_eq!(kind, LeagueFileKind::Skeleton); -/// ``` -/// -/// -/// ## Identifying from a reader -/// ``` -/// # use std::fs::File; -/// # use std::io::{self, Cursor, Read}; -/// # use league_toolkit::league_file::*; -/// # -/// let mut reader = Cursor::new([0x33, 0x22, 0x11, 0x00, 0xDE, 0xAD, 0xBE, 0xEF]); -/// let mut buffer = [0; MAX_MAGIC_SIZE]; -/// reader.read(&mut buffer)?; -/// -/// let kind = identify_league_file(&buffer); -/// assert_eq!(kind, LeagueFileKind::SimpleSkin); -/// # Ok::<(), io::Error>(()) -/// ``` -pub fn identify_league_file(data: &[u8]) -> LeagueFileKind { - for magic_byte in LEAGUE_FILE_MAGIC_BYTES.iter() { - if magic_byte.matches(data) { - return magic_byte.kind; - } - } - - LeagueFileKind::Unknown -}