diff options
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/git_remote_nostr/main.rs | 75 |
1 files changed, 74 insertions, 1 deletions
diff --git a/src/bin/git_remote_nostr/main.rs b/src/bin/git_remote_nostr/main.rs index f5b88ac..f670b7b 100644 --- a/src/bin/git_remote_nostr/main.rs +++ b/src/bin/git_remote_nostr/main.rs | |||
| @@ -30,6 +30,37 @@ struct PushOptions { | |||
| 30 | description: Option<String>, | 30 | description: Option<String>, |
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | /// Strip git's c-style quoting from a push-option value. | ||
| 34 | /// | ||
| 35 | /// When a push-option value contains special characters (like | ||
| 36 | /// backslashes), git wraps the entire `key=value` string in double | ||
| 37 | /// quotes and doubles every backslash. This function reverses that: | ||
| 38 | /// it strips the surrounding quotes and un-doubles backslashes. | ||
| 39 | /// | ||
| 40 | /// If the string is not quoted, it is returned unchanged. | ||
| 41 | fn strip_git_quoting(s: &str) -> String { | ||
| 42 | if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 { | ||
| 43 | let inner = &s[1..s.len() - 1]; | ||
| 44 | let mut result = String::with_capacity(inner.len()); | ||
| 45 | let mut chars = inner.chars().peekable(); | ||
| 46 | while let Some(c) = chars.next() { | ||
| 47 | if c == '\\' { | ||
| 48 | if let Some(&next) = chars.peek() { | ||
| 49 | chars.next(); | ||
| 50 | result.push(next); | ||
| 51 | } else { | ||
| 52 | result.push(c); | ||
| 53 | } | ||
| 54 | } else { | ||
| 55 | result.push(c); | ||
| 56 | } | ||
| 57 | } | ||
| 58 | result | ||
| 59 | } else { | ||
| 60 | s.to_string() | ||
| 61 | } | ||
| 62 | } | ||
| 63 | |||
| 33 | /// Decode escape sequences in push-option values. | 64 | /// Decode escape sequences in push-option values. |
| 34 | /// | 65 | /// |
| 35 | /// Git push-options are transmitted one per line, so literal newlines | 66 | /// Git push-options are transmitted one per line, so literal newlines |
| @@ -145,7 +176,7 @@ async fn main() -> Result<()> { | |||
| 145 | println!("ok"); | 176 | println!("ok"); |
| 146 | } | 177 | } |
| 147 | ["option", "push-option", rest @ ..] => { | 178 | ["option", "push-option", rest @ ..] => { |
| 148 | let option = rest.join(" "); | 179 | let option = strip_git_quoting(&rest.join(" ")); |
| 149 | if let Some((key, value)) = option.split_once('=') { | 180 | if let Some((key, value)) = option.split_once('=') { |
| 150 | match key { | 181 | match key { |
| 151 | "title" => { | 182 | "title" => { |
| @@ -328,4 +359,46 @@ mod tests { | |||
| 328 | "line1\nline2\\nstill line2\nline3" | 359 | "line1\nline2\\nstill line2\nline3" |
| 329 | ); | 360 | ); |
| 330 | } | 361 | } |
| 362 | |||
| 363 | #[test] | ||
| 364 | fn strip_git_quoting_removes_quotes_and_unescapes() { | ||
| 365 | // Git sends: "description=First line\\nSecond line" | ||
| 366 | // After strip: description=First line\nSecond line | ||
| 367 | assert_eq!( | ||
| 368 | strip_git_quoting(r#""description=First line\\nSecond line""#), | ||
| 369 | r"description=First line\nSecond line" | ||
| 370 | ); | ||
| 371 | } | ||
| 372 | |||
| 373 | #[test] | ||
| 374 | fn strip_git_quoting_no_quotes_unchanged() { | ||
| 375 | assert_eq!( | ||
| 376 | strip_git_quoting("description=plain text"), | ||
| 377 | "description=plain text" | ||
| 378 | ); | ||
| 379 | } | ||
| 380 | |||
| 381 | #[test] | ||
| 382 | fn strip_git_quoting_then_decode_produces_newlines() { | ||
| 383 | // Simulates the full pipeline for a git-quoted push option: | ||
| 384 | // User writes: description=line1\n\nline2 | ||
| 385 | // Git sends: "description=line1\\n\\nline2" | ||
| 386 | let git_quoted = r#""description=line1\\n\\nline2""#; | ||
| 387 | let unquoted = strip_git_quoting(git_quoted); | ||
| 388 | assert_eq!(unquoted, r"description=line1\n\nline2"); | ||
| 389 | let (key, value) = unquoted.split_once('=').unwrap(); | ||
| 390 | assert_eq!(key, "description"); | ||
| 391 | assert_eq!(decode_push_option_escapes(value), "line1\n\nline2"); | ||
| 392 | } | ||
| 393 | |||
| 394 | #[test] | ||
| 395 | fn strip_git_quoting_preserves_user_double_backslash() { | ||
| 396 | // User writes: description=keep \\n literal | ||
| 397 | // Git sends: "description=keep \\\\n literal" | ||
| 398 | let git_quoted = r#""description=keep \\\\n literal""#; | ||
| 399 | let unquoted = strip_git_quoting(git_quoted); | ||
| 400 | assert_eq!(unquoted, r"description=keep \\n literal"); | ||
| 401 | let (_, value) = unquoted.split_once('=').unwrap(); | ||
| 402 | assert_eq!(decode_push_option_escapes(value), "keep \\n literal"); | ||
| 403 | } | ||
| 331 | } | 404 | } |