improve path searching

This commit is contained in:
Robin Appelman 2024-09-02 13:52:31 +02:00
commit 15f49fabfb
3 changed files with 76 additions and 8 deletions

12
Cargo.lock generated
View file

@ -521,6 +521,15 @@ dependencies = [
"syn 2.0.76", "syn 2.0.76",
] ]
[[package]]
name = "path-dedot"
version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397"
dependencies = [
"once_cell",
]
[[package]] [[package]]
name = "pest" name = "pest"
version = "2.7.5" version = "2.7.5"
@ -747,8 +756,9 @@ dependencies = [
[[package]] [[package]]
name = "tf-asset-loader" name = "tf-asset-loader"
version = "0.1.6" version = "0.1.7"
dependencies = [ dependencies = [
"path-dedot",
"steamlocate", "steamlocate",
"thiserror", "thiserror",
"tracing", "tracing",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tf-asset-loader" name = "tf-asset-loader"
version = "0.1.6" version = "0.1.7"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Utility for loading assets from tf2 data files" description = "Utility for loading assets from tf2 data files"
@ -14,6 +14,7 @@ vpk = { version = "0.2.0", optional = true }
vbsp = { version = "0.6.0", optional = true } vbsp = { version = "0.6.0", optional = true }
thiserror = "1.0.63" thiserror = "1.0.63"
zip = { package = "zip-lzma", version = "0.6.3", default-features = false, features = ["lzma"], optional = true } zip = { package = "zip-lzma", version = "0.6.3", default-features = false, features = ["lzma"], optional = true }
path-dedot = "3.1.1"
[features] [features]
bsp = ["vbsp", "zip"] bsp = ["vbsp", "zip"]

View file

@ -5,7 +5,7 @@
//! directory. //! directory.
//! //!
//! Supports loading both plain file data and data embedded in `vpk` files. //! Supports loading both plain file data and data embedded in `vpk` files.
//! ```rust,dont-run //! ```rust,no_run
//! # use tf_asset_loader::{Loader, LoaderError}; //! # use tf_asset_loader::{Loader, LoaderError};
//! # //! #
//! fn main() -> Result<(), LoaderError> { //! fn main() -> Result<(), LoaderError> {
@ -19,9 +19,11 @@
pub mod source; pub mod source;
use path_dedot::ParseDot;
pub use source::AssetSource; pub use source::AssetSource;
use std::borrow::Cow;
use std::env::var_os; use std::env::var_os;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use steamlocate::SteamDir; use steamlocate::SteamDir;
@ -119,11 +121,22 @@ impl Loader {
/// Check if a file by path exists. /// Check if a file by path exists.
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub fn exists(&self, name: &str) -> Result<bool, LoaderError> { pub fn exists(&self, name: &str) -> Result<bool, LoaderError> {
let name = clean_path(name);
for source in self.sources.iter() { for source in self.sources.iter() {
if source.has(name)? { if source.has(&name)? {
return Ok(true); return Ok(true);
} }
} }
let lower_name = name.to_ascii_lowercase();
if name != lower_name {
for source in self.sources.iter() {
if source.has(&lower_name)? {
return Ok(true);
}
}
}
Ok(false) Ok(false)
} }
@ -132,26 +145,70 @@ impl Loader {
/// Returns the file data as `Vec<u8>` or `None` if the path doesn't exist. /// Returns the file data as `Vec<u8>` or `None` if the path doesn't exist.
#[tracing::instrument(skip(self))] #[tracing::instrument(skip(self))]
pub fn load(&self, name: &str) -> Result<Option<Vec<u8>>, LoaderError> { pub fn load(&self, name: &str) -> Result<Option<Vec<u8>>, LoaderError> {
let name = clean_path(name);
for source in self.sources.iter() { for source in self.sources.iter() {
if let Some(data) = source.load(name)? { if let Some(data) = source.load(&name)? {
return Ok(Some(data)); return Ok(Some(data));
} }
} }
let lower_name = name.to_ascii_lowercase();
if name != lower_name {
for source in self.sources.iter() {
if let Some(data) = source.load(&lower_name)? {
return Ok(Some(data));
}
}
}
Ok(None) Ok(None)
} }
/// Look for a file by name in one or more paths /// Look for a file by name in one or more paths
pub fn find_in_paths(&self, name: &str, paths: &[String]) -> Option<String> { pub fn find_in_paths<S: Display>(&self, name: &str, paths: &[S]) -> Option<String> {
for path in paths { for path in paths {
let full_path = format!("{}{}", path, name); let full_path = format!("{}{}", path, name);
let full_path = clean_path(&full_path);
if self.exists(&full_path).unwrap_or_default() { if self.exists(&full_path).unwrap_or_default() {
return Some(full_path); return Some(full_path.to_string());
} }
} }
let lower_name = name.to_ascii_lowercase();
if name != lower_name {
for path in paths {
let full_path = format!("{}{}", path, lower_name);
let full_path = clean_path(&full_path);
if self.exists(&full_path).unwrap_or_default() {
return Some(full_path.to_string());
}
}
}
None None
} }
} }
fn clean_path(path: &str) -> Cow<str> {
if path.contains("/../") {
let path_buf = PathBuf::from(format!("/{path}"));
let Ok(absolute_path) = path_buf.parse_dot_from("/") else {
return path.into();
};
let path = absolute_path.to_str().unwrap().trim_start_matches('/');
String::from(path).into()
} else {
path.into()
}
}
#[test]
fn test_clean_path() {
assert_eq!("foo/bar", clean_path("foo/bar"));
assert_eq!("foo/bar", clean_path("foo/asd/../bar"));
assert_eq!("../bar", clean_path("../bar"));
}
fn tf2_path() -> Result<PathBuf, LoaderError> { fn tf2_path() -> Result<PathBuf, LoaderError> {
if let Some(path) = var_os("TF_DIR") { if let Some(path) = var_os("TF_DIR") {
let path: PathBuf = path.into(); let path: PathBuf = path.into();