//! Helpers for working with files use anyhow::ensure; use std::fs::File; use std::io::Read; use std::os::unix::fs::OpenOptionsExt; use std::{fs::OpenOptions, path::Path}; /// Level of secrecy applied for a file pub enum Visibility { /// The file might contain a public key Public, /// The file might contain a secret key Secret, } /// Open a file writeably, truncating the file. /// /// Sensible default permissions are chosen based on the value of `visibility` /// /// # Examples /// /// ``` /// use std::io::{Write, Read}; /// use tempfile::tempdir; /// use rosenpass_util::file::{fopen_r, fopen_w, Visibility}; /// /// const CONTENTS : &[u8] = b"Hello World"; /// /// let dir = tempdir()?; /// let path = dir.path().join("secret_key"); /// /// let mut f = fopen_w(&path, Visibility::Secret)?; /// f.write_all(CONTENTS)?; /// /// let mut f = fopen_r(&path)?; /// let mut b = Vec::new(); /// f.read_to_end(&mut b)?; /// assert_eq!(CONTENTS, &b); /// /// Ok::<(), std::io::Error>(()) /// ``` pub fn fopen_w>(path: P, visibility: Visibility) -> std::io::Result { let mut options = OpenOptions::new(); options.create(true).write(true).read(false).truncate(true); match visibility { Visibility::Public => options.mode(0o644), Visibility::Secret => options.mode(0o600), }; options.open(path) } /// Open a file readably /// /// # Examples /// /// See [fopen_w]. pub fn fopen_r>(path: P) -> std::io::Result { OpenOptions::new() .read(true) .write(false) .create(false) .truncate(false) .open(path) } /// Extension trait for [std::io::Read] adding [read_slice_to_end] pub trait ReadSliceToEnd { /// Error type returned by functions in this trait type Error; /// Read slice asserting that the length of the data to read is at most /// as long as the buffer to read into /// /// Note that this *may* append data read to [buf] even if the function fails, /// so the caller should make no assumptions about the contents of the buffer /// after calling read_slice_to_end if the result is an error. /// /// # Examples /// /// ``` /// use rosenpass_util::file::ReadSliceToEnd; /// /// const DATA : &[u8] = b"Hello World"; /// /// // It is OK if file and buffer are equally long /// let mut buf = vec![b' '; 11]; /// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf[..DATA.len()]); /// assert!(res.is_ok()); // Read is overlong /// assert_eq!(buf, DATA); // Finally, data was successfully read /// /// // It is OK if the buffer is longer than the file /// let mut buf = vec![b' '; 16]; /// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf); /// assert!(matches!(res, Ok(11))); /// assert_eq!(buf, b"Hello World "); // Data was still read to the buffer! /// /// // It is not OK if the buffer is shorter than the file /// let mut buf = vec![b' '; 5]; /// let res = Clone::clone(&DATA).read_slice_to_end(&mut buf); /// assert!(res.is_err()); /// /// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED /// assert_eq!(buf, b"Hello"); // Data was still read to the buffer! /// /// Ok::<(), std::io::Error>(()) /// ``` fn read_slice_to_end(&mut self, buf: &mut [u8]) -> Result; } impl ReadSliceToEnd for R { type Error = anyhow::Error; fn read_slice_to_end(&mut self, buf: &mut [u8]) -> anyhow::Result { let mut dummy = [0u8; 8]; let mut read = 0; while read < buf.len() { let bytes_read = self.read(&mut buf[read..])?; if bytes_read == 0 { break; } read += bytes_read; } ensure!(self.read(&mut dummy)? == 0, "File too long!"); Ok(read) } } /// Extension trait for [std::io::Read] adding [read_exact_to_end] pub trait ReadExactToEnd { /// Error type returned by functions in this trait type Error; /// Read slice asserting that the length of the data to be read /// and the buffer are exactly the same length. /// /// Note that this *may* append data read to [buf] even if the function fails, /// so the caller should make no assumptions about the contents of the buffer /// after calling read_exact_to_end if the result is an error. /// /// # Examples /// /// ``` /// use rosenpass_util::file::ReadExactToEnd; /// /// const DATA : &[u8] = b"Hello World"; /// /// // It is OK if file and buffer are equally long /// let mut buf = vec![b' '; 11]; /// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf[..DATA.len()]); /// assert!(res.is_ok()); // Read is overlong /// assert_eq!(buf, DATA); // Finally, data was successfully read /// /// // It is not OK if the buffer is longer than the file /// let mut buf = vec![b' '; 16]; /// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf); /// assert!(res.is_err()); /// /// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED /// // The read implementation for &[u8] happens not to do this /// assert_eq!(buf, b" "); // Data was still read to the buffer! /// /// // It is not OK if the buffer is shorter than the file /// let mut buf = vec![b' '; 5]; /// let res = Clone::clone(&DATA).read_exact_to_end(&mut buf); /// assert!(res.is_err()); /// /// // THE BUFFER MAY STILL BE FILLED THOUGH, BUT THIS IS NOT GUARANTEED /// assert_eq!(buf, b"Hello"); // Data was still read to the buffer! /// /// Ok::<(), std::io::Error>(()) /// ``` fn read_exact_to_end(&mut self, buf: &mut [u8]) -> Result<(), Self::Error>; } impl ReadExactToEnd for R { type Error = anyhow::Error; fn read_exact_to_end(&mut self, buf: &mut [u8]) -> anyhow::Result<()> { let mut dummy = [0u8; 8]; self.read_exact(buf)?; ensure!(self.read(&mut dummy)? == 0, "File too long!"); Ok(()) } } /// Load a value from a file pub trait LoadValue { /// Error type returned type Error; /// Load a value from a file /// /// # Examples /// /// ``` /// use std::path::Path; /// use std::io::Write; /// use tempfile::tempdir; /// use rosenpass_util::file::{fopen_r, fopen_w, LoadValue, ReadExactToEnd, StoreValue, Visibility}; /// /// #[derive(Debug, PartialEq, Eq)] /// struct MyInt(pub u32); /// /// impl StoreValue for MyInt { /// type Error = std::io::Error; /// /// fn store>(&self, path: P) -> Result<(), Self::Error> { /// let mut f = fopen_w(path, Visibility::Public)?; /// f.write_all(&self.0.to_le_bytes()) /// } /// } /// /// impl LoadValue for MyInt { /// type Error = anyhow::Error; /// /// fn load>(path: P) -> Result /// where /// Self: Sized, /// { /// let mut b = [0u8; 4]; /// fopen_r(path)?.read_exact_to_end(&mut b)?; /// Ok(MyInt(u32::from_le_bytes(b))) /// } /// } /// /// let dir = tempdir()?; /// let path = dir.path().join("my_int"); /// /// let orig = MyInt(17); /// orig.store(&path)?; /// /// let copy = MyInt::load(&path)?; /// assert_eq!(orig, copy); /// /// Ok::<(), anyhow::Error>(()) /// ``` fn load>(path: P) -> Result where Self: Sized; } /// Load a value from a file encoded as base64 pub trait LoadValueB64 { /// Error type returned type Error; /// Load a value from a file encoded as base64 /// /// # Examples /// /// ``` /// use std::path::Path; /// use tempfile::tempdir; /// use rosenpass_util::b64::{b64_decode, b64_encode}; /// use rosenpass_util::file::{ /// fopen_r, fopen_w, LoadValueB64, ReadSliceToEnd, StoreValueB64, StoreValueB64Writer, /// Visibility, /// }; /// /// #[derive(Debug, PartialEq, Eq)] /// struct MyInt(pub u32); /// /// impl StoreValueB64Writer for MyInt { /// type Error = anyhow::Error; /// /// fn store_b64_writer( /// &self, /// mut writer: W, /// ) -> Result<(), Self::Error> { /// // Let me just point out while writing this example, /// // that this API is currently, entirely shit in terms of /// // how it deals with buffer lengths. /// let mut buf = [0u8; F]; /// let b64 = b64_encode(&self.0.to_le_bytes(), &mut buf)?; /// writer.write_all(b64.as_bytes())?; /// Ok(()) /// } /// } /// /// impl StoreValueB64 for MyInt { /// type Error = anyhow::Error; /// /// fn store_b64>(&self, path: P) -> Result<(), Self::Error> /// where /// Self: Sized, /// { /// // The buffer length (first generic arg) is kind of an upper bound /// self.store_b64_writer::(fopen_w(path, Visibility::Public)?) /// } /// } /// /// impl LoadValueB64 for MyInt { /// type Error = anyhow::Error; /// /// fn load_b64>(path: P) -> Result /// where /// Self: Sized, /// { /// // The buffer length is kind of an upper bound /// let mut b64_buf = [0u8; F]; /// let b64_len = fopen_r(path)?.read_slice_to_end(&mut b64_buf)?; /// let b64_dat = &b64_buf[..b64_len]; /// /// let mut buf = [0u8; 4]; /// b64_decode(b64_dat, &mut buf)?; /// Ok(MyInt(u32::from_le_bytes(buf))) /// } /// } /// /// let dir = tempdir()?; /// let path = dir.path().join("my_int"); /// /// let orig = MyInt(17); /// orig.store_b64::<10, _>(&path)?; /// /// let copy = MyInt::load_b64::<10, _>(&path)?; /// assert_eq!(orig, copy); /// /// Ok::<(), anyhow::Error>(()) /// ``` fn load_b64>(path: P) -> Result where Self: Sized; } /// Store a value encoded as base64 in a file. pub trait StoreValueB64 { /// Error type returned type Error; /// Store a value encoded as base64 in a file. /// /// # Examples /// /// See [LoadValueB64::load_b64]. fn store_b64>(&self, path: P) -> Result<(), Self::Error> where Self: Sized; } /// Store a value encoded as base64 to a writable stream pub trait StoreValueB64Writer { /// Error type returned type Error; /// Store a value encoded as base64 to a writable stream /// /// # Examples /// /// See [LoadValueB64::load_b64]. fn store_b64_writer( &self, writer: W, ) -> Result<(), Self::Error>; } /// Store a value in a file pub trait StoreValue { /// Error type returned type Error; /// Store a value in a file /// /// # Examples /// /// See [LoadValue::load]. fn store>(&self, path: P) -> Result<(), Self::Error>; } #[cfg(test)] mod tests { use super::*; use std::fs::File; use std::io::Write; use std::os::unix::fs::PermissionsExt; use tempfile::tempdir; #[test] fn test_fopen_w_public() { let tmp_dir = tempdir().unwrap(); let path = tmp_dir.path().join("test"); let mut file = fopen_w(path, Visibility::Public).unwrap(); file.write_all(b"test").unwrap(); let metadata = file.metadata().unwrap(); let permissions = metadata.permissions(); assert_eq!(permissions.mode(), 0o100644); } #[test] fn test_fopen_w_secret() { let tmp_dir = tempdir().unwrap(); let path = tmp_dir.path().join("test"); let mut file = fopen_w(path, Visibility::Secret).unwrap(); file.write_all(b"test").unwrap(); let metadata = file.metadata().unwrap(); let permissions = metadata.permissions(); assert_eq!(permissions.mode(), 0o100600); } #[test] fn test_fopen_r() { let tmp_dir = tempdir().unwrap(); let path = tmp_dir.path().join("test"); let mut file = File::create(path.clone()).unwrap(); file.write_all(b"test").unwrap(); let mut contents = String::new(); let mut file = fopen_r(path).unwrap(); file.read_to_string(&mut contents).unwrap(); assert_eq!(contents, "test"); } #[test] fn test_read_slice_to_end() { let tmp_dir = tempdir().unwrap(); let path = tmp_dir.path().join("test"); let mut file = File::create(path.clone()).unwrap(); file.write_all(b"test").unwrap(); let mut buf = [0u8; 4]; let mut file = fopen_r(path).unwrap(); file.read_slice_to_end(&mut buf).unwrap(); assert_eq!(buf, [116, 101, 115, 116]); } #[test] fn test_read_exact_to_end() { let tmp_dir = tempdir().unwrap(); let path = tmp_dir.path().join("test"); let mut file = File::create(path.clone()).unwrap(); file.write_all(b"test").unwrap(); let mut buf = [0u8; 4]; let mut file = fopen_r(path).unwrap(); file.read_exact_to_end(&mut buf).unwrap(); assert_eq!(buf, [116, 101, 115, 116]); } #[test] fn test_read_exact_to_end_to_long() { let tmp_dir = tempdir().unwrap(); let path = tmp_dir.path().join("test"); let mut file = File::create(path.clone()).unwrap(); file.write_all(b"test").unwrap(); let mut buf = [0u8; 3]; let mut file = fopen_r(path).unwrap(); let result = file.read_exact_to_end(&mut buf); assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), "File too long!"); } #[test] fn test_read_slice_to_end_to_long() { let tmp_dir = tempdir().unwrap(); let path = tmp_dir.path().join("test"); let mut file = File::create(path.clone()).unwrap(); file.write_all(b"test").unwrap(); let mut buf = [0u8; 3]; let mut file = fopen_r(path).unwrap(); let result = file.read_slice_to_end(&mut buf); assert!(result.is_err()); assert_eq!(result.unwrap_err().to_string(), "File too long!"); } }