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
| 1 | use crate::{ |
| 2 | prelude::*, |
| 3 | string::{ |
| 4 | dec::DecoderConfig, |
| 5 | enc::EncoderConfig, |
| 6 | }, |
| 7 | usr::{ |
| 8 | UsrKind, |
| 9 | UsrKindId, |
| 10 | UsrKindCode, |
| 11 | }, |
| 12 | }; |
| 13 | |
| 14 | use oxedyne_fe2o3_core::{ |
| 15 | prelude::*, |
| 16 | file as core_file, |
| 17 | map::MapMut, |
| 18 | }; |
| 19 | use oxedyne_fe2o3_text::string::Stringer; |
| 20 | |
| 21 | use 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`. |
| 34 | pub 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`. |
| 126 | pub 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)] |
| 171 | pub 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)] |
| 178 | pub enum LoadableJdatMap<J: JdatMapFile> { |
| 179 | Data(J), |
| 180 | Path(PathBuf), |
| 181 | } |
| 182 | |
| 183 | |
| 184 | #[cfg(all(test, unix))] |
| 185 | mod 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 | } |