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
68 pub Version:String,
70
71 pub InstalledAt:PathBuf,
73
74 pub Description:ExtensionDescriptionStateDTO,
76}
77
78struct ManifestFacts {
80 Publisher:String,
81
82 Name:String,
83
84 Version:String,
85}
86
87#[derive(Debug, thiserror::Error)]
90pub enum InstallError {
91 #[error("VSIX path '{0}' does not exist")]
92 SourceMissing(PathBuf),
93
94 #[error("VSIX archive read failure: {0}")]
95 ArchiveRead(String),
96
97 #[error("VSIX manifest missing or unreadable: {0}")]
98 ManifestMissing(String),
99
100 #[error("VSIX manifest missing required field '{0}'")]
101 ManifestFieldMissing(&'static str),
102
103 #[error("Filesystem error during install: {0}")]
104 FilesystemIO(String),
105}
106
107const MANIFEST_ENTRY:&str = "extension/package.json";
108
109const PAYLOAD_PREFIX:&str = "extension/";
110
111pub fn InstallVsix(VsixPath:&Path, InstallRoot:&Path) -> Result<InstallOutcome, InstallError> {
115 if !VsixPath.exists() {
116 return Err(InstallError::SourceMissing(VsixPath.to_path_buf()));
117 }
118
119 let Facts = ReadManifestFacts(VsixPath)?;
120
121 let InstalledAt = InstallRoot.join(format!("{}.{}-{}", Facts.Publisher, Facts.Name, Facts.Version));
122
123 let Identifier = format!("{}.{}", Facts.Publisher, Facts.Name);
124
125 if InstalledAt.exists() {
131 if let Ok(Description) = BuildDescription(&InstalledAt) {
132 #[cfg(unix)]
142 HealExecutableBits(&InstalledAt);
143
144 dev_log!(
145 "extensions",
146 "[VsixInstaller] Reinstall no-op - '{}' v{} already present at {}",
147 Identifier,
148 Facts.Version,
149 InstalledAt.display()
150 );
151
152 return Ok(InstallOutcome { Identifier, Version:Facts.Version, InstalledAt, Description });
153 }
154
155 dev_log!(
157 "extensions",
158 "[VsixInstaller] Existing install at {} is unreadable - wiping and reinstalling",
159 InstalledAt.display()
160 );
161
162 fs::remove_dir_all(&InstalledAt).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
163 }
164
165 CreateParent(&InstalledAt)?;
166
167 ExtractPayload(VsixPath, &InstalledAt)?;
168
169 let Description = BuildDescription(&InstalledAt)?;
170
171 dev_log!(
172 "extensions",
173 "[VsixInstaller] Installed '{}' v{} at {}",
174 Identifier,
175 Facts.Version,
176 InstalledAt.display()
177 );
178
179 Ok(InstallOutcome { Identifier, Version:Facts.Version, InstalledAt, Description })
180}
181
182pub fn UninstallExtension(InstallDir:&Path) -> Result<(), InstallError> {
184 if !InstallDir.exists() {
185 dev_log!(
186 "extensions",
187 "[VsixInstaller] Uninstall skipped - {} already absent",
188 InstallDir.display()
189 );
190
191 return Ok(());
192 }
193
194 fs::remove_dir_all(InstallDir).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
195
196 dev_log!("extensions", "[VsixInstaller] Uninstalled {}", InstallDir.display());
197
198 Ok(())
199}
200
201fn ReadManifestFacts(VsixPath:&Path) -> Result<ManifestFacts, InstallError> {
204 let Manifest = ReadFullManifest(VsixPath)?;
205
206 let Publisher = ReadStringField(&Manifest, "publisher")?;
207
208 let Name = ReadStringField(&Manifest, "name")?;
209
210 let Version = ReadStringField(&Manifest, "version")?;
211
212 Ok(ManifestFacts { Publisher, Name, Version })
213}
214
215pub fn ReadFullManifest(VsixPath:&Path) -> Result<Value, InstallError> {
226 let Archive = File::open(VsixPath).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
227
228 let mut Archive = ZipArchive::new(Archive).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
229
230 let mut Entry = Archive
231 .by_name(MANIFEST_ENTRY)
232 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
233
234 let mut Raw = String::new();
235
236 Entry
237 .read_to_string(&mut Raw)
238 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
239
240 serde_json::from_str(&Raw).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))
241}
242
243fn ReadStringField(Manifest:&Value, Field:&'static str) -> Result<String, InstallError> {
244 Manifest
245 .get(Field)
246 .and_then(|Value| Value.as_str())
247 .filter(|Value| !Value.is_empty())
248 .map(str::to_owned)
249 .ok_or(InstallError::ManifestFieldMissing(Field))
250}
251
252fn CreateParent(InstalledAt:&Path) -> Result<(), InstallError> {
253 if let Some(Parent) = InstalledAt.parent() {
254 fs::create_dir_all(Parent).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
255 }
256
257 Ok(())
258}
259
260fn ExtractPayload(VsixPath:&Path, InstalledAt:&Path) -> Result<(), InstallError> {
261 let Archive = File::open(VsixPath).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
262
263 let mut Archive = ZipArchive::new(Archive).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
264
265 fs::create_dir_all(InstalledAt).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
266
267 for Index in 0..Archive.len() {
268 let mut Entry = Archive
269 .by_index(Index)
270 .map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
271
272 let EntryName = Entry.name().to_string();
273
274 let Stripped = match EntryName.strip_prefix(PAYLOAD_PREFIX) {
278 Some(Path) if !Path.is_empty() => Path,
279
280 _ => continue,
281 };
282
283 let Target = InstalledAt.join(Stripped);
287
288 let CanonicalInstall = InstalledAt.to_path_buf();
289
290 let RejectTraversal = !Target.starts_with(&CanonicalInstall);
291
292 if RejectTraversal {
293 return Err(InstallError::ArchiveRead(format!("zip-slip entry rejected: {}", EntryName)));
294 }
295
296 if Entry.is_dir() {
297 fs::create_dir_all(&Target).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
298
299 continue;
300 }
301
302 if let Some(Parent) = Target.parent() {
303 fs::create_dir_all(Parent).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
304 }
305
306 let mut Output = File::create(&Target).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
307
308 io::copy(&mut Entry, &mut Output).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
309
310 #[cfg(unix)]
317 {
318 use std::os::unix::fs::PermissionsExt;
319
320 let PermissionBits = Entry.unix_mode().map(|Mode| Mode & 0o777).unwrap_or(0);
321
322 let IsBinPath = Stripped
341 .split('/')
342 .any(|Segment| matches!(Segment, "bin" | "server" | "tools" | "omnisharp" | "adapter" | "native"));
343
344 let HasExecBit = PermissionBits & 0o111 != 0;
345
346 let LooksExecutable = if HasExecBit || IsBinPath {
347 true
348 } else {
349 let mut Probe = [0u8; 4];
350
351 match std::fs::File::open(&Target).and_then(|mut Handle| {
352 use std::io::Read as IoRead;
353 IoRead::read(&mut Handle, &mut Probe).map(|BytesRead| (BytesRead, Probe))
354 }) {
355 Ok((BytesRead, Bytes)) if BytesRead >= 2 => {
356 let Shebang = &Bytes[..2] == b"#!";
357
358 let ElfMagic = BytesRead >= 4 && &Bytes[..4] == b"\x7FELF";
359
360 let MachMagic = BytesRead >= 4
361 && matches!(
362 &Bytes[..4],
363 b"\xCF\xFA\xED\xFE"
364 | b"\xCE\xFA\xED\xFE" | b"\xFE\xED\xFA\xCF"
365 | b"\xFE\xED\xFA\xCE" | b"\xCA\xFE\xBA\xBE"
366 | b"\xBE\xBA\xFE\xCA"
367 );
368
369 Shebang || ElfMagic || MachMagic
370 },
371
372 _ => false,
373 }
374 };
375
376 let FinalMode = if LooksExecutable {
377 (PermissionBits | 0o755) & 0o755
378 } else {
379 (PermissionBits | 0o644) & 0o755
380 };
381
382 let _ = fs::set_permissions(&Target, fs::Permissions::from_mode(FinalMode));
383 }
384 }
385
386 Ok(())
387}
388
389#[cfg(unix)]
401pub fn HealExecutableBits(InstalledAt:&Path) {
402 use std::{io::Read, os::unix::fs::PermissionsExt};
403
404 fn IsBinSegment(Segment:&std::ffi::OsStr) -> bool {
405 let Some(Name) = Segment.to_str() else {
406 return false;
407 };
408
409 matches!(Name, "bin" | "server" | "tools" | "omnisharp" | "adapter" | "native")
410 }
411
412 fn LooksExecutable(Target:&Path, RelativeFromRoot:&Path) -> bool {
413 let IsBinPath = RelativeFromRoot
414 .components()
415 .any(|Component| IsBinSegment(Component.as_os_str()));
416
417 if IsBinPath {
418 return true;
419 }
420
421 let Ok(mut Handle) = std::fs::File::open(Target) else {
422 return false;
423 };
424
425 let mut Probe = [0u8; 4];
426
427 let Ok(BytesRead) = Handle.read(&mut Probe) else {
428 return false;
429 };
430
431 if BytesRead < 2 {
432 return false;
433 }
434
435 let Shebang = &Probe[..2] == b"#!";
436
437 let ElfMagic = BytesRead >= 4 && &Probe[..4] == b"\x7FELF";
438
439 let MachMagic = BytesRead >= 4
440 && matches!(
441 &Probe[..4],
442 b"\xCF\xFA\xED\xFE"
443 | b"\xCE\xFA\xED\xFE"
444 | b"\xFE\xED\xFA\xCF"
445 | b"\xFE\xED\xFA\xCE"
446 | b"\xCA\xFE\xBA\xBE"
447 | b"\xBE\xBA\xFE\xCA"
448 );
449
450 Shebang || ElfMagic || MachMagic
451 }
452
453 fn Walk(Dir:&Path, Root:&Path, Healed:&mut usize) {
454 let Ok(Entries) = std::fs::read_dir(Dir) else {
455 return;
456 };
457
458 for Entry in Entries.flatten() {
459 let Path = Entry.path();
460
461 let Ok(Metadata) = Entry.metadata() else {
462 continue;
463 };
464
465 if Metadata.is_dir() {
466 if Entry.file_name() == "node_modules" {
473 continue;
474 }
475
476 Walk(&Path, Root, Healed);
477
478 continue;
479 }
480
481 let Ok(Relative) = Path.strip_prefix(Root) else {
482 continue;
483 };
484
485 let Mode = Metadata.permissions().mode() & 0o777;
486
487 if Mode & 0o100 != 0 {
488 continue;
490 }
491
492 if !LooksExecutable(&Path, Relative) {
493 continue;
494 }
495
496 let Promoted = (Mode | 0o755) & 0o755;
497
498 if std::fs::set_permissions(&Path, std::fs::Permissions::from_mode(Promoted)).is_ok() {
499 *Healed += 1;
500 }
501 }
502 }
503
504 let mut Healed:usize = 0;
505
506 Walk(InstalledAt, InstalledAt, &mut Healed);
507
508 if Healed > 0 {
509 dev_log!(
510 "extensions",
511 "[VsixInstaller] Healed {} executable bit(s) under {}",
512 Healed,
513 InstalledAt.display()
514 );
515 }
516}
517
518fn BuildDescription(InstalledAt:&Path) -> Result<ExtensionDescriptionStateDTO, InstallError> {
519 let ManifestPath = InstalledAt.join("package.json");
520
521 let Raw = fs::read_to_string(&ManifestPath).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
522
523 let mut ManifestValue:Value =
524 serde_json::from_str(&Raw).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
525
526 let mut Description:ExtensionDescriptionStateDTO = serde_json::from_value(ManifestValue.clone())
527 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
528
529 Description.ExtensionLocation = serde_json::to_value(
530 url::Url::from_directory_path(InstalledAt)
531 .unwrap_or_else(|_| url::Url::parse("file:///").expect("file:/// is a valid URL")),
532 )
533 .unwrap_or(Value::Null);
534
535 if Description.Identifier == Value::Null || Description.Identifier == Value::Object(Default::default()) {
536 let Identifier = if Description.Publisher.is_empty() {
537 Description.Name.clone()
538 } else {
539 format!("{}.{}", Description.Publisher, Description.Name)
540 };
541
542 Description.Identifier = serde_json::json!({ "value": Identifier });
543 }
544
545 Description.IsBuiltin = false;
546
547 let _ = &mut ManifestValue;
550
551 Ok(Description)
552}