1mod fs;
4mod install;
5mod receipt;
6
7use std::path::PathBuf;
8
9use thiserror::Error;
10
11pub use install::Installer;
12pub use receipt::InstallReceipt;
13
14#[derive(Debug, Error)]
16pub enum InstallerError {
17 #[error(transparent)]
19 Io(#[from] std::io::Error),
20 #[error("artifact download failed: {0}")]
22 Download(String),
23 #[error("failed to walk directory tree: {0}")]
25 Walk(String),
26 #[error("install source does not exist: {0}")]
28 MissingSource(PathBuf),
29 #[error("failed to parse JSON file at {path}: {message}")]
31 Json { path: PathBuf, message: String },
32 #[error("install validation failed: {0}")]
34 Validation(String),
35 #[error(transparent)]
37 Archive(#[from] zip::result::ZipError),
38}
39
40#[cfg(test)]
41mod tests {
42 use std::error::Error;
43 use std::fs;
44
45 use tempfile::TempDir;
46 use vs_plugin_api::{InstallArtifact, InstallPlan, InstallSource};
47 use zip::ZipWriter;
48 use zip::write::SimpleFileOptions;
49
50 use super::Installer;
51
52 #[test]
53 fn install_should_rollback_when_validation_fails() -> Result<(), Box<dyn Error>> {
54 let temp_dir = TempDir::new()?;
55 let source = temp_dir.path().join("source");
56 fs::create_dir_all(&source)?;
57 fs::write(source.join(".vs-fail-install"), "")?;
58
59 let installer = Installer::new(temp_dir.path().join("home"));
60 let plan = InstallPlan {
61 plugin: String::from("nodejs"),
62 version: String::from("20.11.1"),
63 main: InstallArtifact {
64 name: String::from("nodejs"),
65 version: String::from("20.11.1"),
66 source: InstallSource::Directory { path: source },
67 note: None,
68 checksum: None,
69 },
70 additions: Vec::new(),
71 legacy_filenames: Vec::new(),
72 };
73
74 let error = match installer.install(&plan) {
75 Ok(_) => {
76 return Err(Box::new(std::io::Error::other(
77 "install unexpectedly succeeded",
78 )));
79 }
80 Err(error) => error,
81 };
82 assert!(error.to_string().contains("validation failed"));
83 assert!(!installer.install_dir("nodejs", "20.11.1").exists());
84 Ok(())
85 }
86
87 #[test]
88 fn install_should_preserve_flat_archive_layouts() -> Result<(), Box<dyn Error>> {
89 let temp_dir = TempDir::new()?;
90 let archive = write_zip(
91 &temp_dir,
92 "flat.zip",
93 &[("bin/node", b"#!/bin/sh\necho node\n".as_slice())],
94 )?;
95
96 let installer = Installer::new(temp_dir.path().join("home"));
97 let plan = install_plan_from_archive(&archive);
98 let installed = installer.install(&plan)?;
99
100 assert!(installed.main.path.join("bin/node").exists());
101 Ok(())
102 }
103
104 #[test]
105 fn install_should_collapse_single_wrapped_archive_root() -> Result<(), Box<dyn Error>> {
106 let temp_dir = TempDir::new()?;
107 let archive = write_zip(
108 &temp_dir,
109 "wrapped.zip",
110 &[("package/bin/node", b"#!/bin/sh\necho node\n".as_slice())],
111 )?;
112
113 let installer = Installer::new(temp_dir.path().join("home"));
114 let plan = install_plan_from_archive(&archive);
115 let installed = installer.install(&plan)?;
116
117 assert!(installed.main.path.join("bin/node").exists());
118 assert!(!installed.main.path.join("package").exists());
119 Ok(())
120 }
121
122 fn install_plan_from_archive(path: &std::path::Path) -> InstallPlan {
123 InstallPlan {
124 plugin: String::from("nodejs"),
125 version: String::from("20.11.1"),
126 main: InstallArtifact {
127 name: String::from("nodejs"),
128 version: String::from("20.11.1"),
129 source: InstallSource::File {
130 path: path.to_path_buf(),
131 },
132 note: None,
133 checksum: None,
134 },
135 additions: Vec::new(),
136 legacy_filenames: Vec::new(),
137 }
138 }
139
140 fn write_zip(
141 temp_dir: &TempDir,
142 file_name: &str,
143 entries: &[(&str, &[u8])],
144 ) -> Result<std::path::PathBuf, Box<dyn Error>> {
145 let path = temp_dir.path().join(file_name);
146 let file = fs::File::create(&path)?;
147 let mut zip = ZipWriter::new(file);
148 let options = SimpleFileOptions::default();
149
150 for (name, contents) in entries {
151 zip.start_file(name, options)?;
152 std::io::Write::write_all(&mut zip, contents)?;
153 }
154
155 zip.finish()?;
156 Ok(path)
157 }
158}