Skip to content

Commit f20896f

Browse files
Merge #828
828: Avoid using display on paths. r=Emilgardis a=Alexhuszagh Adds a helper trait `ToUtf8` which converts the path to UTF-8 if possible, and if not, creates a pretty debugging representation of the path. We can then change `display()` to `to_utf8()?` and be completely correct in all cases. On POSIX systems, this doesn't matter since paths must be UTF-8 anyway, but on Windows some paths can be WTF-8 (see rust-lang/rust#12056). Either way, this can avoid creating a copy in cases, is always correct, and is more idiomatic about what we're doing. We might not be able to handle non-UTF-8 paths currently (like ISO-8859-1 paths, which are historically very common). So, this doesn't change ergonomics: the resulting code is as compact and correct. It also requires less copies in most cases, which should be a good thing. But most importantly, if we're mounting data we can't silently fail or produce incorrect data. If something was lossily generated, we probably shouldn't try to mount with a replacement character, and also print more information than there was just an invalid Unicode character. Co-authored-by: Alex Huszagh <[email protected]>
2 parents 2937032 + 0741969 commit f20896f

File tree

10 files changed

+95
-50
lines changed

10 files changed

+95
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
7373

7474
### Internal
7575

76+
- #828 - assume paths are Unicode and provide better error messages for path encoding errors.
7677
- #787 - add installer for git hooks.
7778
- #786, #791 - Migrate build script to rust: `cargo build-docker-image $TARGET`
7879
- #730 - make FreeBSD builds more resilient.

clippy.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
disallowed-methods = [
2+
{ path = "std::path::Path::display", reason = "incorrect handling of non-Unicode paths, use path.to_utf8() or debug (`{path:?}`) instead" },
3+
]

src/docker/custom.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
33

44
use crate::docker::Engine;
55
use crate::{config::Config, docker, CargoMetadata, Target};
6-
use crate::{errors::*, file, CommandExt};
6+
use crate::{errors::*, file, CommandExt, ToUtf8};
77

88
use super::{image_name, parse_docker_opts};
99

@@ -47,7 +47,7 @@ impl<'a> Dockerfile<'a> {
4747
&format!(
4848
"{}.workspace_root={}",
4949
crate::CROSS_LABEL_DOMAIN,
50-
metadata.workspace_root.display()
50+
metadata.workspace_root.to_utf8()?
5151
),
5252
]);
5353

src/docker/local.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use super::shared::*;
66
use crate::cargo::CargoMetadata;
77
use crate::errors::Result;
88
use crate::extensions::CommandExt;
9+
use crate::file::ToUtf8;
910
use crate::{Config, Target};
1011
use atty::Stream;
1112
use eyre::Context;
@@ -49,33 +50,33 @@ pub(crate) fn run(
4950
docker_user_id(&mut docker, engine.kind);
5051

5152
docker
52-
.args(&["-v", &format!("{}:/xargo:Z", dirs.xargo.display())])
53-
.args(&["-v", &format!("{}:/cargo:Z", dirs.cargo.display())])
53+
.args(&["-v", &format!("{}:/xargo:Z", dirs.xargo.to_utf8()?)])
54+
.args(&["-v", &format!("{}:/cargo:Z", dirs.cargo.to_utf8()?)])
5455
// Prevent `bin` from being mounted inside the Docker container.
5556
.args(&["-v", "/cargo/bin"]);
5657
if mount_volumes {
5758
docker.args(&[
5859
"-v",
5960
&format!(
6061
"{}:{}:Z",
61-
dirs.host_root.display(),
62-
dirs.mount_root.display()
62+
dirs.host_root.to_utf8()?,
63+
dirs.mount_root.to_utf8()?
6364
),
6465
]);
6566
} else {
66-
docker.args(&["-v", &format!("{}:/project:Z", dirs.host_root.display())]);
67+
docker.args(&["-v", &format!("{}:/project:Z", dirs.host_root.to_utf8()?)]);
6768
}
6869
docker
69-
.args(&["-v", &format!("{}:/rust:Z,ro", dirs.sysroot.display())])
70-
.args(&["-v", &format!("{}:/target:Z", dirs.target.display())]);
70+
.args(&["-v", &format!("{}:/rust:Z,ro", dirs.sysroot.to_utf8()?)])
71+
.args(&["-v", &format!("{}:/target:Z", dirs.target.to_utf8()?)]);
7172
docker_cwd(&mut docker, metadata, &dirs, cwd, mount_volumes)?;
7273

7374
// When running inside NixOS or using Nix packaging we need to add the Nix
7475
// Store to the running container so it can load the needed binaries.
7576
if let Some(ref nix_store) = dirs.nix_store {
7677
docker.args(&[
7778
"-v",
78-
&format!("{}:{}:Z", nix_store.display(), nix_store.display()),
79+
&format!("{}:{}:Z", nix_store.to_utf8()?, nix_store.to_utf8()?),
7980
]);
8081
}
8182

src/docker/shared.rs

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::cargo::CargoMetadata;
99
use crate::config::Config;
1010
use crate::errors::*;
1111
use crate::extensions::{CommandExt, SafeCommand};
12-
use crate::file::{self, write_file};
12+
use crate::file::{self, write_file, PathExt, ToUtf8};
1313
use crate::id;
1414
use crate::Target;
1515

@@ -180,7 +180,7 @@ pub(crate) fn mount(
180180
let mount_path = canonicalize_mount_path(&host_path, verbose)?;
181181
docker.args(&[
182182
"-v",
183-
&format!("{}:{prefix}{}", host_path.display(), mount_path.display()),
183+
&format!("{}:{prefix}{}", host_path.to_utf8()?, mount_path.to_utf8()?),
184184
]);
185185
Ok(mount_path)
186186
}
@@ -236,20 +236,14 @@ pub(crate) fn docker_cwd(
236236
mount_volumes: bool,
237237
) -> Result<()> {
238238
if mount_volumes {
239-
docker.args(&["-w".as_ref(), dirs.mount_cwd.as_os_str()]);
239+
docker.args(&["-w", dirs.mount_cwd.to_utf8()?]);
240240
} else if dirs.mount_cwd == metadata.workspace_root {
241241
docker.args(&["-w", "/project"]);
242242
} else {
243243
// We do this to avoid clashes with path separators. Windows uses `\` as a path separator on Path::join
244244
let cwd = &cwd;
245245
let working_dir = Path::new("project").join(cwd.strip_prefix(&metadata.workspace_root)?);
246-
// No [T].join for OsStr
247-
let mut mount_wd = std::ffi::OsString::new();
248-
for part in working_dir.iter() {
249-
mount_wd.push("/");
250-
mount_wd.push(part);
251-
}
252-
docker.args(&["-w".as_ref(), mount_wd.as_os_str()]);
246+
docker.args(&["-w", &working_dir.as_posix()?]);
253247
}
254248

255249
Ok(())
@@ -282,15 +276,15 @@ pub(crate) fn docker_mount(
282276

283277
if let Ok(val) = value {
284278
let mount_path = mount_cb(docker, val.as_ref(), verbose)?;
285-
docker.args(&["-e", &format!("{}={}", var, mount_path.display())]);
279+
docker.args(&["-e", &format!("{}={}", var, mount_path.to_utf8()?)]);
286280
store_cb((val, mount_path));
287281
mount_volumes = true;
288282
}
289283
}
290284

291285
for path in metadata.path_dependencies() {
292286
let mount_path = mount_cb(docker, path, verbose)?;
293-
store_cb((path.display().to_string(), mount_path));
287+
store_cb((path.to_utf8()?.to_string(), mount_path));
294288
mount_volumes = true;
295289
}
296290

@@ -310,12 +304,7 @@ fn wslpath(path: &Path, verbose: bool) -> Result<PathBuf> {
310304
.arg("-a")
311305
.arg(path)
312306
.run_and_get_stdout(verbose)
313-
.wrap_err_with(|| {
314-
format!(
315-
"could not get linux compatible path for `{}`",
316-
path.display()
317-
)
318-
})
307+
.wrap_err_with(|| format!("could not get linux compatible path for `{path:?}`"))
319308
.map(|s| s.trim().into())
320309
}
321310

@@ -375,7 +364,7 @@ pub(crate) fn docker_seccomp(
375364
// podman weirdly expects a WSL path here, and fails otherwise
376365
path = wslpath(&path, verbose)?;
377366
}
378-
path.display().to_string()
367+
path.to_utf8()?.to_string()
379368
};
380369

381370
docker.args(&["--security-opt", &format!("seccomp={}", seccomp)]);

src/file.rs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,61 @@ use std::borrow::Cow;
22
use std::ffi::OsStr;
33
use std::fs::{self, File};
44
use std::io::Read;
5-
use std::path::{Path, PathBuf};
5+
use std::path::{Component, Path, PathBuf};
66

77
use crate::errors::*;
88

9+
pub trait ToUtf8 {
10+
fn to_utf8(&self) -> Result<&str>;
11+
}
12+
13+
impl ToUtf8 for OsStr {
14+
fn to_utf8(&self) -> Result<&str> {
15+
self.to_str()
16+
.ok_or_else(|| eyre::eyre!("unable to convert `{self:?}` to UTF-8 string"))
17+
}
18+
}
19+
20+
impl ToUtf8 for Path {
21+
fn to_utf8(&self) -> Result<&str> {
22+
self.as_os_str().to_utf8()
23+
}
24+
}
25+
26+
pub trait PathExt {
27+
fn as_posix(&self) -> Result<String>;
28+
}
29+
30+
fn push_posix_path(path: &mut String, component: &str) {
31+
if !path.is_empty() {
32+
path.push('/');
33+
}
34+
path.push_str(component);
35+
}
36+
37+
impl PathExt for Path {
38+
fn as_posix(&self) -> Result<String> {
39+
if cfg!(target_os = "windows") {
40+
// iterate over components to join them
41+
let mut output = String::new();
42+
for component in self.components() {
43+
match component {
44+
Component::Prefix(prefix) => {
45+
eyre::bail!("unix paths cannot handle windows prefix {prefix:?}.")
46+
}
47+
Component::RootDir => output = "/".to_string(),
48+
Component::CurDir => push_posix_path(&mut output, "."),
49+
Component::ParentDir => push_posix_path(&mut output, ".."),
50+
Component::Normal(path) => push_posix_path(&mut output, path.to_utf8()?),
51+
}
52+
}
53+
Ok(output)
54+
} else {
55+
self.to_utf8().map(|x| x.to_string())
56+
}
57+
}
58+
}
59+
960
pub fn read<P>(path: P) -> Result<String>
1061
where
1162
P: AsRef<Path>,
@@ -16,9 +67,9 @@ where
1667
fn read_(path: &Path) -> Result<String> {
1768
let mut s = String::new();
1869
File::open(path)
19-
.wrap_err_with(|| format!("couldn't open {}", path.display()))?
70+
.wrap_err_with(|| format!("couldn't open {path:?}"))?
2071
.read_to_string(&mut s)
21-
.wrap_err_with(|| format!("couldn't read {}", path.display()))?;
72+
.wrap_err_with(|| format!("couldn't read {path:?}"))?;
2273
Ok(s)
2374
}
2475

@@ -90,11 +141,11 @@ pub fn maybe_canonicalize(path: &Path) -> Cow<'_, OsStr> {
90141
pub fn write_file(path: impl AsRef<Path>, overwrite: bool) -> Result<File> {
91142
let path = path.as_ref();
92143
fs::create_dir_all(
93-
&path.parent().ok_or_else(|| {
94-
eyre::eyre!("could not find parent directory for `{}`", path.display())
95-
})?,
144+
&path
145+
.parent()
146+
.ok_or_else(|| eyre::eyre!("could not find parent directory for `{path:?}`"))?,
96147
)
97-
.wrap_err_with(|| format!("couldn't create directory `{}`", path.display()))?;
148+
.wrap_err_with(|| format!("couldn't create directory `{path:?}`"))?;
98149

99150
let mut open = fs::OpenOptions::new();
100151
open.write(true);
@@ -106,7 +157,7 @@ pub fn write_file(path: impl AsRef<Path>, overwrite: bool) -> Result<File> {
106157
}
107158

108159
open.open(&path)
109-
.wrap_err(format!("couldn't write to file `{}`", path.display()))
160+
.wrap_err(format!("couldn't write to file `{path:?}`"))
110161
}
111162

112163
#[cfg(test)]

src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ use self::rustc::{TargetList, VersionMetaExt};
4646

4747
pub use self::errors::{install_panic_hook, Result};
4848
pub use self::extensions::{CommandExt, OutputExt};
49+
pub use self::file::ToUtf8;
4950

5051
pub const CROSS_LABEL_DOMAIN: &str = "org.cross-rs";
5152

@@ -590,11 +591,11 @@ fn toml(metadata: &CargoMetadata) -> Result<Option<CrossToml>> {
590591
};
591592

592593
if path.exists() {
593-
let content = file::read(&path)
594-
.wrap_err_with(|| format!("could not read file `{}`", path.display()))?;
594+
let content =
595+
file::read(&path).wrap_err_with(|| format!("could not read file `{path:?}`"))?;
595596

596597
let (config, _) = CrossToml::parse(&content)
597-
.wrap_err_with(|| format!("failed to parse file `{}` as TOML", path.display()))?;
598+
.wrap_err_with(|| format!("failed to parse file `{path:?}` as TOML"))?;
598599

599600
Ok(Some(config))
600601
} else {

src/rustup.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ fn rustc_channel(version: &Version) -> Result<Channel> {
126126
pub fn rustc_version(toolchain_path: &Path) -> Result<Option<(Version, Channel, String)>> {
127127
let path = toolchain_path.join("lib/rustlib/multirust-channel-manifest.toml");
128128
if path.exists() {
129-
let contents = std::fs::read(&path)
130-
.wrap_err_with(|| format!("couldn't open file `{}`", path.display()))?;
129+
let contents =
130+
std::fs::read(&path).wrap_err_with(|| format!("couldn't open file `{path:?}`"))?;
131131
let manifest: toml::value::Table = toml::from_slice(&contents)?;
132132
if let Some(rust_version) = manifest
133133
.get("pkg")
@@ -137,9 +137,8 @@ pub fn rustc_version(toolchain_path: &Path) -> Result<Option<(Version, Channel,
137137
{
138138
// Field is `"{version} ({commit} {date})"`
139139
if let Some((version, meta)) = rust_version.split_once(' ') {
140-
let version = Version::parse(version).wrap_err_with(|| {
141-
format!("invalid rust version found in {}", path.display())
142-
})?;
140+
let version = Version::parse(version)
141+
.wrap_err_with(|| format!("invalid rust version found in {path:?}"))?;
143142
let channel = rustc_channel(&version)?;
144143
return Ok(Some((version, channel, meta.to_owned())));
145144
}

src/tests/toml.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ fn toml_check() -> Result<(), Box<dyn std::error::Error>> {
3030
if dir_entry.file_type().is_dir() {
3131
continue;
3232
}
33-
eprintln!("File: {:?}", dir_entry.path().display());
33+
eprintln!("File: {:?}", dir_entry.path());
3434
let mut file = std::fs::File::open(dir_entry.path()).unwrap();
3535
let mut contents = String::new();
3636
file.read_to_string(&mut contents).unwrap();
3737
for matches in TOML_REGEX.captures_iter(&contents) {
3838
let fence = matches.get(1).unwrap();
3939
eprintln!(
40-
"testing snippet at: {}:{:?}",
41-
dir_entry.path().display(),
40+
"testing snippet at: {:?}:{:?}",
41+
dir_entry.path(),
4242
text_line_no(&contents, fence.range().start),
4343
);
4444
assert!(crate::cross_toml::CrossToml::parse(fence.as_str())?

xtask/src/build_docker_image.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::{path::Path, process::Command};
22

33
use clap::Args;
44
use color_eyre::Section;
5-
use cross::CommandExt;
5+
use cross::{CommandExt, ToUtf8};
66
use std::fmt::Write;
77

88
#[derive(Args, Debug)]
@@ -72,7 +72,7 @@ fn locate_dockerfile(
7272
} else {
7373
eyre::bail!("unable to find dockerfile for target \"{target}\"");
7474
};
75-
let dockerfile = dockerfile_root.join(dockerfile_name).display().to_string();
75+
let dockerfile = dockerfile_root.join(dockerfile_name).to_utf8()?.to_string();
7676
Ok((target, dockerfile))
7777
}
7878

0 commit comments

Comments
 (0)