holdon 0.4.0

Wait for anything. Know why if it doesn't.
Documentation
use std::borrow::Cow;
use std::path::Path;

use anyhow::{Context, Result, bail};

const MAX_SECRET_FILE_BYTES: u64 = 64 * 1024;

pub(crate) fn resolve(input: &str) -> Result<Cow<'_, str>> {
    if !input.contains("${") {
        return Ok(Cow::Borrowed(input));
    }
    let mut out = String::with_capacity(input.len());
    let mut cursor = 0;
    let bytes = input.as_bytes();
    while cursor < bytes.len() {
        if !input[cursor..].starts_with("${") {
            let end = input[cursor..]
                .find("${")
                .map_or(bytes.len(), |off| cursor + off);
            out.push_str(&input[cursor..end]);
            cursor = end;
            continue;
        }
        let Some(rel_end) = input[cursor + 2..].find('}') else {
            bail!("unterminated placeholder in target string at byte offset {cursor}");
        };
        let close = cursor + 2 + rel_end;
        let body = &input[cursor + 2..close];
        let Some((kind, arg)) = body.split_once(':') else {
            out.push_str(&input[cursor..=close]);
            cursor = close + 1;
            continue;
        };
        let resolved = match kind {
            "env" => resolve_env(arg)?,
            "file" => resolve_file(arg)?,
            _ => {
                out.push_str(&input[cursor..=close]);
                cursor = close + 1;
                continue;
            }
        };
        out.push_str(&resolved);
        cursor = close + 1;
    }
    Ok(Cow::Owned(out))
}

fn resolve_env(name: &str) -> Result<String> {
    if name.is_empty() {
        bail!("env placeholder requires a variable name");
    }
    std::env::var(name).with_context(|| format!("reading env var `{name}`"))
}

fn resolve_file(path: &str) -> Result<String> {
    use std::io::Read as _;
    if path.is_empty() {
        bail!("file placeholder requires a path");
    }
    let probe = std::fs::symlink_metadata(Path::new(path))
        .with_context(|| format!("reading secret file `{path}`"))?;
    if !probe.is_file() {
        bail!("secret file `{path}` is not a regular file");
    }
    let mut file = std::fs::File::open(Path::new(path))
        .with_context(|| format!("reading secret file `{path}`"))?;
    let metadata = file
        .metadata()
        .with_context(|| format!("reading secret file `{path}`"))?;
    if !metadata.is_file() {
        bail!("secret file `{path}` is not a regular file");
    }
    if metadata.len() > MAX_SECRET_FILE_BYTES {
        bail!(
            "secret file `{path}` is {} bytes, max {} (use a separate file or pre-process the secret)",
            metadata.len(),
            MAX_SECRET_FILE_BYTES
        );
    }
    let mut raw = String::with_capacity(usize::try_from(metadata.len()).unwrap_or(0));
    file.read_to_string(&mut raw)
        .with_context(|| format!("reading secret file `{path}`"))?;
    let trimmed = raw.strip_suffix('\n').unwrap_or(&raw);
    let trimmed = trimmed.strip_suffix('\r').unwrap_or(trimmed);
    Ok(trimmed.to_owned())
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::literal_string_with_formatting_args)]
mod tests {
    use super::*;
    use std::io::Write as _;

    #[test]
    fn passes_through_without_placeholder() {
        let r = resolve("tcp://host:5432").unwrap();
        assert!(matches!(r, Cow::Borrowed(_)));
        assert_eq!(r, "tcp://host:5432");
    }

    #[test]
    fn missing_env_var_is_error() {
        let err = resolve("${env:HOLDON_TEST_DEFINITELY_MISSING}").unwrap_err();
        let msg = format!("{err:#}");
        assert!(msg.contains("HOLDON_TEST_DEFINITELY_MISSING"), "{msg}");
    }

    #[test]
    fn file_secret_substituted_and_newline_trimmed() {
        let mut f = tempfile::NamedTempFile::new().unwrap();
        f.write_all(b"sekrit\n").unwrap();
        let path = f.path().to_string_lossy().into_owned();
        let input = format!("redis://x:${{file:{path}}}@h:6379");
        let r = resolve(&input).unwrap();
        assert_eq!(r, "redis://x:sekrit@h:6379");
    }

    #[test]
    fn unknown_kind_left_untouched() {
        let r = resolve("scheme://x/${vault:foo}/y").unwrap();
        assert_eq!(r, "scheme://x/${vault:foo}/y");
    }

    #[test]
    fn unknown_kind_with_nested_brace_is_untouched() {
        let r = resolve("scheme://x/${vault:ns/${key}}/y").unwrap();
        assert_eq!(r, "scheme://x/${vault:ns/${key}}/y");
    }

    #[test]
    fn unterminated_brace_is_error() {
        let err = resolve("scheme://x/${env:NAME_NO_CLOSE").unwrap_err();
        let msg = format!("{err:#}");
        assert!(msg.contains("unterminated"), "{msg}");
    }

    #[test]
    fn known_kind_with_adjacent_trailing_brace_uses_first_close() {
        let err = resolve("http://h/{${env:HOLDON_TEST_BOGUS_ID}}/p").unwrap_err();
        let msg = format!("{err:#}");
        assert!(msg.contains("HOLDON_TEST_BOGUS_ID"), "{msg}");
        assert!(!msg.contains("BOGUS_ID}"), "{msg}");
    }

    #[test]
    fn oversized_file_rejected() {
        let mut f = tempfile::NamedTempFile::new().unwrap();
        let size = usize::try_from(MAX_SECRET_FILE_BYTES + 1).unwrap();
        let big = vec![b'a'; size];
        f.write_all(&big).unwrap();
        let p = f.path().to_string_lossy().into_owned();
        let err = resolve(&format!("redis://x:${{file:{p}}}@h")).unwrap_err();
        assert!(format!("{err:#}").contains("max"));
    }

    #[test]
    fn empty_file_path_is_error() {
        let err = resolve("scheme://x/${file:}").unwrap_err();
        assert!(format!("{err:#}").contains("requires a path"));
    }

    #[test]
    fn empty_env_name_is_error() {
        let err = resolve("scheme://x/${env:}").unwrap_err();
        assert!(format!("{err:#}").contains("requires a variable name"));
    }

    #[test]
    fn file_missing_is_error() {
        let r = resolve("redis://x:${file:/no/such/file/anywhere}@h");
        assert!(r.is_err());
    }
}