1use hasp_core::{secret_mem::wrap_secret, Backend, Entry, Error, SecretString};
15use url::Url;
16
17pub struct EnvUrl {
21 pub var_name: String,
22}
23
24impl TryFrom<&Url> for EnvUrl {
25 type Error = Error;
26
27 fn try_from(url: &Url) -> Result<Self, Self::Error> {
28 if url.scheme() != "env" {
29 return Err(Error::InvalidUrl("expected env:// scheme".into()));
30 }
31 let var_name = url
32 .host_str()
33 .ok_or_else(|| Error::InvalidUrl("env:// requires a host (variable name)".into()))?
34 .to_owned();
35 if var_name.is_empty() {
36 return Err(Error::InvalidUrl(
37 "env:// variable name must not be empty".into(),
38 ));
39 }
40 if url.path() != "/" && !url.path().is_empty() {
41 return Err(Error::InvalidUrl("env:// does not accept a path".into()));
42 }
43 if url.query().is_some() {
44 return Err(Error::InvalidUrl(
45 "env:// does not accept query parameters".into(),
46 ));
47 }
48 Ok(EnvUrl { var_name })
49 }
50}
51
52pub struct EnvBackend;
57
58impl Backend for EnvBackend {
59 fn scheme(&self) -> &'static str {
60 "env"
61 }
62
63 fn validate(&self, url: &Url) -> Result<(), Error> {
64 EnvUrl::try_from(url).map(|_| ())
65 }
66
67 fn get(&self, url: &Url) -> Result<SecretString, Error> {
68 let env_url = EnvUrl::try_from(url)?;
69 match std::env::var(&env_url.var_name) {
70 Ok(value) => Ok(wrap_secret(value)),
71 Err(std::env::VarError::NotPresent) => Err(Error::NotFound(format!(
72 "env var '{}' not set",
73 env_url.var_name
74 ))),
75 Err(std::env::VarError::NotUnicode(_)) => Err(Error::Backend {
76 scheme: "env",
77 kind: hasp_core::BackendFailureKind::Permanent,
78 message: format!("env var '{}' is not valid Unicode", env_url.var_name),
79 }),
80 }
81 }
82
83 fn put(&self, _url: &Url, _value: &SecretString) -> Result<(), Error> {
84 Err(Error::UnsupportedOperation {
85 scheme: "env",
86 operation: "put",
87 })
88 }
89
90 fn list(&self, _url: &Url) -> Result<Vec<Entry>, Error> {
91 Err(Error::UnsupportedOperation {
92 scheme: "env",
93 operation: "list",
94 })
95 }
96
97 fn delete(&self, _url: &Url) -> Result<(), Error> {
98 Err(Error::UnsupportedOperation {
99 scheme: "env",
100 operation: "delete",
101 })
102 }
103
104 fn exists(&self, url: &Url) -> Result<bool, Error> {
105 let env_url = EnvUrl::try_from(url)?;
106 Ok(std::env::var(&env_url.var_name).is_ok())
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn parse_valid_url() {
116 let url = Url::parse("env://HOME").unwrap();
117 let env_url = EnvUrl::try_from(&url).unwrap();
118 assert_eq!(env_url.var_name, "HOME");
119 }
120
121 #[test]
122 fn parse_missing_host_fails() {
123 let url = Url::parse("env:///").unwrap();
124 assert!(EnvUrl::try_from(&url).is_err());
125 }
126
127 #[test]
128 fn parse_path_fails() {
129 let url = Url::parse("env://HOME/extra").unwrap();
130 assert!(EnvUrl::try_from(&url).is_err());
131 }
132
133 #[test]
134 fn parse_query_fails() {
135 let url = Url::parse("env://HOME?foo=bar").unwrap();
136 assert!(EnvUrl::try_from(&url).is_err());
137 }
138}