fix(notstrap): wire age identity to decryption; fix parse_env_line quoting
Closes #14, closes #16
This commit is contained in:
@@ -100,20 +100,21 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
match resolve_identities(sources) {
|
let identities = match resolve_identities(sources) {
|
||||||
Ok(_identities) => {
|
Ok(ids) => {
|
||||||
report.add("age key", StepStatus::Ok);
|
report.add("age key", StepStatus::Ok);
|
||||||
|
ids
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
report.add("age key", StepStatus::Failed(e.to_string()));
|
report.add("age key", StepStatus::Failed(e.to_string()));
|
||||||
return Ok(report);
|
return Ok(report);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// 5. Decrypt sops secrets (optional)
|
// 5. Decrypt sops secrets (optional)
|
||||||
if let Some(injector) = opts.env_injector {
|
if let Some(injector) = opts.env_injector {
|
||||||
let sops_path = dotfiles_dir.join(&cfg.bootstrap.sops_file);
|
let sops_path = dotfiles_dir.join(&cfg.bootstrap.sops_file);
|
||||||
match injector(&sops_path) {
|
match injector(&sops_path, identities) {
|
||||||
Ok(env_content) => {
|
Ok(env_content) => {
|
||||||
for line in env_content.lines() {
|
for line in env_content.lines() {
|
||||||
match parse_env_line(line) {
|
match parse_env_line(line) {
|
||||||
@@ -187,6 +188,44 @@ pub fn run(opts: BootstrapOptions) -> Result<Report> {
|
|||||||
Ok(report)
|
Ok(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strip outer quotes from an env value and unescape inner escape sequences.
|
||||||
|
///
|
||||||
|
/// - `"val\"ue"` → `val"ue` (double-quoted, with backslash escapes processed)
|
||||||
|
/// - `'value'` → `value` (single-quoted, no escape processing)
|
||||||
|
/// - `value` → `value` (unquoted, returned as-is)
|
||||||
|
fn unquote_env_value(v: &str) -> String {
|
||||||
|
if v.len() >= 2 {
|
||||||
|
if v.starts_with('"') && v.ends_with('"') {
|
||||||
|
let inner = &v[1..v.len() - 1];
|
||||||
|
// Process backslash escapes within double-quoted strings.
|
||||||
|
let mut result = String::with_capacity(inner.len());
|
||||||
|
let mut chars = inner.chars();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c == '\\' {
|
||||||
|
match chars.next() {
|
||||||
|
Some('"') => result.push('"'),
|
||||||
|
Some('\\') => result.push('\\'),
|
||||||
|
Some('n') => result.push('\n'),
|
||||||
|
Some('t') => result.push('\t'),
|
||||||
|
Some(other) => {
|
||||||
|
result.push('\\');
|
||||||
|
result.push(other);
|
||||||
|
}
|
||||||
|
None => result.push('\\'),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if v.starts_with('\'') && v.ends_with('\'') {
|
||||||
|
return v[1..v.len() - 1].to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse and validate a single env line (`KEY=value`), returning `(key, value)` if safe to inject.
|
/// Parse and validate a single env line (`KEY=value`), returning `(key, value)` if safe to inject.
|
||||||
/// Returns `Ok(None)` for blank lines and comments.
|
/// Returns `Ok(None)` for blank lines and comments.
|
||||||
/// Returns `Err` for null bytes or dangerous keys.
|
/// Returns `Err` for null bytes or dangerous keys.
|
||||||
@@ -195,7 +234,7 @@ pub fn parse_env_line(line: &str) -> Result<Option<(String, String)>> {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let k = k.trim().to_string();
|
let k = k.trim().to_string();
|
||||||
let v = v.trim().trim_matches('"').to_string();
|
let v = unquote_env_value(v.trim());
|
||||||
|
|
||||||
if k.is_empty() || k.starts_with('#') {
|
if k.is_empty() || k.starts_with('#') {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -289,10 +328,34 @@ mod tests {
|
|||||||
assert_eq!(parse_env_line("# THIS_IS_A_COMMENT=value").unwrap(), None);
|
assert_eq!(parse_env_line("# THIS_IS_A_COMMENT=value").unwrap(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quoted values are unquoted.
|
/// Double-quoted values are unquoted.
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_env_line_strips_quotes() {
|
fn parse_env_line_strips_double_quotes() {
|
||||||
let result = parse_env_line(r#"API_KEY="secret""#).unwrap();
|
let result = parse_env_line(r#"API_KEY="secret""#).unwrap();
|
||||||
assert_eq!(result, Some(("API_KEY".to_string(), "secret".to_string())));
|
assert_eq!(result, Some(("API_KEY".to_string(), "secret".to_string())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Single-quoted values are unquoted (no escape processing).
|
||||||
|
#[test]
|
||||||
|
fn parse_env_line_strips_single_quotes() {
|
||||||
|
let result = parse_env_line("MY_VAR='hello world'").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Some(("MY_VAR".to_string(), "hello world".to_string()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Backslash-escaped double-quote within double-quoted value is handled.
|
||||||
|
#[test]
|
||||||
|
fn parse_env_line_unescapes_inner_double_quote() {
|
||||||
|
let result = parse_env_line(r#"MSG="val\"ue""#).unwrap();
|
||||||
|
assert_eq!(result, Some(("MSG".to_string(), r#"val"ue"#.to_string())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unquoted values pass through unchanged.
|
||||||
|
#[test]
|
||||||
|
fn parse_env_line_unquoted_passthrough() {
|
||||||
|
let result = parse_env_line("TOKEN=abc123").unwrap();
|
||||||
|
assert_eq!(result, Some(("TOKEN".to_string(), "abc123".to_string())));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user