##// END OF EJS Templates
rust: Add Vfs::write_atomic...
Simon Sapin -
r49246:abeae090 default
parent child Browse files
Show More
@@ -1,199 +1,205 b''
1 use crate::config::ConfigValueParseError;
1 use crate::config::ConfigValueParseError;
2 use crate::exit_codes;
2 use crate::exit_codes;
3 use std::fmt;
3 use std::fmt;
4
4
5 /// Common error cases that can happen in many different APIs
5 /// Common error cases that can happen in many different APIs
6 #[derive(Debug, derive_more::From)]
6 #[derive(Debug, derive_more::From)]
7 pub enum HgError {
7 pub enum HgError {
8 IoError {
8 IoError {
9 error: std::io::Error,
9 error: std::io::Error,
10 context: IoErrorContext,
10 context: IoErrorContext,
11 },
11 },
12
12
13 /// A file under `.hg/` normally only written by Mercurial is not in the
13 /// A file under `.hg/` normally only written by Mercurial is not in the
14 /// expected format. This indicates a bug in Mercurial, filesystem
14 /// expected format. This indicates a bug in Mercurial, filesystem
15 /// corruption, or hardware failure.
15 /// corruption, or hardware failure.
16 ///
16 ///
17 /// The given string is a short explanation for users, not intended to be
17 /// The given string is a short explanation for users, not intended to be
18 /// machine-readable.
18 /// machine-readable.
19 CorruptedRepository(String),
19 CorruptedRepository(String),
20
20
21 /// The respository or requested operation involves a feature not
21 /// The respository or requested operation involves a feature not
22 /// supported by the Rust implementation. Falling back to the Python
22 /// supported by the Rust implementation. Falling back to the Python
23 /// implementation may or may not work.
23 /// implementation may or may not work.
24 ///
24 ///
25 /// The given string is a short explanation for users, not intended to be
25 /// The given string is a short explanation for users, not intended to be
26 /// machine-readable.
26 /// machine-readable.
27 UnsupportedFeature(String),
27 UnsupportedFeature(String),
28
28
29 /// Operation cannot proceed for some other reason.
29 /// Operation cannot proceed for some other reason.
30 ///
30 ///
31 /// The message is a short explanation for users, not intended to be
31 /// The message is a short explanation for users, not intended to be
32 /// machine-readable.
32 /// machine-readable.
33 Abort {
33 Abort {
34 message: String,
34 message: String,
35 detailed_exit_code: exit_codes::ExitCode,
35 detailed_exit_code: exit_codes::ExitCode,
36 },
36 },
37
37
38 /// A configuration value is not in the expected syntax.
38 /// A configuration value is not in the expected syntax.
39 ///
39 ///
40 /// These errors can happen in many places in the code because values are
40 /// These errors can happen in many places in the code because values are
41 /// parsed lazily as the file-level parser does not know the expected type
41 /// parsed lazily as the file-level parser does not know the expected type
42 /// and syntax of each value.
42 /// and syntax of each value.
43 #[from]
43 #[from]
44 ConfigValueParseError(ConfigValueParseError),
44 ConfigValueParseError(ConfigValueParseError),
45 }
45 }
46
46
47 /// Details about where an I/O error happened
47 /// Details about where an I/O error happened
48 #[derive(Debug)]
48 #[derive(Debug)]
49 pub enum IoErrorContext {
49 pub enum IoErrorContext {
50 /// `std::fs::metadata`
50 /// `std::fs::metadata`
51 ReadingMetadata(std::path::PathBuf),
51 ReadingMetadata(std::path::PathBuf),
52 ReadingFile(std::path::PathBuf),
52 ReadingFile(std::path::PathBuf),
53 WritingFile(std::path::PathBuf),
53 WritingFile(std::path::PathBuf),
54 RemovingFile(std::path::PathBuf),
54 RemovingFile(std::path::PathBuf),
55 RenamingFile {
55 RenamingFile {
56 from: std::path::PathBuf,
56 from: std::path::PathBuf,
57 to: std::path::PathBuf,
57 to: std::path::PathBuf,
58 },
58 },
59 /// `std::fs::canonicalize`
59 /// `std::fs::canonicalize`
60 CanonicalizingPath(std::path::PathBuf),
60 CanonicalizingPath(std::path::PathBuf),
61 /// `std::env::current_dir`
61 /// `std::env::current_dir`
62 CurrentDir,
62 CurrentDir,
63 /// `std::env::current_exe`
63 /// `std::env::current_exe`
64 CurrentExe,
64 CurrentExe,
65 }
65 }
66
66
67 impl HgError {
67 impl HgError {
68 pub fn corrupted(explanation: impl Into<String>) -> Self {
68 pub fn corrupted(explanation: impl Into<String>) -> Self {
69 // TODO: capture a backtrace here and keep it in the error value
69 // TODO: capture a backtrace here and keep it in the error value
70 // to aid debugging?
70 // to aid debugging?
71 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
71 // https://doc.rust-lang.org/std/backtrace/struct.Backtrace.html
72 HgError::CorruptedRepository(explanation.into())
72 HgError::CorruptedRepository(explanation.into())
73 }
73 }
74
74
75 pub fn unsupported(explanation: impl Into<String>) -> Self {
75 pub fn unsupported(explanation: impl Into<String>) -> Self {
76 HgError::UnsupportedFeature(explanation.into())
76 HgError::UnsupportedFeature(explanation.into())
77 }
77 }
78
78
79 pub fn abort(
79 pub fn abort(
80 explanation: impl Into<String>,
80 explanation: impl Into<String>,
81 exit_code: exit_codes::ExitCode,
81 exit_code: exit_codes::ExitCode,
82 ) -> Self {
82 ) -> Self {
83 HgError::Abort {
83 HgError::Abort {
84 message: explanation.into(),
84 message: explanation.into(),
85 detailed_exit_code: exit_code,
85 detailed_exit_code: exit_code,
86 }
86 }
87 }
87 }
88 }
88 }
89
89
90 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
90 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
91 impl fmt::Display for HgError {
91 impl fmt::Display for HgError {
92 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
92 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
93 match self {
93 match self {
94 HgError::Abort { message, .. } => write!(f, "{}", message),
94 HgError::Abort { message, .. } => write!(f, "{}", message),
95 HgError::IoError { error, context } => {
95 HgError::IoError { error, context } => {
96 write!(f, "abort: {}: {}", context, error)
96 write!(f, "abort: {}: {}", context, error)
97 }
97 }
98 HgError::CorruptedRepository(explanation) => {
98 HgError::CorruptedRepository(explanation) => {
99 write!(f, "abort: {}", explanation)
99 write!(f, "abort: {}", explanation)
100 }
100 }
101 HgError::UnsupportedFeature(explanation) => {
101 HgError::UnsupportedFeature(explanation) => {
102 write!(f, "unsupported feature: {}", explanation)
102 write!(f, "unsupported feature: {}", explanation)
103 }
103 }
104 HgError::ConfigValueParseError(error) => error.fmt(f),
104 HgError::ConfigValueParseError(error) => error.fmt(f),
105 }
105 }
106 }
106 }
107 }
107 }
108
108
109 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
109 // TODO: use `DisplayBytes` instead to show non-Unicode filenames losslessly?
110 impl fmt::Display for IoErrorContext {
110 impl fmt::Display for IoErrorContext {
111 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
111 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
112 match self {
112 match self {
113 IoErrorContext::ReadingMetadata(path) => {
113 IoErrorContext::ReadingMetadata(path) => {
114 write!(f, "when reading metadata of {}", path.display())
114 write!(f, "when reading metadata of {}", path.display())
115 }
115 }
116 IoErrorContext::ReadingFile(path) => {
116 IoErrorContext::ReadingFile(path) => {
117 write!(f, "when reading {}", path.display())
117 write!(f, "when reading {}", path.display())
118 }
118 }
119 IoErrorContext::WritingFile(path) => {
119 IoErrorContext::WritingFile(path) => {
120 write!(f, "when writing {}", path.display())
120 write!(f, "when writing {}", path.display())
121 }
121 }
122 IoErrorContext::RemovingFile(path) => {
122 IoErrorContext::RemovingFile(path) => {
123 write!(f, "when removing {}", path.display())
123 write!(f, "when removing {}", path.display())
124 }
124 }
125 IoErrorContext::RenamingFile { from, to } => write!(
125 IoErrorContext::RenamingFile { from, to } => write!(
126 f,
126 f,
127 "when renaming {} to {}",
127 "when renaming {} to {}",
128 from.display(),
128 from.display(),
129 to.display()
129 to.display()
130 ),
130 ),
131 IoErrorContext::CanonicalizingPath(path) => {
131 IoErrorContext::CanonicalizingPath(path) => {
132 write!(f, "when canonicalizing {}", path.display())
132 write!(f, "when canonicalizing {}", path.display())
133 }
133 }
134 IoErrorContext::CurrentDir => {
134 IoErrorContext::CurrentDir => {
135 write!(f, "error getting current working directory")
135 write!(f, "error getting current working directory")
136 }
136 }
137 IoErrorContext::CurrentExe => {
137 IoErrorContext::CurrentExe => {
138 write!(f, "error getting current executable")
138 write!(f, "error getting current executable")
139 }
139 }
140 }
140 }
141 }
141 }
142 }
142 }
143
143
144 pub trait IoResultExt<T> {
144 pub trait IoResultExt<T> {
145 /// Annotate a possible I/O error as related to a reading a file at the
145 /// Annotate a possible I/O error as related to a reading a file at the
146 /// given path.
146 /// given path.
147 ///
147 ///
148 /// This allows printing something like β€œFile not found when reading
148 /// This allows printing something like β€œFile not found when reading
149 /// example.txt” instead of just β€œFile not found”.
149 /// example.txt” instead of just β€œFile not found”.
150 ///
150 ///
151 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
151 /// Converts a `Result` with `std::io::Error` into one with `HgError`.
152 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
152 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError>;
153
153
154 fn when_writing_file(self, path: &std::path::Path) -> Result<T, HgError>;
155
154 fn with_context(
156 fn with_context(
155 self,
157 self,
156 context: impl FnOnce() -> IoErrorContext,
158 context: impl FnOnce() -> IoErrorContext,
157 ) -> Result<T, HgError>;
159 ) -> Result<T, HgError>;
158 }
160 }
159
161
160 impl<T> IoResultExt<T> for std::io::Result<T> {
162 impl<T> IoResultExt<T> for std::io::Result<T> {
161 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
163 fn when_reading_file(self, path: &std::path::Path) -> Result<T, HgError> {
162 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
164 self.with_context(|| IoErrorContext::ReadingFile(path.to_owned()))
163 }
165 }
164
166
167 fn when_writing_file(self, path: &std::path::Path) -> Result<T, HgError> {
168 self.with_context(|| IoErrorContext::WritingFile(path.to_owned()))
169 }
170
165 fn with_context(
171 fn with_context(
166 self,
172 self,
167 context: impl FnOnce() -> IoErrorContext,
173 context: impl FnOnce() -> IoErrorContext,
168 ) -> Result<T, HgError> {
174 ) -> Result<T, HgError> {
169 self.map_err(|error| HgError::IoError {
175 self.map_err(|error| HgError::IoError {
170 error,
176 error,
171 context: context(),
177 context: context(),
172 })
178 })
173 }
179 }
174 }
180 }
175
181
176 pub trait HgResultExt<T> {
182 pub trait HgResultExt<T> {
177 /// Handle missing files separately from other I/O error cases.
183 /// Handle missing files separately from other I/O error cases.
178 ///
184 ///
179 /// Wraps the `Ok` type in an `Option`:
185 /// Wraps the `Ok` type in an `Option`:
180 ///
186 ///
181 /// * `Ok(x)` becomes `Ok(Some(x))`
187 /// * `Ok(x)` becomes `Ok(Some(x))`
182 /// * An I/O "not found" error becomes `Ok(None)`
188 /// * An I/O "not found" error becomes `Ok(None)`
183 /// * Other errors are unchanged
189 /// * Other errors are unchanged
184 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
190 fn io_not_found_as_none(self) -> Result<Option<T>, HgError>;
185 }
191 }
186
192
187 impl<T> HgResultExt<T> for Result<T, HgError> {
193 impl<T> HgResultExt<T> for Result<T, HgError> {
188 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
194 fn io_not_found_as_none(self) -> Result<Option<T>, HgError> {
189 match self {
195 match self {
190 Ok(x) => Ok(Some(x)),
196 Ok(x) => Ok(Some(x)),
191 Err(HgError::IoError { error, .. })
197 Err(HgError::IoError { error, .. })
192 if error.kind() == std::io::ErrorKind::NotFound =>
198 if error.kind() == std::io::ErrorKind::NotFound =>
193 {
199 {
194 Ok(None)
200 Ok(None)
195 }
201 }
196 Err(other_error) => Err(other_error),
202 Err(other_error) => Err(other_error),
197 }
203 }
198 }
204 }
199 }
205 }
@@ -1,136 +1,157 b''
1 use crate::errors::{HgError, IoErrorContext, IoResultExt};
1 use crate::errors::{HgError, IoErrorContext, IoResultExt};
2 use memmap2::{Mmap, MmapOptions};
2 use memmap2::{Mmap, MmapOptions};
3 use std::io::ErrorKind;
3 use std::io::{ErrorKind, Write};
4 use std::path::{Path, PathBuf};
4 use std::path::{Path, PathBuf};
5
5
6 /// Filesystem access abstraction for the contents of a given "base" diretory
6 /// Filesystem access abstraction for the contents of a given "base" diretory
7 #[derive(Clone, Copy)]
7 #[derive(Clone, Copy)]
8 pub struct Vfs<'a> {
8 pub struct Vfs<'a> {
9 pub(crate) base: &'a Path,
9 pub(crate) base: &'a Path,
10 }
10 }
11
11
12 struct FileNotFound(std::io::Error, PathBuf);
12 struct FileNotFound(std::io::Error, PathBuf);
13
13
14 impl Vfs<'_> {
14 impl Vfs<'_> {
15 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
15 pub fn join(&self, relative_path: impl AsRef<Path>) -> PathBuf {
16 self.base.join(relative_path)
16 self.base.join(relative_path)
17 }
17 }
18
18
19 pub fn symlink_metadata(
19 pub fn symlink_metadata(
20 &self,
20 &self,
21 relative_path: impl AsRef<Path>,
21 relative_path: impl AsRef<Path>,
22 ) -> Result<std::fs::Metadata, HgError> {
22 ) -> Result<std::fs::Metadata, HgError> {
23 let path = self.join(relative_path);
23 let path = self.join(relative_path);
24 std::fs::symlink_metadata(&path).when_reading_file(&path)
24 std::fs::symlink_metadata(&path).when_reading_file(&path)
25 }
25 }
26
26
27 pub fn read_link(
27 pub fn read_link(
28 &self,
28 &self,
29 relative_path: impl AsRef<Path>,
29 relative_path: impl AsRef<Path>,
30 ) -> Result<PathBuf, HgError> {
30 ) -> Result<PathBuf, HgError> {
31 let path = self.join(relative_path);
31 let path = self.join(relative_path);
32 std::fs::read_link(&path).when_reading_file(&path)
32 std::fs::read_link(&path).when_reading_file(&path)
33 }
33 }
34
34
35 pub fn read(
35 pub fn read(
36 &self,
36 &self,
37 relative_path: impl AsRef<Path>,
37 relative_path: impl AsRef<Path>,
38 ) -> Result<Vec<u8>, HgError> {
38 ) -> Result<Vec<u8>, HgError> {
39 let path = self.join(relative_path);
39 let path = self.join(relative_path);
40 std::fs::read(&path).when_reading_file(&path)
40 std::fs::read(&path).when_reading_file(&path)
41 }
41 }
42
42
43 fn mmap_open_gen(
43 fn mmap_open_gen(
44 &self,
44 &self,
45 relative_path: impl AsRef<Path>,
45 relative_path: impl AsRef<Path>,
46 ) -> Result<Result<Mmap, FileNotFound>, HgError> {
46 ) -> Result<Result<Mmap, FileNotFound>, HgError> {
47 let path = self.join(relative_path);
47 let path = self.join(relative_path);
48 let file = match std::fs::File::open(&path) {
48 let file = match std::fs::File::open(&path) {
49 Err(err) => {
49 Err(err) => {
50 if let ErrorKind::NotFound = err.kind() {
50 if let ErrorKind::NotFound = err.kind() {
51 return Ok(Err(FileNotFound(err, path)));
51 return Ok(Err(FileNotFound(err, path)));
52 };
52 };
53 return (Err(err)).when_reading_file(&path);
53 return (Err(err)).when_reading_file(&path);
54 }
54 }
55 Ok(file) => file,
55 Ok(file) => file,
56 };
56 };
57 // TODO: what are the safety requirements here?
57 // TODO: what are the safety requirements here?
58 let mmap = unsafe { MmapOptions::new().map(&file) }
58 let mmap = unsafe { MmapOptions::new().map(&file) }
59 .when_reading_file(&path)?;
59 .when_reading_file(&path)?;
60 Ok(Ok(mmap))
60 Ok(Ok(mmap))
61 }
61 }
62
62
63 pub fn mmap_open_opt(
63 pub fn mmap_open_opt(
64 &self,
64 &self,
65 relative_path: impl AsRef<Path>,
65 relative_path: impl AsRef<Path>,
66 ) -> Result<Option<Mmap>, HgError> {
66 ) -> Result<Option<Mmap>, HgError> {
67 self.mmap_open_gen(relative_path).map(|res| res.ok())
67 self.mmap_open_gen(relative_path).map(|res| res.ok())
68 }
68 }
69
69
70 pub fn mmap_open(
70 pub fn mmap_open(
71 &self,
71 &self,
72 relative_path: impl AsRef<Path>,
72 relative_path: impl AsRef<Path>,
73 ) -> Result<Mmap, HgError> {
73 ) -> Result<Mmap, HgError> {
74 match self.mmap_open_gen(relative_path)? {
74 match self.mmap_open_gen(relative_path)? {
75 Err(FileNotFound(err, path)) => Err(err).when_reading_file(&path),
75 Err(FileNotFound(err, path)) => Err(err).when_reading_file(&path),
76 Ok(res) => Ok(res),
76 Ok(res) => Ok(res),
77 }
77 }
78 }
78 }
79
79
80 pub fn rename(
80 pub fn rename(
81 &self,
81 &self,
82 relative_from: impl AsRef<Path>,
82 relative_from: impl AsRef<Path>,
83 relative_to: impl AsRef<Path>,
83 relative_to: impl AsRef<Path>,
84 ) -> Result<(), HgError> {
84 ) -> Result<(), HgError> {
85 let from = self.join(relative_from);
85 let from = self.join(relative_from);
86 let to = self.join(relative_to);
86 let to = self.join(relative_to);
87 std::fs::rename(&from, &to)
87 std::fs::rename(&from, &to)
88 .with_context(|| IoErrorContext::RenamingFile { from, to })
88 .with_context(|| IoErrorContext::RenamingFile { from, to })
89 }
89 }
90
90
91 pub fn remove_file(
91 pub fn remove_file(
92 &self,
92 &self,
93 relative_path: impl AsRef<Path>,
93 relative_path: impl AsRef<Path>,
94 ) -> Result<(), HgError> {
94 ) -> Result<(), HgError> {
95 let path = self.join(relative_path);
95 let path = self.join(relative_path);
96 std::fs::remove_file(&path)
96 std::fs::remove_file(&path)
97 .with_context(|| IoErrorContext::RemovingFile(path))
97 .with_context(|| IoErrorContext::RemovingFile(path))
98 }
98 }
99
99
100 #[cfg(unix)]
100 #[cfg(unix)]
101 pub fn create_symlink(
101 pub fn create_symlink(
102 &self,
102 &self,
103 relative_link_path: impl AsRef<Path>,
103 relative_link_path: impl AsRef<Path>,
104 target_path: impl AsRef<Path>,
104 target_path: impl AsRef<Path>,
105 ) -> Result<(), HgError> {
105 ) -> Result<(), HgError> {
106 let link_path = self.join(relative_link_path);
106 let link_path = self.join(relative_link_path);
107 std::os::unix::fs::symlink(target_path, &link_path)
107 std::os::unix::fs::symlink(target_path, &link_path)
108 .with_context(|| IoErrorContext::WritingFile(link_path))
108 .when_writing_file(&link_path)
109 }
110
111 /// Write `contents` into a temporary file, then rename to `relative_path`.
112 /// This makes writing to a file "atomic": a reader opening that path will
113 /// see either the previous contents of the file or the complete new
114 /// content, never a partial write.
115 pub fn atomic_write(
116 &self,
117 relative_path: impl AsRef<Path>,
118 contents: &[u8],
119 ) -> Result<(), HgError> {
120 let mut tmp = tempfile::NamedTempFile::new_in(self.base)
121 .when_writing_file(self.base)?;
122 tmp.write_all(contents)
123 .and_then(|()| tmp.flush())
124 .when_writing_file(tmp.path())?;
125 let path = self.join(relative_path);
126 tmp.persist(&path)
127 .map_err(|e| e.error)
128 .when_writing_file(&path)?;
129 Ok(())
109 }
130 }
110 }
131 }
111
132
112 fn fs_metadata(
133 fn fs_metadata(
113 path: impl AsRef<Path>,
134 path: impl AsRef<Path>,
114 ) -> Result<Option<std::fs::Metadata>, HgError> {
135 ) -> Result<Option<std::fs::Metadata>, HgError> {
115 let path = path.as_ref();
136 let path = path.as_ref();
116 match std::fs::metadata(path) {
137 match std::fs::metadata(path) {
117 Ok(meta) => Ok(Some(meta)),
138 Ok(meta) => Ok(Some(meta)),
118 Err(error) => match error.kind() {
139 Err(error) => match error.kind() {
119 // TODO: when we require a Rust version where `NotADirectory` is
140 // TODO: when we require a Rust version where `NotADirectory` is
120 // stable, invert this logic and return None for it and `NotFound`
141 // stable, invert this logic and return None for it and `NotFound`
121 // and propagate any other error.
142 // and propagate any other error.
122 ErrorKind::PermissionDenied => Err(error).with_context(|| {
143 ErrorKind::PermissionDenied => Err(error).with_context(|| {
123 IoErrorContext::ReadingMetadata(path.to_owned())
144 IoErrorContext::ReadingMetadata(path.to_owned())
124 }),
145 }),
125 _ => Ok(None),
146 _ => Ok(None),
126 },
147 },
127 }
148 }
128 }
149 }
129
150
130 pub(crate) fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> {
151 pub(crate) fn is_dir(path: impl AsRef<Path>) -> Result<bool, HgError> {
131 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir()))
152 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_dir()))
132 }
153 }
133
154
134 pub(crate) fn is_file(path: impl AsRef<Path>) -> Result<bool, HgError> {
155 pub(crate) fn is_file(path: impl AsRef<Path>) -> Result<bool, HgError> {
135 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file()))
156 Ok(fs_metadata(path)?.map_or(false, |meta| meta.is_file()))
136 }
157 }
General Comments 0
You need to be logged in to leave comments. Login now