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