@@ -170,7 +170,16 @@ def test_rewriting_multiline_value_does_not_create_option(self, rw_dir):
170170 @with_rw_directory
171171 def test_writer_escapes_special_characters_without_newline (self , rw_dir ):
172172 config_path = osp .join (rw_dir , "config" )
173- values = {"tab" : "\t value\t " , "backspace" : "a\b b" , "quote" : 'a"b' , "backslash" : "a\\ qb" }
173+ values = {
174+ "tab" : "\t value\t " ,
175+ "backspace" : "a\b b" ,
176+ "quote" : 'a"b' ,
177+ "backslash" : "a\\ qb" ,
178+ "hash" : "value#fragment" ,
179+ "semicolon" : "value;fragment" ,
180+ "leading" : " value" ,
181+ "trailing" : "value " ,
182+ }
174183
175184 with GitConfigParser (config_path , read_only = False ) as git_config :
176185 for key , value in values .items ():
@@ -190,6 +199,54 @@ def test_writer_escapes_special_characters_without_newline(self, rw_dir):
190199 with open (config_path , "rb" ) as config_file :
191200 self .assertNotIn (b"\x08 " , config_file .read ())
192201
202+ @with_rw_directory
203+ def test_writer_preserves_escaped_and_non_ascii_values_safely (self , rw_dir ):
204+ config_path = osp .join (rw_dir , "config" )
205+ with open (config_path , "wb" ) as config_file :
206+ config_file .write (
207+ (
208+ '[section]\n newline = "first\\ nsecond"\n quote = "a\\ "b"\n backslash = "a\\ \\ b"\n '
209+ 'unicode = "café\\ \\ path"\n '
210+ ).encode ()
211+ )
212+
213+ with GitConfigParser (config_path , read_only = False ) as config :
214+ config .set_value ("unrelated" , "key" , "value" )
215+
216+ expected = {
217+ "newline" : "first\n second" ,
218+ "quote" : 'a"b' ,
219+ "backslash" : "a\\ b" ,
220+ "unicode" : "café\\ path" ,
221+ }
222+ with GitConfigParser (config_path , read_only = True ) as config :
223+ for key , value in expected .items ():
224+ self .assertEqual (config .get_value ("section" , key ), value )
225+ self .assertEqual (
226+ subprocess .run (
227+ ["git" , "config" , "--file" , config_path , "--get" , "section.%s" % key ],
228+ stdout = subprocess .PIPE ,
229+ check = True ,
230+ ).stdout ,
231+ value .encode () + b"\n " ,
232+ )
233+
234+ with open (config_path , "rb" ) as config_file :
235+ contents = config_file .read ()
236+ self .assertNotIn (b"\r " , contents )
237+ self .assertNotIn (b"\x00 " , contents )
238+
239+ for name , value in (("return" , b"first\\ rsecond" ), ("nul" , b"first\x00 second" )):
240+ unsafe_path = osp .join (rw_dir , "%s-config" % name )
241+ unsafe_contents = b'[section]\n value = "' + value + b'"\n '
242+ with open (unsafe_path , "wb" ) as config_file :
243+ config_file .write (unsafe_contents )
244+ with self .assertRaisesRegex (ValueError , "CR or NUL" ):
245+ with GitConfigParser (unsafe_path , read_only = False ) as config :
246+ config .set_value ("unrelated" , "key" , "value" )
247+ with open (unsafe_path , "rb" ) as config_file :
248+ self .assertEqual (config_file .read (), unsafe_contents )
249+
193250 @with_rw_directory
194251 def test_set_value_rejects_config_injection (self , rw_dir ):
195252 config_path = osp .join (rw_dir , "config" )
@@ -745,15 +802,14 @@ def test_config_with_quotes_with_whitespace_outside_value(self):
745802 self .assertEqual (cr .get ("init" , "defaultBranch" ), "trunk" )
746803
747804 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 ."""
805+ """Interpret Git's quoted escapes without changing malformed values ."""
749806 cr = GitConfigParser (fixture_path ("git_config_with_quotes_escapes" ), read_only = True )
750807
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"' )
808+ self .assertEqual (cr .get ("custom" , "hasnewline" ), "first\n second" )
809+ self .assertEqual (cr .get ("custom" , "hasbackslash" ), R"foo\bar" )
810+ self .assertEqual (cr .get ("custom" , "hasquote" ), 'ab"cd' )
811+ self .assertEqual (cr .get ("custom" , "hastrailingbackslash" ), "word\\ " )
812+ self .assertEqual (cr .get ("custom" , "hasunrecognized" ), R"p\qrs" )
757813
758814 # It is less obvious whether and what to eventually do with this.
759815 self .assertEqual (cr .get ("custom" , "hasunescapedquotes" ), '"ab"cd"e"' )
0 commit comments