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}