Skip to content

Commit

Permalink
Fix more inconsistent behaviour with symlinks in paths.
Browse files Browse the repository at this point in the history
Both redo and minimal/do were doing slightly weird things with
symlinked directories, especially when combined with "..".  For
example, if x is a link to ., then x/x/x/x/../y should resolve to
"../y", which is quite non-obvious.

Added some tests to make sure this stays fixed.
  • Loading branch information
apenwarr committed Dec 17, 2018
1 parent 1f64cc4 commit 686c381
Show file tree
Hide file tree
Showing 13 changed files with 121 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/.do_built.dir
/minimal/.do_built
/minimal/.do_built.dir
/minimal/y
*~
*.tmp
*.did
Expand Down
2 changes: 1 addition & 1 deletion clean.do
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ if [ -e .do_built ]; then
done <.do_built
fi
[ -z "$DO_BUILT" ] && rm -rf .do_built .do_built.dir
rm -rf minimal/.do_built minimal/.do_built.dir docs.out
rm -rf minimal/.do_built minimal/.do_built.dir minimal/y docs.out
redo t/clean docs/clean redo/clean
rm -f *~ .*~ */*~ */.*~ *.pyc install.wrapper
find . -name '*.tmp' -exec rm -f {} \;
Expand Down
5 changes: 5 additions & 0 deletions do
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ case $target in
build && bin/redo $args "$target"
;;
test)
# Be intentionally confusing about paths, to try to
# detect bugs.
rm -f 't/symlink path'
ln -s .. 't/symlink path' || die 'failed to make test dir.'
cd 't/symlink path/t/symlink path'
# First test minimal/do
build
# Add ./redo to PATH so we launch with redo/sh as the shell
Expand Down
36 changes: 34 additions & 2 deletions minimal/do
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,38 @@ _normpath()
)


# Prints a "real" path, with all symlinks resolved where possible.
_realpath()
{
local path="$1" relto="$2" isabs= rest=
if _startswith "$path" "/"; then
isabs=1
else
path="${relto%/}/$path"
fi
(
for d in $(seq 100); do
#echo "Trying: $PWD--$path" >&2
if cd -P "$path" 2>/dev/null; then
# success
pwd=$(/bin/pwd)
#echo " chdir ok: $pwd--$rest" >&2
np=$(_normpath "${pwd%/}/$rest" "$relto")
if [ -n "$isabs" ]; then
echo "$np"
else
_relpath "$np" "$relto"
fi
break
fi
_dirsplit "${path%/}"
path=$_dirsplit_dir
rest="$_dirsplit_base/$rest"
done
)
}


# List the possible names for default*.do files in dir $1 matching the target
# pattern in $2. We stop searching when we find the first one that exists.
_find_dofiles_pwd()
Expand Down Expand Up @@ -241,7 +273,7 @@ _find_dofiles()
[ -n "$dodir" ] && dodir=${dodir%/}/
#echo "_find_dofiles: '$dodir' '$dofile'" >&2
_find_dofiles_pwd "$dodir" "$dofile" && return 0
newdir=$(_normpath "${dodir}.." "$PWD")
newdir=$(_realpath "${dodir}.." "$PWD")
[ "$newdir" = "$dodir" ] && break
dodir=$newdir
done
Expand Down Expand Up @@ -369,7 +401,7 @@ _redo()
i=$(_abspath "$i" "$startdir")
(
cd "$DO_STARTDIR" || return 99
i=$(_normpath "$(_relpath "$i" "$PWD")" "$PWD")
i=$(_realpath "$(_relpath "$i" "$PWD")" "$PWD")
_dirsplit "$i"
dir=$_dirsplit_dir base=$_dirsplit_base
_do "$dir" "$base"
Expand Down
11 changes: 11 additions & 0 deletions minimal/do.test
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ check ".." _normpath ../ "$x"
check ".." _normpath .. "$x"


SECTION _realpath
rm -rf y
mkdir y
ln -s . y/x
check "/usr/__does_not/b" _realpath "/usr/__does_not/a/../b" "$x"
check "foo" _realpath "y/x/x/x/x/x/../foo" "$PWD"
check "$(/bin/pwd)/foo" _realpath "$PWD/y/x/x/x/x/x/../foo" "$PWD"
check "foo/blam" _realpath "y/x/x/x/x/x/../foo/spam/../blam" "$PWD"
check "$(/bin/pwd)/foo/blam" _realpath "$PWD/y/x/x/../foo/spam/../blam" "$PWD"


SECTION _find_dofile
check "test.do" _find_dofiles test
check "test.do" _find_dofile test
Expand Down
22 changes: 19 additions & 3 deletions redo/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,28 @@ def check_sane():
return not _insane


def _realdirpath(t):
"""Like realpath(), but don't follow symlinks for the last element.
redo needs this because targets can be symlinks themselves, and we want
to talk about the symlink, not what it points at. However, all the path
elements along the way could result in pathname aliases for a *particular*
target, so we want to resolve it to one unique name.
"""
dname, fname = os.path.split(t)
if dname:
dname = os.path.realpath(dname)
return os.path.join(dname, fname)


_cwd = None
def relpath(t, base):
"""Given a relative or absolute path t, express it relative to base."""
global _cwd
if not _cwd:
_cwd = os.getcwd()
t = os.path.normpath(os.path.join(_cwd, t))
base = os.path.normpath(base)
t = os.path.normpath(_realdirpath(os.path.join(_cwd, t)))
base = os.path.normpath(_realdirpath(base))
tparts = t.split('/')
bparts = base.split('/')
for tp, bp in zip(tparts, bparts):
Expand All @@ -172,7 +186,9 @@ def relpath(t, base):
return join('/', tparts)


# Return a path for t, if cwd were the dirname of env.v.TARGET.
# Return a relative path for t that will work after we do
# chdir(dirname(env.v.TARGET)).
#
# This is tricky! STARTDIR+PWD is the directory for the *dofile*, when
# the dofile was started. However, inside the dofile, someone may have done
# a chdir to anywhere else. env.v.TARGET is relative to the dofile path, so
Expand Down
2 changes: 2 additions & 0 deletions t/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
/shellfail
/shelltest.warned
/shelltest.failed
/shlink
/stress.log
/symlink path
/flush-cache
5 changes: 5 additions & 0 deletions t/105-sympath/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/*.dyn
/src
/x
/y

31 changes: 31 additions & 0 deletions t/105-sympath/all.do
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
redo-ifchange ../flush-cache
rm -f src
: >src

for iter in 10 20; do
rm -rf y
rm -f x *.dyn static
mkdir y
: >y/static
ln -s . y/x
../flush-cache

(
cd y/x/x/x/x/x
IFS=$(printf '\n')
redo-ifchange static x/x/x/static $PWD/static \
$(/bin/pwd)/static /etc/passwd
redo-ifchange $PWD/../static 2>/dev/null && exit 35
redo-ifchange 1.dyn x/x/x/2.dyn $PWD/3.dyn \
$PWD/../4.dyn $(/bin/pwd)/5.dyn
)
[ -e y/1.dyn ] || exit $((iter + 1))
[ -e y/2.dyn ] || exit $((iter + 2))
[ -e y/3.dyn ] || exit $((iter + 3))
[ -e 4.dyn ] || exit $((iter + 4))
[ -e y/5.dyn ] || exit $((iter + 5))

# Second iteration won't work in minimal/do since it only ever
# builds things once.
. ../skip-if-minimal-do.sh
done
2 changes: 2 additions & 0 deletions t/105-sympath/clean.do
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
rm -rf y
rm -f src x *.dyn *~ .*~
2 changes: 2 additions & 0 deletions t/105-sympath/default.dyn.do
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
redo-ifchange src
echo dynamic >$3
4 changes: 2 additions & 2 deletions t/clean.do
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
sed 's/\.do$//' |
xargs redo

rm -f broken shellfile shellfail shelltest.warned shelltest.failed \
*~ .*~ stress.log flush-cache
rm -f broken shellfile shellfail shelltest.warned shelltest.failed shlink \
*~ .*~ stress.log flush-cache 'symlink path'
rm -rf 'space home dir'
6 changes: 6 additions & 0 deletions t/shelltest.od
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@ set x y z
x=$(printf "a%-5sc" "b")
[ "$x" = "ab c" ] || warn 119

# Make sure cd supports -L and -P options properly
rm -f shlink
ln -s . shlink
(cd -L shlink/shlink/shlink/../shlink) || fail 120
(cd -P shlink/shlink/shlink/../shlink) && fail 121

[ -e shelltest.failed ] && exit 41
[ -e shelltest.warned ] && exit 42
exit 40

0 comments on commit 686c381

Please sign in to comment.