1pub mod source;
21
22use path_dedot::ParseDot;
23pub use source::AssetSource;
24use std::borrow::Cow;
25use std::env::var_os;
26use std::fmt::{Debug, Display, Formatter};
27use std::path::{Path, PathBuf};
28use std::sync::Arc;
29use steamlocate::SteamDir;
30use thiserror::Error;
31use tracing::warn;
32#[cfg(feature = "bsp")]
33use vbsp::BspError;
34
35#[derive(Debug, Error)]
36pub enum LoaderError {
37 #[error("Failed to find tf2 install location")]
38 Tf2NotFound,
39 #[error(transparent)]
40 Io(#[from] std::io::Error),
41 #[cfg(feature = "zip")]
42 #[error(transparent)]
43 Zip(#[from] zip::result::ZipError),
44 #[error("{0}")]
45 Other(String),
46}
47
48#[cfg(feature = "bsp")]
49impl From<BspError> for LoaderError {
50 fn from(value: BspError) -> Self {
51 match value {
52 BspError::Zip(err) => LoaderError::Zip(err),
53 BspError::IO(err) => LoaderError::Io(err),
54 err => LoaderError::Other(err.to_string()),
55 }
56 }
57}
58
59#[derive(Clone)]
61pub struct Loader {
62 sources: Vec<Arc<dyn AssetSource + Send + Sync>>,
63}
64
65impl Debug for Loader {
66 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct("Loader")
68 .field("sources", &self.sources.len())
69 .finish_non_exhaustive()
70 }
71}
72
73impl Loader {
74 pub fn new() -> Result<Self, LoaderError> {
76 let tf2_dir = tf2_path()?;
77 Self::with_tf2_dir(tf2_dir)
78 }
79
80 pub fn with_tf2_dir<P: AsRef<Path>>(tf2_dir: P) -> Result<Self, LoaderError> {
82 let tf2_dir = tf2_dir.as_ref();
83
84 let tf_dir = tf2_dir.join("tf");
85 let hl_dir = tf2_dir.join("hl2");
86 let download = tf_dir.join("download");
87
88 #[cfg(feature = "vpk")]
89 let vpks = tf_dir
90 .read_dir()?
91 .chain(hl_dir.read_dir()?)
92 .filter_map(|item| item.ok())
93 .filter_map(|item| Some(item.path().to_str()?.to_string()))
94 .filter(|path| path.ends_with("dir.vpk"))
95 .map(|path| vpk::from_path(&path))
96 .filter_map(|res| {
97 if let Err(e) = &res {
98 warn!(error = ?e, "error while loading vpk");
99 }
100 res.ok()
101 })
102 .map(|vpk| Arc::new(vpk) as Arc<dyn AssetSource + Send + Sync>);
103
104 #[allow(unused_mut)]
105 let mut sources = vec![
106 Arc::new(tf_dir) as Arc<dyn AssetSource + Send + Sync>,
107 Arc::new(hl_dir),
108 ];
109
110 if download.exists() {
111 sources.push(Arc::new(download));
112 }
113
114 #[cfg(feature = "vpk")]
115 sources.extend(vpks);
116
117 Ok(Loader { sources })
118 }
119
120 pub fn add_source<S: AssetSource + Send + Sync + 'static>(&mut self, source: S) {
124 self.sources.push(Arc::new(source))
125 }
126
127 #[tracing::instrument(skip(self))]
129 pub fn exists(&self, name: &str) -> Result<bool, LoaderError> {
130 let name = clean_path(name);
131 for source in self.sources.iter() {
132 if source.has(&name)? {
133 return Ok(true);
134 }
135 }
136
137 let lower_name = name.to_ascii_lowercase();
138 if name != lower_name {
139 for source in self.sources.iter() {
140 if source.has(&lower_name)? {
141 return Ok(true);
142 }
143 }
144 }
145
146 Ok(false)
147 }
148
149 #[tracing::instrument(skip(self))]
153 pub fn load(&self, name: &str) -> Result<Option<Vec<u8>>, LoaderError> {
154 let name = clean_path(name);
155 for source in self.sources.iter() {
156 if let Some(data) = source.load(&name)? {
157 return Ok(Some(data));
158 }
159 }
160
161 let lower_name = name.to_ascii_lowercase();
162 if name != lower_name {
163 for source in self.sources.iter() {
164 if let Some(data) = source.load(&lower_name)? {
165 return Ok(Some(data));
166 }
167 }
168 }
169
170 Ok(None)
171 }
172
173 pub fn find_in_paths<S: Display>(&self, name: &str, paths: &[S]) -> Option<String> {
175 for path in paths {
176 let full_path = format!("{}{}", path, name);
177 let full_path = clean_path(&full_path);
178 if self.exists(&full_path).unwrap_or_default() {
179 return Some(full_path.to_string());
180 }
181 }
182
183 let lower_name = name.to_ascii_lowercase();
184 if name != lower_name {
185 for path in paths {
186 let full_path = format!("{}{}", path, lower_name);
187 let full_path = clean_path(&full_path);
188 if self.exists(&full_path).unwrap_or_default() {
189 return Some(full_path.to_string());
190 }
191 }
192 }
193
194 None
195 }
196}
197
198fn clean_path(path: &str) -> Cow<str> {
199 if path.contains("/../") {
200 let path_buf = PathBuf::from(format!("/{path}"));
201 let Ok(absolute_path) = path_buf.parse_dot_from("/") else {
202 return path.into();
203 };
204 let path = absolute_path.to_str().unwrap().trim_start_matches('/');
205 String::from(path).into()
206 } else {
207 path.into()
208 }
209}
210
211#[test]
212fn test_clean_path() {
213 assert_eq!("foo/bar", clean_path("foo/bar"));
214 assert_eq!("foo/bar", clean_path("foo/asd/../bar"));
215 assert_eq!("../bar", clean_path("../bar"));
216}
217
218fn tf2_path() -> Result<PathBuf, LoaderError> {
219 if let Some(path) = var_os("TF_DIR") {
220 let path: PathBuf = path.into();
221 if path.is_dir() {
222 Ok(path)
223 } else {
224 Err(LoaderError::Tf2NotFound)
225 }
226 } else {
227 let (app, library) = SteamDir::locate()
228 .map_err(|_| LoaderError::Tf2NotFound)?
229 .find_app(440)
230 .map_err(|_| LoaderError::Tf2NotFound)?
231 .ok_or(LoaderError::Tf2NotFound)?;
232 Ok(library.resolve_app_dir(&app))
233 }
234}