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