1use crate::{Error, ErrorKind, Result, ResultExt};
7use futures::future::BoxFuture;
8use std::{borrow::Cow, io, str::FromStr};
9use time::{format_description::well_known, OffsetDateTime};
10use url::Url;
11
12pub fn parse_date_time_opt(value: &str) -> Result<OffsetDateTime> {
14 OffsetDateTime::parse(value, &well_known::Rfc3339)
15 .with_context(ErrorKind::InvalidData, "failed to parse date-time")
16}
17
18pub fn parse_key_value<T>(value: &str) -> Result<(String, T)>
20where
21 T: FromStr,
22 Error: From<<T as FromStr>::Err>,
23{
24 let idx = value
25 .find("=")
26 .ok_or_else(|| format!("no '=' found in '{value}'"))?;
27 Ok((value[..idx].to_string(), value[idx + 1..].parse()?))
28}
29
30pub fn parse_key_value_opt<T>(value: &str) -> Result<(String, Option<T>)>
32where
33 T: FromStr,
34 Error: From<<T as FromStr>::Err>,
35{
36 if let Some(idx) = value.find("=") {
37 return Ok((value[..idx].to_string(), Some(value[idx + 1..].parse()?)));
38 }
39
40 Ok((value.to_string(), None))
41}
42
43pub async fn replace_expressions<W, F>(mut template: &str, w: &mut W, f: F) -> Result<()>
72where
73 W: io::Write,
74 F: Fn(&str) -> BoxFuture<'_, Result<String>>,
75{
76 const START: &str = "{{";
77 const START_LEN: usize = START.len();
78 const END: &str = "}}";
79 const END_LEN: usize = END.len();
80
81 while let Some(mut start) = template.find(START) {
82 let Some(mut end) = template[start + START_LEN..].find(END) else {
84 return Err(Error::with_message(
85 ErrorKind::InvalidData,
86 "missing closing '}}'",
87 ));
88 };
89 end += start + START_LEN;
90
91 w.write_all(&template.as_bytes()[..start])?;
92 start += START_LEN;
93
94 let id = template[start..end].trim();
95 let secret = f(id).await?;
96
97 w.write_all(secret.as_bytes())?;
98 end += END_LEN;
99
100 template = &template[end..];
101 }
102
103 w.write_all(template.as_bytes())?;
104 Ok(())
105}
106
107pub fn replace_vars<F>(input: &str, f: F) -> Result<Cow<'_, str>>
109where
110 F: Fn(&str) -> Result<String>,
111{
112 let mut cur = input;
113 let mut output = String::new();
114
115 while let Some(start) = cur.find('$') {
116 output += &cur[..start];
117 cur = &cur[start + 1..];
118
119 let mut end = cur.len();
120 for (i, c) in cur.char_indices() {
121 if !c.is_ascii_alphanumeric() && c != '_' {
122 end = i;
123 break;
124 }
125 }
126
127 let name = &cur[..end];
128 if !name.is_empty() {
129 output += &f(name)?;
130 }
131 cur = &cur[end..];
132 }
133
134 if output.is_empty() {
135 Ok(Cow::Borrowed(input))
136 } else {
137 output += cur;
138 Ok(Cow::Owned(output))
139 }
140}
141
142#[derive(Clone, Debug)]
144pub struct Resource {
145 pub vault_url: String,
147
148 pub name: String,
150
151 pub version: Option<String>,
153}
154
155impl TryFrom<Url> for Resource {
156 type Error = crate::Error;
157
158 #[inline]
159 fn try_from(url: Url) -> std::result::Result<Self, Self::Error> {
160 Self::try_from(&url)
161 }
162}
163
164impl TryFrom<&Url> for Resource {
165 type Error = crate::Error;
166
167 fn try_from(url: &Url) -> std::result::Result<Self, Self::Error> {
168 Ok(azure_security_keyvault_secrets::ResourceId::try_from(url)
169 .map(From::from)
170 .or_else(|_| azure_security_keyvault_keys::ResourceId::try_from(url).map(From::from))
171 .or_else(|_| {
172 azure_security_keyvault_certificates::ResourceId::try_from(url).map(From::from)
173 })?)
174 }
175}
176
177impl From<azure_security_keyvault_secrets::ResourceId> for Resource {
178 fn from(value: azure_security_keyvault_secrets::ResourceId) -> Self {
179 Self {
180 vault_url: value.vault_url,
181 name: value.name,
182 version: value.version,
183 }
184 }
185}
186
187impl From<azure_security_keyvault_keys::ResourceId> for Resource {
188 fn from(value: azure_security_keyvault_keys::ResourceId) -> Self {
189 Self {
190 vault_url: value.vault_url,
191 name: value.name,
192 version: value.version,
193 }
194 }
195}
196
197impl From<azure_security_keyvault_certificates::ResourceId> for Resource {
198 fn from(value: azure_security_keyvault_certificates::ResourceId) -> Self {
199 Self {
200 vault_url: value.vault_url,
201 name: value.name,
202 version: value.version,
203 }
204 }
205}
206
207impl FromStr for Resource {
208 type Err = crate::Error;
209
210 fn from_str(s: &str) -> Result<Self> {
211 let url: Url = s.parse()?;
212 url.try_into()
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use futures::FutureExt as _;
220
221 #[test]
222 fn resource_from_secret_url() {
223 let url: Url = "https://my-vault.vault.azure.net/secrets/my-secret"
224 .parse()
225 .unwrap();
226 let resource = Resource::try_from(url).expect("valid secret URL");
227 assert_eq!(resource.vault_url, "https://my-vault.vault.azure.net");
228 assert_eq!(resource.name, "my-secret");
229 assert!(resource.version.is_none());
230 }
231
232 #[test]
233 fn resource_from_secret_url_with_version() {
234 let url: Url =
235 "https://my-vault.vault.azure.net/secrets/my-secret/746984e474594896aad9aff48aca0849"
236 .parse()
237 .unwrap();
238 let resource = Resource::try_from(url).expect("valid secret URL with version");
239 assert_eq!(resource.vault_url, "https://my-vault.vault.azure.net");
240 assert_eq!(resource.name, "my-secret");
241 assert_eq!(
242 resource.version.as_deref(),
243 Some("746984e474594896aad9aff48aca0849")
244 );
245 }
246
247 #[test]
248 fn resource_from_key_url() {
249 let url: Url = "https://my-vault.vault.azure.net/keys/my-key"
250 .parse()
251 .unwrap();
252 let resource = Resource::try_from(url).expect("valid key URL");
253 assert_eq!(resource.vault_url, "https://my-vault.vault.azure.net");
254 assert_eq!(resource.name, "my-key");
255 assert!(resource.version.is_none());
256 }
257
258 #[test]
259 fn resource_from_key_url_with_version() {
260 let url: Url = "https://my-vault.vault.azure.net/keys/my-key/1234567890abcdef"
261 .parse()
262 .unwrap();
263 let resource = Resource::try_from(url).expect("valid key URL with version");
264 assert_eq!(resource.vault_url, "https://my-vault.vault.azure.net");
265 assert_eq!(resource.name, "my-key");
266 assert_eq!(resource.version.as_deref(), Some("1234567890abcdef"));
267 }
268
269 #[test]
270 fn resource_from_certificate_url() {
271 let url: Url = "https://my-vault.vault.azure.net/certificates/my-cert"
272 .parse()
273 .unwrap();
274 let resource = Resource::try_from(url).expect("valid certificate URL");
275 assert_eq!(resource.vault_url, "https://my-vault.vault.azure.net");
276 assert_eq!(resource.name, "my-cert");
277 assert!(resource.version.is_none());
278 }
279
280 #[test]
281 fn resource_from_certificate_url_with_version() {
282 let url: Url = "https://my-vault.vault.azure.net/certificates/my-cert/abcdef1234567890"
283 .parse()
284 .unwrap();
285 let resource = Resource::try_from(url).expect("valid certificate URL with version");
286 assert_eq!(resource.vault_url, "https://my-vault.vault.azure.net");
287 assert_eq!(resource.name, "my-cert");
288 assert_eq!(resource.version.as_deref(), Some("abcdef1234567890"));
289 }
290
291 #[test]
292 fn resource_from_str() {
293 let resource: Resource = "https://my-vault.vault.azure.net/secrets/my-secret"
294 .parse()
295 .expect("valid secret URL string");
296 assert_eq!(resource.vault_url, "https://my-vault.vault.azure.net");
297 assert_eq!(resource.name, "my-secret");
298 assert!(resource.version.is_none());
299 }
300
301 #[test]
302 fn resource_from_invalid_url() {
303 let url: Url = "https://my-vault.vault.azure.net".parse().unwrap();
304 Resource::try_from(url).expect_err("vault URL without resource path should fail");
305 }
306
307 #[test]
308 fn test_parse_key_value() {
309 let kv = parse_key_value::<String>("key=value");
310 assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == "value"));
311
312 let kv = parse_key_value::<String>("key=value=other");
313 assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == "value=other"));
314
315 parse_key_value::<String>("key").expect_err("requires '='");
316
317 let k = parse_key_value::<i32>("key=1");
318 assert!(matches!(k, Ok(k) if k.0 == "key" && k.1 == 1));
319
320 parse_key_value::<i32>("key=value").expect_err("should not parse 'value' as i32");
321 }
322
323 #[test]
324 fn test_parse_key_value_opt() {
325 let kv = parse_key_value_opt::<String>("key=value");
326 assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == Some("value".into())));
327
328 let kv = parse_key_value_opt::<String>("key=value=other");
329 assert!(matches!(kv, Ok(kv) if kv.0 == "key" && kv.1 == Some("value=other".into())));
330
331 let k = parse_key_value_opt::<i32>("key");
332 assert!(matches!(k, Ok(k) if k.0 == "key" && k.1.is_none()));
333
334 parse_key_value_opt::<i32>("key=value").expect_err("should not parse 'value' as i32");
335 }
336
337 #[tokio::test]
338 async fn test_replace_expressions() {
339 let s = "Hello, {{ var }}!";
340 let mut buf = Vec::new();
341
342 replace_expressions(s, &mut buf, |v| {
343 assert_eq!(v, "var");
344 async { Ok(String::from("world")) }.boxed()
345 })
346 .await
347 .unwrap();
348 assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
349 }
350
351 #[tokio::test]
352 async fn replace_expressions_overlap() {
353 let s = "Hello, {{ {{var}} }}!";
354 let mut buf = Vec::new();
355
356 replace_expressions(s, &mut buf, |v| {
357 assert_eq!(v, "{{var");
358 async { Ok(String::from("world")) }.boxed()
359 })
360 .await
361 .unwrap();
362 assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world }}!");
363 }
364
365 #[tokio::test]
366 async fn replace_expressions_missing_end() {
367 let s = "Hello, {{ var!";
368 let mut buf = Vec::new();
369
370 replace_expressions(s, &mut buf, |_| async { Ok(String::from("world")) }.boxed())
371 .await
372 .expect_err("missing end");
373 }
374
375 #[tokio::test]
376 async fn replace_expressions_missing_empty() {
377 let s = "";
378 let mut buf = Vec::new();
379
380 replace_expressions(s, &mut buf, |_| async { Ok(String::from("world")) }.boxed())
381 .await
382 .unwrap();
383 assert_eq!(String::from_utf8(buf).unwrap(), "");
384 }
385
386 #[tokio::test]
387 async fn replace_expressions_missing_no_template() {
388 let s = "Hello, world!";
389 let mut buf = Vec::new();
390
391 replace_expressions(s, &mut buf, |_| {
392 async { Ok(String::from("Ferris")) }.boxed()
393 })
394 .await
395 .unwrap();
396 assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
397 }
398
399 #[test]
400 fn replace_vars_borrowed() {
401 let s = "echo NONE";
402 let out = replace_vars(s, |name| {
403 assert_eq!(name, "VAR");
404 Ok(String::from("VALUE"))
405 })
406 .expect("replaces $VAR with VALUE");
407 assert!(matches!(out, Cow::Borrowed(out) if out == "echo NONE"));
408 }
409
410 #[test]
411 fn replace_vars_owned() {
412 let s = "echo $VAR";
413 let out = replace_vars(s, |name| {
414 assert_eq!(name, "VAR");
415 Ok(String::from("VALUE"))
416 })
417 .expect("replaces $VAR with VALUE");
418 assert!(matches!(out, Cow::Owned(out) if out == "echo VALUE"));
419 }
420
421 #[test]
422 fn replace_only_vars() {
423 let s = "$VAR";
424 let out = replace_vars(s, |name| {
425 assert_eq!(name, "VAR");
426 Ok(String::from("VALUE"))
427 })
428 .expect("replaces $VAR with VALUE");
429 assert!(matches!(out, Cow::Owned(out) if out == "VALUE"));
430 }
431
432 #[test]
433 fn replace_vars_errs() {
434 let s = "echo $VAR";
435 replace_vars(s, |name| {
436 assert_eq!(name, "VAR");
437 Err(Error::with_message(ErrorKind::Other, "test"))
438 })
439 .expect_err("expected error");
440 }
441
442 #[tokio::test]
443 async fn replace_expression_with_var() {
444 let s = "Hello, {{ $VAR }}!";
445 let mut buf = Vec::new();
446
447 replace_expressions(s, &mut buf, |expr| {
448 async move {
449 assert_eq!(expr, "$VAR");
450 replace_vars(expr, |var| {
451 assert_eq!(var, "VAR");
452 Ok(String::from("world"))
453 })
454 .map(Into::into)
455 }
456 .boxed()
457 })
458 .await
459 .expect("replaces $VAR with 'world'");
460 assert_eq!(String::from_utf8(buf).unwrap(), "Hello, world!");
461 }
462}