Skip to main content

polars_utils/
io.rs

1use std::fs::File;
2use std::path::{Path, PathBuf};
3use std::{fmt, io};
4
5use polars_error::*;
6
7/// An IO error together with the full path it occurred on.
8///
9/// The `Display` output truncates long paths so they stay readable in error messages, but
10/// the full path remains available to consumers that downcast the payload of the
11/// [`io::Error`] built by [`_limit_path_len_io_err`].
12#[derive(Debug)]
13pub struct PathIoError {
14    pub path: PathBuf,
15    pub source: io::Error,
16}
17
18impl PathIoError {
19    const MAX_DISPLAYED_PATH_CHARS: usize = 88;
20}
21
22impl fmt::Display for PathIoError {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        let path = self.path.to_string_lossy();
25        let n_chars = path.chars().count();
26        if n_chars > Self::MAX_DISPLAYED_PATH_CHARS && !polars_config::config().verbose() {
27            let truncated_path: String = path
28                .chars()
29                .skip(n_chars - Self::MAX_DISPLAYED_PATH_CHARS)
30                .collect();
31            write!(
32                f,
33                "{}: ...{truncated_path} (set POLARS_VERBOSE=1 to see full path)",
34                self.source
35            )
36        } else {
37            write!(f, "{}: {path}", self.source)
38        }
39    }
40}
41
42impl std::error::Error for PathIoError {
43    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
44        Some(&self.source)
45    }
46}
47
48/// Attaches `path` to `err`, keeping its [`io::ErrorKind`].
49///
50/// The path is available in full through [`PathIoError`], and truncated in the message.
51pub fn _limit_path_len_io_err(path: &Path, err: io::Error) -> PolarsError {
52    io::Error::new(
53        err.kind(),
54        PathIoError {
55            path: path.to_path_buf(),
56            source: err,
57        },
58    )
59    .into()
60}
61
62pub fn open_file(path: &Path) -> PolarsResult<File> {
63    File::open(path).map_err(|err| _limit_path_len_io_err(path, err))
64}
65
66pub fn open_file_write(path: &Path) -> PolarsResult<File> {
67    std::fs::OpenOptions::new()
68        .write(true)
69        .create(true)
70        .truncate(true)
71        .open(path)
72        .map_err(|err| _limit_path_len_io_err(path, err))
73}
74
75pub fn create_file(path: &Path) -> PolarsResult<File> {
76    File::create(path).map_err(|err| _limit_path_len_io_err(path, err))
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    fn permission_denied() -> io::Error {
84        io::Error::new(
85            io::ErrorKind::PermissionDenied,
86            "Permission denied (os error 13)",
87        )
88    }
89
90    fn io_error_of(err: &PolarsError) -> &io::Error {
91        match err {
92            PolarsError::IO { error, .. } => error,
93            other => panic!("expected an IO error, got {other:?}"),
94        }
95    }
96
97    #[test]
98    fn keeps_kind_and_full_path() {
99        let path = PathBuf::from(format!("/{}/file.parquet", "a".repeat(200)));
100        let err = _limit_path_len_io_err(&path, permission_denied());
101        let io_err = io_error_of(&err);
102
103        assert_eq!(io_err.kind(), io::ErrorKind::PermissionDenied);
104
105        let with_path = io_err
106            .get_ref()
107            .and_then(|e| e.downcast_ref::<PathIoError>())
108            .expect("payload should be a PathIoError");
109        assert_eq!(with_path.path, path);
110        assert_eq!(with_path.source.kind(), io::ErrorKind::PermissionDenied);
111    }
112
113    #[test]
114    fn display_truncates_long_paths() {
115        let path = PathBuf::from(format!("/{}/file.parquet", "a".repeat(200)));
116        let msg = _limit_path_len_io_err(&path, permission_denied()).to_string();
117
118        assert!(
119            msg.starts_with("Permission denied (os error 13): ..."),
120            "{msg}"
121        );
122        assert!(
123            msg.ends_with("(set POLARS_VERBOSE=1 to see full path)"),
124            "{msg}"
125        );
126        assert!(!msg.contains(path.to_str().unwrap()), "{msg}");
127        assert!(msg.contains("aaa/file.parquet"), "{msg}");
128    }
129
130    #[test]
131    fn display_keeps_short_paths() {
132        let path = PathBuf::from("/tmp/out/file.parquet");
133        let msg = _limit_path_len_io_err(&path, permission_denied()).to_string();
134        assert_eq!(
135            msg,
136            "Permission denied (os error 13): /tmp/out/file.parquet"
137        );
138    }
139}