Skip to content

Commit 1096716

Browse files
authored
Merge pull request #22 from LeagueToolkit/feat/modpkg-formaty
feat: add mod package format reading
2 parents d29ea9f + 9c1fc5d commit 1096716

File tree

44 files changed

+413
-132
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+413
-132
lines changed

crates/io-ext/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "io-ext"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
thiserror = "1.0.60"
8+
league-primitives = { path = "../league-primitives" }
9+
byteorder = "1.5.0"
10+
glam = { version = "0.27.0", features = ["glam-assert"] }

crates/io-ext/src/lib.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
pub mod reader;
2+
pub mod writer;
3+
4+
pub use reader::*;
5+
pub use writer::*;
6+
7+
/// Measures the differnece in cursor position of an `io::Seek`, before and after calling `inner`
8+
/// **NOTE**: this function assumes the cursor position after calling `inner()` will always be >= the cursor after calling `inner()`. Negative position differences will clamp to 0.
9+
///
10+
/// The `inner` function's Err type **must** impl `From<std::io::Error>`, since
11+
/// `std::io::Seek::stream_position()` is fallable
12+
pub fn measure<S, T, E>(
13+
seekable: &mut S,
14+
mut inner: impl FnMut(&mut S) -> Result<T, E>,
15+
) -> Result<(u64, T), E>
16+
where
17+
S: std::io::Seek + ?Sized,
18+
E: std::error::Error + From<std::io::Error>,
19+
{
20+
let start = seekable.stream_position()?;
21+
22+
let val = inner(seekable)?;
23+
Ok((seekable.stream_position()?.saturating_sub(start), val))
24+
}
25+
26+
/// Temporarily seeks an `io::Seek` to `SeekFrom::Start(at)`, for the duration of the `inner` call.
27+
/// Returns back to the seek position afterwards.
28+
///
29+
/// # Example
30+
/// ```rs
31+
/// let mut buf = Cursor::new(Vec::new());
32+
///
33+
/// let size_pos = buf.stream_position().unwrap();
34+
///
35+
/// // things happen....
36+
///
37+
/// window(&mut buf, size_pos, |buf| {
38+
/// let size = 10;
39+
/// buf.write_u32::<LE>(size)
40+
/// })
41+
/// ```
42+
pub fn window<S, T, E>(
43+
seekable: &mut S,
44+
at: u64,
45+
mut inner: impl FnMut(&mut S) -> Result<T, E>,
46+
) -> Result<T, E>
47+
where
48+
S: std::io::Seek + ?Sized,
49+
E: std::error::Error + From<std::io::Error>,
50+
{
51+
let original = seekable.stream_position()?;
52+
seekable.seek(std::io::SeekFrom::Start(at))?;
53+
let val = inner(seekable)?;
54+
seekable.seek(std::io::SeekFrom::Start(original))?;
55+
Ok(val)
56+
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ use std::io::{self, Read};
22

33
use byteorder::{ByteOrder, ReadBytesExt};
44
use glam::{Mat4, Quat, Vec2, Vec3, Vec4};
5-
6-
use crate::core::primitives::{Color, Sphere, AABB};
5+
use league_primitives::{Color, Sphere, AABB};
76

87
#[derive(Debug, thiserror::Error)]
98
pub enum ReaderError {
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use std::io::{self, Write};
22

33
use byteorder::{ByteOrder, WriteBytesExt};
4-
use glam::{Mat4, Quat, Vec2, Vec3, Vec4};
4+
use glam::{Mat4, Quat};
55

6-
use crate::core::primitives::{Color, Sphere, AABB};
6+
use league_primitives::{Color, Sphere, AABB};
77

88
pub trait WriterExt: Write {
99
fn write_padded_string<const N: usize>(&mut self, str: &str) -> io::Result<()> {

crates/league-modpkg/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
thiserror = "1.0.60"
8+
byteorder = "1.5.0"
9+
10+
io-ext = { path = "../io-ext" }

crates/league-modpkg/src/chunk.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use std::{
2+
borrow::Cow,
3+
io::{BufReader, Read},
4+
};
5+
6+
use byteorder::{ReadBytesExt as _, LE};
7+
use io_ext::ReaderExt as _;
8+
9+
use crate::error::ModpkgError;
10+
11+
#[derive(Debug, PartialEq, PartialOrd)]
12+
pub struct ModpkgChunk {
13+
path: Cow<'static, str>,
14+
path_hash: u64,
15+
compressed_size: usize,
16+
uncompressed_size: usize,
17+
data_offset: usize,
18+
checksum: u64,
19+
}
20+
21+
impl ModpkgChunk {
22+
pub fn read(reader: &mut BufReader<impl Read>) -> Result<Self, ModpkgError> {
23+
let path = reader.read_len_prefixed_string::<LE>()?;
24+
let path_hash = reader.read_u64::<LE>()?;
25+
let compressed_size = reader.read_u64::<LE>()?;
26+
let uncompressed_size = reader.read_u64::<LE>()?;
27+
let data_offset = reader.read_u64::<LE>()?;
28+
let checksum = reader.read_u64::<LE>()?;
29+
30+
Ok(Self {
31+
path: Cow::from(path),
32+
path_hash,
33+
compressed_size: compressed_size as usize,
34+
uncompressed_size: uncompressed_size as usize,
35+
data_offset: data_offset as usize,
36+
checksum,
37+
})
38+
}
39+
40+
pub fn path(&self) -> &str {
41+
&self.path
42+
}
43+
pub fn path_hash(&self) -> u64 {
44+
self.path_hash
45+
}
46+
pub fn compressed_size(&self) -> usize {
47+
self.compressed_size
48+
}
49+
pub fn uncompressed_size(&self) -> usize {
50+
self.uncompressed_size
51+
}
52+
pub fn data_offset(&self) -> usize {
53+
self.data_offset
54+
}
55+
pub fn checksum(&self) -> u64 {
56+
self.checksum
57+
}
58+
}

crates/league-modpkg/src/error.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug)]
4+
pub enum ModpkgError {
5+
#[error("IO error: {0}")]
6+
Io(#[from] std::io::Error),
7+
#[error("IO error: {0}")]
8+
IoExtError(#[from] io_ext::ReaderError),
9+
10+
#[error("Invalid modpkg compression type: {0}")]
11+
InvalidCompressionType(u8),
12+
#[error("Invalid modpkg license type: {0}")]
13+
InvalidLicenseType(u8),
14+
#[error("Invalid modpkg magic: {0}")]
15+
InvalidMagic(u64),
16+
#[error("Invalid modpkg version: {0}")]
17+
InvalidVersion(u32),
18+
#[error("Duplicate chunk: {0}")]
19+
DuplicateChunk(u64),
20+
}

crates/league-modpkg/src/lib.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,72 @@
1+
use chunk::ModpkgChunk;
2+
use license::ModpkgLicense;
3+
use std::collections::HashMap;
14

5+
mod chunk;
6+
mod error;
7+
mod license;
8+
mod read;
9+
10+
#[derive(Debug, PartialEq)]
11+
pub struct Modpkg {
12+
name: String,
13+
display_name: String,
14+
description: Option<String>,
15+
version: String,
16+
distributor: Option<String>,
17+
authors: Vec<ModpkgAuthor>,
18+
license: ModpkgLicense,
19+
20+
chunks: HashMap<u64, ModpkgChunk>,
21+
}
22+
23+
impl Modpkg {
24+
pub fn name(&self) -> &str {
25+
&self.name
26+
}
27+
pub fn display_name(&self) -> &str {
28+
&self.display_name
29+
}
30+
pub fn description(&self) -> Option<&str> {
31+
self.description.as_deref()
32+
}
33+
pub fn version(&self) -> &str {
34+
&self.version
35+
}
36+
pub fn distributor(&self) -> Option<&str> {
37+
self.distributor.as_deref()
38+
}
39+
pub fn authors(&self) -> &[ModpkgAuthor] {
40+
&self.authors
41+
}
42+
pub fn license(&self) -> &ModpkgLicense {
43+
&self.license
44+
}
45+
pub fn chunks(&self) -> &HashMap<u64, ModpkgChunk> {
46+
&self.chunks
47+
}
48+
}
49+
50+
#[derive(Debug, PartialEq)]
51+
pub struct ModpkgAuthor {
52+
name: String,
53+
role: Option<String>,
54+
}
55+
56+
#[derive(Debug, PartialEq)]
57+
pub enum ModpkgCompression {
58+
None = 0,
59+
Zstd = 1,
60+
}
61+
62+
impl TryFrom<u8> for ModpkgCompression {
63+
type Error = &'static str;
64+
65+
fn try_from(value: u8) -> Result<Self, Self::Error> {
66+
Ok(match value {
67+
0 => ModpkgCompression::None,
68+
1 => ModpkgCompression::Zstd,
69+
_ => return Err("Invalid modpkg compression value"),
70+
})
71+
}
72+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use std::io::{self, BufReader};
2+
3+
use byteorder::{ReadBytesExt as _, LE};
4+
use io_ext::ReaderExt as _;
5+
6+
use crate::error::ModpkgError;
7+
8+
#[derive(Debug, PartialEq)]
9+
pub enum ModpkgLicense {
10+
None,
11+
Spdx { spdx_id: String },
12+
Custom { name: String, url: String },
13+
}
14+
15+
impl ModpkgLicense {
16+
pub fn read(reader: &mut BufReader<impl io::Read>) -> Result<Self, ModpkgError> {
17+
let license_type = reader.read_u8()?;
18+
19+
Ok(match license_type {
20+
0 => Self::None,
21+
1 => {
22+
let spdx_id = reader.read_len_prefixed_string::<LE>()?;
23+
Self::Spdx { spdx_id }
24+
}
25+
2 => {
26+
let name = reader.read_len_prefixed_string::<LE>()?;
27+
let url = reader.read_len_prefixed_string::<LE>()?;
28+
Self::Custom { name, url }
29+
}
30+
_ => return Err(ModpkgError::InvalidLicenseType(license_type)),
31+
})
32+
}
33+
34+
pub fn write(&self, writer: &mut impl io::Write) -> Result<(), ModpkgError> {
35+
unimplemented!("TODO: modpkg writing");
36+
}
37+
}

crates/league-modpkg/src/read.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use byteorder::{ReadBytesExt as _, LE};
2+
use io_ext::ReaderExt as _;
3+
use std::{
4+
collections::{hash_map::Entry, HashMap},
5+
io::{BufReader, Read},
6+
};
7+
8+
use crate::{error::ModpkgError, Modpkg, ModpkgAuthor, ModpkgChunk, ModpkgLicense};
9+
10+
impl Modpkg {
11+
pub const MAGIC: u64 = u64::from_le_bytes(*b"_modpkg_");
12+
13+
pub fn read(reader: &mut BufReader<impl Read>) -> Result<Self, ModpkgError> {
14+
let magic = reader.read_u64::<LE>()?;
15+
if magic != Self::MAGIC {
16+
return Err(ModpkgError::InvalidMagic(magic));
17+
}
18+
19+
let version = reader.read_u32::<LE>()?;
20+
if version != 1 {
21+
return Err(ModpkgError::InvalidVersion(version));
22+
}
23+
24+
let name = reader.read_len_prefixed_string::<LE>()?;
25+
let display_name = reader.read_len_prefixed_string::<LE>()?;
26+
let description = reader.read_len_prefixed_string::<LE>()?;
27+
let version = reader.read_len_prefixed_string::<LE>()?;
28+
let distributor = reader.read_len_prefixed_string::<LE>()?;
29+
30+
let authors = Self::read_authors(reader)?;
31+
let license = ModpkgLicense::read(reader)?;
32+
let chunks = Self::read_chunks(reader)?;
33+
Ok(Self {
34+
name,
35+
display_name,
36+
description: match description.len() {
37+
0 => None,
38+
_ => Some(description),
39+
},
40+
version,
41+
distributor: match distributor.len() {
42+
0 => None,
43+
_ => Some(distributor),
44+
},
45+
authors,
46+
license,
47+
chunks,
48+
})
49+
}
50+
51+
fn read_authors(reader: &mut BufReader<impl Read>) -> Result<Vec<ModpkgAuthor>, ModpkgError> {
52+
let count = reader.read_u32::<LE>()?;
53+
let mut authors = Vec::with_capacity(count as usize);
54+
for _ in 0..count {
55+
let name = reader.read_len_prefixed_string::<LE>()?;
56+
let role = reader.read_len_prefixed_string::<LE>()?;
57+
58+
authors.push(ModpkgAuthor {
59+
name,
60+
role: match role.len() {
61+
0 => None,
62+
_ => Some(role),
63+
},
64+
});
65+
}
66+
67+
Ok(authors)
68+
}
69+
70+
fn read_chunks(
71+
reader: &mut BufReader<impl Read>,
72+
) -> Result<HashMap<u64, ModpkgChunk>, ModpkgError> {
73+
let chunk_count = reader.read_u32::<LE>()?;
74+
let mut chunks = HashMap::with_capacity(chunk_count as usize);
75+
for _ in 0..chunk_count {
76+
let chunk = ModpkgChunk::read(reader)?;
77+
match chunks.entry(chunk.path_hash()) {
78+
Entry::Occupied(_) => {
79+
return Err(ModpkgError::DuplicateChunk(chunk.path_hash()));
80+
}
81+
Entry::Vacant(entry) => {
82+
entry.insert(chunk);
83+
}
84+
}
85+
}
86+
87+
Ok(chunks)
88+
}
89+
}

0 commit comments

Comments
 (0)