1414import pytest
1515
1616from git import GitConfigParser
17+ from git .compat import defenc
1718from git .config import _OMD , cp
1819from git .util import cwd , rmfile
1920from test .lib import SkipTest , TestCase , fixture_path , with_rw_directory
@@ -170,7 +171,16 @@ def test_rewriting_multiline_value_does_not_create_option(self, rw_dir):
170171 @with_rw_directory
171172 def test_writer_escapes_special_characters_without_newline (self , rw_dir ):
172173 config_path = osp .join (rw_dir , "config" )
173- values = {"tab" : "\t value\t " , "backspace" : "a\b b" , "quote" : 'a"b' , "backslash" : "a\\ qb" }
174+ values = {
175+ "tab" : "\t value\t " ,
176+ "backspace" : "a\b b" ,
177+ "quote" : 'a"b' ,
178+ "backslash" : "a\\ qb" ,
179+ "hash" : "value#fragment" ,
180+ "semicolon" : "value;fragment" ,
181+ "leading" : " value" ,
182+ "trailing" : "value " ,
183+ }
174184
175185 with GitConfigParser (config_path , read_only = False ) as git_config :
176186 for key , value in values .items ():
@@ -185,11 +195,72 @@ def test_writer_escapes_special_characters_without_newline(self, rw_dir):
185195 stdout = subprocess .PIPE ,
186196 check = True ,
187197 ).stdout ,
188- value .encode () + b"\n " ,
198+ value .encode (defenc ) + b"\n " ,
189199 )
190200 with open (config_path , "rb" ) as config_file :
191201 self .assertNotIn (b"\x08 " , config_file .read ())
192202
203+ @with_rw_directory
204+ def test_writer_preserves_escaped_and_non_ascii_values_safely (self , rw_dir ):
205+ config_path = osp .join (rw_dir , "config" )
206+ with open (config_path , "wb" ) as config_file :
207+ config_file .write (
208+ (
209+ '[section]\n newline = "first\\ nsecond"\n quote = "a\\ "b"\n backslash = "a\\ \\ b"\n '
210+ 'unicode = "café\\ \\ path"\n '
211+ ).encode (defenc )
212+ )
213+
214+ with GitConfigParser (config_path , read_only = False ) as config :
215+ config .set_value ("unrelated" , "key" , "value" )
216+
217+ expected = {
218+ "newline" : "first\n second" ,
219+ "quote" : 'a"b' ,
220+ "backslash" : "a\\ b" ,
221+ "unicode" : "café\\ path" ,
222+ }
223+ with GitConfigParser (config_path , read_only = True ) as config :
224+ for key , value in expected .items ():
225+ self .assertEqual (
226+ config .get_value ("section" , key ),
227+ value ,
228+ "GitPython should preserve values when rewriting unrelated entries" ,
229+ )
230+ self .assertEqual (
231+ subprocess .run (
232+ ["git" , "config" , "--file" , config_path , "--get" , "section.%s" % key ],
233+ stdout = subprocess .PIPE ,
234+ check = True ,
235+ ).stdout ,
236+ value .encode (defenc ) + b"\n " ,
237+ "git should read rewritten values with the same semantics" ,
238+ )
239+
240+ with open (config_path , "rb" ) as config_file :
241+ contents = config_file .read ()
242+ self .assertNotIn (b"\r " , contents , "the writer should never emit carriage returns" )
243+ self .assertNotIn (b"\x00 " , contents , "the writer should never emit NUL bytes" )
244+
245+ for name , value in (("return" , b"first\\ rsecond" ), ("nul" , b"first\x00 second" )):
246+ unsafe_path = osp .join (rw_dir , "%s-config" % name )
247+ unsafe_contents = b'[section]\n value = "' + value + b'"\n '
248+ with open (unsafe_path , "wb" ) as config_file :
249+ config_file .write (unsafe_contents )
250+ with self .assertRaisesRegex (
251+ ValueError ,
252+ "CR or NUL" ,
253+ msg = "unsafe existing values should abort rewrites" ,
254+ ):
255+ with GitConfigParser (unsafe_path , read_only = False ) as config :
256+ config .set_value ("unrelated" , "key" , "value" )
257+ with open (unsafe_path , "rb" ) as config_file :
258+ self .assertEqual (
259+ config_file .read (),
260+ unsafe_contents ,
261+ "rejected rewrites should leave the original file unchanged" ,
262+ )
263+
193264 @with_rw_directory
194265 def test_set_value_rejects_config_injection (self , rw_dir ):
195266 config_path = osp .join (rw_dir , "config" )
@@ -745,15 +816,14 @@ def test_config_with_quotes_with_whitespace_outside_value(self):
745816 self .assertEqual (cr .get ("init" , "defaultBranch" ), "trunk" )
746817
747818 def test_config_with_quotes_containing_escapes (self ):
748- """For now just suppress quote removal. But it would be good to interpret most of these ."""
819+ """Interpret Git's quoted escapes without changing malformed values ."""
749820 cr = GitConfigParser (fixture_path ("git_config_with_quotes_escapes" ), read_only = True )
750821
751- # These can eventually be supported by substituting the represented character.
752- self .assertEqual (cr .get ("custom" , "hasnewline" ), R'"first\nsecond"' )
753- self .assertEqual (cr .get ("custom" , "hasbackslash" ), R'"foo\\bar"' )
754- self .assertEqual (cr .get ("custom" , "hasquote" ), R'"ab\"cd"' )
755- self .assertEqual (cr .get ("custom" , "hastrailingbackslash" ), R'"word\\"' )
756- self .assertEqual (cr .get ("custom" , "hasunrecognized" ), R'"p\qrs"' )
822+ self .assertEqual (cr .get ("custom" , "hasnewline" ), "first\n second" )
823+ self .assertEqual (cr .get ("custom" , "hasbackslash" ), R"foo\bar" )
824+ self .assertEqual (cr .get ("custom" , "hasquote" ), 'ab"cd' )
825+ self .assertEqual (cr .get ("custom" , "hastrailingbackslash" ), "word\\ " )
826+ self .assertEqual (cr .get ("custom" , "hasunrecognized" ), R"p\qrs" )
757827
758828 # It is less obvious whether and what to eventually do with this.
759829 self .assertEqual (cr .get ("custom" , "hasunescapedquotes" ), '"ab"cd"e"' )
0 commit comments