yarn.rs

  1//! This module deals with everything related to path handling for Yarn, the package manager for Web ecosystem.
  2//! Yarn is a bit peculiar, because it references paths within .zip files, which we obviously can't handle.
  3//! It also uses virtual paths for peer dependencies.
  4//!
  5//! Long story short, before we attempt to resolve a path as a "real" path, we try to treat is as a yarn path;
  6//! for .zip handling, we unpack the contents into the temp directory (yes, this is bad, against the spirit of Yarn and what-not)
  7
  8use std::{
  9    ffi::OsStr,
 10    path::{Path, PathBuf},
 11    sync::Arc,
 12};
 13
 14use anyhow::Result;
 15use collections::HashMap;
 16use fs::Fs;
 17use gpui::{App, AppContext as _, Context, Entity, Task};
 18use util::{ResultExt, archive::extract_zip};
 19
 20pub(crate) struct YarnPathStore {
 21    temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
 22    fs: Arc<dyn Fs>,
 23}
 24
 25/// Returns `None` when passed path is a malformed virtual path or it's not a virtual path at all.
 26fn resolve_virtual(path: &Path) -> Option<Arc<Path>> {
 27    let components: Vec<_> = path.components().collect();
 28    let mut non_virtual_path = PathBuf::new();
 29
 30    let mut i = 0;
 31    let mut is_virtual = false;
 32    while i < components.len() {
 33        if let Some(os_str) = components[i].as_os_str().to_str() {
 34            // Detect the __virtual__ segment
 35            if os_str == "__virtual__" {
 36                let pop_count = components
 37                    .get(i + 2)?
 38                    .as_os_str()
 39                    .to_str()?
 40                    .parse::<usize>()
 41                    .ok()?;
 42
 43                // Apply dirname operation pop_count times
 44                for _ in 0..pop_count {
 45                    non_virtual_path.pop();
 46                }
 47                i += 3; // Skip hash and pop_count components
 48                is_virtual = true;
 49                continue;
 50            }
 51        }
 52        non_virtual_path.push(components[i]);
 53        i += 1;
 54    }
 55
 56    is_virtual.then(|| Arc::from(non_virtual_path))
 57}
 58
 59impl YarnPathStore {
 60    pub(crate) fn new(fs: Arc<dyn Fs>, cx: &mut App) -> Entity<Self> {
 61        cx.new(|_| Self {
 62            temp_dirs: Default::default(),
 63            fs,
 64        })
 65    }
 66    pub(crate) fn process_path(
 67        &mut self,
 68        path: &Path,
 69        protocol: &str,
 70        cx: &Context<Self>,
 71    ) -> Task<Option<(Arc<Path>, Arc<Path>)>> {
 72        let mut is_zip = protocol.eq("zip");
 73
 74        let path: &Path = if let Some(non_zip_part) = path
 75            .as_os_str()
 76            .as_encoded_bytes()
 77            .strip_prefix("/zip:".as_bytes())
 78        {
 79            // typescript-language-server prepends the paths with zip:, which is messy.
 80            is_zip = true;
 81            Path::new(OsStr::new(
 82                std::str::from_utf8(non_zip_part).expect("Invalid UTF-8"),
 83            ))
 84        } else {
 85            path
 86        };
 87
 88        let as_virtual = resolve_virtual(path);
 89        let Some(path) = as_virtual.or_else(|| is_zip.then(|| Arc::from(path))) else {
 90            return Task::ready(None);
 91        };
 92        if let Some(zip_file) = zip_path(&path) {
 93            let zip_file: Arc<Path> = Arc::from(zip_file);
 94            cx.spawn(async move |this, cx| {
 95                let dir = this
 96                    .read_with(cx, |this, _| {
 97                        this.temp_dirs
 98                            .get(&zip_file)
 99                            .map(|temp| temp.path().to_owned())
100                    })
101                    .ok()?;
102                let zip_root = if let Some(dir) = dir {
103                    dir
104                } else {
105                    let fs = this.update(cx, |this, _| this.fs.clone()).ok()?;
106                    let tempdir = dump_zip(zip_file.clone(), fs).await.log_err()?;
107                    let new_path = tempdir.path().to_owned();
108                    this.update(cx, |this, _| {
109                        this.temp_dirs.insert(zip_file.clone(), tempdir);
110                    })
111                    .ok()?;
112                    new_path
113                };
114                // Rebase zip-path onto new temp path.
115                let as_relative = path.strip_prefix(zip_file).ok()?.into();
116                Some((zip_root.into(), as_relative))
117            })
118        } else {
119            Task::ready(None)
120        }
121    }
122}
123
124fn zip_path(path: &Path) -> Option<&Path> {
125    let path_str = path.to_str()?;
126    let zip_end = path_str.find(".zip/")?;
127    let zip_path = &path_str[..zip_end + 4]; // ".zip" is 4 characters long
128    Some(Path::new(zip_path))
129}
130
131async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
132    let dir = tempfile::tempdir()?;
133    let contents = fs.load_bytes(&path).await?;
134    extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
135    Ok(dir)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use std::path::Path;
142
143    #[test]
144    fn test_resolve_virtual() {
145        let test_cases = vec![
146            (
147                "/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat",
148                Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
149            ),
150            (
151                "/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat",
152                Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
153            ),
154            (
155                "/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat",
156                Some(Path::new("/path/to/some/subpath/to/file.dat")),
157            ),
158            (
159                "/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat",
160                Some(Path::new("/path/subpath/to/file.dat")),
161            ),
162            ("/path/to/nonvirtual/", None),
163            ("/path/to/malformed/__virtual__", None),
164            ("/path/to/malformed/__virtual__/a0b1c2d3", None),
165            (
166                "/path/to/malformed/__virtual__/a0b1c2d3/this-should-be-a-number",
167                None,
168            ),
169        ];
170
171        for (input, expected) in test_cases {
172            let input_path = Path::new(input);
173            let resolved_path = resolve_virtual(input_path);
174            assert_eq!(resolved_path.as_deref(), expected);
175        }
176    }
177}