0.1.0-0-pre.9

This commit is contained in:
2026-09-16 10:01:13 +02:00
parent 00808ea5a6
commit 4fcbdf316d
22 changed files with 507 additions and 18 deletions

View 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

View 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 {}

View 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;

View 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;

View 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));
});
}