Mountain/ExtensionManagement/
VsixInstaller.rs1#![allow(non_snake_case)]
50
51use std::{
52 fs::{self, File},
53 io::{self, Read},
54 path::{Path, PathBuf},
55};
56
57use serde_json::Value;
58use zip::ZipArchive;
59
60use crate::{ApplicationState::DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO, dev_log};
61
62#[derive(Debug)]
64pub struct InstallOutcome {
65 pub Identifier:String,
67 pub Version:String,
69 pub InstalledAt:PathBuf,
71 pub Description:ExtensionDescriptionStateDTO,
73}
74
75struct ManifestFacts {
77 Publisher:String,
78 Name:String,
79 Version:String,
80}
81
82#[derive(Debug, thiserror::Error)]
85pub enum InstallError {
86 #[error("VSIX path '{0}' does not exist")]
87 SourceMissing(PathBuf),
88
89 #[error("VSIX archive read failure: {0}")]
90 ArchiveRead(String),
91
92 #[error("VSIX manifest missing or unreadable: {0}")]
93 ManifestMissing(String),
94
95 #[error("VSIX manifest missing required field '{0}'")]
96 ManifestFieldMissing(&'static str),
97
98 #[error("Filesystem error during install: {0}")]
99 FilesystemIO(String),
100}
101
102const MANIFEST_ENTRY:&str = "extension/package.json";
103const PAYLOAD_PREFIX:&str = "extension/";
104
105pub fn InstallVsix(VsixPath:&Path, InstallRoot:&Path) -> Result<InstallOutcome, InstallError> {
109 if !VsixPath.exists() {
110 return Err(InstallError::SourceMissing(VsixPath.to_path_buf()));
111 }
112
113 let Facts = ReadManifestFacts(VsixPath)?;
114 let InstalledAt = InstallRoot.join(format!("{}.{}-{}", Facts.Publisher, Facts.Name, Facts.Version));
115 let Identifier = format!("{}.{}", Facts.Publisher, Facts.Name);
116
117 if InstalledAt.exists() {
123 if let Ok(Description) = BuildDescription(&InstalledAt) {
124 #[cfg(unix)]
134 HealExecutableBits(&InstalledAt);
135
136 dev_log!(
137 "extensions",
138 "[VsixInstaller] Reinstall no-op - '{}' v{} already present at {}",
139 Identifier,
140 Facts.Version,
141 InstalledAt.display()
142 );
143
144 return Ok(InstallOutcome { Identifier, Version:Facts.Version, InstalledAt, Description });
145 }
146
147 dev_log!(
149 "extensions",
150 "[VsixInstaller] Existing install at {} is unreadable - wiping and reinstalling",
151 InstalledAt.display()
152 );
153
154 fs::remove_dir_all(&InstalledAt).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
155 }
156
157 CreateParent(&InstalledAt)?;
158 ExtractPayload(VsixPath, &InstalledAt)?;
159
160 let Description = BuildDescription(&InstalledAt)?;
161
162 dev_log!(
163 "extensions",
164 "[VsixInstaller] Installed '{}' v{} at {}",
165 Identifier,
166 Facts.Version,
167 InstalledAt.display()
168 );
169
170 Ok(InstallOutcome { Identifier, Version:Facts.Version, InstalledAt, Description })
171}
172
173pub fn UninstallExtension(InstallDir:&Path) -> Result<(), InstallError> {
175 if !InstallDir.exists() {
176 dev_log!(
177 "extensions",
178 "[VsixInstaller] Uninstall skipped - {} already absent",
179 InstallDir.display()
180 );
181
182 return Ok(());
183 }
184
185 fs::remove_dir_all(InstallDir).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
186
187 dev_log!("extensions", "[VsixInstaller] Uninstalled {}", InstallDir.display());
188
189 Ok(())
190}
191
192fn ReadManifestFacts(VsixPath:&Path) -> Result<ManifestFacts, InstallError> {
195 let Manifest = ReadFullManifest(VsixPath)?;
196
197 let Publisher = ReadStringField(&Manifest, "publisher")?;
198 let Name = ReadStringField(&Manifest, "name")?;
199 let Version = ReadStringField(&Manifest, "version")?;
200
201 Ok(ManifestFacts { Publisher, Name, Version })
202}
203
204pub fn ReadFullManifest(VsixPath:&Path) -> Result<Value, InstallError> {
215 let Archive = File::open(VsixPath).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
216 let mut Archive = ZipArchive::new(Archive).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
217
218 let mut Entry = Archive
219 .by_name(MANIFEST_ENTRY)
220 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
221
222 let mut Raw = String::new();
223
224 Entry
225 .read_to_string(&mut Raw)
226 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
227
228 serde_json::from_str(&Raw).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))
229}
230
231fn ReadStringField(Manifest:&Value, Field:&'static str) -> Result<String, InstallError> {
232 Manifest
233 .get(Field)
234 .and_then(|Value| Value.as_str())
235 .filter(|Value| !Value.is_empty())
236 .map(str::to_owned)
237 .ok_or(InstallError::ManifestFieldMissing(Field))
238}
239
240fn CreateParent(InstalledAt:&Path) -> Result<(), InstallError> {
241 if let Some(Parent) = InstalledAt.parent() {
242 fs::create_dir_all(Parent).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
243 }
244
245 Ok(())
246}
247
248fn ExtractPayload(VsixPath:&Path, InstalledAt:&Path) -> Result<(), InstallError> {
249 let Archive = File::open(VsixPath).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
250 let mut Archive = ZipArchive::new(Archive).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
251
252 fs::create_dir_all(InstalledAt).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
253
254 for Index in 0..Archive.len() {
255 let mut Entry = Archive
256 .by_index(Index)
257 .map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
258
259 let EntryName = Entry.name().to_string();
260
261 let Stripped = match EntryName.strip_prefix(PAYLOAD_PREFIX) {
265 Some(Path) if !Path.is_empty() => Path,
266 _ => continue,
267 };
268
269 let Target = InstalledAt.join(Stripped);
273
274 let CanonicalInstall = InstalledAt.to_path_buf();
275
276 let RejectTraversal = !Target.starts_with(&CanonicalInstall);
277
278 if RejectTraversal {
279 return Err(InstallError::ArchiveRead(format!("zip-slip entry rejected: {}", EntryName)));
280 }
281
282 if Entry.is_dir() {
283 fs::create_dir_all(&Target).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
284
285 continue;
286 }
287
288 if let Some(Parent) = Target.parent() {
289 fs::create_dir_all(Parent).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
290 }
291
292 let mut Output = File::create(&Target).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
293
294 io::copy(&mut Entry, &mut Output).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
295
296 #[cfg(unix)]
303 {
304 use std::os::unix::fs::PermissionsExt;
305 let PermissionBits = Entry.unix_mode().map(|Mode| Mode & 0o777).unwrap_or(0);
306 let IsBinPath = Stripped
325 .split('/')
326 .any(|Segment| matches!(Segment, "bin" | "server" | "tools" | "omnisharp" | "adapter" | "native"));
327 let HasExecBit = PermissionBits & 0o111 != 0;
328 let LooksExecutable = if HasExecBit || IsBinPath {
329 true
330 } else {
331 let mut Probe = [0u8; 4];
332 match std::fs::File::open(&Target).and_then(|mut Handle| {
333 use std::io::Read as IoRead;
334 IoRead::read(&mut Handle, &mut Probe).map(|BytesRead| (BytesRead, Probe))
335 }) {
336 Ok((BytesRead, Bytes)) if BytesRead >= 2 => {
337 let Shebang = &Bytes[..2] == b"#!";
338 let ElfMagic = BytesRead >= 4 && &Bytes[..4] == b"\x7FELF";
339 let MachMagic = BytesRead >= 4
340 && matches!(
341 &Bytes[..4],
342 b"\xCF\xFA\xED\xFE"
343 | b"\xCE\xFA\xED\xFE" | b"\xFE\xED\xFA\xCF"
344 | b"\xFE\xED\xFA\xCE" | b"\xCA\xFE\xBA\xBE"
345 | b"\xBE\xBA\xFE\xCA"
346 );
347 Shebang || ElfMagic || MachMagic
348 },
349 _ => false,
350 }
351 };
352 let FinalMode = if LooksExecutable {
353 (PermissionBits | 0o755) & 0o755
354 } else {
355 (PermissionBits | 0o644) & 0o755
356 };
357 let _ = fs::set_permissions(&Target, fs::Permissions::from_mode(FinalMode));
358 }
359 }
360
361 Ok(())
362}
363
364#[cfg(unix)]
376pub fn HealExecutableBits(InstalledAt:&Path) {
377 use std::{io::Read, os::unix::fs::PermissionsExt};
378
379 fn IsBinSegment(Segment:&std::ffi::OsStr) -> bool {
380 let Some(Name) = Segment.to_str() else {
381 return false;
382 };
383 matches!(Name, "bin" | "server" | "tools" | "omnisharp" | "adapter" | "native")
384 }
385
386 fn LooksExecutable(Target:&Path, RelativeFromRoot:&Path) -> bool {
387 let IsBinPath = RelativeFromRoot
388 .components()
389 .any(|Component| IsBinSegment(Component.as_os_str()));
390 if IsBinPath {
391 return true;
392 }
393 let Ok(mut Handle) = std::fs::File::open(Target) else {
394 return false;
395 };
396 let mut Probe = [0u8; 4];
397 let Ok(BytesRead) = Handle.read(&mut Probe) else {
398 return false;
399 };
400 if BytesRead < 2 {
401 return false;
402 }
403 let Shebang = &Probe[..2] == b"#!";
404 let ElfMagic = BytesRead >= 4 && &Probe[..4] == b"\x7FELF";
405 let MachMagic = BytesRead >= 4
406 && matches!(
407 &Probe[..4],
408 b"\xCF\xFA\xED\xFE"
409 | b"\xCE\xFA\xED\xFE"
410 | b"\xFE\xED\xFA\xCF"
411 | b"\xFE\xED\xFA\xCE"
412 | b"\xCA\xFE\xBA\xBE"
413 | b"\xBE\xBA\xFE\xCA"
414 );
415 Shebang || ElfMagic || MachMagic
416 }
417
418 fn Walk(Dir:&Path, Root:&Path, Healed:&mut usize) {
419 let Ok(Entries) = std::fs::read_dir(Dir) else {
420 return;
421 };
422 for Entry in Entries.flatten() {
423 let Path = Entry.path();
424 let Ok(Metadata) = Entry.metadata() else {
425 continue;
426 };
427 if Metadata.is_dir() {
428 if Entry.file_name() == "node_modules" {
435 continue;
436 }
437 Walk(&Path, Root, Healed);
438 continue;
439 }
440 let Ok(Relative) = Path.strip_prefix(Root) else {
441 continue;
442 };
443 let Mode = Metadata.permissions().mode() & 0o777;
444 if Mode & 0o100 != 0 {
445 continue;
447 }
448 if !LooksExecutable(&Path, Relative) {
449 continue;
450 }
451 let Promoted = (Mode | 0o755) & 0o755;
452 if std::fs::set_permissions(&Path, std::fs::Permissions::from_mode(Promoted)).is_ok() {
453 *Healed += 1;
454 }
455 }
456 }
457
458 let mut Healed:usize = 0;
459 Walk(InstalledAt, InstalledAt, &mut Healed);
460 if Healed > 0 {
461 dev_log!(
462 "extensions",
463 "[VsixInstaller] Healed {} executable bit(s) under {}",
464 Healed,
465 InstalledAt.display()
466 );
467 }
468}
469
470fn BuildDescription(InstalledAt:&Path) -> Result<ExtensionDescriptionStateDTO, InstallError> {
471 let ManifestPath = InstalledAt.join("package.json");
472
473 let Raw = fs::read_to_string(&ManifestPath).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
474
475 let mut ManifestValue:Value =
476 serde_json::from_str(&Raw).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
477
478 let mut Description:ExtensionDescriptionStateDTO = serde_json::from_value(ManifestValue.clone())
479 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
480
481 Description.ExtensionLocation = serde_json::to_value(
482 url::Url::from_directory_path(InstalledAt)
483 .unwrap_or_else(|_| url::Url::parse("file:///").expect("file:/// is a valid URL")),
484 )
485 .unwrap_or(Value::Null);
486
487 if Description.Identifier == Value::Null || Description.Identifier == Value::Object(Default::default()) {
488 let Identifier = if Description.Publisher.is_empty() {
489 Description.Name.clone()
490 } else {
491 format!("{}.{}", Description.Publisher, Description.Name)
492 };
493
494 Description.Identifier = serde_json::json!({ "value": Identifier });
495 }
496
497 Description.IsBuiltin = false;
498
499 let _ = &mut ManifestValue;
502
503 Ok(Description)
504}