History

Merge pull request #2134 from EliahKagan/user-program-files

Extend `ALTERNATIVE_LOCATIONS` for per-user installations

Eliah Kagan committed 1 year ago

Commit 2fffc502 parents e365244 7e77d404 files · +396 −57Browse files

Unified · Split

4 files
  1. Cargo.lock +0 −10
  2. gix-path/Cargo.toml +5 −2
  3. gix-path/src/env/git/mod.rs +18 −2
  4. gix-path/src/env/git/tests.rs +373 −43
Cargo.lock+0−10
@@ -2134,7 +2134,6 @@ dependencies = [ · 2133 unmodified lines
2134 "gix-trace",2134 "gix-trace",
2135 "gix-validate",2135 "gix-validate",
2136 "home",2136 "home",
2137 "known-folders",
2138 "once_cell",2137 "once_cell",
2139 "serial_test",2138 "serial_test",
2140 "thiserror 2.0.12",2139 "thiserror 2.0.12",
@@ -3220,15 +3219,6 @@ dependencies = [ · 1079 unmodified lines
3220 "rayon",3219 "rayon",
3221]3220]
3222 3221
3223[[package]]
3224name = "known-folders"
3225version = "1.3.1"
3226source = "registry+https://github.com/rust-lang/crates.io-index"
3227checksum = "c644f4623d1c55eb60a9dac35e0858a59f982fb87db6ce34c872372b0a5b728f"
3228dependencies = [
3229 "windows-sys 0.60.2",
3230]
3231
3232[[package]]3222[[package]]
3233name = "kstring"3223name = "kstring"
3234version = "2.0.2"3224version = "2.0.2"
gix-path/Cargo.toml+5−2
@@ -29,6 +29,9 @@ gix-testtools = { path = "../tests/tools" } · 28 unmodified lines
29serial_test = { version = "3.1.0", default-features = false }29serial_test = { version = "3.1.0", default-features = false }
30 30
31[target.'cfg(windows)'.dev-dependencies]31[target.'cfg(windows)'.dev-dependencies]
32known-folders = "1.3.1"32windows = { version = "0.61.3", features = [
33windows = { version = "0.61.3", features = ["Win32_System_Threading"] }33 "Win32_System_Com",
34 "Win32_System_Threading",
35 "Win32_UI_Shell",
36] }
34winreg = "0.55.0"37winreg = "0.55.0"
gix-path/src/env/git/mod.rs+18−2
@@ -38,13 +38,21 @@ where · 37 unmodified lines
38 // known. So the situation where a process only passes down `ProgramFiles` sometimes happens.38 // known. So the situation where a process only passes down `ProgramFiles` sometimes happens.
39 let varname_current = "ProgramFiles";39 let varname_current = "ProgramFiles";
40 40
41 // Should give the user's local application data path on any system. If a user program files
42 // directory exists for this user, then it should be the `Programs` subdirectory of this. If it
43 // doesn't exist, or on a future or extremely strangely configured Windows setup where it is
44 // somewhere else, it should still be safe to attempt to use it. (This differs from global
45 // program files paths, which are usually subdirectories of the root of the system drive, which
46 // limited user accounts can usually create their own arbitrarily named directories inside.)
47 let varname_user_appdata_local = "LocalAppData";
48
41 // 64-bit relative bin dirs. So far, this is always `mingw64` or `clangarm64`, not `urct64` or49 // 64-bit relative bin dirs. So far, this is always `mingw64` or `clangarm64`, not `urct64` or
42 // `clang64`. We check `clangarm64` before `mingw64`, because in the strange case that both are50 // `clang64`. We check `clangarm64` before `mingw64`, because in the strange case that both are
43 // available, we don't want to skip over a native ARM64 executable for an emulated x86_64 one.51 // available, we don't want to skip over a native ARM64 executable for an emulated x86_64 one.
44 let suffixes_64 = [r"Git\clangarm64\bin", r"Git\mingw64\bin"].as_slice();52 let suffixes_64 = &[r"Git\clangarm64\bin", r"Git\mingw64\bin"][..];
45 53
46 // 32-bit relative bin dirs. So far, this is only ever `mingw32`, not `clang32`.54 // 32-bit relative bin dirs. So far, this is only ever `mingw32`, not `clang32`.
47 let suffixes_32 = [r"Git\mingw32\bin"].as_slice();55 let suffixes_32 = &[r"Git\mingw32\bin"][..];
48 56
49 // Whichever of the 64-bit or 32-bit relative bin better matches this process's architecture.57 // Whichever of the 64-bit or 32-bit relative bin better matches this process's architecture.
50 // Unlike the system architecture, the process architecture is always known at compile time.58 // Unlike the system architecture, the process architecture is always known at compile time.
@@ -53,7 +61,15 @@ where · 2 unmodified lines
53 #[cfg(target_pointer_width = "32")]61 #[cfg(target_pointer_width = "32")]
54 let suffixes_current = suffixes_32;62 let suffixes_current = suffixes_32;
55 63
64 // Bin dirs relative to a user's local application data directory. We try each architecture.
65 let suffixes_user = &[
66 r"Programs\Git\clangarm64\bin",
67 r"Programs\Git\mingw64\bin",
68 r"Programs\Git\mingw32\bin",
69 ][..];
70
56 let rules = [71 let rules = [
72 (varname_user_appdata_local, suffixes_user),
57 (varname_64bit, suffixes_64),73 (varname_64bit, suffixes_64),
58 (varname_x86, suffixes_32),74 (varname_x86, suffixes_32),
59 (varname_current, suffixes_current),75 (varname_current, suffixes_current),
gix-path/src/env/git/tests.rs+373−43
@@ -3,15 +3,24 @@ use std::path::Path; · 2 unmodified lines
3#[cfg(windows)]3#[cfg(windows)]
4mod locations {4mod locations {
5 use std::{5 use std::{
6 ffi::{OsStr, OsString},6 ffi::{c_void, OsStr, OsString},
7 io::ErrorKind,7 io::ErrorKind,
8 os::windows::ffi::OsStringExt,
8 path::{Path, PathBuf},9 path::{Path, PathBuf},
9 };10 };
10 11
11 use known_folders::{get_known_folder_path, KnownFolder};
12 use windows::{12 use windows::{
13 core::{Result as WindowsResult, BOOL},13 core::{Result as WindowsResult, BOOL, GUID, PWSTR},
14 Win32::System::Threading::{GetCurrentProcess, IsWow64Process},14 Win32::{
15 System::{
16 Com::CoTaskMemFree,
17 Threading::{GetCurrentProcess, IsWow64Process},
18 },
19 UI::Shell::{
20 FOLDERID_LocalAppData, FOLDERID_ProgramFiles, FOLDERID_ProgramFilesX86, FOLDERID_UserProgramFiles,
21 SHGetKnownFolderPath, KF_FLAG_DEFAULT, KF_FLAG_DONT_VERIFY, KNOWN_FOLDER_FLAG,
22 },
23 },
15 };24 };
16 use winreg::{25 use winreg::{
17 enums::{HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE},26 enums::{HKEY_LOCAL_MACHINE, KEY_QUERY_VALUE},
@@ -55,7 +64,7 @@ mod locations { · 37 unmodified lines
55 }64 }
56 65
57 #[test]66 #[test]
58 fn locations_under_program_files_ordinary_values_current_var_only() {67 fn locations_under_program_files_global_only_ordinary_values_current_var_only() {
59 assert_eq!(68 assert_eq!(
60 locations_from!(69 locations_from!(
61 "ProgramFiles" => r"C:\Program Files",70 "ProgramFiles" => r"C:\Program Files",
@@ -72,7 +81,7 @@ mod locations { · 10 unmodified lines
72 }81 }
73 82
74 #[test]83 #[test]
75 fn locations_under_program_files_ordinary_values_all_vars() {84 fn locations_under_program_files_global_only_ordinary_values_all_vars() {
76 assert_eq!(85 assert_eq!(
77 locations_from!(86 locations_from!(
78 "ProgramFiles" => {87 "ProgramFiles" => {
@@ -94,7 +103,7 @@ mod locations { · 15 unmodified lines
94 }103 }
95 104
96 #[test]105 #[test]
97 fn locations_under_program_files_strange_values_all_vars_distinct() {106 fn locations_under_program_files_global_only_strange_values_all_vars_distinct() {
98 assert_eq!(107 assert_eq!(
99 locations_from!(108 locations_from!(
100 "ProgramFiles" => r"X:\cur\rent",109 "ProgramFiles" => r"X:\cur\rent",
@@ -121,7 +130,7 @@ mod locations { · 20 unmodified lines
121 }130 }
122 131
123 #[test]132 #[test]
124 fn locations_under_program_files_strange_values_64bit_var_only() {133 fn locations_under_program_files_global_only_strange_values_64bit_var_only() {
125 assert_eq!(134 assert_eq!(
126 locations_from!(135 locations_from!(
127 "ProgramW6432" => r"Z:\wi\de",136 "ProgramW6432" => r"Z:\wi\de",
@@ -131,7 +140,7 @@ mod locations { · 3 unmodified lines
131 }140 }
132 141
133 #[test]142 #[test]
134 fn locations_under_program_files_strange_values_all_vars_path_cruft() {143 fn locations_under_program_files_global_only_strange_values_all_vars_path_cruft() {
135 assert_eq!(144 assert_eq!(
136 locations_from!(145 locations_from!(
137 "ProgramFiles" => r"Z:/wi//de/",146 "ProgramFiles" => r"Z:/wi//de/",
@@ -156,7 +165,7 @@ mod locations { · 18 unmodified lines
156 }165 }
157 166
158 #[test]167 #[test]
159 fn locations_under_program_files_strange_values_some_relative() {168 fn locations_under_program_files_global_only_strange_values_some_relative() {
160 assert_eq!(169 assert_eq!(
161 locations_from!(170 locations_from!(
162 "ProgramFiles" => r"foo\bar",171 "ProgramFiles" => r"foo\bar",
@@ -167,6 +176,235 @@ mod locations { · 4 unmodified lines
167 );176 );
168 }177 }
169 178
179 #[test]
180 fn locations_under_program_files_local_only_ordinary_value() {
181 assert_eq!(
182 locations_from!(
183 "LocalAppData" => r"C:\Users\alice\AppData\Local",
184 ),
185 pathbuf_vec![
186 r"C:\Users\alice\AppData\Local\Programs\Git\clangarm64\bin",
187 r"C:\Users\alice\AppData\Local\Programs\Git\mingw64\bin",
188 r"C:\Users\alice\AppData\Local\Programs\Git\mingw32\bin",
189 ],
190 );
191 }
192
193 #[test]
194 fn locations_under_program_files_local_only_strange_value_path_cruft() {
195 assert_eq!(
196 locations_from!(
197 "LocalAppData" => r"\\.\Q:\Documents and Settings/bob\/weird\.\sub//dir",
198 ),
199 pathbuf_vec![
200 r"\\.\Q:\Documents and Settings\bob\weird\sub\dir\Programs\Git\clangarm64\bin",
201 r"\\.\Q:\Documents and Settings\bob\weird\sub\dir\Programs\Git\mingw64\bin",
202 r"\\.\Q:\Documents and Settings\bob\weird\sub\dir\Programs\Git\mingw32\bin",
203 ],
204 );
205 }
206
207 #[test]
208 fn locations_under_program_files_local_only_strange_value_empty() {
209 assert_eq!(
210 locations_from!(
211 "LocalAppData" => "",
212 ),
213 Vec::<PathBuf>::new(),
214 );
215 }
216
217 #[test]
218 fn locations_under_program_files_local_only_strange_value_relative_nonempty() {
219 assert_eq!(
220 locations_from!(
221 "LocalAppData" => r"AppData\Local",
222 ),
223 Vec::<PathBuf>::new(),
224 );
225 }
226
227 #[test]
228 fn locations_under_program_files_local_and_global_ordinary_values_limited_vars() {
229 assert_eq!(
230 locations_from!(
231 "LocalAppData" => r"C:\Users\alice\AppData\Local",
232 "ProgramFiles" => r"C:\Program Files",
233 ),
234 if cfg!(target_pointer_width = "64") {
235 pathbuf_vec![
236 r"C:\Users\alice\AppData\Local\Programs\Git\clangarm64\bin",
237 r"C:\Users\alice\AppData\Local\Programs\Git\mingw64\bin",
238 r"C:\Users\alice\AppData\Local\Programs\Git\mingw32\bin",
239 r"C:\Program Files\Git\clangarm64\bin",
240 r"C:\Program Files\Git\mingw64\bin",
241 ]
242 } else {
243 pathbuf_vec![
244 r"C:\Users\alice\AppData\Local\Programs\Git\clangarm64\bin",
245 r"C:\Users\alice\AppData\Local\Programs\Git\mingw64\bin",
246 r"C:\Users\alice\AppData\Local\Programs\Git\mingw32\bin",
247 r"C:\Program Files\Git\mingw32\bin",
248 ]
249 },
250 );
251 }
252
253 #[test]
254 fn locations_under_program_files_local_and_global_ordinary_values_all_vars() {
255 assert_eq!(
256 locations_from!(
257 "LocalAppData" => r"C:\Users\bob\AppData\Local",
258 "ProgramFiles" => {
259 if cfg!(target_pointer_width = "64") {
260 r"C:\Program Files"
261 } else {
262 r"C:\Program Files (x86)"
263 }
264 },
265 "ProgramFiles(x86)" => r"C:\Program Files (x86)",
266 "ProgramW6432" => r"C:\Program Files",
267 ),
268 pathbuf_vec![
269 r"C:\Users\bob\AppData\Local\Programs\Git\clangarm64\bin",
270 r"C:\Users\bob\AppData\Local\Programs\Git\mingw64\bin",
271 r"C:\Users\bob\AppData\Local\Programs\Git\mingw32\bin",
272 r"C:\Program Files\Git\clangarm64\bin",
273 r"C:\Program Files\Git\mingw64\bin",
274 r"C:\Program Files (x86)\Git\mingw32\bin",
275 ],
276 );
277 }
278
279 #[test]
280 fn locations_under_program_files_local_and_global_strange_values_all_vars_distinct() {
281 assert_eq!(
282 locations_from!(
283 "LocalAppData" => r"W:\us\er",
284 "ProgramFiles" => r"X:\cur\rent",
285 "ProgramFiles(x86)" => r"Y:\nar\row",
286 "ProgramW6432" => r"Z:\wi\de",
287 ),
288 if cfg!(target_pointer_width = "64") {
289 pathbuf_vec![
290 r"W:\us\er\Programs\Git\clangarm64\bin",
291 r"W:\us\er\Programs\Git\mingw64\bin",
292 r"W:\us\er\Programs\Git\mingw32\bin",
293 r"Z:\wi\de\Git\clangarm64\bin",
294 r"Z:\wi\de\Git\mingw64\bin",
295 r"Y:\nar\row\Git\mingw32\bin",
296 r"X:\cur\rent\Git\clangarm64\bin",
297 r"X:\cur\rent\Git\mingw64\bin",
298 ]
299 } else {
300 pathbuf_vec![
301 r"W:\us\er\Programs\Git\clangarm64\bin",
302 r"W:\us\er\Programs\Git\mingw64\bin",
303 r"W:\us\er\Programs\Git\mingw32\bin",
304 r"Z:\wi\de\Git\clangarm64\bin",
305 r"Z:\wi\de\Git\mingw64\bin",
306 r"Y:\nar\row\Git\mingw32\bin",
307 r"X:\cur\rent\Git\mingw32\bin",
308 ]
309 },
310 );
311 }
312
313 #[test]
314 fn locations_under_program_files_local_and_global_strange_values_limited_64bit_var() {
315 assert_eq!(
316 locations_from!(
317 "LocalAppData" => r"W:\us\er",
318 "ProgramW6432" => r"Z:\wi\de",
319 ),
320 pathbuf_vec![
321 r"W:\us\er\Programs\Git\clangarm64\bin",
322 r"W:\us\er\Programs\Git\mingw64\bin",
323 r"W:\us\er\Programs\Git\mingw32\bin",
324 r"Z:\wi\de\Git\clangarm64\bin",
325 r"Z:\wi\de\Git\mingw64\bin",
326 ],
327 );
328 }
329
330 #[test]
331 fn locations_under_program_files_local_and_global_strange_values_crufty_cross_clash() {
332 assert_eq!(
333 locations_from!(
334 "LocalAppData" => r"Y:\nar\row",
335 "ProgramFiles" => r"Z:/wi//de/",
336 "ProgramFiles(x86)" => r"Y:/\nar/row\Programs",
337 "ProgramW6432" => r"Z:\wi\.\de",
338 ),
339 if cfg!(target_pointer_width = "64") {
340 pathbuf_vec![
341 r"Y:\nar\row\Programs\Git\clangarm64\bin",
342 r"Y:\nar\row\Programs\Git\mingw64\bin",
343 r"Y:\nar\row\Programs\Git\mingw32\bin",
344 r"Z:\wi\de\Git\clangarm64\bin",
345 r"Z:\wi\de\Git\mingw64\bin",
346 ]
347 } else {
348 pathbuf_vec![
349 r"Y:\nar\row\Programs\Git\clangarm64\bin",
350 r"Y:\nar\row\Programs\Git\mingw64\bin",
351 r"Y:\nar\row\Programs\Git\mingw32\bin",
352 r"Z:\wi\de\Git\clangarm64\bin",
353 r"Z:\wi\de\Git\mingw64\bin",
354 r"Z:\wi\de\Git\mingw32\bin",
355 ]
356 },
357 );
358 }
359
360 #[test]
361 fn locations_under_program_files_local_and_global_strange_values_some_relative() {
362 assert_eq!(
363 locations_from!(
364 "LocalAppData" => "dir",
365 "ProgramFiles" => r"foo\bar",
366 "ProgramFiles(x86)" => r"\\host\share\subdir",
367 "ProgramW6432" => r"",
368 ),
369 pathbuf_vec![r"\\host\share\subdir\Git\mingw32\bin"],
370 );
371 }
372
373 /// Owner of a null-terminated `PWSTR` that must be freed with `CoTaskMemFree`.
374 struct CoStr {
375 pwstr: PWSTR,
376 }
377
378 impl CoStr {
379 /// SAFETY: The caller must ensure `pwstr` is a non-null pointer to the beginning of a
380 /// null-terminated (zero-codepoint-terminated) wide string releasable by `CoTaskMemFree`.
381 unsafe fn new(pwstr: PWSTR) -> Self {
382 Self { pwstr }
383 }
384
385 fn to_os_string(&self) -> OsString {
386 // SAFETY: We know `pwstr` is derefrenceable and the string is null-terminated.
387 let wide = unsafe { self.pwstr.as_wide() };
388 OsString::from_wide(wide)
389 }
390 }
391
392 impl Drop for CoStr {
393 fn drop(&mut self) {
394 // SAFETY: `pwstr` is allowed to be passed to `CoTaskMemFree`. (We happen to know it's
395 // non-null, but `CoTaskMemFree` permits null as well, so the cast is doubly safe.)
396 unsafe { CoTaskMemFree(Some(self.pwstr.as_ptr().cast::<c_void>())) };
397 }
398 }
399
400 fn get_known_folder_path_with_flag(id: GUID, flag: KNOWN_FOLDER_FLAG) -> WindowsResult<PathBuf> {
401 // SAFETY: `SHGetKnownFolderPath` in the `windows` crate wraps the API function and returns
402 // a non-null pointer to a null-terminated wide string, or an error, not a null pointer.
403 // As in the wrapped API function, the pointer it returns can be passed to `CoTaskMemFree`.
404 let costr = unsafe { CoStr::new(SHGetKnownFolderPath(&id, flag, None)?) };
405 Ok(PathBuf::from(costr.to_os_string()))
406 }
407
170 #[derive(Clone, Copy, Debug)]408 #[derive(Clone, Copy, Debug)]
171 enum PlatformBitness {409 enum PlatformBitness {
172 Is32on32,410 Is32on32,
@@ -205,9 +443,10 @@ mod locations { · 32 unmodified lines
205 Some(folded_text.ends_with(&folded_pattern))443 Some(folded_text.ends_with(&folded_pattern))
206 }444 }
207 445
208 /// The most common global program files paths on this system, by process and system architecture.446 /// The most common program files paths, as they are available in this environment.
209 ///447 ///
210 /// This omits the 32-bit ARM program files directory, as Git for Windows is never installed there.448 /// This omits the global 32-bit ARM program files directory, because Git for Windows is never
449 /// installed there.
211 #[derive(Clone, Debug)]450 #[derive(Clone, Debug)]
212 struct ProgramFilesPaths {451 struct ProgramFilesPaths {
213 /// The program files directory used for whatever architecture this program was built for.452 /// The program files directory used for whatever architecture this program was built for.
@@ -224,10 +463,13 @@ mod locations { · 10 unmodified lines
224 /// AMD64 programs use the same program files directory while 32-bit x86 and 32-bit ARM463 /// AMD64 programs use the same program files directory while 32-bit x86 and 32-bit ARM
225 /// programs use two others. Only a 32-bit system has no 64-bit program files directory.464 /// programs use two others. Only a 32-bit system has no 64-bit program files directory.
226 maybe_64bit: Option<PathBuf>,465 maybe_64bit: Option<PathBuf>,
466
467 /// The per-user `Programs` subdirectory of the user's local application data directory.
468 user: PathBuf,
227 }469 }
228 470
229 impl ProgramFilesPaths {471 impl ProgramFilesPaths {
230 /// Get the three most common kinds of global program files paths without environment variables.472 /// Get the four most common kinds of program files paths without environment variables.
231 ///473 ///
232 /// The idea here is to obtain this information, which the `alternative_locations()` unit474 /// The idea here is to obtain this information, which the `alternative_locations()` unit
233 /// test uses to learn the expected alternative locations, without duplicating *any* of the475 /// test uses to learn the expected alternative locations, without duplicating *any* of the
@@ -235,9 +477,10 @@ mod locations { · 1 unmodified line
235 /// here is also more reliable than using environment variables, but it is a bit more477 /// here is also more reliable than using environment variables, but it is a bit more
236 /// complex, and it requires either additional dependencies or the use of unsafe code.478 /// complex, and it requires either additional dependencies or the use of unsafe code.
237 ///479 ///
238 /// This gets `pf_current` and `pf_x86` by the [known folders][known-folders] system. But480 /// This gets `pf_user`, `pf_current`, and `pf_x86` by the [known folders][known-folders]
239 /// it gets `maybe_pf_64bit` from the registry, as the corresponding known folder is not481 /// system. But it gets `maybe_pf_64bit` from the registry, as the corresponding known
240 /// available to 32-bit processes. See the [`KNOWNFOLDDERID`][knownfolderid] documentation.482 /// folder is not available to 32-bit processes. See the [`KNOWNFOLDDERID`][knownfolderid]
483 /// documentation.
241 ///484 ///
242 /// If in the future the implementation of `ALTERNATIVE_LOCATIONS` uses these techniques,485 /// If in the future the implementation of `ALTERNATIVE_LOCATIONS` uses these techniques,
243 /// then this function can be changed to use environment variables and renamed accordingly.486 /// then this function can be changed to use environment variables and renamed accordingly.
@@ -245,10 +488,10 @@ mod locations { · 1 unmodified line
245 /// [known-folders]: https://learn.microsoft.com/en-us/windows/win32/shell/known-folders488 /// [known-folders]: https://learn.microsoft.com/en-us/windows/win32/shell/known-folders
246 /// [knownfolderid]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#remarks489 /// [knownfolderid]: https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#remarks
247 fn obtain_envlessly() -> Self {490 fn obtain_envlessly() -> Self {
248 let pf_current = get_known_folder_path(KnownFolder::ProgramFiles)491 let pf_current = get_known_folder_path_with_flag(FOLDERID_ProgramFiles, KF_FLAG_DEFAULT)
249 .expect("The process architecture specific program files folder is always available");492 .expect("The process architecture specific program files folder is always available");
250 493
251 let pf_x86 = get_known_folder_path(KnownFolder::ProgramFilesX86)494 let pf_x86 = get_known_folder_path_with_flag(FOLDERID_ProgramFilesX86, KF_FLAG_DEFAULT)
252 .expect("The x86 program files folder will in practice always be available");495 .expect("The x86 program files folder will in practice always be available");
253 496
254 let maybe_pf_64bit = RegKey::predef(HKEY_LOCAL_MACHINE)497 let maybe_pf_64bit = RegKey::predef(HKEY_LOCAL_MACHINE)
@@ -262,10 +505,14 @@ mod locations { · 7 unmodified lines
262 })505 })
263 .ok();506 .ok();
264 507
508 let pf_user = get_known_folder_path_with_flag(FOLDERID_UserProgramFiles, KF_FLAG_DONT_VERIFY)
509 .expect("The path where the user's local Programs folder is, or would be created, is known");
510
265 Self {511 Self {
266 current: pf_current,512 current: pf_current,
267 x86: pf_x86,513 x86: pf_x86,
268 maybe_64bit: maybe_pf_64bit,514 maybe_64bit: maybe_pf_64bit,
515 user: pf_user,
269 }516 }
270 }517 }
271 518
@@ -274,6 +521,12 @@ mod locations { · 2 unmodified lines
274 /// This checks that `obtain_envlessly()` returned paths that are likely to be correct and521 /// This checks that `obtain_envlessly()` returned paths that are likely to be correct and
275 /// that satisfy the most important properties based on the current system and process.522 /// that satisfy the most important properties based on the current system and process.
276 fn validated(self) -> Self {523 fn validated(self) -> Self {
524 self.validate_global_folders();
525 self.validate_user_folder();
526 self
527 }
528
529 fn validate_global_folders(&self) {
277 match PlatformBitness::current().expect("Process and system 'bitness' should be available") {530 match PlatformBitness::current().expect("Process and system 'bitness' should be available") {
278 PlatformBitness::Is32on32 => {531 PlatformBitness::Is32on32 => {
279 assert_eq!(532 assert_eq!(
@@ -325,59 +578,114 @@ mod locations { · 45 unmodified lines
325 );578 );
326 }579 }
327 }580 }
581 }
328 582
329 self583 fn validate_user_folder(&self) {
584 let expected = get_known_folder_path_with_flag(FOLDERID_LocalAppData, KF_FLAG_DEFAULT)
585 .expect("The user's local application data directory is available")
586 .join("Programs");
587 assert_eq!(
588 self.user, expected,
589 "The user program files directory is Programs in the local application data directory.",
590 );
330 }591 }
331 }592 }
332 593
333 /// Paths relative to process architecture specific program files directories.594 /// Architecture-specific Git for Windows paths relative to the user program files directory.
334 #[derive(Clone, Debug)]595 #[derive(Clone, Debug)]
335 struct RelativeGitBinPaths<'a> {596 struct RelativeUserGitBinPaths<'a> {
597 x86: &'a Path,
598 x64: &'a Path,
599 arm64: &'a Path,
600 }
601
602 impl<'a> RelativeUserGitBinPaths<'a> {
603 /// Assert that `locations` leads with the given user path prefix, and extract the suffixes.
604 fn assert_from(pf_user: &'a Path, locations: &'static [PathBuf]) -> Self {
605 match locations {
606 [path1, path2, path3, ..] => {
607 let suffix_user_arm64 = path1
608 .strip_prefix(pf_user)
609 .expect("It gives a per-user 64-bit ARM64 path and lists it first");
610 let suffix_user_x64 = path2
611 .strip_prefix(pf_user)
612 .expect("It gives a per-user 64-bit x86 path and lists it second");
613 let suffix_user_x86 = path3
614 .strip_prefix(pf_user)
615 .expect("It gives a per-user 32-bit x86 path and lists it third");
616 Self {
617 x86: suffix_user_x86,
618 x64: suffix_user_x64,
619 arm64: suffix_user_arm64,
620 }
621 }
622 other => panic!(
623 "{:?} has length {}, so some expected leading user program files paths are absent",
624 other,
625 other.len()
626 ),
627 }
628 }
629
630 /// Assert that suffixes are common Git install locations relative to a program files directory.
631 fn assert_architectures(&self) {
632 assert_eq!(self.x86, Path::new("Git/mingw32/bin"));
633 assert_eq!(self.x64, Path::new("Git/mingw64/bin"));
634 assert_eq!(self.arm64, Path::new("Git/clangarm64/bin"));
635 }
636 }
637
638 /// Architecture-specific Git for Windows paths relative to global program files directories.
639 #[derive(Clone, Debug)]
640 struct RelativeGlobalGitBinPaths<'a> {
336 x86: &'a Path,641 x86: &'a Path,
337 maybe_x64: Option<&'a Path>,642 maybe_x64: Option<&'a Path>,
338 maybe_arm64: Option<&'a Path>,643 maybe_arm64: Option<&'a Path>,
339 }644 }
340 645
341 impl<'a> RelativeGitBinPaths<'a> {646 impl<'a> RelativeGlobalGitBinPaths<'a> {
342 /// Assert that `locations` has the given path prefixes, and extract the suffixes.647 /// Assert that `locations` trails with the given global path prefixes, and extract the suffixes.
343 fn assert_from(pf: &'a ProgramFilesPaths, locations: &'static [PathBuf]) -> Self {648 fn assert_from(pf: &'a ProgramFilesPaths, locations: &'static [PathBuf]) -> Self {
344 match locations {649 match locations {
345 [primary, secondary, tertiary] => {650 [_, _, _, path4, path5, path6] => {
346 let prefix_64bit = pf651 let prefix_64bit = pf
347 .maybe_64bit652 .maybe_64bit
348 .as_ref()653 .as_ref()
349 .expect("It gives three paths only if some can be 64-bit");654 .expect("It gives 6 paths only if some global paths can be 64-bit");
350 let suffix_arm64 = primary655 let suffix_global_arm64 = path4
351 .strip_prefix(prefix_64bit)656 .strip_prefix(prefix_64bit)
352 .expect("It gives the 64-bit ARM64 path and lists it first");657 .expect("It gives a global 64-bit ARM64 path and lists it fourth");
353 let suffix_x64 = secondary658 let suffix_global_x64 = path5
354 .strip_prefix(prefix_64bit)659 .strip_prefix(prefix_64bit)
355 .expect("It gives the 64-bit x86 path and lists it second");660 .expect("It gives a global 64-bit x86 path and lists it fifth");
356 let suffix_x86 = tertiary661 let suffix_global_x86 = path6
357 .strip_prefix(pf.x86.as_path())662 .strip_prefix(&pf.x86)
358 .expect("It gives the 32-bit path and lists it third");663 .expect("It gives a global 32-bit path and lists it sixth");
359 Self {664 Self {
360 x86: suffix_x86,665 x86: suffix_global_x86,
361 maybe_x64: Some(suffix_x64),666 maybe_x64: Some(suffix_global_x64),
362 maybe_arm64: Some(suffix_arm64),667 maybe_arm64: Some(suffix_global_arm64),
363 }668 }
364 }669 }
365 [only] => {670 [_, _, _, path4] => {
366 assert_eq!(pf.maybe_64bit, None, "It gives one path only if none can be 64-bit.");671 assert_eq!(
367 let suffix_x86 = only672 pf.maybe_64bit, None,
368 .strip_prefix(pf.x86.as_path())673 "It gives 4 paths only if no global paths can be 64-bit.",
369 .expect("The one path it gives is the 32-bit path");674 );
675 let suffix_global_x86 = path4
676 .strip_prefix(&pf.x86)
677 .expect("It gives a global 32-bit path and lists it fourth");
370 Self {678 Self {
371 x86: suffix_x86,679 x86: suffix_global_x86,
372 maybe_x64: None,680 maybe_x64: None,
373 maybe_arm64: None,681 maybe_arm64: None,
374 }682 }
375 }683 }
376 other => panic!("{:?} has length {}, expected 1 or 3.", other, other.len()),684 other => panic!("{:?} has length {}, expected 4 or 6.", other, other.len()),
377 }685 }
378 }686 }
379 687
380 /// Assert that the suffixes (relative subdirectories) are the common per-architecture Git install locations.688 /// Assert that suffixes are common Git install locations relative to a program files directory.
381 fn assert_architectures(&self) {689 fn assert_architectures(&self) {
382 assert_eq!(self.x86, Path::new("Git/mingw32/bin"));690 assert_eq!(self.x86, Path::new("Git/mingw32/bin"));
383 691
@@ -390,6 +698,28 @@ mod locations { · 6 unmodified lines
390 }698 }
391 }699 }
392 700
701 /// Architecture-specific Git for Windows paths relative to particular program files directories.
702 #[derive(Clone, Debug)]
703 struct RelativeGitBinPaths<'a> {
704 global: RelativeGlobalGitBinPaths<'a>,
705 user: RelativeUserGitBinPaths<'a>,
706 }
707
708 impl<'a> RelativeGitBinPaths<'a> {
709 /// Assert that `locations` has the given path prefixes, and extract the suffixes.
710 fn assert_from(pf: &'a ProgramFilesPaths, locations: &'static [PathBuf]) -> Self {
711 let user = RelativeUserGitBinPaths::assert_from(&pf.user, locations);
712 let global = RelativeGlobalGitBinPaths::assert_from(pf, locations);
713 Self { global, user }
714 }
715
716 /// Assert that global and user suffixes (relative subdirectories) are common Git install locations.
717 fn assert_architectures(&self) {
718 self.global.assert_architectures();
719 self.user.assert_architectures();
720 }
721 }
722
393 #[test]723 #[test]
394 fn alternative_locations() {724 fn alternative_locations() {
395 // Obtain program files directory paths by other means and check that they seem correct.725 // Obtain program files directory paths by other means and check that they seem correct.