88import ntpath
99import os
1010import os .path as osp
11+ import re
1112import shlex
1213import stat
1314import sys
4748 IterableList ,
4849 RemoteProgress ,
4950 _to_relative_path ,
51+ _validate_repo_path ,
5052 join_path_native ,
5153 rmtree ,
5254 to_native_path_linux ,
@@ -305,20 +307,55 @@ def _config_parser_constrained(self, read_only: bool) -> SectionConstraint:
305307 def _validated_name (cls , name : str ) -> str :
306308 if (
307309 not name
310+ or "\0 " in name
308311 or name .startswith (("/" , "\\ " ))
309312 or ntpath .splitdrive (name )[0 ]
310313 or ".." in name .replace ("\\ " , "/" ).split ("/" )
311314 ):
312315 raise ValueError ("Invalid submodule name %r" % name )
316+ cls ._validate_windows_path (name )
313317 return name
314318
319+ @staticmethod
320+ def _validate_windows_path (path : PathLike ) -> None :
321+ """Apply Git for Windows' filename checks before creating directories."""
322+ if sys .platform == "win32" :
323+ for component in ntpath .splitdrive (os .fspath (path ))[1 ].replace ("\\ " , "/" ).split ("/" ):
324+ if component in ("." , ".." ):
325+ continue
326+ stem = component .split ("." , 1 )[0 ].rstrip (" " ).upper ()
327+ if (
328+ component .endswith ((" " , "." ))
329+ or any (ord (char ) < 32 or char in '<>:"|?*' for char in component )
330+ or re .fullmatch (r"CON(?:IN\$|OUT\$)?|PRN|AUX|NUL|COM[1-9]|LPT[1-9]" , stem )
331+ ):
332+ raise ValueError ("Invalid submodule path on Windows: %r" % path )
333+
315334 @classmethod
316- def _module_abspath (cls , parent_repo : "Repo" , path : PathLike , name : str ) -> PathLike :
335+ def _module_abspath (
336+ cls , parent_repo : "Repo" , path : PathLike , name : str , * , moving_from : Union [PathLike , None ] = None
337+ ) -> PathLike :
338+ """Reject nested Git directories, allowing the source of a pending rename."""
339+ from git .repo .fun import is_git_dir
340+
317341 name = cls ._validated_name (name )
318342 if cls ._need_gitfile_submodules (parent_repo .git ):
343+ directory = osp .join (parent_repo .git_dir , "modules" )
344+ for component in to_native_path_linux (name ).split ("/" )[:- 1 ]:
345+ directory = osp .join (directory , component )
346+ if is_git_dir (directory ) and (
347+ moving_from is None or Path (directory ).resolve () != Path (moving_from ).resolve ()
348+ ):
349+ raise ValueError (
350+ "Submodule metadata for %r is inside another Git directory: %r" % (name , directory )
351+ )
319352 return osp .join (parent_repo .git_dir , "modules" , name )
320353 if parent_repo .working_tree_dir :
321- return cls ._checked_abspath (parent_repo .working_tree_dir , cls ._to_relative_path (parent_repo , path ))
354+ return cls ._checked_abspath (
355+ parent_repo .working_tree_dir ,
356+ cls ._to_relative_path (parent_repo , path ),
357+ git_dirs = (parent_repo .git_dir , parent_repo .common_dir ),
358+ )
322359 raise NotADirectoryError ()
323360
324361 @classmethod
@@ -360,7 +397,9 @@ def _clone_repo(
360397 path = cls ._to_relative_path (repo , path )
361398 if repo .working_tree_dir is None :
362399 raise NotADirectoryError ("Submodules require a working tree" )
363- module_checkout_path = cls ._checked_abspath (repo .working_tree_dir , path )
400+ module_checkout_path = cls ._checked_abspath (
401+ repo .working_tree_dir , path , git_dirs = (repo .git_dir , repo .common_dir )
402+ )
364403 module_abspath = cls ._module_abspath (repo , path , name )
365404 if cls ._need_gitfile_submodules (repo .git ):
366405 if not allow_unsafe_options :
@@ -402,6 +441,16 @@ def _clone_repo(
402441 ** kwargs ,
403442 )
404443 if cls ._need_gitfile_submodules (repo .git ):
444+ # A concurrent clone may have turned a leading directory into a repository.
445+ try :
446+ cls ._module_abspath (repo , path , name )
447+ except ValueError :
448+ clone .close ()
449+ try :
450+ os .remove (osp .join (clone .git_dir , "HEAD" ))
451+ except FileNotFoundError :
452+ pass
453+ raise
405454 cls ._write_git_file_and_module_config (module_checkout_path , module_abspath )
406455
407456 return clone
@@ -411,8 +460,9 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
411460 """:return: A path guaranteed to be relative to the given parent repository
412461
413462 :raise ValueError:
414- If path is not contained in the parent repository's working tree .
463+ If path is outside the working tree or is unsafe as a submodule checkout .
415464 """
465+ cls ._validate_windows_path (path )
416466 if parent_repo .working_tree_dir :
417467 path = _to_relative_path (parent_repo .working_tree_dir , path )
418468 else :
@@ -422,6 +472,7 @@ def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
422472 if not path or path == "." :
423473 raise ValueError ("Submodule checkout path must not be the repository root" )
424474
475+ _validate_repo_path (path )
425476 return path
426477
427478 @property
@@ -433,22 +484,35 @@ def abspath(self) -> PathLike:
433484
434485 def _checkout_abspath (self , relative_path : PathLike , allow_final_symlink : bool = False ) -> PathLike :
435486 """Check a checkout path already normalized by :meth:`_to_relative_path`."""
436- return self ._checked_abspath (self .repo .working_tree_dir , relative_path , allow_final_symlink )
487+ return self ._checked_abspath (
488+ self .repo .working_tree_dir ,
489+ relative_path ,
490+ allow_final_symlink ,
491+ git_dirs = (self .repo .git_dir , self .repo .common_dir ),
492+ )
437493
438494 @classmethod
439495 def _checked_abspath (
440- cls , root : Union [PathLike , None ], relative_path : PathLike , allow_final_symlink : bool = False
496+ cls ,
497+ root : Union [PathLike , None ],
498+ relative_path : PathLike ,
499+ allow_final_symlink : bool = False ,
500+ * ,
501+ git_dirs : Sequence [PathLike ] = (),
441502 ) -> str :
442- """Reject symlinks below a trusted root before accessing submodule paths ."""
503+ """Reject symlinks and checkout aliases of Git directories below a trusted root ."""
443504 if root is None :
444505 raise NotADirectoryError ("Submodules require a working tree" )
445506 path = os .fspath (root )
507+ metadata_dirs = set (git_dirs )
446508 components = to_native_path_linux (relative_path ).split ("/" )
447509 for index , component in enumerate (components ):
448510 path = os .fspath (join_path_native (path , component ))
449- if allow_final_symlink and index == len ( components ) - 1 :
450- break
511+ if metadata_dirs and osp . exists ( path ) and any ( osp . samefile ( path , directory ) for directory in metadata_dirs ) :
512+ raise ValueError ( "Submodule checkout path aliases Git metadata: %r" % relative_path )
451513 if osp .islink (path ):
514+ if allow_final_symlink and index == len (components ) - 1 :
515+ break
452516 raise ValueError ("Submodule path %r contains a symbolic link" % relative_path )
453517 return path
454518
@@ -1122,6 +1186,11 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
11221186 self ._checked_abspath (self .repo .working_tree_dir , self .k_modules_file )
11231187 # Validate the source before removing the destination.
11241188 cur_path = self .abspath
1189+ module_abspath = self ._module_abspath (self .repo , self .path , self .name )
1190+ if self .path == self .name :
1191+ self ._module_abspath (
1192+ self .repo , module_checkout_path , os .fspath (module_checkout_path ), moving_from = module_abspath
1193+ )
11251194 module_checkout_abspath = self ._checkout_abspath (module_checkout_path , allow_final_symlink = True )
11261195 if osp .isfile (module_checkout_abspath ):
11271196 raise ValueError ("Cannot move repository onto a file: %s" % module_checkout_abspath )
@@ -1160,7 +1229,6 @@ def move(self, module_path: PathLike, configuration: bool = True, module: bool =
11601229 renamed_module = True
11611230
11621231 if osp .isfile (osp .join (module_checkout_abspath , ".git" )):
1163- module_abspath = self ._module_abspath (self .repo , self .path , self .name )
11641232 self ._write_git_file_and_module_config (module_checkout_abspath , module_abspath )
11651233 # END handle git file rewrite
11661234 # END move physical module
@@ -1522,8 +1590,8 @@ def rename(self, new_name: str) -> "Submodule":
15221590
15231591 self ._validated_name (self .name )
15241592 self ._validated_name (new_name )
1525- destination_module_abspath = self ._module_abspath (self .repo , self .path , new_name )
15261593 mod = self .module ()
1594+ destination_module_abspath = self ._module_abspath (self .repo , self .path , new_name , moving_from = mod .git_dir )
15271595 self ._checked_abspath (self .repo .working_tree_dir , self .k_modules_file )
15281596
15291597 # .git/config
@@ -1573,6 +1641,7 @@ def module(self) -> "Repo":
15731641 """
15741642 self ._validated_name (self .name )
15751643 module_checkout_abspath = self .abspath
1644+ self ._module_abspath (self .repo , self .path , self .name )
15761645 try :
15771646 repo = git .Repo (module_checkout_abspath )
15781647 if repo != self .repo :
0 commit comments