Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_jdat/src/file.rs

8.3 KiB, 21 runs

created by r1870400018:465, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1use crate::{
2 prelude::*,
3 string::{
4 dec::DecoderConfig,
5 enc::EncoderConfig,
6 },
7 usr::{
8 UsrKind,
9 UsrKindId,
10 UsrKindCode,
11 },
12};
13
14use oxedyne_fe2o3_core::{
15 prelude::*,
16 file as core_file,
17 map::MapMut,
18};
19use oxedyne_fe2o3_text::string::Stringer;
20
21use std::{
22 fmt,
23 fs,
24 io::Write,
25 path::{
26 Path,
27 PathBuf,
28 },
29};
30
31
32/// `JdatFile` is suitable for more complex `struct`s with manual implementations of `FromDat` and
33/// `ToDat`.
34pub trait JdatFile: FromDat + ToDat {
35
36 fn load<
37 P: AsRef<Path>,
38 M1: MapMut<UsrKindCode, UsrKind> + Clone + fmt::Debug + Default,
39 M2: MapMut<String, UsrKindId> + Clone + fmt::Debug + Default,
40 >(
41 path: P,
42 dec_cfg_opt: Option<DecoderConfig<M1, M2>>,
43 )
44 -> Outcome<Self> where Self: Sized
45 {
46 let path = path.as_ref();
47 match fs::read_to_string(path) {
48 Ok(s) => {
49 let dat = if let Some(cfg) = dec_cfg_opt {
50 res!(Dat::decode_string_with_config(s, &cfg))
51 } else {
52 res!(Dat::decode_string(s))
53 };
54 Self::from_dat(dat)
55 },
56 Err(e) => return Err(err!(e,
57 "While trying to read file '{}' as a Dat.", path.display();
58 IO, File, Read)),
59 }
60 }
61
62 fn save<
63 P: AsRef<Path>,
64 M1: MapMut<UsrKindCode, UsrKind> + Clone + fmt::Debug + Default,
65 M2: MapMut<String, UsrKindId> + Clone + fmt::Debug + Default,
66 >(
67 &self,
68 path: P,
69 tab: &str,
70 //print_kinds: bool,
71 enc_cfg_opt: Option<EncoderConfig<M1, M2>>,
72 )
73 -> Outcome<()>
74 {
75 let path = path.as_ref();
76 let mut file = res!(fs::File::create(&path));
77 let dat = res!(self.to_dat());
78 let s = if let Some(cfg) = enc_cfg_opt {
79 res!(dat.encode_string_with_config(&cfg))
80 } else {
81 fmt!("{:?}", dat)
82 };
83 for mut line in Stringer::new(s).to_lines(tab) {
84 line.push_str("\n");
85 res!(file.write(line.as_bytes()));
86 }
87 Ok(())
88 }
89
90 /// As [`save`](Self::save), but for a file holding key material: the
91 /// write is atomic and the file ends at mode 0600 whatever the caller's
92 /// umask, even when it already existed at a more permissive mode.
93 /// `save` itself is untouched, so every other caller keeps its current
94 /// permissions behaviour.
95 fn save_secret<
96 P: AsRef<Path>,
97 M1: MapMut<UsrKindCode, UsrKind> + Clone + fmt::Debug + Default,
98 M2: MapMut<String, UsrKindId> + Clone + fmt::Debug + Default,
99 >(
100 &self,
101 path: P,
102 tab: &str,
103 enc_cfg_opt: Option<EncoderConfig<M1, M2>>,
104 )
105 -> Outcome<()>
106 {
107 let path = path.as_ref();
108 let dat = res!(self.to_dat());
109 let s = if let Some(cfg) = enc_cfg_opt {
110 res!(dat.encode_string_with_config(&cfg))
111 } else {
112 fmt!("{:?}", dat)
113 };
114 let mut text = String::new();
115 for mut line in Stringer::new(s).to_lines(tab) {
116 line.push_str("\n");
117 text.push_str(&line);
118 }
119 res!(core_file::save_secret(path, text.as_bytes()));
120 Ok(())
121 }
122
123}
124
125/// `JdatMapFile` is suitable for simpler `struct`s that have derived `FromDatMap` and `ToDatMap`.
126pub trait JdatMapFile: FromDatMap + ToDatMap + Clone {
127
128 fn load<P: AsRef<Path>>(path: P) -> Outcome<Self> {
129 let path = path.as_ref();
130 match fs::read_to_string(path) {
131 Ok(s) => {
132 let dat = res!(Dat::decode_string(s)).normalise();
133 if let Dat::Map(map) = dat {
134 let s = res!(Self::from_datmap(map));
135 Ok(s)
136 } else {
137 return Err(err!(
138 "Expected a daticle map at '{}', found a {:?}",
139 path.display(), dat.kind();
140 Input, Invalid));
141 }
142 },
143 Err(e) => return Err(err!(e,
144 "While trying to read file '{}' as a Dat.", path.display();
145 IO, File)),
146 }
147 }
148
149 fn save<P: AsRef<Path>>(
150 &self,
151 path: P,
152 tab: &str,
153 print_kinds: bool,
154 )
155 -> Outcome<()>
156 {
157 let path = path.as_ref();
158 let mut file = res!(fs::File::create(&path));
159 let dat = Self::to_datmap(self.clone());
160 for mut line in dat.to_lines(tab, print_kinds) {
161 line.push_str("\n");
162 res!(file.write(line.as_bytes()));
163 }
164 Ok(())
165 }
166
167}
168
169/// Allows data to be specified either directly or via a file.
170#[derive(Debug)]
171pub enum LoadableJdat<J: JdatFile> {
172 Data(J),
173 Path(PathBuf),
174}
175
176/// Allows data to be specified either directly or via a file.
177#[derive(Debug)]
178pub enum LoadableJdatMap<J: JdatMapFile> {
179 Data(J),
180 Path(PathBuf),
181}
182
183
184#[cfg(all(test, unix))]
185mod tests {
186 use super::*;
187
188 use std::{
189 os::unix::fs::PermissionsExt,
190 sync::atomic::{
191 AtomicU64,
192 Ordering,
193 },
194 };
195
196 static COUNTER: AtomicU64 = AtomicU64::new(0);
197
198 /// A minimal `JdatFile` implementor, in the shape of `Wallet` in
199 /// `fe2o3_crypto` -- a thin wrapper that hands its `Dat` straight
200 /// through -- so `save`/`save_secret` can be exercised here without a
201 /// dependency this crate cannot take (`fe2o3_crypto` depends on
202 /// `fe2o3_jdat`, not the other way round).
203 #[derive(Clone, Debug)]
204 struct TestDoc(Dat);
205
206 impl ToDat for TestDoc {
207 fn to_dat(&self) -> Outcome<Dat> { Ok(self.0.clone()) }
208 }
209
210 impl FromDat for TestDoc {
211 fn from_dat(dat: Dat) -> Outcome<Self> { Ok(Self(dat)) }
212 }
213
214 impl JdatFile for TestDoc {}
215
216 fn scratch_path(label: &str) -> PathBuf {
217 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
218 std::env::temp_dir().join(fmt!(
219 "fe2o3_jdat_file_test_{}_{}_{}", std::process::id(), n, label,
220 ))
221 }
222
223 fn mode_of(path: &Path) -> Outcome<u32> {
224 match fs::metadata(path) {
225 Ok(m) => Ok(m.permissions().mode() & 0o777),
226 Err(e) => Err(err!(e, "Could not stat {:?}.", path; Test, File, IO, Read)),
227 }
228 }
229
230 /// `save` is the path every existing caller relies on for non-secret
231 /// files, e.g. `ServerConfig`. It must keep leaving a file's mode alone,
232 /// so an already-permissive config file stays exactly as permissive.
233 #[test]
234 fn test_ordinary_save_does_not_restrict_an_existing_files_mode() -> Outcome<()> {
235 let path = scratch_path("ordinary_save");
236 if let Err(e) = fs::write(&path, b"placeholder") {
237 return Err(err!(e, "Could not pre-seed {:?}.", path; Test, File, IO, Write));
238 }
239 if let Err(e) = fs::set_permissions(&path, fs::Permissions::from_mode(0o644)) {
240 return Err(err!(e, "Could not set 0644 on {:?}.", path; Test, File, IO));
241 }
242
243 let doc = TestDoc(Dat::Str("not a secret".to_string()));
244 let save_res = doc.save(&path, " ", Some(EncoderConfig::<(), ()>::default()));
245
246 let mode = mode_of(&path);
247 let _ = fs::remove_file(&path);
248 res!(save_res);
249 if res!(mode) != 0o644 {
250 return Err(err!(
251 "The ordinary JdatFile::save changed {:?}'s mode away from 0644; \
252 a non-secret save must leave permissions alone.", path;
253 Test, Mismatch));
254 }
255 Ok(())
256 }
257
258 /// `save_secret` is the path key material -- the wallet, TLS and DKIM
259 /// keys -- must go through instead: the file must end at 0600 even
260 /// though nothing here asked for a restrictive mode explicitly.
261 #[test]
262 fn test_save_secret_restricts_a_new_files_mode() -> Outcome<()> {
263 let path = scratch_path("save_secret");
264 let doc = TestDoc(Dat::Str("top secret".to_string()));
265 let save_res = doc.save_secret(&path, " ", Some(EncoderConfig::<(), ()>::default()));
266
267 let mode = mode_of(&path);
268 let _ = fs::remove_file(&path);
269 res!(save_res);
270 if res!(mode) != 0o600 {
271 return Err(err!(
272 "save_secret left {:?} at a mode other than 0600.", path;
273 Test, Mismatch));
274 }
275 Ok(())
276 }
277}