upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/git_remote_nostr/main.rs75
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.
41fn 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}