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