@@ -318,25 +318,33 @@ def _directory_entries(self):
318318 return self ._directory_cache
319319
320320 def _follow_symlink (self , entry ):
321- """Follow an symlink `TarInfo` to find a concrete entry."""
321+ """Follow an symlink `TarInfo` to find a concrete entry.
322+
323+ Returns ``None`` if the symlink is dangling.
324+ """
325+ done = set ()
322326 _entry = entry
323327 while _entry .issym ():
324328 linkname = normpath (
325329 join (dirname (self ._decode (_entry .name )), self ._decode (_entry .linkname ))
326330 )
327331 resolved = self ._resolve (linkname )
328332 if resolved is None :
329- raise errors .ResourceNotFound (linkname )
333+ return None
334+ done .add (_entry )
330335 _entry = self ._directory_entries [resolved ]
336+ # if we already saw this symlink, then we are following cyclic
337+ # symlinks and we should break the loop
338+ if _entry in done :
339+ return None
331340
332341 return _entry
333342
334343 def _resolve (self , path ):
335344 """Replace path components that are symlinks with concrete components.
336345
337- Returns:
338-
339-
346+ Returns ``None`` when the path could not be resolved to an existing
347+ entry in the archive.
340348 """
341349 if path in self ._directory_entries or not path :
342350 return path
@@ -441,17 +449,17 @@ def isdir(self, path):
441449 _path = relpath (self .validatepath (path ))
442450 realpath = self ._resolve (_path )
443451 if realpath is not None :
444- entry = self ._directory_entries [realpath ]
445- return self . _follow_symlink ( entry ) .isdir ()
452+ entry = self ._follow_symlink ( self . _directory_entries [realpath ])
453+ return False if entry is None else entry .isdir ()
446454 else :
447455 return False
448456
449457 def isfile (self , path ):
450458 _path = relpath (self .validatepath (path ))
451459 realpath = self ._resolve (_path )
452460 if realpath is not None :
453- entry = self ._directory_entries [realpath ]
454- return self . _follow_symlink ( entry ) .isfile ()
461+ entry = self ._follow_symlink ( self . _directory_entries [realpath ])
462+ return False if entry is None else entry .isfile ()
455463 else :
456464 return False
457465
@@ -480,12 +488,12 @@ def listdir(self, path):
480488 elif realpath :
481489 target = self ._follow_symlink (self ._directory_entries [realpath ])
482490 # check the path is either a symlink mapping to a directory or a directory
483- if target .isdir ():
484- base = target .name
485- elif target .issym ():
486- base = target .linkname
487- else :
491+ if target is None :
492+ raise errors .ResourceNotFound (path )
493+ elif not target .isdir ():
488494 raise errors .DirectoryExpected (path )
495+ else :
496+ base = target .name
489497 else :
490498 base = ""
491499
@@ -515,11 +523,16 @@ def openbin(self, path, mode="r", buffering=-1, **options):
515523 if "w" in mode or "+" in mode or "a" in mode :
516524 raise errors .ResourceReadOnly (path )
517525
518- # check the path actually resolves after following symlinks
526+ # check the path actually resolves after following symlink components
519527 _realpath = self ._resolve (_path )
520528 if _realpath is None :
521529 raise errors .ResourceNotFound (path )
522530
531+ # get the entry at the resolved path and follow all symlinks
532+ entry = self ._follow_symlink (self ._directory_entries [_realpath ])
533+ if entry is None :
534+ raise errors .ResourceNotFound (path )
535+
523536 # TarFile.extractfile returns None if the entry is not a file
524537 # neither a file nor a symlink
525538 reader = self ._tar .extractfile (self ._directory_entries [_realpath ])
0 commit comments