veloren_server/settings/editable.rs
1use atomicwrites::{AtomicFile, Error as AtomicError, OverwriteBehavior};
2use core::{convert::TryInto, fmt};
3use serde::{Serialize, de::DeserializeOwned};
4use std::{
5 fs,
6 io::{Seek, Write},
7 path::{Path, PathBuf},
8};
9use tracing::{error, info, warn};
10
11/// Errors that can occur during edits to a settings file.
12pub enum Error<S: EditableSetting> {
13 /// An error occurred validating the settings file.
14 Integrity(S::Error),
15 /// An IO error occurred when writing to the settings file.
16 Io(std::io::Error),
17}
18
19impl<S: EditableSetting> fmt::Debug for Error<S> {
20 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
21 match self {
22 Error::Integrity(err) => fmt::Formatter::debug_tuple(f, "Integrity")
23 .field(err)
24 .finish(),
25 Error::Io(err) => fmt::Formatter::debug_tuple(f, "Io").field(err).finish(),
26 }
27 }
28}
29
30/// Same as Error, but carries the validated settings in the Io case.
31enum ErrorInternal<S: EditableSetting> {
32 Integrity(S::Error),
33 Io(std::io::Error, S),
34}
35
36impl<S: EditableSetting> fmt::Debug for ErrorInternal<S> {
37 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
38 match self {
39 ErrorInternal::Integrity(err) => fmt::Formatter::debug_tuple(f, "Integrity")
40 .field(err)
41 .finish(),
42 ErrorInternal::Io(err, _setting) => fmt::Formatter::debug_tuple(f, "Io")
43 .field(err)
44 .field(&"EditableSetting not required to impl Debug")
45 .finish(),
46 }
47 }
48}
49
50pub enum Version {
51 /// This was an old version of the settings file, so overwrite with the
52 /// modern config.
53 Old,
54 /// Latest version of the settings file.
55 Latest,
56}
57
58pub trait EditableSetting: Clone + Default {
59 const FILENAME: &'static str;
60
61 /// Please use this error sparingly, since we ideally want to preserve
62 /// forwards compatibility for all migrations. In particular, this
63 /// error should be used to fail validation *of the original settings
64 /// file* that cannot be caught with ordinary parsing, rather than used
65 /// to signal errors that occurred during migrations.
66 ///
67 /// The best error type is Infallible.
68 type Error: fmt::Debug;
69
70 /// Into<Setting> is expected to migrate directly to the latest version,
71 /// which can be implemented using "chaining". The use of `Into` here
72 /// rather than TryInto is intended (together with the expected use of
73 /// chaining) to prevent migrations from invalidating old save files
74 /// without warning; there should always be a non-failing migration path
75 /// from the oldest to latest format (if the migration path fails, we can
76 /// panic).
77 type Legacy: Serialize + DeserializeOwned + Into<Self>;
78
79 /// TryInto<(Version, Self)> is expected to migrate to the latest version
80 /// from any older version, using "chaining" (see [super::banlist] for
81 /// examples).
82 ///
83 /// From<Self> is intended to construct the latest version of the
84 /// configuratino file from Self, which we use to save the config file
85 /// on migration or modification. Note that it should always be the
86 /// case that if x is constructed from any of Self::clone, Self::default, or
87 /// Setting::try_into, then Setting::try_from(Self::into(x)).is_ok() must be
88 /// true!
89 ///
90 /// The error should be used to fail validation *of the original settings
91 /// file* that cannot be caught with parsing. If we can possibly avoid
92 /// it, we should not create errors in valid settings files during
93 /// migration, to ensure forwards compatibility.
94 type Setting: Serialize
95 + DeserializeOwned
96 + TryInto<(Version, Self), Error = Self::Error>
97 + From<Self>;
98
99 fn load(data_dir: &Path) -> Self {
100 let path = Self::get_path(data_dir);
101
102 if let Ok(mut file) = fs::File::open(&path) {
103 match ron::de::from_reader(&mut file)
104 .map(|setting: Self::Setting| setting.try_into())
105 .or_else(|orig_err| {
106 file.rewind()?;
107 ron::de::from_reader(file)
108 .map(|legacy| Ok((Version::Old, Self::Legacy::into(legacy))))
109 // When both legacy and non-legacy have parse errors, prioritize the
110 // non-legacy one, since we can't tell which one is "right" and legacy
111 // formats are simple, early, and uncommon enough that we expect
112 // few parse errors in those.
113 .or(Err(orig_err))
114 })
115 .map_err(|e| {
116 warn!(
117 ?e,
118 "Failed to parse setting file! Falling back to default and moving \
119 existing file to a .invalid"
120 );
121 })
122 .and_then(|inner| {
123 inner.map_err(|e| {
124 warn!(
125 ?e,
126 "Failed to parse setting file! Falling back to default and moving \
127 existing file to a .invalid"
128 );
129 })
130 }) {
131 Ok((version, mut settings)) => {
132 if matches!(version, Version::Old) {
133 // Old version, which means we either performed a migration or there was
134 // some needed update to the file. If this is the case, we preemptively
135 // overwrite the settings file (not strictly needed, but useful for
136 // people who do manual editing).
137 info!("Settings were changed on load, updating file...");
138 // We don't care if we encountered an error on saving updates to a
139 // settings file that we just loaded (it's already logged and migrated).
140 // However, we should crash if it reported an integrity failure, since we
141 // supposedly just validated it.
142 if let Err(Error::Integrity(err)) = settings
143 .edit(data_dir, |_| Some(()))
144 .expect("Some always returns Some")
145 .1
146 {
147 panic!(
148 "The identity conversion from a validated settings file must
149 always be valid, but we found an integrity error: {:?}",
150 err
151 );
152 }
153 }
154 settings
155 },
156 Err(()) => {
157 // Rename existing file to .invalid.ron
158 let mut new_path = path.with_extension("invalid.ron");
159
160 // If invalid path already exists append number
161 for i in 1.. {
162 if !new_path.exists() {
163 break;
164 }
165
166 warn!(
167 ?new_path,
168 "Path to move invalid settings exists, appending number"
169 );
170 new_path = path.with_extension(format!("invalid{}.ron", i));
171 }
172
173 warn!("Renaming invalid settings file to: {}", new_path.display());
174 if let Err(e) = fs::rename(&path, &new_path) {
175 warn!(?e, ?path, ?new_path, "Failed to rename settings file.");
176 }
177
178 create_and_save_default(&path)
179 },
180 }
181 } else {
182 create_and_save_default(&path)
183 }
184 }
185
186 /// If the result of calling f is None, we return None (this constitutes an
187 /// early return and lets us abandon the in-progress edit). For
188 /// example, this can be used to avoid adding a new ban entry if someone
189 /// is already banned and the user didn't explicitly specify that they
190 /// wanted to add a new ban record, even though it would be completely
191 /// valid to attach one.
192 ///
193 /// Otherwise (the result of calling f was Some(r)), we always return
194 /// Some((r, res)), where:
195 ///
196 /// If res is Ok(()), validation succeeded for the edited, and changes made
197 /// inside the closure are applied both in memory (to self) and
198 /// atomically on disk.
199 ///
200 /// Otherwise (res is Err(e)), some step in the edit process failed.
201 /// Specifically:
202 ///
203 /// * If e is Integrity, validation failed and the settings were not
204 /// updated.
205 /// * If e is Io, validation succeeded and the settings were updated in
206 /// memory, but they could not be saved to storage (and a warning was
207 /// logged). The reason we return an error even though the operation was
208 /// partially successful is so we can alert the player who ran the command
209 /// about the failure, as they will often be an administrator who can
210 /// usefully act upon that information.
211 #[must_use]
212 fn edit<R>(
213 &mut self,
214 data_dir: &Path,
215 f: impl FnOnce(&mut Self) -> Option<R>,
216 ) -> Option<(R, Result<(), Error<Self>>)> {
217 let path = Self::get_path(data_dir);
218
219 // First, edit a copy.
220 let mut copy = self.clone();
221 let r = f(&mut copy)?;
222 // Validate integrity of the raw data before saving (by making sure that
223 // converting to and from the Settings format still produces a valid
224 // file).
225 Some((r, match save_to_file(copy, &path) {
226 Ok(new_settings) => {
227 *self = new_settings;
228 Ok(())
229 },
230 Err(ErrorInternal::Io(err, new_settings)) => {
231 warn!("Failed to save setting: {:?}", err);
232 *self = new_settings;
233 Err(Error::Io(err))
234 },
235 Err(ErrorInternal::Integrity(err)) => Err(Error::Integrity(err)),
236 }))
237 }
238
239 fn get_path(data_dir: &Path) -> PathBuf {
240 let mut path = super::with_config_dir(data_dir);
241 path.push(Self::FILENAME);
242 path
243 }
244}
245
246fn save_to_file<S: EditableSetting>(setting: S, path: &Path) -> Result<S, ErrorInternal<S>> {
247 let raw: <S as EditableSetting>::Setting = setting.into();
248 let ron = ron::ser::to_string_pretty(&raw, ron::ser::PrettyConfig::default())
249 .expect("RON does not throw any parse errors during serialization to string.");
250 // This has the side effect of validating the copy, meaning it's safe to save
251 // the file.
252 let (_, settings): (Version, S) = raw.try_into().map_err(ErrorInternal::Integrity)?;
253 // Create dir if it doesn't exist
254 if let Some(dir) = path.parent() {
255 if let Err(err) = fs::create_dir_all(dir) {
256 return Err(ErrorInternal::Io(err, settings));
257 }
258 }
259 // Atomically write the validated string to the settings file.
260 let atomic_file = AtomicFile::new(path, OverwriteBehavior::AllowOverwrite);
261 match atomic_file.write(|file| file.write_all(ron.as_bytes())) {
262 Ok(()) => Ok(settings),
263 Err(AtomicError::Internal(err)) | Err(AtomicError::User(err)) => {
264 Err(ErrorInternal::Io(err, settings))
265 },
266 }
267}
268
269fn create_and_save_default<S: EditableSetting>(path: &Path) -> S {
270 let default = S::default();
271 match save_to_file(default, path) {
272 Ok(settings) => settings,
273 Err(ErrorInternal::Io(e, settings)) => {
274 error!(?e, "Failed to create default setting file!");
275 settings
276 },
277 Err(ErrorInternal::Integrity(err)) => {
278 panic!(
279 "The default settings file must always be valid, but we found an integrity error: \
280 {:?}",
281 err
282 );
283 },
284 }
285}