1#[cfg(windows)]
2use std::os::windows::process::CommandExt;
3use std::{
4 env,
5 ffi::OsString,
6 path::{Path, PathBuf},
7 process::Command,
8};
9
10use serde::{Deserialize, Serialize};
11
12use crate::{
13 build_file_tree_manifest, error::io_path, sha256_file, verify_file_tree, Error, FileDigest,
14 FileTreeManifest, Result, TreeVerification,
15};
16
17#[cfg(windows)]
18const CREATE_NO_WINDOW: u32 = 0x0800_0000;
19
20pub const DEFAULT_DIRECTORY_ALGORITHM: &str = "hdiffpatch-v5-dir-single-lzma2";
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct CreateDirectoryPatchOptions {
25 pub old_path: PathBuf,
26 pub new_path: PathBuf,
27 pub patch_path: PathBuf,
28 pub managed_paths: Vec<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub hdiffz_path: Option<PathBuf>,
31 #[serde(default)]
32 pub force: bool,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub step_size: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub compression: Option<String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub checksum: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub parallel_threads: Option<u16>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct ApplyDirectoryPatchOptions {
46 pub old_path: PathBuf,
47 pub patch_path: PathBuf,
48 pub output_path: PathBuf,
49 pub managed_paths: Vec<String>,
50 pub expected_tree: FileTreeManifest,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub hpatchz_path: Option<PathBuf>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
54 pub cache_size: Option<String>,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub parallel_threads: Option<u16>,
57 #[serde(default = "default_true")]
58 pub verify_checksums: bool,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct DirectoryPatchCreateResult {
64 pub old: FileTreeManifest,
65 pub new: FileTreeManifest,
66 pub patch: FileDigest,
67 pub algorithm: String,
68 pub tool: PathBuf,
69 pub stdout: String,
70 pub stderr: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct DirectoryPatchApplyResult {
76 pub patch: FileDigest,
77 pub output: TreeVerification,
78 pub tool: PathBuf,
79 pub stdout: String,
80 pub stderr: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub struct ToolOutput {
86 pub tool: PathBuf,
87 pub args: Vec<String>,
88 pub stdout: String,
89 pub stderr: String,
90}
91
92pub fn create_directory_patch(
93 options: &CreateDirectoryPatchOptions,
94) -> Result<DirectoryPatchCreateResult> {
95 if !options.old_path.is_dir() || !options.new_path.is_dir() {
96 return Err(Error::Message(
97 "directory patch inputs must both be directories".to_string(),
98 ));
99 }
100 let hdiffz = resolve_tool_path(options.hdiffz_path.as_deref(), "HDIFFZ_PATH", "hdiffz.exe")?;
101 if let Some(parent) = options.patch_path.parent() {
102 std::fs::create_dir_all(parent).map_err(|error| io_path(parent, error))?;
103 }
104
105 let mut args = Vec::<OsString>::new();
106 if options.force {
107 args.push("-f".into());
108 }
109 args.push("-m-0".into());
110 args.push("-block-0".into());
111 args.push(format!("-SD-{}", options.step_size.as_deref().unwrap_or("256k")).into());
112 args.push(
113 format!(
114 "-c-{}",
115 options.compression.as_deref().unwrap_or("lzma2-9-64m")
116 )
117 .into(),
118 );
119 args.push(format!("-C-{}", options.checksum.as_deref().unwrap_or("xxh128")).into());
120 args.push(format!("-p-{}", options.parallel_threads.unwrap_or(4)).into());
121 args.push(options.old_path.as_os_str().to_os_string());
122 args.push(options.new_path.as_os_str().to_os_string());
123 args.push(options.patch_path.as_os_str().to_os_string());
124
125 let output = run_tool(&hdiffz, &args)?;
126 Ok(DirectoryPatchCreateResult {
127 old: build_file_tree_manifest(&options.old_path, &options.managed_paths)?,
128 new: build_file_tree_manifest(&options.new_path, &options.managed_paths)?,
129 patch: sha256_file(&options.patch_path)?,
130 algorithm: DEFAULT_DIRECTORY_ALGORITHM.to_string(),
131 tool: hdiffz,
132 stdout: output.stdout,
133 stderr: output.stderr,
134 })
135}
136
137pub fn apply_directory_patch(
138 options: &ApplyDirectoryPatchOptions,
139) -> Result<DirectoryPatchApplyResult> {
140 if !options.old_path.is_dir() {
141 return Err(Error::Message(format!(
142 "directory patch source is not a directory: {}",
143 options.old_path.display()
144 )));
145 }
146 if options.output_path.exists() {
147 return Err(Error::Message(format!(
148 "directory patch output must not already exist: {}",
149 options.output_path.display()
150 )));
151 }
152 let hpatchz = resolve_tool_path(
153 options.hpatchz_path.as_deref(),
154 "HPATCHZ_PATH",
155 "hpatchz.exe",
156 )?;
157 if let Some(parent) = options.output_path.parent() {
158 std::fs::create_dir_all(parent).map_err(|error| io_path(parent, error))?;
159 }
160
161 let mut args = Vec::<OsString>::new();
162 if options.verify_checksums {
163 args.push("-C-all".into());
164 }
165 args.push(format!("-s-{}", options.cache_size.as_deref().unwrap_or("64m")).into());
166 args.push(format!("-p-{}", options.parallel_threads.unwrap_or(4)).into());
167 args.push(options.old_path.as_os_str().to_os_string());
168 args.push(options.patch_path.as_os_str().to_os_string());
169 args.push(options.output_path.as_os_str().to_os_string());
170
171 let output = run_tool(&hpatchz, &args)?;
172 let verification = verify_file_tree(
173 &options.output_path,
174 &options.managed_paths,
175 &options.expected_tree,
176 )?;
177 Ok(DirectoryPatchApplyResult {
178 patch: sha256_file(&options.patch_path)?,
179 output: verification,
180 tool: hpatchz,
181 stdout: output.stdout,
182 stderr: output.stderr,
183 })
184}
185
186pub fn inspect_diff(
187 diff_path: impl AsRef<Path>,
188 hpatchz_path: Option<&Path>,
189) -> Result<ToolOutput> {
190 let hpatchz = resolve_tool_path(hpatchz_path, "HPATCHZ_PATH", "hpatchz.exe")?;
191 let args = vec![
192 "-info".into(),
193 diff_path.as_ref().as_os_str().to_os_string(),
194 ];
195 let output = run_tool(&hpatchz, &args)?;
196 Ok(ToolOutput {
197 tool: hpatchz,
198 args: args.iter().map(os_to_string).collect(),
199 stdout: output.stdout,
200 stderr: output.stderr,
201 })
202}
203
204pub fn resolve_tool_path(
205 explicit: Option<&Path>,
206 env_var: &str,
207 default_name: &str,
208) -> Result<PathBuf> {
209 let mut candidates = Vec::<PathBuf>::new();
210 if let Some(explicit) = explicit {
211 candidates.push(explicit.to_path_buf());
212 }
213 if let Ok(from_env) = env::var(env_var) {
214 if !from_env.trim().is_empty() {
215 candidates.push(PathBuf::from(from_env));
216 }
217 }
218 if let Ok(current_exe) = env::current_exe() {
219 if let Some(parent) = current_exe.parent() {
220 candidates.push(parent.join(default_name));
221 }
222 }
223 if let Ok(current_dir) = env::current_dir() {
224 candidates.push(current_dir.join(default_name));
225 }
226 candidates.extend(path_candidates(default_name));
227
228 for candidate in candidates {
229 if candidate.is_file() {
230 return Ok(candidate);
231 }
232 }
233 Err(Error::ToolNotFound {
234 tool: default_name.to_string(),
235 })
236}
237
238struct CapturedOutput {
239 stdout: String,
240 stderr: String,
241}
242
243fn run_tool(program: &Path, args: &[OsString]) -> Result<CapturedOutput> {
244 let mut command = Command::new(program);
245 #[cfg(windows)]
246 command.creation_flags(CREATE_NO_WINDOW);
247 let output = command
248 .args(args)
249 .output()
250 .map_err(|error| io_path(program, error))?;
251 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
252 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
253 if !output.status.success() {
254 return Err(Error::ProcessFailed {
255 program: program.to_path_buf(),
256 args: args.iter().map(os_to_string).collect(),
257 status: output.status,
258 stdout,
259 stderr,
260 });
261 }
262 Ok(CapturedOutput { stdout, stderr })
263}
264
265fn path_candidates(default_name: &str) -> Vec<PathBuf> {
266 let Some(paths) = env::var_os("PATH") else {
267 return Vec::new();
268 };
269 env::split_paths(&paths)
270 .flat_map(|path| {
271 let direct = path.join(default_name);
272 #[cfg(windows)]
273 {
274 let mut names = vec![direct];
275 if !default_name.ends_with(".exe") {
276 names.push(path.join(format!("{default_name}.exe")));
277 }
278 names
279 }
280 #[cfg(not(windows))]
281 {
282 vec![direct]
283 }
284 })
285 .collect()
286}
287
288fn os_to_string(value: &OsString) -> String {
289 value.to_string_lossy().to_string()
290}
291
292fn default_true() -> bool {
293 true
294}