Skip to content

Commit f0ff687

Browse files
committed
Work with std::path::* during interpolation (#331)
This means we don't necessarily enforce slashes everywhere. Intiially the implementation was meant to be as close to `git` as possible, even though certain requirements there stem from the assumption that the path separtor is `/`. In Rust, however, we now return `std::path::Path` which deals with that for us and allows to operate on paths more comfortably and safely. We now leverage this fully even if that means that the paths we output aren't entirely conformant with what git would use, but it's an implementation detail. Lastly, the interpolated path is a transformation step so there should be no easy way to accidentally put that version of the path back into the file, which would be undesirable due to the various transformations.
1 parent f6d9693 commit f0ff687

File tree

2 files changed

+55
-57
lines changed

2 files changed

+55
-57
lines changed

Diff for: git-config/src/values.rs

+51-48
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Rust containers for valid `git-config` types.
22
3+
use std::path::PathBuf;
34
use std::{borrow::Cow, convert::TryFrom, fmt::Display, str::FromStr};
45

56
#[cfg(not(target_os = "windows"))]
@@ -352,26 +353,20 @@ impl<'a> Path<'a> {
352353
}
353354

354355
const PREFIX: &[u8] = b"%(prefix)/";
356+
const USER_HOME: &[u8] = b"~/";
355357
if self.starts_with(PREFIX) {
356-
let mut expanded = git_features::path::into_bytes(git_install_dir.ok_or(PathError::Missing {
358+
let git_install_dir = git_install_dir.ok_or(PathError::Missing {
357359
what: "git install dir",
358-
})?)
359-
.context("git install dir")?
360-
.into_owned();
361-
let (_prefix, val) = self.split_at(PREFIX.len() - "/".len());
362-
expanded.extend(val);
363-
Ok(git_features::path::from_byte_vec(expanded)
364-
.context("prefix-expanded path")?
365-
.into())
366-
} else if self.starts_with(b"~/") {
360+
})?;
361+
let (_prefix, path_without_trailing_slash) = self.split_at(PREFIX.len());
362+
let path_without_trailing_slash =
363+
git_features::path::from_byte_vec(path_without_trailing_slash).context("path past %(prefix)")?;
364+
Ok(git_install_dir.join(path_without_trailing_slash).into())
365+
} else if self.starts_with(USER_HOME) {
367366
let home_path = dirs::home_dir().ok_or(PathError::Missing { what: "home dir" })?;
368-
let mut expanded = git_features::path::into_bytes(home_path)
369-
.context("home dir")?
370-
.into_owned();
371-
let (_prefix, val) = self.split_at("~".len());
372-
expanded.extend(val);
373-
let expanded = git_features::path::convert::to_unix_separators(expanded);
374-
Ok(git_features::path::from_bytes(expanded).context("tilde expanded path")?)
367+
let (_prefix, val) = self.split_at(USER_HOME.len());
368+
let val = git_features::path::from_bytes(val).context("path past ~/")?;
369+
Ok(home_path.join(val).into())
375370
} else if self.starts_with(b"~") && self.contains(&b'/') {
376371
self.interpolate_user()
377372
} else {
@@ -391,17 +386,15 @@ impl<'a> Path<'a> {
391386
.iter()
392387
.position(|&e| e == b'/')
393388
.ok_or(PathError::Missing { what: "/" })?;
394-
let (username, val) = val.split_at(i);
389+
let (username, path_with_leading_slash) = val.split_at(i);
395390
let username = std::str::from_utf8(username)?;
396391
let home = Passwd::from_name(username)
397392
.map_err(|_| PathError::PwdFileQuery)?
398393
.ok_or(PathError::Missing { what: "pwd user info" })?
399394
.dir;
400-
let mut expanded = home.as_bytes().to_owned();
401-
expanded.extend(val);
402-
Ok(git_features::path::from_byte_vec(expanded)
403-
.context("tilded user expanded path")?
404-
.into())
395+
let path_past_user_prefix =
396+
git_features::path::from_byte_slice(&path_with_leading_slash[1..]).context("path past ~user/")?;
397+
Ok(PathBuf::from(home).join(path_past_user_prefix).into())
405398
}
406399
}
407400

@@ -1550,19 +1543,26 @@ mod path {
15501543

15511544
#[test]
15521545
fn prefix_interpolated() {
1553-
let val = Cow::Borrowed(&b"%(prefix)/foo/bar"[..]);
1554-
let git_install_dir = "/tmp/git";
1555-
let expected = &std::path::PathBuf::from(format!("{}/foo/bar", git_install_dir));
1556-
assert_eq!(
1557-
&*Path::from(val)
1558-
.interpolate(Some(std::path::Path::new(git_install_dir)))
1559-
.unwrap(),
1560-
expected
1561-
);
1546+
for git_install_dir in &["/tmp/git", "C:\\git"] {
1547+
for (val, expected) in &[
1548+
(&b"%(prefix)/foo/bar"[..], "foo/bar"),
1549+
(b"%(prefix)/foo\\bar", "foo\\bar"),
1550+
] {
1551+
let expected =
1552+
&std::path::PathBuf::from(format!("{}{}{}", git_install_dir, std::path::MAIN_SEPARATOR, expected));
1553+
assert_eq!(
1554+
&*Path::from(Cow::Borrowed(*val))
1555+
.interpolate(Some(std::path::Path::new(git_install_dir)))
1556+
.unwrap(),
1557+
expected,
1558+
"prefix interpolation keeps separators as they are"
1559+
);
1560+
}
1561+
}
15621562
}
15631563

15641564
#[test]
1565-
fn disabled_prefix_interpoldation() {
1565+
fn disabled_prefix_interpolation() {
15661566
let path = "./%(prefix)/foo/bar";
15671567
let git_install_dir = "/tmp/git";
15681568
assert_eq!(
@@ -1576,17 +1576,15 @@ mod path {
15761576
#[test]
15771577
fn tilde_interpolated() {
15781578
let path = &b"~/foo/bar"[..];
1579-
let home = dirs::home_dir()
1580-
.expect("empty home")
1581-
.to_str()
1582-
.expect("invalid unicode")
1583-
.to_owned();
1584-
#[cfg(target_os = "windows")]
1585-
let home = home.replace("\\", "/");
1586-
let expected = format!("{}/foo/bar", home);
1579+
let expected = format!(
1580+
"{}{}foo/bar",
1581+
dirs::home_dir().expect("empty home").display(),
1582+
std::path::MAIN_SEPARATOR
1583+
);
15871584
assert_eq!(
15881585
Path::from(Cow::Borrowed(path)).interpolate(None).unwrap().as_ref(),
1589-
std::path::Path::new(&expected)
1586+
std::path::Path::new(&expected),
1587+
"note that path separators are not turned into slashes as we work with `std::path::Path`"
15901588
);
15911589
}
15921590

@@ -1603,12 +1601,17 @@ mod path {
16031601
#[test]
16041602
fn user_interpolated() {
16051603
let user = std::env::var("USER").unwrap();
1606-
let path = format!("~{}/foo/bar", user);
16071604
let home = std::env::var("HOME").unwrap();
1608-
let expected = format!("{}/foo/bar", home);
1609-
assert_eq!(
1610-
&*Path::from(Cow::Borrowed(path.as_bytes())).interpolate(None).unwrap(),
1611-
std::path::Path::new(&expected)
1612-
);
1605+
let specific_user_home = format!("~{}", user);
1606+
1607+
for path_suffix in &["foo/bar", "foo\\bar", ""] {
1608+
let path = format!("{}{}{}", specific_user_home, std::path::MAIN_SEPARATOR, path_suffix);
1609+
let expected = format!("{}{}{}", home, std::path::MAIN_SEPARATOR, path_suffix);
1610+
assert_eq!(
1611+
&*Path::from(Cow::Borrowed(path.as_bytes())).interpolate(None).unwrap(),
1612+
std::path::Path::new(&expected),
1613+
"it keeps path separators as is"
1614+
);
1615+
}
16131616
}
16141617
}

Diff for: git-config/tests/integration_tests/file_integeration_test.rs

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::path::PathBuf;
12
use std::{borrow::Cow, convert::TryFrom, path::Path};
23

34
use git_config::{file::GitConfig, values::*};
@@ -73,20 +74,14 @@ fn get_value_for_all_provided_values() -> crate::Result {
7374
Value::Other(Cow::Borrowed(b"hello world"))
7475
);
7576

76-
let home = dirs::home_dir()
77-
.expect("empty home dir")
78-
.to_str()
79-
.expect("invalid unicode")
80-
.to_owned();
81-
#[cfg(target_os = "windows")]
82-
let home = home.replace("\\", "/");
8377
let actual = file.value::<git_config::values::Path>("core", None, "location")?;
8478
assert_eq!(
8579
&*actual,
8680
"~/tmp".as_bytes(),
87-
"no interpolation occours when querying a path due to lack of context"
81+
"no interpolation occurs when querying a path due to lack of context"
8882
);
89-
assert_eq!(actual.interpolate(None).unwrap(), Path::new(&format!("{}/tmp", home)));
83+
let expected = PathBuf::from(format!("{}/tmp", dirs::home_dir().expect("empty home dir").display()));
84+
assert_eq!(actual.interpolate(None).unwrap(), expected);
9085

9186
Ok(())
9287
}

0 commit comments

Comments
 (0)