Mountain/ExtensionManagement/
VsixInstaller.rs1use std::{
50 fs::{self, File},
51 io::{self, Read},
52 path::{Path, PathBuf},
53};
54
55use serde_json::Value;
56use zip::ZipArchive;
57
58use crate::{ApplicationState::DTO::ExtensionDescriptionStateDTO::ExtensionDescriptionStateDTO, dev_log};
59
60#[derive(Debug)]
62pub struct InstallOutcome {
63 pub Identifier:String,
65
66 pub Version:String,
68
69 pub InstalledAt:PathBuf,
71
72 pub Description:ExtensionDescriptionStateDTO,
74}
75
76struct ManifestFacts {
78 Publisher:String,
79
80 Name:String,
81
82 Version:String,
83}
84
85#[derive(Debug, thiserror::Error)]
88pub enum InstallError {
89 #[error("VSIX path '{0}' does not exist")]
90 SourceMissing(PathBuf),
91
92 #[error("VSIX archive read failure: {0}")]
93 ArchiveRead(String),
94
95 #[error("VSIX manifest missing or unreadable: {0}")]
96 ManifestMissing(String),
97
98 #[error("VSIX manifest missing required field '{0}'")]
99 ManifestFieldMissing(&'static str),
100
101 #[error("Filesystem error during install: {0}")]
102 FilesystemIO(String),
103}
104
105const MANIFEST_ENTRY:&str = "extension/package.json";
106
107const PAYLOAD_PREFIX:&str = "extension/";
108
109pub fn InstallVsix(VsixPath:&Path, InstallRoot:&Path) -> Result<InstallOutcome, InstallError> {
113 if !VsixPath.exists() {
114 return Err(InstallError::SourceMissing(VsixPath.to_path_buf()));
115 }
116
117 let Facts = ReadManifestFacts(VsixPath)?;
118
119 let InstalledAt = InstallRoot.join(format!("{}.{}-{}", Facts.Publisher, Facts.Name, Facts.Version));
120
121 let Identifier = format!("{}.{}", Facts.Publisher, Facts.Name);
122
123 if InstalledAt.exists() {
129 if let Ok(Description) = BuildDescription(&InstalledAt) {
130 #[cfg(unix)]
140 HealExecutableBits(&InstalledAt);
141
142 dev_log!(
143 "extensions",
144 "[VsixInstaller] Reinstall no-op - '{}' v{} already present at {}",
145 Identifier,
146 Facts.Version,
147 InstalledAt.display()
148 );
149
150 return Ok(InstallOutcome { Identifier, Version:Facts.Version, InstalledAt, Description });
151 }
152
153 dev_log!(
155 "extensions",
156 "[VsixInstaller] Existing install at {} is unreadable - wiping and reinstalling",
157 InstalledAt.display()
158 );
159
160 fs::remove_dir_all(&InstalledAt).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
161 }
162
163 CreateParent(&InstalledAt)?;
164
165 ExtractPayload(VsixPath, &InstalledAt)?;
166
167 let Description = BuildDescription(&InstalledAt)?;
168
169 dev_log!(
170 "extensions",
171 "[VsixInstaller] Installed '{}' v{} at {}",
172 Identifier,
173 Facts.Version,
174 InstalledAt.display()
175 );
176
177 Ok(InstallOutcome { Identifier, Version:Facts.Version, InstalledAt, Description })
178}
179
180pub fn UninstallExtension(InstallDir:&Path) -> Result<(), InstallError> {
182 if !InstallDir.exists() {
183 dev_log!(
184 "extensions",
185 "[VsixInstaller] Uninstall skipped - {} already absent",
186 InstallDir.display()
187 );
188
189 return Ok(());
190 }
191
192 fs::remove_dir_all(InstallDir).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
193
194 dev_log!("extensions", "[VsixInstaller] Uninstalled {}", InstallDir.display());
195
196 Ok(())
197}
198
199fn ReadManifestFacts(VsixPath:&Path) -> Result<ManifestFacts, InstallError> {
202 let Manifest = ReadFullManifest(VsixPath)?;
203
204 let Publisher = ReadStringField(&Manifest, "publisher")?;
205
206 let Name = ReadStringField(&Manifest, "name")?;
207
208 let Version = ReadStringField(&Manifest, "version")?;
209
210 Ok(ManifestFacts { Publisher, Name, Version })
211}
212
213pub fn ReadFullManifest(VsixPath:&Path) -> Result<Value, InstallError> {
224 let Archive = File::open(VsixPath).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
225
226 let mut Archive = ZipArchive::new(Archive).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
227
228 let mut Entry = Archive
229 .by_name(MANIFEST_ENTRY)
230 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
231
232 let mut Raw = String::new();
233
234 Entry
235 .read_to_string(&mut Raw)
236 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
237
238 serde_json::from_str(&Raw).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))
239}
240
241fn ReadStringField(Manifest:&Value, Field:&'static str) -> Result<String, InstallError> {
242 Manifest
243 .get(Field)
244 .and_then(|Value| Value.as_str())
245 .filter(|Value| !Value.is_empty())
246 .map(str::to_owned)
247 .ok_or(InstallError::ManifestFieldMissing(Field))
248}
249
250fn CreateParent(InstalledAt:&Path) -> Result<(), InstallError> {
251 if let Some(Parent) = InstalledAt.parent() {
252 fs::create_dir_all(Parent).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
253 }
254
255 Ok(())
256}
257
258fn ExtractPayload(VsixPath:&Path, InstalledAt:&Path) -> Result<(), InstallError> {
259 let Archive = File::open(VsixPath).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
260
261 let mut Archive = ZipArchive::new(Archive).map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
262
263 fs::create_dir_all(InstalledAt).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
264
265 for Index in 0..Archive.len() {
266 let mut Entry = Archive
267 .by_index(Index)
268 .map_err(|Error| InstallError::ArchiveRead(Error.to_string()))?;
269
270 let EntryName = Entry.name().to_string();
271
272 let Stripped = match EntryName.strip_prefix(PAYLOAD_PREFIX) {
276 Some(Path) if !Path.is_empty() => Path,
277
278 _ => continue,
279 };
280
281 let Target = InstalledAt.join(Stripped);
285
286 let CanonicalInstall = InstalledAt.to_path_buf();
287
288 let RejectTraversal = !Target.starts_with(&CanonicalInstall);
289
290 if RejectTraversal {
291 return Err(InstallError::ArchiveRead(format!("zip-slip entry rejected: {}", EntryName)));
292 }
293
294 if Entry.is_dir() {
295 fs::create_dir_all(&Target).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
296
297 continue;
298 }
299
300 if let Some(Parent) = Target.parent() {
301 fs::create_dir_all(Parent).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
302 }
303
304 let mut Output = File::create(&Target).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
305
306 io::copy(&mut Entry, &mut Output).map_err(|Error| InstallError::FilesystemIO(Error.to_string()))?;
307
308 #[cfg(unix)]
315 {
316 use std::os::unix::fs::PermissionsExt;
317
318 let PermissionBits = Entry.unix_mode().map(|Mode| Mode & 0o777).unwrap_or(0);
319
320 let IsBinPath = Stripped
339 .split('/')
340 .any(|Segment| matches!(Segment, "bin" | "server" | "tools" | "omnisharp" | "adapter" | "native"));
341
342 let HasExecBit = PermissionBits & 0o111 != 0;
343
344 let LooksExecutable = if HasExecBit || IsBinPath {
345 true
346 } else {
347 let mut Probe = [0u8; 4];
348
349 match std::fs::File::open(&Target).and_then(|mut Handle| {
350 use std::io::Read as IoRead;
351
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}