secrets_rs/sources/
file.rs1use std::io;
2use std::path::{Path, PathBuf};
3
4use crate::{error::SourceError, source::Source};
5
6pub struct FileSource {
42 base: Option<PathBuf>,
43}
44
45impl FileSource {
46 pub fn new() -> Self {
49 Self { base: None }
50 }
51
52 pub fn with_base(base: impl Into<PathBuf>) -> Self {
61 Self {
62 base: Some(base.into()),
63 }
64 }
65
66 fn resolve(&self, name: &str) -> PathBuf {
67 let p = Path::new(name);
68 if p.is_absolute() {
69 p.to_path_buf()
70 } else if let Some(base) = &self.base {
71 base.join(p)
72 } else {
73 p.to_path_buf()
74 }
75 }
76}
77
78impl Default for FileSource {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl Source for FileSource {
85 fn get(&self, name: &str) -> Result<Vec<u8>, SourceError> {
86 let path = self.resolve(name);
87 std::fs::read(&path).map_err(|e| match e.kind() {
88 io::ErrorKind::NotFound => SourceError::NotFound {
89 name: name.to_owned(),
90 },
91 _ => SourceError::Other(format!("failed to read file `{}`: {}", name, e)),
92 })
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use std::io::Write;
99
100 use super::*;
101 use tempfile::NamedTempFile;
102
103 #[test]
104 fn returns_bytes_for_existing_file() {
105 let mut f = NamedTempFile::new().unwrap();
106 f.write_all(b"file-secret").unwrap();
107 let result = FileSource::new().get(f.path().to_str().unwrap()).unwrap();
108 assert_eq!(result, b"file-secret");
109 }
110
111 #[test]
112 fn returns_not_found_for_missing_file() {
113 let dir = tempfile::tempdir().unwrap();
114 let missing = dir.path().join("nonexistent.key");
115 let err = FileSource::new()
116 .get(missing.to_str().unwrap())
117 .unwrap_err();
118 assert!(matches!(err, SourceError::NotFound { .. }));
119 }
120
121 #[test]
122 fn with_base_resolves_relative_against_base() {
123 let dir = tempfile::tempdir().unwrap();
124 std::fs::write(dir.path().join("secret.txt"), b"base-secret").unwrap();
125
126 let src = FileSource::with_base(dir.path());
127 let result = src.get("secret.txt").unwrap();
128 assert_eq!(result, b"base-secret");
129 }
130
131 #[test]
132 fn with_base_absolute_name_ignores_base() {
133 let base_dir = tempfile::tempdir().unwrap();
134 let other_dir = tempfile::tempdir().unwrap();
135 std::fs::write(other_dir.path().join("abs.txt"), b"abs-secret").unwrap();
136
137 let abs_path = other_dir.path().join("abs.txt");
138 let src = FileSource::with_base(base_dir.path());
139 let result = src.get(abs_path.to_str().unwrap()).unwrap();
140 assert_eq!(result, b"abs-secret");
141 }
142
143 #[test]
144 fn with_base_not_found_uses_original_name_in_error() {
145 let base_dir = tempfile::tempdir().unwrap();
146 let src = FileSource::with_base(base_dir.path());
147 let err = src.get("missing.key").unwrap_err();
148 assert!(
149 matches!(&err, SourceError::NotFound { name } if name == "missing.key"),
150 "unexpected error: {err:?}"
151 );
152 }
153
154 #[test]
155 fn other_error_uses_original_name_not_resolved_path() {
156 let base_dir = tempfile::tempdir().unwrap();
160 let sub_dir = base_dir.path().join("subdir");
161 std::fs::create_dir(&sub_dir).unwrap();
162
163 let src = FileSource::with_base(base_dir.path());
164 let err = src.get("subdir").unwrap_err();
165
166 let msg = match &err {
167 SourceError::Other(m) => m.clone(),
168 other => panic!("expected Other, got {other:?}"),
169 };
170 assert!(msg.contains("subdir"), "original name missing from: {msg}");
171 assert!(
172 !msg.contains(base_dir.path().to_str().unwrap()),
173 "base directory disclosed in: {msg}"
174 );
175 }
176}