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}