1use std::fs::File;
2use std::path::{Path, PathBuf};
3use std::{fmt, io};
4
5use polars_error::*;
6
7#[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
48pub 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}