runner_core/packs/resolver/
fs.rs1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result, anyhow};
4use url::Url;
5
6use crate::path_safety::normalize_under_root;
7
8use super::{FetchResponse, PackResolver};
9
10#[derive(Debug)]
11pub struct FsResolver {
12 root: PathBuf,
13}
14
15impl FsResolver {
16 pub fn new(root: PathBuf) -> Self {
17 Self { root }
18 }
19
20 fn parse_path(&self, locator: &str) -> Result<PathBuf> {
21 if let Some(stripped) = locator.strip_prefix("fs://") {
22 if stripped.starts_with('/')
23 || stripped.starts_with("./")
24 || stripped.starts_with("../")
25 {
26 return Ok(PathBuf::from(stripped));
27 }
28 if cfg!(windows) && stripped.chars().nth(1) == Some(':') {
29 return Ok(PathBuf::from(stripped));
30 }
31 let file_url = format!("file://{stripped}");
32 let url = Url::parse(&file_url).context("failed to parse fs:// locator as file URL")?;
33 return url
34 .to_file_path()
35 .map_err(|_| anyhow!("fs locator {locator} cannot be represented as a path"));
36 }
37 Ok(PathBuf::from(locator))
38 }
39
40 fn normalize(&self, path: PathBuf) -> Result<PathBuf> {
41 if path.is_absolute() {
42 let parent = path
43 .parent()
44 .ok_or_else(|| anyhow!("fs locator missing parent: {}", path.display()))?;
45 let root = parent
46 .canonicalize()
47 .with_context(|| format!("failed to canonicalize {}", parent.display()))?;
48 let file = path
49 .file_name()
50 .ok_or_else(|| anyhow!("fs locator missing file name: {}", path.display()))?;
51 return normalize_under_root(&root, Path::new(file));
52 }
53 normalize_under_root(&self.root, &path)
54 }
55}
56
57impl PackResolver for FsResolver {
58 fn scheme(&self) -> &'static str {
59 "fs"
60 }
61
62 fn fetch(&self, locator: &str) -> Result<FetchResponse> {
63 let path = self.parse_path(locator)?;
64 let path = self.normalize(path)?;
65 if !path.exists() {
66 anyhow::bail!("fs resolver: {} does not exist", path.display());
67 }
68 Ok(FetchResponse::from_path(path))
69 }
70}