@@ -318,25 +318,33 @@ def _directory_entries(self):
318
318
return self ._directory_cache
319
319
320
320
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 ()
322
326
_entry = entry
323
327
while _entry .issym ():
324
328
linkname = normpath (
325
329
join (dirname (self ._decode (_entry .name )), self ._decode (_entry .linkname ))
326
330
)
327
331
resolved = self ._resolve (linkname )
328
332
if resolved is None :
329
- raise errors .ResourceNotFound (linkname )
333
+ return None
334
+ done .add (_entry )
330
335
_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
331
340
332
341
return _entry
333
342
334
343
def _resolve (self , path ):
335
344
"""Replace path components that are symlinks with concrete components.
336
345
337
- Returns:
338
-
339
-
346
+ Returns ``None`` when the path could not be resolved to an existing
347
+ entry in the archive.
340
348
"""
341
349
if path in self ._directory_entries or not path :
342
350
return path
@@ -406,7 +414,7 @@ def getinfo(self, path, namespaces=None):
406
414
target = normpath (join (
407
415
dirname (self ._decode (member .name )),
408
416
self ._decode (member .linkname ),
409
- )) # type: Option [Text]
417
+ )) # type: Optional [Text]
410
418
else :
411
419
target = None
412
420
raw_info ["link" ] = {"target" : target }
@@ -441,17 +449,17 @@ def isdir(self, path):
441
449
_path = relpath (self .validatepath (path ))
442
450
realpath = self ._resolve (_path )
443
451
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 ()
446
454
else :
447
455
return False
448
456
449
457
def isfile (self , path ):
450
458
_path = relpath (self .validatepath (path ))
451
459
realpath = self ._resolve (_path )
452
460
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 ()
455
463
else :
456
464
return False
457
465
@@ -480,12 +488,12 @@ def listdir(self, path):
480
488
elif realpath :
481
489
target = self ._follow_symlink (self ._directory_entries [realpath ])
482
490
# 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 ():
488
494
raise errors .DirectoryExpected (path )
495
+ else :
496
+ base = target .name
489
497
else :
490
498
base = ""
491
499
@@ -515,11 +523,16 @@ def openbin(self, path, mode="r", buffering=-1, **options):
515
523
if "w" in mode or "+" in mode or "a" in mode :
516
524
raise errors .ResourceReadOnly (path )
517
525
518
- # check the path actually resolves after following symlinks
526
+ # check the path actually resolves after following symlink components
519
527
_realpath = self ._resolve (_path )
520
528
if _realpath is None :
521
529
raise errors .ResourceNotFound (path )
522
530
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
+
523
536
# TarFile.extractfile returns None if the entry is not a file
524
537
# neither a file nor a symlink
525
538
reader = self ._tar .extractfile (self ._directory_entries [_realpath ])
0 commit comments