send: use normpath() on argument to remove trailing slashes

This ought to help with #251, where bash-on-windows makes it easy to add a
forward-slash, and os.path.normpath() knows how to remove them, but os.sep is
a backslash.
This commit is contained in:
Brian Warner 2017-10-22 11:32:22 +08:00
parent 64f973b0e9
commit d727531e6d
2 changed files with 81 additions and 2 deletions

View File

@ -243,11 +243,33 @@ class Sender:
# be unicode or bytes. We need it to be something that can be
# os.path.joined with the unicode args.what .
what = os.path.join(args.cwd, args.what)
what = what.rstrip(os.sep)
# We always tell the receiver to create a file (or directory) with the
# same basename as what the local user typed, even if the local object
# is a symlink to something with a different name. The normpath() is
# there to remove trailing slashes.
basename = os.path.basename(os.path.normpath(what))
assert basename != "", what # normpath shouldn't allow this
# We use realpath() instead of normpath() to locate the actual
# file/directory, because the path might contain symlinks, and
# normpath() would collapse those before resolving them.
# Unfortunately on windows, realpath() is built out of normpath()
# because of a no-longer-true belief that windows does not have a
# working os.path.islink(): see https://bugs.python.org/issue9949
# The consequence is that "wormhole send PATH" might send the wrong
# file, if:
# * we're running on windows
# * PATH goes down through a symlink and then up with parent-directory
# navigation (".."), then back down again
# * the back-down-again portion of the path also exists under the
# original directory (an error is thrown if not)
what = os.path.realpath(what)
if not os.path.exists(what):
raise TransferError("Cannot send: no file/directory named '%s'" %
args.what)
basename = os.path.basename(what)
if os.path.isfile(what):
# we're sending a file

View File

@ -183,6 +183,63 @@ class OfferData(unittest.TestCase):
self.assertEqual(str(e),
"'%s' is neither file nor directory" % filename)
def test_symlink(self):
if not hasattr(os, 'symlink'):
raise unittest.SkipTest("host OS does not support symlinks")
# build A/B1 -> B2 (==A/B2), and A/B2/C.txt
parent_dir = self.mktemp()
os.mkdir(parent_dir)
os.mkdir(os.path.join(parent_dir, "B2"))
with open(os.path.join(parent_dir, "B2", "C.txt"), "wb") as f:
f.write(b"success")
os.symlink("B2", os.path.join(parent_dir, "B1"))
# now send "B1/C.txt" from A, and it should get the right file
self.cfg.cwd = parent_dir
self.cfg.what = os.path.join("B1", "C.txt")
d, fd_to_send = build_offer(self.cfg)
self.assertEqual(d["file"]["filename"], "C.txt")
self.assertEqual(fd_to_send.read(), b"success")
def test_symlink_collapse(self):
if not hasattr(os, 'symlink'):
raise unittest.SkipTest("host OS does not support symlinks")
if os.name == "nt":
# ntpath.py's realpath() is built out of normpath(), and does not
# follow symlinks properly, so this test always fails. "wormhole
# send PATH" on windows will do the wrong thing. See
# https://bugs.python.org/issue9949" for details.
raise unittest.SkipTest("host OS has broken os.path.realpath(), see https://bugs.python.org/issue9949")
# build A/B1, A/B1/D.txt
# A/B2/C2, A/B2/D.txt
# symlink A/B1/C1 -> A/B2/C2
parent_dir = self.mktemp()
os.mkdir(parent_dir)
os.mkdir(os.path.join(parent_dir, "B1"))
with open(os.path.join(parent_dir, "B1", "D.txt"), "wb") as f:
f.write(b"fail")
os.mkdir(os.path.join(parent_dir, "B2"))
os.mkdir(os.path.join(parent_dir, "B2", "C2"))
with open(os.path.join(parent_dir, "B2", "D.txt"), "wb") as f:
f.write(b"success")
os.symlink(os.path.abspath(os.path.join(parent_dir, "B2", "C2")),
os.path.join(parent_dir, "B1", "C1"))
# Now send "B1/C1/../D.txt" from A. The correct traversal will be:
# * start: A
# * B1: A/B1
# * C1: follow symlink to A/B2/C2
# * ..: climb to A/B2
# * D.txt: open A/B2/D.txt, which contains "success"
# If the code mistakenly uses normpath(), it would do:
# * normpath turns B1/C1/../D.txt into B1/D.txt
# * start: A
# * B1: A/B1
# * D.txt: open A/B1/D.txt , which contains "fail"
self.cfg.cwd = parent_dir
self.cfg.what = os.path.join("B1", "C1", os.pardir, "D.txt")
d, fd_to_send = build_offer(self.cfg)
self.assertEqual(d["file"]["filename"], "D.txt")
self.assertEqual(fd_to_send.read(), b"success")
class LocaleFinder:
def __init__(self):
self._run_once = False