@@ -350,6 +350,21 @@ def test_index_reader_and_writer_reject_unsafe_paths(self, path):
350350 "gitmod~4" ,
351351 "gi7eba~1" ,
352352 "gi7eba~9" ,
353+ "GI7EB~10" ,
354+ "GI7EB~99" ,
355+ "GI7E~100" ,
356+ "GI7E~999" ,
357+ "GI7~1000" ,
358+ "GI7~9999" ,
359+ "GI~10000" ,
360+ "GI~99999" ,
361+ "G~100000" ,
362+ "G~999999" ,
363+ "~1000000" ,
364+ "~9999999" ,
365+ "GI7EB~10. " ,
366+ "GI7E~100:$DATA" ,
367+ "sub/~1000000" ,
353368 "sub/.gitmodules" ,
354369 )
355370 def test_index_reader_and_writer_reject_gitmodules_symlinks (self , path ):
@@ -367,6 +382,26 @@ def test_index_reader_and_writer_reject_gitmodules_symlinks(self, path):
367382 stream .seek (0 )
368383 assert next (iter (read_cache (stream )[1 ])) == (path , 0 )
369384
385+ @ddt .data ("GI7EB~10" , "GI7E~100" , "GI7~1000" , "GI~10000" , "G~100000" , "~1000000" , "~9999999" )
386+ @with_rw_directory
387+ def test_index_add_and_write_tree_reject_gitmodules_fallback_symlinks (self , rw_dir , path ):
388+ with Repo .init (rw_dir ) as repo :
389+ binsha = repo .odb .store (IStream ("blob" , 6 , BytesIO (b"target" ))).binsha
390+ index = repo .index
391+ for item in (Blob (repo , binsha , 0o120000 , path ), BaseIndexEntry ((0o120000 , binsha , 0 , path ))):
392+ with pytest .raises (ValueError , match = "submodule configuration" ):
393+ index .add ([item ], write = False )
394+ assert not index .entries
395+
396+ index .entries [(path , 0 )] = IndexEntry ((0o120000 , binsha , 0 , path ))
397+ with pytest .raises (ValueError , match = "submodule configuration" ):
398+ index .write_tree ()
399+ assert not Path (index .path ).exists ()
400+
401+ index .add ([BaseIndexEntry ((0o100644 , binsha , 0 , path ))])
402+ assert repo .index .entries [(path , 0 )].mode == 0o100644
403+ assert index .write_tree ()[path ].mode == 0o100644
404+
370405 def test_valid_unusual_index_names_round_trip (self ):
371406 names = ["a b" , "a\n b" , "a\t b" , "name:value" , "dir/.gitignore" , "café" ]
372407 if os .name != "nt" :
@@ -408,20 +443,26 @@ def _cmp_tree_index(self, tree, index):
408443
409444 @with_rw_repo ("0.1.6" )
410445 def test_index_lock_handling (self , rw_repo ):
411- def add_bad_blob ():
412- rw_repo .index .add ([Blob (rw_repo , b"f" * 20 , "bad-permissions" , "foo" )])
413-
414- try :
415- ## First, fail on purpose adding into index.
416- add_bad_blob ()
417- except Exception as ex :
418- assert "required argument is not an integer" in str (ex )
419-
420- ## The second time should not fail due to stray lock file.
421- try :
422- add_bad_blob ()
423- except Exception as ex :
424- assert "index.lock' could not be obtained" not in str (ex )
446+ index = rw_repo .index
447+ index_path = Path (index .path )
448+ lock_path = Path (str (index_path ) + ".lock" )
449+ before = index_path .read_bytes ()
450+
451+ def fail_serialize (stream , ignore_extension_data ):
452+ assert lock_path .exists ()
453+ stream .write (b"partial index" )
454+ raise OSError ("simulated index write failure" )
455+
456+ with mock .patch .object (IndexFile , "_serialize" , side_effect = fail_serialize ):
457+ for _ in range (2 ):
458+ with pytest .raises (OSError , match = "simulated index write failure" ):
459+ index .write ()
460+ assert not lock_path .exists ()
461+ assert index_path .read_bytes () == before
462+
463+ index .add ([Blob (rw_repo , b"f" * 20 , 0o100644 , "foo" )])
464+ assert not lock_path .exists ()
465+ assert rw_repo .index .entries [("foo" , 0 )].mode == 0o100644
425466
426467 @with_rw_repo ("0.1.6" )
427468 def test_read_tree_methods_reject_index_output (self , rw_repo ):
@@ -1248,6 +1289,60 @@ def test_staging_rejects_unsafe_object_paths_even_without_writing(self, rw_dir,
12481289 index .add ([item ], write = False , ** kwargs )
12491290 assert not index .entries
12501291
1292+ @ddt .data (* product (("path" , "blob" , "entry" , "stored-blob" , "stored-entry" ), (False , True ), (False , True )))
1293+ @ddt .unpack
1294+ @with_rw_directory
1295+ def test_staging_gitmodules_symlink_check_uses_rewritten_path (self , rw_dir , kind , unsafe_destination , write ):
1296+ with Repo .init (rw_dir ) as repo :
1297+ source , destination = ("safe-link" , ".gitmodules" ) if unsafe_destination else (".gitmodules" , "safe-link" )
1298+ binsha = Blob .NULL_BIN_SHA
1299+ if kind .startswith ("stored-" ):
1300+ binsha = repo .odb .store (IStream ("blob" , 6 , BytesIO (b"target" ))).binsha
1301+ else :
1302+ try :
1303+ (Path (rw_dir ) / source ).symlink_to ("target" )
1304+ except OSError :
1305+ pytest .skip ("Symlinks unavailable" )
1306+ if kind == "path" :
1307+ item = source
1308+ elif kind .endswith ("blob" ):
1309+ item = Blob (repo , binsha , 0o120000 , source )
1310+ else :
1311+ item = BaseIndexEntry ((0o120000 , binsha , 0 , source ))
1312+ index = repo .index
1313+ rewriter = mock .Mock (return_value = destination )
1314+ if unsafe_destination :
1315+ with pytest .raises (ValueError , match = "submodule configuration" ):
1316+ index .add ([item ], path_rewriter = rewriter , write = write )
1317+ assert not index .entries
1318+ assert not Path (index .path ).exists ()
1319+ else :
1320+ added = index .add ([item ], path_rewriter = rewriter , write = write )
1321+ assert [(entry .path , entry .mode ) for entry in added ] == [(destination , 0o120000 )]
1322+ assert set (index .entries ) == {(destination , 0 )}
1323+ assert index .write_tree ()[destination ].mode == 0o120000
1324+ if write :
1325+ assert repo .index .entries [(destination , 0 )].mode == 0o120000
1326+ rewriter .assert_called_once ()
1327+ assert rewriter .call_args [0 ][0 ].path == source
1328+
1329+ @ddt .data (* product (("blob" , "entry" ), ("../outside" , ".git/config" )))
1330+ @ddt .unpack
1331+ @with_rw_directory
1332+ def test_staging_rewriter_cannot_sanitize_unsafe_source_paths (self , rw_dir , kind , path ):
1333+ with Repo .init (rw_dir ) as repo :
1334+ item = (
1335+ Blob (repo , b"a" * 20 , 0o120000 , path )
1336+ if kind == "blob"
1337+ else BaseIndexEntry ((0o120000 , b"a" * 20 , 0 , path ))
1338+ )
1339+ index = repo .index
1340+ rewriter = mock .Mock (return_value = "safe-link" )
1341+ with pytest .raises (ValueError ):
1342+ index .add ([item ], path_rewriter = rewriter , write = False )
1343+ rewriter .assert_not_called ()
1344+ assert not index .entries
1345+
12511346 @with_rw_directory
12521347 def test_staging_root_preserves_symlinks_and_skips_git_metadata (self , rw_dir ):
12531348 tmp_path = Path (rw_dir )
0 commit comments