1use std::borrow::Cow;
2use std::env::var_os;
3use std::fs::File;
4use std::io::{BufRead, BufReader};
5use std::path::PathBuf;
6
7pub fn load_password(
9 host: &str,
10 port: u16,
11 username: &str,
12 database: Option<&str>,
13) -> Option<String> {
14 let custom_file = var_os("PGPASSFILE");
15 if let Some(file) = custom_file {
16 if let Some(password) =
17 load_password_from_file(PathBuf::from(file), host, port, username, database)
18 {
19 return Some(password);
20 }
21 }
22
23 #[cfg(not(target_os = "windows"))]
24 let default_file = home::home_dir().map(|path| path.join(".pgpass"));
25 #[cfg(target_os = "windows")]
26 let default_file = {
27 use etcetera::BaseStrategy;
28
29 etcetera::base_strategy::Windows::new()
30 .ok()
31 .map(|basedirs| basedirs.data_dir().join("postgres").join("pgpass.conf"))
32 };
33 load_password_from_file(default_file?, host, port, username, database)
34}
35
36fn load_password_from_file(
38 path: PathBuf,
39 host: &str,
40 port: u16,
41 username: &str,
42 database: Option<&str>,
43) -> Option<String> {
44 let file = File::open(&path)
45 .map_err(|e| {
46 match e.kind() {
47 std::io::ErrorKind::NotFound => {
48 tracing::debug!(
49 path = %path.display(),
50 "`.pgpass` file not found",
51 );
52 }
53 _ => {
54 tracing::warn!(
55 path = %path.display(),
56 "Failed to open `.pgpass` file: {e:?}",
57 );
58 }
59 };
60 })
61 .ok()?;
62
63 #[cfg(target_os = "linux")]
64 {
65 use std::os::unix::fs::PermissionsExt;
66
67 let metadata = file.metadata().ok()?;
70 let permissions = metadata.permissions();
71 let mode = permissions.mode();
72 if mode & 0o77 != 0 {
73 tracing::warn!(
74 path = %path.display(),
75 permissions = format!("{mode:o}"),
76 "Ignoring path. Permissions are not strict enough",
77 );
78 return None;
79 }
80 }
81
82 let reader = BufReader::new(file);
83 load_password_from_reader(reader, host, port, username, database)
84}
85
86fn load_password_from_reader(
87 mut reader: impl BufRead,
88 host: &str,
89 port: u16,
90 username: &str,
91 database: Option<&str>,
92) -> Option<String> {
93 let mut line = String::new();
94
95 fn trim_newline(s: &mut String) {
97 if s.ends_with('\n') {
98 s.pop();
99 if s.ends_with('\r') {
100 s.pop();
101 }
102 }
103 }
104
105 while let Ok(n) = reader.read_line(&mut line) {
106 if n == 0 {
107 break;
108 }
109
110 if line.starts_with('#') {
111 } else {
113 trim_newline(&mut line);
115 if let Some(password) = load_password_from_line(&line, host, port, username, database) {
116 return Some(password);
117 }
118 }
119
120 line.clear();
121 }
122
123 None
124}
125
126fn load_password_from_line(
128 mut line: &str,
129 host: &str,
130 port: u16,
131 username: &str,
132 database: Option<&str>,
133) -> Option<String> {
134 let whole_line = line;
135
136 match line.trim_start().chars().next() {
139 None | Some('#') => None,
140 _ => {
141 matches_next_field(whole_line, &mut line, host)?;
142 matches_next_field(whole_line, &mut line, &port.to_string())?;
143 matches_next_field(whole_line, &mut line, database.unwrap_or_default())?;
144 matches_next_field(whole_line, &mut line, username)?;
145 Some(line.to_owned())
146 }
147 }
148}
149
150fn matches_next_field(whole_line: &str, line: &mut &str, value: &str) -> Option<()> {
152 let field = find_next_field(line);
153 match field {
154 Some(field) => {
155 if field == "*" || field == value {
156 Some(())
157 } else {
158 None
159 }
160 }
161 None => {
162 tracing::warn!(line = whole_line, "Malformed line in pgpass file");
163 None
164 }
165 }
166}
167
168fn find_next_field<'a>(line: &mut &'a str) -> Option<Cow<'a, str>> {
172 let mut escaping = false;
173 let mut escaped_string = None;
174 let mut last_added = 0;
175
176 let char_indicies = line.char_indices();
177 for (idx, c) in char_indicies {
178 if c == ':' && !escaping {
179 let (field, rest) = line.split_at(idx);
180 *line = &rest[1..];
181
182 if let Some(mut escaped_string) = escaped_string {
183 escaped_string += &field[last_added..];
184 return Some(Cow::Owned(escaped_string));
185 } else {
186 return Some(Cow::Borrowed(field));
187 }
188 } else if c == '\\' {
189 let s = escaped_string.get_or_insert_with(String::new);
190
191 if escaping {
192 s.push('\\');
193 } else {
194 *s += &line[last_added..idx];
195 }
196
197 escaping = !escaping;
198 last_added = idx + 1;
199 } else {
200 escaping = false;
201 }
202 }
203
204 None
205}
206
207#[cfg(test)]
208mod tests {
209 use super::{find_next_field, load_password_from_line, load_password_from_reader};
210 use std::borrow::Cow;
211
212 #[test]
213 fn test_find_next_field() {
214 fn test_case<'a>(mut input: &'a str, result: Option<Cow<'a, str>>, rest: &str) {
215 assert_eq!(find_next_field(&mut input), result);
216 assert_eq!(input, rest);
217 }
218
219 test_case("foo:bar:baz", Some(Cow::Borrowed("foo")), "bar:baz");
221 test_case(
223 "foo\\\\:bar:baz",
224 Some(Cow::Owned("foo\\".to_owned())),
225 "bar:baz",
226 );
227 test_case(
229 "foo\\::bar:baz",
230 Some(Cow::Owned("foo:".to_owned())),
231 "bar:baz",
232 );
233 test_case(
235 "foo\\a:bar:baz",
236 Some(Cow::Owned("fooa".to_owned())),
237 "bar:baz",
238 );
239 test_case(
241 "foo\\\\a:bar:baz",
242 Some(Cow::Owned("foo\\a".to_owned())),
243 "bar:baz",
244 );
245 test_case(
247 "foo\\\\\\\\a:bar:baz",
248 Some(Cow::Owned("foo\\\\a".to_owned())),
249 "bar:baz",
250 );
251 test_case("🦀:bar:baz", Some(Cow::Borrowed("🦀")), "bar:baz");
253
254 test_case("foo", None, "foo");
256 test_case("foo\\:", None, "foo\\:");
258 test_case("foo\\", None, "foo\\");
260 }
261
262 #[test]
263 fn test_load_password_from_line() {
264 assert_eq!(
266 load_password_from_line(
267 "localhost:5432:bar:foo:baz",
268 "localhost",
269 5432,
270 "foo",
271 Some("bar")
272 ),
273 Some("baz".to_owned())
274 );
275 assert_eq!(
277 load_password_from_line("*:5432:bar:foo:baz", "localhost", 5432, "foo", Some("bar")),
278 Some("baz".to_owned())
279 );
280 assert_eq!(
282 load_password_from_line("localhost:5432:*:foo:baz", "localhost", 5432, "foo", None),
283 Some("baz".to_owned())
284 );
285
286 assert_eq!(
288 load_password_from_line(
289 "thishost:5432:bar:foo:baz",
290 "thathost",
291 5432,
292 "foo",
293 Some("bar")
294 ),
295 None
296 );
297 assert_eq!(
299 load_password_from_line(
300 "localhost:5432:bar:foo",
301 "localhost",
302 5432,
303 "foo",
304 Some("bar")
305 ),
306 None
307 );
308 }
309
310 #[test]
311 fn test_load_password_from_reader() {
312 let file = b"\
313 localhost:5432:bar:foo:baz\n\
314 # mixed line endings (also a comment!)\n\
315 *:5432:bar:foo:baz\r\n\
316 # trailing space, comment with CRLF! \r\n\
317 thishost:5432:bar:foo:baz \n\
318 # malformed line \n\
319 thathost:5432:foobar:foo\n\
320 # missing trailing newline\n\
321 localhost:5432:*:foo:baz
322 ";
323
324 assert_eq!(
326 load_password_from_reader(&mut &file[..], "localhost", 5432, "foo", Some("bar")),
327 Some("baz".to_owned())
328 );
329 assert_eq!(
331 load_password_from_reader(&mut &file[..], "localhost", 5432, "foo", Some("foobar")),
332 Some("baz".to_owned())
333 );
334 assert_eq!(
336 load_password_from_reader(&mut &file[..], "localhost", 5432, "foo", None),
337 Some("baz".to_owned())
338 );
339
340 assert_eq!(
342 load_password_from_reader(&mut &file[..], "thathost", 5432, "foo", Some("foobar")),
343 None
344 );
345 assert_eq!(
347 load_password_from_reader(&mut &file[..], "thathost", 5432, "foo", Some("foobar")),
348 None
349 );
350 }
351}