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, paths::PathStyle, rel_path::RelPath};
 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
 67    pub(crate) fn process_path(
 68        &mut self,
 69        path: &Path,
 70        protocol: &str,
 71        cx: &Context<Self>,
 72    ) -> Task<Option<(Arc<Path>, Arc<RelPath>)>> {
 73        let mut is_zip = protocol.eq("zip");
 74
 75        let path: &Path = if let Some(non_zip_part) = path
 76            .as_os_str()
 77            .as_encoded_bytes()
 78            .strip_prefix("/zip:".as_bytes())
 79        {
 80            // typescript-language-server prepends the paths with zip:, which is messy.
 81            is_zip = true;
 82            Path::new(OsStr::new(
 83                std::str::from_utf8(non_zip_part).expect("Invalid UTF-8"),
 84            ))
 85        } else {
 86            path
 87        };
 88
 89        let as_virtual = resolve_virtual(path);
 90        let Some(path) = as_virtual.or_else(|| is_zip.then(|| Arc::from(path))) else {
 91            return Task::ready(None);
 92        };
 93        if let Some(zip_file) = zip_path(&path) {
 94            let zip_file: Arc<Path> = Arc::from(zip_file);
 95            cx.spawn(async move |this, cx| {
 96                let dir = this
 97                    .read_with(cx, |this, _| {
 98                        this.temp_dirs
 99                            .get(&zip_file)
100                            .map(|temp| temp.path().to_owned())
101                    })
102                    .ok()?;
103                let zip_root = if let Some(dir) = dir {
104                    dir
105                } else {
106                    let fs = this.update(cx, |this, _| this.fs.clone()).ok()?;
107                    let tempdir = dump_zip(zip_file.clone(), fs).await.log_err()?;
108                    let new_path = tempdir.path().to_owned();
109                    this.update(cx, |this, _| {
110                        this.temp_dirs.insert(zip_file.clone(), tempdir);
111                    })
112                    .ok()?;
113                    new_path
114                };
115                // Rebase zip-path onto new temp path.
116                let as_relative =
117                    RelPath::new(path.strip_prefix(zip_file).ok()?, PathStyle::local()).ok()?;
118                Some((zip_root.into(), as_relative.into_arc()))
119            })
120        } else {
121            Task::ready(None)
122        }
123    }
124}
125
126fn zip_path(path: &Path) -> Option<&Path> {
127    let path_str = path.to_str()?;
128    let zip_end = path_str.find(".zip/")?;
129    let zip_path = &path_str[..zip_end + 4]; // ".zip" is 4 characters long
130    Some(Path::new(zip_path))
131}
132
133async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
134    let dir = tempfile::tempdir()?;
135    let contents = fs.load_bytes(&path).await?;
136    extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
137    Ok(dir)
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use std::path::Path;
144
145    #[test]
146    fn test_resolve_virtual() {
147        let test_cases = vec![
148            (
149                "/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat",
150                Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
151            ),
152            (
153                "/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat",
154                Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
155            ),
156            (
157                "/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat",
158                Some(Path::new("/path/to/some/subpath/to/file.dat")),
159            ),
160            (
161                "/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat",
162                Some(Path::new("/path/subpath/to/file.dat")),
163            ),
164            ("/path/to/nonvirtual/", None),
165            ("/path/to/malformed/__virtual__", None),
166            ("/path/to/malformed/__virtual__/a0b1c2d3", None),
167            (
168                "/path/to/malformed/__virtual__/a0b1c2d3/this-should-be-a-number",
169                None,
170            ),
171        ];
172
173        for (input, expected) in test_cases {
174            let input_path = Path::new(input);
175            let resolved_path = resolve_virtual(input_path);
176            assert_eq!(resolved_path.as_deref(), expected);
177        }
178    }
179}