Skip to content

Commit b0d3f66

Browse files
Benoit Aubuchonclaude
andcommitted
fix: handle escaped quotes in fill_hidden_credentials credential extraction
`fill_hidden_credentials` used `find('\'')` to locate the closing quote of a credential value, which stops prematurely on backslash-escaped quotes (`\'`) that `escape_clickhouse_string` emits for passwords containing single quotes. Replace with `find_closing_quote`, which skips `\'` pairs, so the full credential value is extracted correctly. Add a test to cover this case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 12889da commit b0d3f66

1 file changed

Lines changed: 26 additions & 1 deletion

File tree

apps/framework-cli/src/framework/core/infra_reality_checker.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ fn normalize_database(db: &Option<String>, default_database: &str) -> String {
127127

128128
/// Returns true if two dictionary DDL strings are structurally equivalent.
129129
///
130+
/// Returns the byte offset of the closing single quote in `s`, skipping
131+
/// backslash-escaped characters (`\'` is an embedded quote, not a closer).
132+
fn find_closing_quote(s: &str) -> Option<usize> {
133+
let mut chars = s.char_indices().peekable();
134+
while let Some((i, c)) = chars.next() {
135+
if c == '\\' {
136+
chars.next(); // skip escaped character
137+
continue;
138+
}
139+
if c == '\'' {
140+
return Some(i);
141+
}
142+
}
143+
None
144+
}
145+
130146
/// Substitutes `'[HIDDEN]'` credential placeholders in `actual` with the
131147
/// corresponding values from `desired`, matched by the keyword immediately
132148
/// preceding each placeholder.
@@ -152,7 +168,7 @@ fn fill_hidden_credentials(actual: &str, desired: &str) -> String {
152168
let search = format!("{} '", kw);
153169
if let Some(kw_pos) = desired.find(&search) {
154170
let val_start = kw_pos + search.len();
155-
if let Some(val_end) = desired[val_start..].find('\'') {
171+
if let Some(val_end) = find_closing_quote(&desired[val_start..]) {
156172
let replacement = format!("'{}'", &desired[val_start..val_start + val_end]);
157173
result.replace_range(abs..abs + placeholder.len(), &replacement);
158174
offset = abs + replacement.len();
@@ -2448,6 +2464,15 @@ mod tests {
24482464
assert!(!dicts_ddl_equivalent(actual, desired));
24492465
}
24502466

2467+
#[test]
2468+
fn test_dicts_ddl_equivalent_hidden_credentials_escaped_quote_in_password() {
2469+
// A password containing a single quote is backslash-escaped in the desired DDL
2470+
// (`pass\'word`). fill_hidden_credentials must not stop at the escaped quote.
2471+
let actual = "CREATE DICTIONARY `db`.`d` (\n `id` UInt64\n)\nPRIMARY KEY `id`\nSOURCE(MYSQL(HOST 'localhost' PORT 3306 USER 'user' PASSWORD '[HIDDEN]' TABLE 'src' DB 'mydb'))\nLAYOUT(HASHED())\nLIFETIME(MIN 0 MAX 300)";
2472+
let desired = "CREATE DICTIONARY IF NOT EXISTS `db`.`d` (\n `id` UInt64\n)\nPRIMARY KEY `id`\nSOURCE(MYSQL(HOST 'localhost' PORT 3306 USER 'user' PASSWORD 'pass\\'word' TABLE 'src' DB 'mydb'))\nLAYOUT(HASHED())\nLIFETIME(MIN 0 MAX 300)";
2473+
assert!(dicts_ddl_equivalent(actual, desired));
2474+
}
2475+
24512476
#[test]
24522477
fn test_dicts_ddl_equivalent_backtick_keyword_identifier() {
24532478
// A column named `SOURCE` (a reserved keyword) in PRIMARY KEY must not be

0 commit comments

Comments
 (0)