DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_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 IoRead::read(&mut Handle, &mut Probe).map(|BytesRead| (BytesRead, Probe))
352 }) {
353 Ok((BytesRead, Bytes)) if BytesRead >= 2 => {
354 let Shebang = &Bytes[..2] == b"#!";
355
356 let ElfMagic = BytesRead >= 4 && &Bytes[..4] == b"\x7FELF";
357
358 let MachMagic = BytesRead >= 4
359 && matches!(
360 &Bytes[..4],
361 b"\xCF\xFA\xED\xFE"
362 | b"\xCE\xFA\xED\xFE" | b"\xFE\xED\xFA\xCF"
363 | b"\xFE\xED\xFA\xCE" | b"\xCA\xFE\xBA\xBE"
364 | b"\xBE\xBA\xFE\xCA"
365 );
366
367 Shebang || ElfMagic || MachMagic
368 },
369
370 _ => false,
371 }
372 };
373
374 let FinalMode = if LooksExecutable {
375 (PermissionBits | 0o755) & 0o755
376 } else {
377 (PermissionBits | 0o644) & 0o755
378 };
379
380 let _ = fs::set_permissions(&Target, fs::Permissions::from_mode(FinalMode));
381 }
382 }
383
384 Ok(())
385}
386
387#[cfg(unix)]
399pub fn HealExecutableBits(InstalledAt:&Path) {
400 use std::{io::Read, os::unix::fs::PermissionsExt};
401
402 fn IsBinSegment(Segment:&std::ffi::OsStr) -> bool {
403 let Some(Name) = Segment.to_str() else {
404 return false;
405 };
406
407 matches!(Name, "bin" | "server" | "tools" | "omnisharp" | "adapter" | "native")
408 }
409
410 fn LooksExecutable(Target:&Path, RelativeFromRoot:&Path) -> bool {
411 let IsBinPath = RelativeFromRoot
412 .components()
413 .any(|Component| IsBinSegment(Component.as_os_str()));
414
415 if IsBinPath {
416 return true;
417 }
418
419 let Ok(mut Handle) = std::fs::File::open(Target) else {
420 return false;
421 };
422
423 let mut Probe = [0u8; 4];
424
425 let Ok(BytesRead) = Handle.read(&mut Probe) else {
426 return false;
427 };
428
429 if BytesRead < 2 {
430 return false;
431 }
432
433 let Shebang = &Probe[..2] == b"#!";
434
435 let ElfMagic = BytesRead >= 4 && &Probe[..4] == b"\x7FELF";
436
437 let MachMagic = BytesRead >= 4
438 && matches!(
439 &Probe[..4],
440 b"\xCF\xFA\xED\xFE"
441 | b"\xCE\xFA\xED\xFE"
442 | b"\xFE\xED\xFA\xCF"
443 | b"\xFE\xED\xFA\xCE"
444 | b"\xCA\xFE\xBA\xBE"
445 | b"\xBE\xBA\xFE\xCA"
446 );
447
448 Shebang || ElfMagic || MachMagic
449 }
450
451 fn Walk(Dir:&Path, Root:&Path, Healed:&mut usize) {
452 let Ok(Entries) = std::fs::read_dir(Dir) else {
453 return;
454 };
455
456 for Entry in Entries.flatten() {
457 let Path = Entry.path();
458
459 let Ok(Metadata) = Entry.metadata() else {
460 continue;
461 };
462
463 if Metadata.is_dir() {
464 if Entry.file_name() == "node_modules" {
471 continue;
472 }
473
474 Walk(&Path, Root, Healed);
475
476 continue;
477 }
478
479 let Ok(Relative) = Path.strip_prefix(Root) else {
480 continue;
481 };
482
483 let Mode = Metadata.permissions().mode() & 0o777;
484
485 if Mode & 0o100 != 0 {
486 continue;
488 }
489
490 if !LooksExecutable(&Path, Relative) {
491 continue;
492 }
493
494 let Promoted = (Mode | 0o755) & 0o755;
495
496 if std::fs::set_permissions(&Path, std::fs::Permissions::from_mode(Promoted)).is_ok() {
497 *Healed += 1;
498 }
499 }
500 }
501
502 let mut Healed:usize = 0;
503
504 Walk(InstalledAt, InstalledAt, &mut Healed);
505
506 if Healed > 0 {
507 dev_log!(
508 "extensions",
509 "[VsixInstaller] Healed {} executable bit(s) under {}",
510 Healed,
511 InstalledAt.display()
512 );
513 }
514}
515
516fn BuildDescription(InstalledAt:&Path) -> Result<ExtensionDescriptionStateDTO, InstallError> {
517 let ManifestPath = InstalledAt.join("package.json");
518
519 let Raw = fs::read_to_string(&ManifestPath).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
520
521 let mut ManifestValue:Value =
522 serde_json::from_str(&Raw).map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
523
524 let mut Description:ExtensionDescriptionStateDTO = serde_json::from_value(ManifestValue.clone())
525 .map_err(|Error| InstallError::ManifestMissing(Error.to_string()))?;
526
527 Description.ExtensionLocation = serde_json::to_value(
528 url::Url::from_directory_path(InstalledAt)
529 .unwrap_or_else(|_| url::Url::parse("file:///").expect("file:/// is a valid URL")),
530 )
531 .unwrap_or(Value::Null);
532
533 if Description.Identifier == Value::Null || Description.Identifier == Value::Object(Default::default()) {
534 let Identifier = if Description.Publisher.is_empty() {
535 Description.Name.clone()
536 } else {
537 format!("{}.{}", Description.Publisher, Description.Name)
538 };
539
540 Description.Identifier = serde_json::json!({ "value": Identifier });
541 }
542
543 Description.IsBuiltin = false;
544
545 let _ = &mut ManifestValue;
548
549 Ok(Description)
550}