chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
use common::CliError;
|
||||
use url::Url;
|
||||
|
||||
use crate::model::{ConnectionSource, Engine};
|
||||
|
||||
pub(crate) fn source_from_parts(
|
||||
engine: Option<Engine>,
|
||||
raw_url: String,
|
||||
) -> Result<ConnectionSource, CliError> {
|
||||
let source_redacted = redact_source(&raw_url);
|
||||
let engine = engine.or_else(|| infer_engine(&raw_url)).ok_or_else(|| {
|
||||
CliError::usage(format!(
|
||||
"--engine is required for ambiguous SQL source '{source_redacted}'"
|
||||
))
|
||||
})?;
|
||||
Ok(ConnectionSource {
|
||||
engine,
|
||||
raw_url,
|
||||
source_redacted,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn infer_engine(raw_url: &str) -> Option<Engine> {
|
||||
Url::parse(raw_url)
|
||||
.ok()
|
||||
.and_then(|url| Engine::parse(url.scheme()))
|
||||
}
|
||||
|
||||
pub(crate) fn redact_source(raw: &str) -> String {
|
||||
if let Ok(mut url) = Url::parse(raw) {
|
||||
if url.password().is_some() {
|
||||
let _ = url.set_password(Some("***"));
|
||||
}
|
||||
if url
|
||||
.query_pairs()
|
||||
.any(|(key, _)| is_sensitive_connection_key(&key))
|
||||
{
|
||||
let pairs = url
|
||||
.query_pairs()
|
||||
.map(|(key, value)| {
|
||||
let rendered = if is_sensitive_connection_key(&key) {
|
||||
"***".into()
|
||||
} else {
|
||||
value
|
||||
};
|
||||
(key.into_owned(), rendered.into_owned())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
url.query_pairs_mut().clear().extend_pairs(pairs);
|
||||
}
|
||||
return url.to_string();
|
||||
}
|
||||
redact_key_value_passwords(raw)
|
||||
}
|
||||
|
||||
fn redact_key_value_passwords(raw: &str) -> String {
|
||||
let mut changed = false;
|
||||
let parts = raw
|
||||
.split(';')
|
||||
.map(|part| {
|
||||
let Some((key, value)) = part.split_once('=') else {
|
||||
return part.to_string();
|
||||
};
|
||||
let key_trimmed = key.trim();
|
||||
if is_sensitive_connection_key(key_trimmed) {
|
||||
changed = true;
|
||||
format!("{key}=***")
|
||||
} else {
|
||||
format!("{key}={value}")
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if changed {
|
||||
parts.join(";")
|
||||
} else {
|
||||
raw.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_sensitive_connection_key(key: &str) -> bool {
|
||||
let normalized = key
|
||||
.chars()
|
||||
.filter(|character| !matches!(character, '_' | '-' | ' '))
|
||||
.flat_map(char::to_lowercase)
|
||||
.collect::<String>();
|
||||
matches!(
|
||||
normalized.as_str(),
|
||||
"password"
|
||||
| "pwd"
|
||||
| "token"
|
||||
| "accesstoken"
|
||||
| "apikey"
|
||||
| "secret"
|
||||
| "clientsecret"
|
||||
| "credential"
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn redacts_url_and_ado_passwords() {
|
||||
assert_eq!(
|
||||
redact_source("postgres://user:secret@example.test/db"),
|
||||
"postgres://user:***@example.test/db"
|
||||
);
|
||||
assert_eq!(
|
||||
redact_source("server=tcp:host,1433;User ID=sa;Password=secret;Database=db"),
|
||||
"server=tcp:host,1433;User ID=sa;Password=***;Database=db"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redacts_url_query_and_key_value_secrets() {
|
||||
assert_eq!(
|
||||
redact_source(
|
||||
"postgres://example.test/db?password=secret&access_token=abc&sslmode=require"
|
||||
),
|
||||
"postgres://example.test/db?password=***&access_token=***&sslmode=require"
|
||||
);
|
||||
assert_eq!(
|
||||
redact_source("server=tcp:host,1433;User ID=sa;Access Token=abc;ApiKey=key"),
|
||||
"server=tcp:host,1433;User ID=sa;Access Token=***;ApiKey=***"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn infers_engines_from_url_schemes_only() {
|
||||
assert_eq!(
|
||||
infer_engine("postgres://localhost/db"),
|
||||
Some(Engine::Postgres)
|
||||
);
|
||||
assert_eq!(infer_engine("duckdb://local/path"), Some(Engine::DuckDb));
|
||||
assert_eq!(infer_engine(r".\local.db"), None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user