0.1.0-0-pre.9
This commit is contained in:
17
crates/common/game-assets-lib/Cargo.toml
Normal file
17
crates/common/game-assets-lib/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
# file: crates/common/game-assets-lib/Cargo.toml
|
||||
# version: 1
|
||||
|
||||
[package]
|
||||
name = "game-assets-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
game-logging-lib = { path = "../game-logging-lib" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
25
crates/common/game-assets-lib/src/error.rs
Normal file
25
crates/common/game-assets-lib/src/error.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
// file: crates/common/game-assets-lib/src/error.rs
|
||||
// version: 1
|
||||
|
||||
/// Asset URI validation or resolution failure.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AssetError {
|
||||
/// The URI does not use a supported logical asset namespace.
|
||||
UnsupportedScheme,
|
||||
/// The URI contains no relative asset path.
|
||||
EmptyPath,
|
||||
/// The URI attempts to escape its logical asset namespace.
|
||||
InvalidPath,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for AssetError {
|
||||
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
return match self {
|
||||
crate::AssetError::UnsupportedScheme => formatter.write_str("unsupported asset URI scheme"),
|
||||
crate::AssetError::EmptyPath => formatter.write_str("asset URI path is empty"),
|
||||
crate::AssetError::InvalidPath => formatter.write_str("asset URI path is invalid"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for AssetError {}
|
||||
16
crates/common/game-assets-lib/src/lib.rs
Normal file
16
crates/common/game-assets-lib/src/lib.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
// file: crates/common/game-assets-lib/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Logical asset URI validation and source-root resolution for games.sasedev.
|
||||
|
||||
mod error;
|
||||
mod resolver;
|
||||
|
||||
/// Re-export of asset URI and resolution failures.
|
||||
pub use self::error::AssetError;
|
||||
/// Re-export of the source-root asset resolver.
|
||||
pub use self::resolver::AssetResolver;
|
||||
45
crates/common/game-assets-lib/src/resolver.rs
Normal file
45
crates/common/game-assets-lib/src/resolver.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
// file: crates/common/game-assets-lib/src/resolver.rs
|
||||
// version: 1
|
||||
|
||||
/// Resolves logical asset URIs against separate common and game roots.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AssetResolver {
|
||||
common_root: std::path::PathBuf,
|
||||
game_root: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl AssetResolver {
|
||||
/// Creates a resolver from physical common and game asset roots.
|
||||
#[must_use]
|
||||
pub fn new(common_root: impl Into<std::path::PathBuf>, game_root: impl Into<std::path::PathBuf>) -> Self {
|
||||
return Self { common_root: common_root.into(), game_root: game_root.into() };
|
||||
}
|
||||
|
||||
/// Resolves `common://` or `game://` URI into a physical path without touching the filesystem.
|
||||
pub fn resolve(&self, uri: &str) -> Result<std::path::PathBuf, crate::AssetError> {
|
||||
let (root, relative) = if let Some(relative) = uri.strip_prefix("common://") {
|
||||
(&self.common_root, relative)
|
||||
} else if let Some(relative) = uri.strip_prefix("game://") {
|
||||
(&self.game_root, relative)
|
||||
} else {
|
||||
return Err(crate::AssetError::UnsupportedScheme);
|
||||
};
|
||||
if relative.is_empty() {
|
||||
return Err(crate::AssetError::EmptyPath);
|
||||
}
|
||||
let relative_path = std::path::Path::new(relative);
|
||||
if relative_path.is_absolute() || relative.contains('\\') {
|
||||
return Err(crate::AssetError::InvalidPath);
|
||||
}
|
||||
for component in relative_path.components() {
|
||||
if !matches!(component, std::path::Component::Normal(_)) {
|
||||
return Err(crate::AssetError::InvalidPath);
|
||||
}
|
||||
}
|
||||
return Ok(root.join(relative_path));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/resolver.rs"]
|
||||
mod tests;
|
||||
21
crates/common/game-assets-lib/unit_tests/resolver.rs
Normal file
21
crates/common/game-assets-lib/unit_tests/resolver.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// file: crates/common/game-assets-lib/unit_tests/resolver.rs
|
||||
// version: 1
|
||||
|
||||
#[test]
|
||||
fn logical_namespaces_resolve_to_separate_roots() {
|
||||
game_logging_lib::with_test_tracing(|| {
|
||||
let resolver = crate::AssetResolver::new("assets/common", "assets/game-reflex-poc");
|
||||
assert_eq!(resolver.resolve("common://data/runtime.json"), Ok(std::path::PathBuf::from("assets/common/data/runtime.json")));
|
||||
assert_eq!(resolver.resolve("game://data/game.json"), Ok(std::path::PathBuf::from("assets/game-reflex-poc/data/game.json")));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn traversal_and_unknown_schemes_are_rejected() {
|
||||
game_logging_lib::with_test_tracing(|| {
|
||||
let resolver = crate::AssetResolver::new("assets/common", "assets/game-reflex-poc");
|
||||
assert_eq!(resolver.resolve("game://../secret"), Err(crate::AssetError::InvalidPath));
|
||||
assert_eq!(resolver.resolve("file://data/game.json"), Err(crate::AssetError::UnsupportedScheme));
|
||||
assert_eq!(resolver.resolve("common://"), Err(crate::AssetError::EmptyPath));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user