1use std::borrow::Cow;
2use std::future::Future;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5use std::sync::Arc;
6
7use asset_container::{self as assets, Asset, AssetManager, Progress};
8use bytes::Bytes;
9use normpath::PathExt;
10use parking_lot::RwLock;
11use tokio::io::AsyncReadExt;
12use tokio::sync::Mutex;
13use tokio_stream::Stream;
14use tracing::debug;
15use wick_oci_utils::OciOptions;
16
17use crate::{normalize_path, Error};
18
19#[derive(Debug, Clone)]
20pub struct FetchableAssetReference<'a>(&'a AssetReference, OciOptions);
21
22impl<'a> FetchableAssetReference<'a> {
23 pub async fn bytes(&self) -> Result<Bytes, Error> {
24 self.0.bytes(&self.1).await
25 }
26}
27
28impl<'a> std::ops::Deref for FetchableAssetReference<'a> {
29 type Target = AssetReference;
30
31 fn deref(&self) -> &Self::Target {
32 self.0
33 }
34}
35
36#[derive(Debug, Clone, serde::Serialize)]
37#[must_use]
38pub struct AssetReference {
39 pub(crate) location: String,
40 #[serde(skip)]
41 pub(crate) cache_location: Arc<RwLock<Option<PathBuf>>>,
42 #[serde(skip)]
43 pub(crate) baseurl: Arc<RwLock<Option<PathBuf>>>,
44 #[serde(skip)]
45 #[allow(unused)]
46 pub(crate) fetch_lock: Arc<Mutex<()>>,
47}
48
49impl<'a> From<assets::AssetRef<'a, AssetReference>> for AssetReference {
50 fn from(asset_ref: assets::AssetRef<'a, AssetReference>) -> Self {
51 std::ops::Deref::deref(&asset_ref).clone()
52 }
53}
54
55impl FromStr for AssetReference {
56 type Err = Error;
57
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 Ok(Self::new(s))
60 }
61}
62
63impl From<&str> for AssetReference {
64 fn from(s: &str) -> Self {
65 Self::new(s)
66 }
67}
68
69impl From<&String> for AssetReference {
70 fn from(s: &String) -> Self {
71 Self::new(s)
72 }
73}
74
75impl From<&Path> for AssetReference {
76 fn from(s: &Path) -> Self {
77 Self::new(s.to_string_lossy())
78 }
79}
80
81impl From<&PathBuf> for AssetReference {
82 fn from(s: &PathBuf) -> Self {
83 Self::new(s.to_string_lossy())
84 }
85}
86
87impl std::fmt::Display for AssetReference {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "{}", self.location)
90 }
91}
92
93impl PartialEq for AssetReference {
94 fn eq(&self, other: &Self) -> bool {
95 self.location == other.location && *self.baseurl.read() == *other.baseurl.read()
96 }
97}
98
99impl Eq for AssetReference {}
100
101impl std::hash::Hash for AssetReference {
102 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
103 self.location.hash(state);
104 self.baseurl.read().hash(state);
105 }
106}
107
108impl AssetReference {
109 pub fn new<T: Into<String>>(location: T) -> Self {
111 Self {
112 location: location.into(),
113 cache_location: Default::default(),
114 baseurl: Default::default(),
115 fetch_lock: Default::default(),
116 }
117 }
118
119 #[must_use]
121 pub const fn with_options(&self, options: OciOptions) -> FetchableAssetReference<'_> {
122 FetchableAssetReference(self, options)
123 }
124
125 pub fn get_relative_part(&self) -> Result<PathBuf, Error> {
127 let path = self.path()?;
128 let base_dir = self.baseurl().unwrap(); let base_dir = base_dir.normalize().map_err(|_| Error::NotFound(path.clone()))?;
131 let mut base_dir = base_dir.as_path().to_string_lossy().to_string();
132 if !base_dir.ends_with(std::path::MAIN_SEPARATOR_STR) {
133 base_dir.push_str(std::path::MAIN_SEPARATOR_STR);
134 }
135
136 path.strip_prefix(&base_dir).map_or_else(
137 |_e| Err(Error::FileEscapesRoot(path.clone(), base_dir)),
138 |s| Ok(s.to_owned()),
139 )
140 }
141
142 #[must_use]
143 pub fn baseurl(&self) -> Option<PathBuf> {
144 self.baseurl.read().clone()
145 }
146
147 pub fn path(&self) -> Result<PathBuf, Error> {
148 self.resolve_path(true)
149 }
150
151 #[allow(clippy::option_if_let_else)]
152 fn resolve_path(&self, use_cache: bool) -> Result<PathBuf, Error> {
154 if let Some(cache_loc) = self.cache_location.read().as_ref() {
155 if use_cache {
156 return Ok(cache_loc.clone());
157 }
158 }
159 if let Ok(path) = normalize_path(self.location.as_ref(), self.baseurl()) {
160 if path.exists() {
161 return Ok(path);
162 }
163 }
164 Err(Error::Unresolvable(self.location.clone()))
165 }
166
167 #[must_use]
168 pub fn location(&self) -> &str {
169 &self.location
170 }
171
172 #[must_use]
173 pub fn is_directory(&self) -> bool {
174 self.path().map_or(false, |path| path.is_dir())
175 }
176
177 pub async fn bytes(&self, options: &OciOptions) -> Result<Bytes, Error> {
178 match self.fetch(options.clone()).await {
179 Ok(bytes) => Ok(bytes.into()),
180 Err(_err) => Err(Error::LoadError(self.path()?)),
181 }
182 }
183
184 #[must_use]
186 pub fn exists_outside_cache(&self) -> bool {
187 let path = self.resolve_path(false);
188 path.is_ok() && path.unwrap().exists()
189 }
190}
191
192impl Asset for AssetReference {
193 type Options = OciOptions;
194
195 #[allow(clippy::expect_used)]
196 fn update_baseurl(&self, baseurl: &Path) {
197 let baseurl = if baseurl.starts_with(".") {
198 let mut path = std::env::current_dir().expect("failed to get current dir");
199 path.push(baseurl);
200
201 path
202 } else {
203 baseurl.to_owned()
204 };
205
206 *self.baseurl.write() = Some(baseurl);
207 }
208
209 fn fetch_with_progress(&self, _options: OciOptions) -> std::pin::Pin<Box<dyn Stream<Item = Progress> + Send + '_>> {
210 unimplemented!()
211 }
212
213 fn fetch(
214 &self,
215 options: OciOptions,
216 ) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<u8>, assets::Error>> + Send + Sync>> {
217 let asset = self.clone();
218
219 Box::pin(async move {
220 let lock = asset.fetch_lock.lock().await;
221 let path = asset.path();
222
223 let exists = path.as_ref().map_or(false, |p| p.exists());
224 if exists {
225 drop(lock);
227 let path = path.unwrap();
228 if path.is_dir() {
229 return Err(assets::Error::IsDirectory(path.clone()));
230 }
231
232 debug!(path = %path.display(), "fetching local asset");
233 let mut file = tokio::fs::File::open(&path)
234 .await
235 .map_err(|err| assets::Error::FileOpen(path.clone(), err.to_string()))?;
236 let mut bytes = Vec::new();
237
238 file.read_to_end(&mut bytes).await?;
239 Ok(bytes)
240 } else {
241 let path = asset.location();
242 debug!(%path, "fetching remote asset");
243 let (cache_loc, bytes) = retrieve_remote(path, options)
244 .await
245 .map_err(|err| assets::Error::RemoteFetch(path.to_owned(), err.to_string()))?;
246
247 *asset.cache_location.write() = Some(cache_loc);
248 Ok(bytes)
249 }
250 })
251 }
252
253 fn name(&self) -> &str {
254 self.location.as_str()
255 }
256}
257
258async fn retrieve_remote(location: &str, options: OciOptions) -> Result<(PathBuf, Vec<u8>), Error> {
259 let result = wick_oci_utils::package::pull(location, &options)
260 .await
261 .map_err(|e| Error::PullFailed(PathBuf::from(location), e.to_string()))?;
262 let cache_location = result.base_dir.join(result.root_path);
263 let bytes = tokio::fs::read(&cache_location)
264 .await
265 .map_err(|_| Error::LoadError(cache_location.clone()))?;
266 Ok((cache_location, bytes))
267}
268
269impl AssetManager for AssetReference {
270 type Asset = AssetReference;
271
272 fn set_baseurl(&self, baseurl: &Path) {
273 self.update_baseurl(baseurl);
274 }
275
276 fn assets(&self) -> assets::Assets<Self::Asset> {
277 assets::Assets::new(vec![Cow::Borrowed(self)], 0)
278 }
279}
280
281impl TryFrom<String> for AssetReference {
282 type Error = Error;
283 fn try_from(val: String) -> Result<Self, Error> {
284 Ok(Self::new(val))
285 }
286}
287
288#[cfg(test)]
289mod test {
290
291 use std::path::PathBuf;
292
293 use anyhow::Result;
294
295 use super::*;
296 use crate::Error;
297
298 #[test_logger::test]
299 fn test_no_baseurl() -> Result<()> {
300 let location = AssetReference::new("Cargo.toml");
301 println!("location: {:?}", location);
302 let mut expected = std::env::current_dir().unwrap();
303 expected.push("Cargo.toml");
304 assert_eq!(location.path()?, expected);
305 assert!((location.path()?).exists());
306 Ok(())
307 }
308
309 #[test_logger::test]
310 fn test_baseurl() -> Result<()> {
311 let location = AssetReference::new("Cargo.toml");
312 let mut root_project_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
313 root_project_dir.pop();
314 root_project_dir.pop();
315 root_project_dir.pop();
316
317 location.set_baseurl(&root_project_dir);
318 let mut expected = root_project_dir;
319 expected.push("Cargo.toml");
320 assert_eq!(location.path()?, expected);
321 assert!((location.path()?).exists());
322
323 Ok(())
324 }
325
326 #[test_logger::test]
327 fn test_relative_with_baseurl() -> Result<()> {
328 let location = AssetReference::new("../Cargo.toml");
329 let mut root_project_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
330
331 root_project_dir.pop();
332 root_project_dir.pop();
333
334 location.set_baseurl(&root_project_dir);
335 let mut expected = root_project_dir;
336 expected.pop();
337 expected.push("Cargo.toml");
338 assert_eq!(location.path()?, expected);
339
340 Ok(())
341 }
342
343 #[test_logger::test]
344 fn test_relative_with_baseurl2() -> Result<()> {
345 let location = AssetReference::new("../src/utils.rs");
346 let mut crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
347 crate_dir.push("src");
348 location.set_baseurl(&crate_dir);
349 println!("crate_dir: {}", crate_dir.to_string_lossy());
350 println!("actual: {:#?}", location);
351 let expected = PathBuf::from(&crate_dir);
352 let expected = expected.join("..").join("src").join("utils.rs");
353 println!("expected: {}", expected.to_string_lossy());
354 println!("actual: {}", location.path()?.to_string_lossy());
355
356 let expected = expected.normalize()?;
357 assert_eq!(location.path()?, expected);
358
359 Ok(())
360 }
361
362 #[rstest::rstest]
363 #[case("./files/assets/test.fake.wasm", Ok("files/assets/test.fake.wasm"))]
364 #[case("./files/./assets/test.fake.wasm", Ok("files/assets/test.fake.wasm"))]
365 #[case("./files/../files/assets/test.fake.wasm", Ok("files/assets/test.fake.wasm"))]
366 fn test_relativity(#[case] path: &str, #[case] expected: Result<&str, Error>) -> Result<()> {
367 let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
368 let testdata_dir = crate_dir.join("../../../integration-tests/testdata");
369
370 println!("base dir: {}", testdata_dir.display());
371 println!("asset location: {}", path);
372 let asset = AssetReference::new(path);
373 asset.update_baseurl(&testdata_dir);
374
375 let result = asset.get_relative_part();
376 let expected = expected.map(|s| PathBuf::from(s.to_owned()));
377 assert_eq!(result, expected);
378
379 Ok(())
380 }
381
382 #[rstest::rstest]
383 #[case("org/repo:0.6.0")]
384 #[case("registry.candle.dev/org/repo:0.6.0")]
385 fn test_unresolvable_paths(#[case] path: &str) -> Result<()> {
386 println!("asset location: {}", path);
387 let asset = AssetReference::new(path);
388
389 assert_eq!(asset.path(), Err(Error::Unresolvable(path.to_owned())));
390
391 Ok(())
392 }
393
394 #[rstest::rstest]
395 #[case(
396 "./integration-tests/testdata/files/assets/test.fake.wasm",
397 Ok("integration-tests/testdata/files/assets/test.fake.wasm")
398 )]
399 #[case(
400 "./integration-tests/../integration-tests/testdata/files/./assets/test.fake.wasm",
401 Ok("integration-tests/testdata/files/assets/test.fake.wasm")
402 )]
403 #[case(
404 "./integration-tests/./testdata/./files/../files/assets/test.fake.wasm",
405 Ok("integration-tests/testdata/files/assets/test.fake.wasm")
406 )]
407 fn test_baseurl_normalization(#[case] path: &str, #[case] expected: Result<&str, Error>) -> Result<()> {
408 let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
409 let ws_dir = crate_dir.join("../../../");
410 let base_dir = ws_dir.join("integration-tests/./testdata/../../integration-tests/./testdata/../..");
411
412 println!("base dir: {}", base_dir.display());
413 println!("asset location: {}", path);
414 let asset = AssetReference::new(path);
415 asset.update_baseurl(&base_dir);
416
417 let result = asset.get_relative_part();
418 let expected = expected.map(|s| PathBuf::from(s.to_owned()));
419 assert_eq!(result, expected);
420
421 Ok(())
422 }
423
424 #[rstest::rstest]
425 #[case(
426 "/integration-tests/testdata/files/assets/test.fake.wasm",
427 Ok("files/assets/test.fake.wasm")
428 )]
429 #[case(
430 "/integration-tests/../integration-tests/testdata/files/./assets/test.fake.wasm",
431 Ok("files/assets/test.fake.wasm")
432 )]
433 #[case(
434 "/integration-tests/./testdata/./files/../files/assets/test.fake.wasm",
435 Ok("files/assets/test.fake.wasm")
436 )]
437 fn test_path_normalization_absolute(#[case] path: &str, #[case] expected: Result<&str, Error>) -> Result<()> {
438 let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
439 let ws_dir = crate_dir.join("../../../");
440 let testdata_dir = ws_dir.join("integration-tests/./testdata/../../integration-tests/./testdata");
441 let path = format!("{}/{}", ws_dir.display(), path);
442
443 println!("base dir: {}", testdata_dir.display());
444 println!("asset location: {}", path);
445 let asset = AssetReference::new(path);
446 asset.update_baseurl(&testdata_dir);
447
448 let result = asset.get_relative_part();
449 let expected = expected.map(|s| PathBuf::from(s.to_owned()));
450 assert_eq!(result, expected);
451
452 Ok(())
453 }
454}