Merge remote-tracking branch 'toybox/master' into HEAD

Change-Id: I4eda86cd36e580d8c24e576fdf639d3b71437606
diff --git a/generated/flags.h b/generated/flags.h
index 7019fc9..b4bbe0c 100644
--- a/generated/flags.h
+++ b/generated/flags.h
@@ -2742,9 +2742,9 @@
 #undef FLAG_f
 #endif
 
-// tar &(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz] &(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz]
+// tar &(restrict)(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz] &(restrict)(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz]
 #undef OPTSTR_tar
-#define OPTSTR_tar "&(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz]"
+#define OPTSTR_tar "&(restrict)(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz]"
 #ifdef CLEANUP_tar
 #undef CLEANUP_tar
 #undef FOR_tar
@@ -2774,6 +2774,7 @@
 #undef FLAG_numeric_owner
 #undef FLAG_no_recursion
 #undef FLAG_full_time
+#undef FLAG_restrict
 #endif
 
 // taskset <1^pa <1^pa
@@ -5671,6 +5672,7 @@
 #define FLAG_numeric_owner (1<<23)
 #define FLAG_no_recursion (1<<24)
 #define FLAG_full_time (1<<25)
+#define FLAG_restrict (1<<26)
 #endif
 
 #ifdef FOR_taskset
diff --git a/generated/help.h b/generated/help.h
index f4728dc..407e590 100644
--- a/generated/help.h
+++ b/generated/help.h
@@ -340,7 +340,7 @@
 
 #define HELP_tcpsvd "usage: tcpsvd [-hEv] [-c N] [-C N[:MSG]] [-b N] [-u User] [-l Name] IP Port Prog\nusage: udpsvd [-hEv] [-c N] [-u User] [-l Name] IP Port Prog\n\nCreate TCP/UDP socket, bind to IP:PORT and listen for incoming connection.\nRun PROG for each connection.\n\nIP            IP to listen on, 0 = all\nPORT          Port to listen on\nPROG ARGS     Program to run\n-l NAME       Local hostname (else looks up local hostname in DNS)\n-u USER[:GRP] Change to user/group after bind\n-c N          Handle up to N (> 0) connections simultaneously\n-b N          (TCP Only) Allow a backlog of approximately N TCP SYNs\n-C N[:MSG]    (TCP Only) Allow only up to N (> 0) connections from the same IP\n              New connections from this IP address are closed\n              immediately. MSG is written to the peer before close\n-h            Look up peer's hostname\n-E            Don't set up environment variables\n-v            Verbose\n\n"
 
-#define HELP_tar "usage: tar [-cxtjzhmvO] [-X FILE] [-T FILE] [-f TARFILE] [-C DIR]\n\nCreate, extract, or list files in a .tar (or compressed t?z) file.\n\nOptions:\nc  Create                x  Extract               t  Test\nf  Name of TARFILE       C  Change to DIR first   v  Verbose: show filenames\no  Ignore owner          h  Follow symlinks       m  Ignore mtime\nj  bzip2 compression     z  gzip compression\nO  Extract to stdout     X  exclude names in FILE T  include names in FILE\n--exclude=FILE File pattern(s) to exclude\n\n"
+#define HELP_tar "usage: tar [-cxtfvohmjkO] [-XT FILE] [-f TARFILE] [-C DIR]\n\nCreate, extract, or list files in a .tar (or compressed t?z) file.\n\nOptions:\nc  Create                x  Extract               t  Test\nf  Name of TARFILE       C  Change to DIR first   v  Verbose: show filenames\no  Ignore owner          h  Follow symlinks       m  Ignore mtime\nj  bzip2 compression     z  gzip compression\nO  Extract to stdout     X  exclude names in FILE T  include names in FILE\n--exclude=FILE File pattern(s) to exclude\n--restrict  All archive contents must extract under a single subdirctory.\n\n"
 
 #define HELP_syslogd "usage: syslogd  [-a socket] [-O logfile] [-f config file] [-m interval]\n                [-p socket] [-s SIZE] [-b N] [-R HOST] [-l N] [-nSLKD]\n\nSystem logging utility\n\n-a      Extra unix socket for listen\n-O FILE Default log file <DEFAULT: /var/log/messages>\n-f FILE Config file <DEFAULT: /etc/syslog.conf>\n-p      Alternative unix domain socket <DEFAULT : /dev/log>\n-n      Avoid auto-backgrounding\n-S      Smaller output\n-m MARK interval <DEFAULT: 20 minutes> (RANGE: 0 to 71582787)\n-R HOST Log to IP or hostname on PORT (default PORT=514/UDP)\"\n-L      Log locally and via network (default is network only if -R)\"\n-s SIZE Max size (KB) before rotation (default:200KB, 0=off)\n-b N    rotated logs to keep (default:1, max=99, 0=purge)\n-K      Log to kernel printk buffer (use dmesg to read it)\n-l N    Log only messages more urgent than prio(default:8 max:8 min:1)\n-D      Drop duplicates\n\n"
 
diff --git a/generated/newtoys.h b/generated/newtoys.h
index e820557..0a5c77b 100644
--- a/generated/newtoys.h
+++ b/generated/newtoys.h
@@ -246,7 +246,7 @@
 USE_SYSLOGD(NEWTOY(syslogd,">0l#<1>8=8R:b#<0>99=1s#<0=200m#<0>71582787=20O:p:f:a:nSKLD", TOYFLAG_SBIN|TOYFLAG_STAYROOT))
 USE_TAC(NEWTOY(tac, NULL, TOYFLAG_USR|TOYFLAG_BIN))
 USE_TAIL(NEWTOY(tail, "?fc-n-[-cn]", TOYFLAG_USR|TOYFLAG_BIN))
-USE_TAR(NEWTOY(tar, "&(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz]", TOYFLAG_USR|TOYFLAG_BIN))
+USE_TAR(NEWTOY(tar, "&(restrict)(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz]", TOYFLAG_USR|TOYFLAG_BIN))
 USE_TASKSET(NEWTOY(taskset, "<1^pa", TOYFLAG_USR|TOYFLAG_BIN|TOYFLAG_STAYROOT))
 USE_TCPSVD(NEWTOY(tcpsvd, "^<3c#=30<1C:b#=20<0u:l:hEv", TOYFLAG_USR|TOYFLAG_BIN))
 USE_TEE(NEWTOY(tee, "ia", TOYFLAG_USR|TOYFLAG_BIN))
diff --git a/scripts/runtest.sh b/scripts/runtest.sh
index ab10bf4..8b99258 100644
--- a/scripts/runtest.sh
+++ b/scripts/runtest.sh
@@ -74,7 +74,15 @@
   else
     eval "$@"
   fi
-  [ $? -eq 0 ] || SKIPNOT=1
+  [ $? -eq 0 ] || SKIPNEXT=1
+}
+
+toyonly()
+{
+  IS_TOYBOX="$("$C" --version 2>/dev/null)"
+  [ "${IS_TOYBOX/toybox/}" == "$IS_TOYBOX" ] && SKIPNEXT=1
+
+  "$@"
 }
 
 wrong_args()
@@ -97,10 +105,10 @@
 
   [ -n "$DEBUG" ] && set -x
 
-  if [ -n "$SKIP" -o -n "$SKIP_HOST" -a -n "$TEST_HOST" -o -n "$SKIPNOT" ]
+  if [ -n "$SKIP" -o -n "$SKIP_HOST" -a -n "$TEST_HOST" -o -n "$SKIPNEXT" ]
   then
     [ ! -z "$VERBOSE" ] && echo "$SHOWSKIP: $NAME"
-    unset SKIPNOT
+    unset SKIPNEXT
     return 0
   fi
 
diff --git a/tests/files/tar/tar.tar b/tests/files/tar/tar.tar
new file mode 100644
index 0000000..ca7c53d
--- /dev/null
+++ b/tests/files/tar/tar.tar
Binary files differ
diff --git a/tests/tar.test b/tests/tar.test
index 27586b6..40cf9c3 100644
--- a/tests/tar.test
+++ b/tests/tar.test
@@ -24,34 +24,38 @@
 export BLOCKS=3
 SUM='head -c $(($BLOCKS*512)) | sha1sum | sed "s/ .*//"'
 [ -n "$TARHD" ] && SUM="tee >(hd >&2) | $SUM"
-LST='tar tv | sed "s/[ \t][ \t]*/ /g"'
+
+function LST()
+{
+  tar tv $LSTARG | sed "s/[ \t][ \t]*/ /g"
+}
 
 touch file
 testing "create file" "$TAR file | $SUM" \
   "fecaecba936e604bb115627a6ef4db7c7a3a8f81\n" "" ""
 
-testing "pass file" "$TAR file | $LST" \
+testing "pass file" "$TAR file | LST" \
   "-rw-rw-r-- root/root 0 2009-02-13 23:31 file\n" "" ""
 
 # The kernel has two hardwired meaningful UIDs: 0 (root) and 65534 (nobody).
 # (Technically changeable via /proc/sys/*/overflowuid but nobody ever does)
 skipnot id nobody >/dev/null
-testing "pass user" "tar -c --owner nobody --group root --mtime @0 file | $LST" \
+testing "pass user" "tar -c --owner nobody --group root --mtime @0 file | LST" \
   "-rw-rw-r-- nobody/root 0 1970-01-01 00:00 file\n" "" ""
 skipnot grep nobody /etc/group >/dev/null
-testing "pass group" "tar c --owner root --group nobody --mtime @0 file | $LST" \
+testing "pass group" "tar c --owner root --group nobody --mtime @0 file | LST" \
   "-rw-rw-r-- root/nobody 0 1970-01-01 00:00 file\n" "" ""
 
 touch -t 198701231234.56 file
-testing "pass mtime" \
-  "tar c --owner root --group root file | tar tv --full-time | sed 's/[ \t][ \t]*/ /g'" \
+LSTARG=--full-time testing "pass mtime" \
+  "tar c --owner root --group root file | LST" \
   "-rw-rw-r-- root/root 0 1987-01-23 12:34:56 file\n" "" ""
 
 mkdir dir
 testing "create dir" "$TAR dir | $SUM" \
   "05739c423d7d4a7f12b3dbb7c94149acb2bb4f8d\n" "" ""
 
-testing "pass dir" "$TAR dir | $LST" \
+testing "pass dir" "$TAR dir | LST" \
   "drwxrwxr-x root/root 0 2009-02-13 23:31 dir/\n" "" ""
 
 # note: does _not_ include dir entry in archive, just file
@@ -63,9 +67,13 @@
 testing "create dir and dir/file" "$TAR dir | $SUM" \
   "0bcc8005a3e07eb63c9b735267aecc5b774795d7\n" "" ""
 
-testing "pass dir/file" "$TAR dir | $LST" \
+testing "pass dir/file" "$TAR dir | LST" \
   "drwxrwxr-x root/root 0 2009-02-13 23:31 dir/\n-rw-rw-r-- root/root 0 2009-02-13 23:31 dir/file\n" "" ""
 
+echo boing > dir/that
+testing "tar C" "$TAR -C dir that | $SUM" \
+  "f0deff71bf4858eb0c5f49d99d052f12f1831feb\n" "" ""
+
 # / and .. only stripped from name, not symlink target.
 ln -s ../name.././.. dir/link
 testing "create symlink" "$TAR dir/link | $SUM" \
@@ -106,28 +114,34 @@
   "55652846506cf0a9d43b3ef03ccf9e98123befaf\n" "" ""
 
 ln -s /dev/null dir/linknull
-testing "pass absolute symlink" "$TAR dir/linknull | $LST" \
+testing "pass absolute symlink" "$TAR dir/linknull | LST" \
   "lrwxrwxrwx root/root 0 2009-02-13 23:31 dir/linknull -> /dev/null\n" "" ""
 
 ln -s rel/broken dir/relbrok
-testing "pass broken symlink" "$TAR dir/relbrok | $LST" \
+testing "pass broken symlink" "$TAR dir/relbrok | LST" \
   "lrwxrwxrwx root/root 0 2009-02-13 23:31 dir/relbrok -> rel/broken\n" "" ""
 
 ln -s /does/not/exist dir/linkabsbrok
-testing "pass broken absolute symlink" "$TAR dir/linkabsbrok | $LST" \
+testing "pass broken absolute symlink" "$TAR dir/linkabsbrok | LST" \
   "lrwxrwxrwx root/root 0 2009-02-13 23:31 dir/linkabsbrok -> /does/not/exist\n" \
   "" ""
 
 # this expects devtmpfs values
 
 testing "pass /dev/null" \
-  "tar c --mtime @0 /dev/null 2>/dev/null | $LST" \
+  "tar c --mtime @0 /dev/null 2>/dev/null | LST" \
   "crw-rw-rw- root/root 1,3 1970-01-01 00:00 dev/null\n" "" ""
 
 testing "pass /dev/loop0" \
-  "tar c --numeric-owner --mtime @0 /dev/loop0 2>/dev/null | $LST" \
+  "tar c --numeric-owner --mtime @0 /dev/loop0 2>/dev/null | LST" \
   "brw-rw---- 0/6 7,0 1970-01-01 00:00 dev/loop0\n" "" ""
 
+# compression types
+testing "autodetect gzip" \
+  'tar tvf $FILES/tar/tar.tgz | sed "s/[ \t][ \t]*/ /g"' \
+  "drwxr-x--- enh/eng 0 2017-05-13 01:05 dir/\n-rw-r----- enh/eng 12 2017-05-13 01:05 dir/file\n" \
+  "" ""
+
 skipnot mknod dir/char c 12 34
 testing "create char2" "$TAR /dev/null | $SUM" \
   "" "" ""
@@ -142,6 +156,42 @@
 testing "ownership" "$TAR dir/block | $SUM" \
   "blat" "" ""
 
+mkdir -p dd/sub/blah &&
+tar cf test.tar dd/sub/blah &&
+rm -rf dd/sub &&
+ln -s ../.. dd/sub || SKIPNEXT=1
+toyonly testing "symlink out of cwd" \
+  "tar xf test.tar 2> /dev/null || echo yes ; [ ! -e dd/sub/blah ] && echo yes" \
+  "yes\nyes\n" "" ""
+
+# If not root can't preserve ownership, so don't try yet.
+
+testing "extract dir/file from tar" \
+  "tar xvCf dd $FILES/tar/tar.tar && stat -c '%A %Y %n' dd/dir dd/dir/file" \
+  "dir/\ndir/file\ndrwxr-x--- 1494637555 dd/dir\n-rw-r----- 1494637555 dd/dir/file\n" \
+  "" ""
+
+testing "extract dir/file from tgz (autodetect)" \
+  "tar xvCf dd $FILES/tar/tar.tgz && stat -c '%A %Y %n' dd/dir dd/dir/file" \
+  "dir/\ndir/file\ndrwxr-x--- 1494637555 dd/dir\n-rw-r----- 1494637555 dd/dir/file\n" \
+  "" ""
+
+toyonly testing "cat tgz | extract dir/file (autodetect)" \
+  "cat $FILES/tar/tar.tgz | tar xvC dd && stat -c '%A %Y %n' dd/dir dd/dir/file" \
+  "dir/\ndir/file\ndrwxr-x--- 1494637555 dd/dir\n-rw-r----- 1494637555 dd/dir/file\n" \
+  "" ""
+
+testing "extract dir/file from tbz2 (autodetect)" \
+  "tar xvCf dd $FILES/tar/tar.tbz2 && stat -c '%A %Y %n' dd/dir dd/dir/file" \
+  "dir/\ndir/file\ndrwxr-x--- 1494637555 dd/dir\n-rw-r----- 1494637555 dd/dir/file\n" \
+  "" ""
+
+toyonly testing "cat tbz | extract dir/file (autodetect)" \
+  "cat $FILES/tar/tar.tbz2 | tar xvC dd && stat -c '%A %Y %n' dd/dir dd/dir/file" \
+  "dir/\ndir/file\ndrwxr-x--- 1494637555 dd/dir\n-rw-r----- 1494637555 dd/dir/file\n" \
+  "" ""
+
 TZ="$OLDTZ"
 umask $OLDUMASK
-unset LONG TAR SUM OLDUMASK OLDTZ LST
+unset LONG TAR SUM OLDUMASK OLDTZ
+unset -f LST
diff --git a/toys/other/stat.c b/toys/other/stat.c
index d828534..3f1d176 100644
--- a/toys/other/stat.c
+++ b/toys/other/stat.c
@@ -108,10 +108,10 @@
     }
     llist_traverse(mt, free);
   } else if (type == 'N') {
-    xprintf("%s", TT.file);
+    printf("%s", TT.file);
     if (S_ISLNK(stat->st_mode))
       if (readlink0(TT.file, toybuf, sizeof(toybuf)))
-        xprintf(" -> `%s'", toybuf);
+        printf(" -> `%s'", toybuf);
   } else if (type == 'o') out('u', stat->st_blksize);
   else if (type == 's') out('u', stat->st_size);
   else if (type == 't') out('x', dev_major(stat->st_rdev));
@@ -124,7 +124,7 @@
   else if (type == 'Y') out('u', stat->st_mtime);
   else if (type == 'z') date_stat_format(&stat->st_ctim);
   else if (type == 'Z') out('u', stat->st_ctime);
-  else xprintf("?");
+  else putchar('?');
 }
 
 static void print_statfs(char type) {
@@ -166,13 +166,13 @@
 
 void stat_main(void)
 {
-  int flagf = toys.optflags & FLAG_f, i;
+  int flagf = FLAG(f), i;
   char *format, *f;
 
-  if (toys.optflags&FLAG_t) {
-    format = flagf ? "%n %i %l %t %s %S %b %f %a %c %d" :
-                     "%n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %o";
-  } else format = flagf
+  if (FLAG(t)) format = flagf
+    ? "%n %i %l %t %s %S %b %f %a %c %d"
+    : "%n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %o";
+  else format = flagf
     ? "  File: \"%n\"\n    ID: %i Namelen: %l    Type: %T\n"
       "Block Size: %s    Fundamental block size: %S\n"
       "Blocks: Total: %b\tFree: %f\tAvailable: %a\n"
@@ -182,24 +182,27 @@
       "Access: (0%a/%A)\tUid: (%5u/%8U)\tGid: (%5g/%8G)\n"
       "Access: %x\nModify: %y\nChange: %z";
 
-  if (toys.optflags & FLAG_c) format = TT.c;
+  if (FLAG(c)) format = TT.c;
 
+  // loop through files listed on command line
   for (i = 0; toys.optargs[i]; i++) {
-    int L = toys.optflags & FLAG_L;
 
+    // stat the file or filesystem
     TT.file = toys.optargs[i];
     if (flagf && !statfs(TT.file, (void *)&TT.stat));
-    else if (flagf || (L ? stat : lstat)(TT.file, (void *)&TT.stat)) {
+    else if (flagf || (FLAG(L) ? stat : lstat)(TT.file, (void *)&TT.stat)) {
       perror_msg("'%s'", TT.file);
       continue;
     }
 
+    // parse format and print what it says
     for (f = format; *f; f++) {
-      if (*f != '%') putchar(*f);
+      if (*f != '%' || !f[1]) putchar(*f);
+      else if (f[1]=='%') putchar(*f++);
       else {
         f = next_printf(f, &TT.pattern);
         TT.patlen = f-TT.pattern;
-        if (TT.patlen>99) error_exit("bad %s", TT.pattern);
+        if (!*f || TT.patlen>99) error_exit("bad %s", TT.pattern);
         if (*f == 'n') strout(TT.file);
         else if (flagf) print_statfs(*f);
         else print_stat(*f);
diff --git a/toys/pending/tar.c b/toys/pending/tar.c
index b32a509..1f71590 100644
--- a/toys/pending/tar.c
+++ b/toys/pending/tar.c
@@ -18,13 +18,13 @@
  * Extract into dir same as filename, --restrict? "Tarball is splodey"
  *
 
-USE_TAR(NEWTOY(tar, "&(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz]", TOYFLAG_USR|TOYFLAG_BIN))
+USE_TAR(NEWTOY(tar, "&(restrict)(full-time)(no-recursion)(numeric-owner)(no-same-permissions)(overwrite)(exclude)*(mtime):(group):(owner):(to-command):o(no-same-owner)p(same-permissions)k(keep-old)c(create)|h(dereference)x(extract)|t(list)|v(verbose)j(bzip2)z(gzip)O(to-stdout)m(touch)X(exclude-from)*T(files-from)*C(directory):f(file):[!txc][!jz]", TOYFLAG_USR|TOYFLAG_BIN))
 
 config TAR
   bool "tar"
   default n
   help
-    usage: tar [-cxtjzhmvO] [-X FILE] [-T FILE] [-f TARFILE] [-C DIR]
+    usage: tar [-cxtfvohmjkO] [-XT FILE] [-f TARFILE] [-C DIR]
 
     Create, extract, or list files in a .tar (or compressed t?z) file. 
 
@@ -35,6 +35,7 @@
     j  bzip2 compression     z  gzip compression
     O  Extract to stdout     X  exclude names in FILE T  include names in FILE
     --exclude=FILE File pattern(s) to exclude
+    --restrict  All archive contents must extract under a single subdirctory.
 */
 
 #define FOR_tar
@@ -150,6 +151,7 @@
   struct double_list *end = lst;
 
   if (lst)
+    // constant is FNM_LEADING_DIR
     do if (!fnmatch(lst->data, name, 1<<3)) return lst;
     while (end != (lst = lst->next));
 
@@ -193,9 +195,11 @@
   name = dirtree_path(node, &i);
 
   // exclusion defaults to --no-anchored and --wildcards-match-slash
-  for (p = name; *p; p++)
-    if ((p == name || p[-1] == '/') && *p != '/' && filter(TT.excl, p))
-      goto done;
+  for (p = name; *p;) {
+    if (filter(TT.excl, p)) goto done;
+    while (*p && *p!='/') p++;
+    while (*p=='/') p++;
+  }
 
   // Consume the 1 extra byte alocated in dirtree_path()
   if (S_ISDIR(st->st_mode) && name[i-1] != '/') strcat(name, "/");
@@ -328,7 +332,7 @@
     setenv("TAR_FILETYPE", "f", 1);
     sprintf(buf, "%0o", TT.hdr.mode);
     setenv("TAR_MODE", buf, 1);
-    sprintf(buf, "%ld", (long)TT.hdr.size);
+    sprintf(buf, "%lld", (long long)TT.hdr.size);
     setenv("TAR_SIZE", buf, 1);
     setenv("TAR_FILENAME", TT.hdr.name, 1);
     setenv("TAR_UNAME", TT.hdr.uname, 1);
@@ -354,6 +358,14 @@
   }
 }
 
+static void wsettime(char *s, long long sec)
+{
+  struct timespec times[2] = {{sec, 0},{sec, 0}};
+
+  if (utimensat(AT_FDCWD, s, times, AT_SYMLINK_NOFOLLOW))
+    perror_msg("settime %lld %s", sec, s);
+}
+
 // Do pending directory utimes(), NULL to flush all.
 static int dirflush(char *name)
 {
@@ -368,20 +380,22 @@
 
       return 1;
     }
+
+    if (FLAG(restrict)) {
+      free(TT.cwd);
+      TT.cwd = strdup(s);
+      toys.optflags ^= FLAG_restrict;
+    }
   }
 
   // Set deferred utimes() for directories this file isn't under.
   // (Files must be depth-first ordered in tarball for this to matter.)
   while (TT.dirs) {
-    long long ll = *(long long *)TT.dirs->str;
-    struct timeval times[2] = {{ll, 0},{ll, 0}};
 
     // If next file is under (or equal to) this dir, keep waiting
     if (name && strstart(&ss, ss = s) && (!*ss || *ss=='/')) break;
 
-    if (utimes(TT.dirs->str+sizeof(long long), times))
-      perror_msg("utimes %lld %s", ll,
-        TT.dirs->str+sizeof(long long));
+    wsettime(TT.dirs->str+sizeof(long long), *(long long *)TT.dirs->str);
     free(llist_pop(&TT.dirs));
   }
   free(s);
@@ -453,7 +467,7 @@
   }
 
   // || !FLAG(no_same_permissions))
-  if (FLAG(p) && !S_ISLNK(ala)) chmod(TT.hdr.name, ala);
+  if (!S_ISLNK(ala)) chmod(TT.hdr.name, FLAG(p) ? ala : ala&0777);
 
   // Apply mtime.
   if (!FLAG(m)) {
@@ -468,10 +482,7 @@
       strcpy(sl->str+sizeof(long long), name);
       sl->next = TT.dirs;
       TT.dirs = sl;
-    } else {
-      struct timeval times[2] = {{TT.hdr.mtime, 0},{TT.hdr.mtime, 0}};
-      utimes(TT.hdr.name, times);
-    }
+    } else wsettime(TT.hdr.name, TT.hdr.mtime);
   }
 }
 
@@ -493,14 +504,11 @@
       i = readall(TT.fd, &tar, 512);
     }
 
-    if (i && i != 512) error_exit("read error");
+    if (i && i!=512) error_exit("short header");
 
     // Two consecutive empty headers ends tar even if there's more data
     if (!i || !*tar.name) {
-      if (!i || and++) {
-        dirflush(0);
-        return;
-      }
+      if (!i || and++) return;
       TT.hdr.size = 0;
       continue;
     }
@@ -591,6 +599,7 @@
 
     // Files are seen even if excluded, so check them here.
     // TT.seen points to first seen entry in TT.incl, or NULL if none yet.
+
     if ((delete = filter(TT.incl, TT.hdr.name)) && TT.incl != TT.seen) {
       if (!TT.seen) TT.seen = delete;
 
@@ -630,7 +639,7 @@
       skippy(TT.hdr.size);
     } else {
       if (FLAG(v)) printf("%s\n", TT.hdr.name);
-      if (FLAG(O)) xsendfile_len(TT.fd, 0, TT.hdr.size);
+      if (FLAG(O)) xsendfile_len(TT.fd, 1, TT.hdr.size);
       else if (FLAG(to_command)) extract_to_command();
       else extract_to_disk();
     }
@@ -643,18 +652,24 @@
   }
 }
 
-// Add copy of filename to TT.incl or TT.excl, minus trailing \n and /
-static void trim_list(char **pline, long len)
+// Add copy of filename (minus trailing \n and /) to dlist **
+static void trim2list(void *list, char *pline)
 {
-  char *n = strdup(*pline);
+  char *n = xstrdup(pline);
   int i = strlen(n);
 
-  dlist_add(TT.X ? &TT.excl : &TT.incl, n);
+  dlist_add(list, n);
   if (i && n[i-1]=='\n') i--;
   while (i && n[i-1] == '/') i--;
   n[i] = 0;
 }
 
+// do_lines callback, selects TT.incl or TT.excl based on call order
+static void do_XT(char **pline, long len)
+{
+  if (pline) trim2list(TT.X ? &TT.excl : &TT.incl, *pline);
+}
+
 void tar_main(void)
 {
   char *s, **args = toys.optargs;
@@ -669,10 +684,12 @@
   if (TT.group) TT.ggid = xgetgid(TT.group);
   if (TT.mtime) xparsedate(TT.mtime, &TT.mtt, (void *)&s, 1); 
 
-  // Collect file list. Note: trim_list appends to TT.incl when !TT.X
-  for (;TT.X; TT.X = TT.X->next) do_lines(xopenro(TT.X->arg), '\n', trim_list);
-  for (args = toys.optargs; *args; args++) trim_list(args, strlen(*args));
-  for (;TT.T; TT.T = TT.T->next) do_lines(xopenro(TT.T->arg), '\n', trim_list);
+  // Collect file list.
+  for (; TT.exclude; TT.exclude = TT.exclude->next)
+    trim2list(&TT.excl, TT.exclude->arg);
+  for (;TT.X; TT.X = TT.X->next) do_lines(xopenro(TT.X->arg), '\n', do_XT);
+  for (args = toys.optargs; *args; args++) trim2list(&TT.incl, *args);
+  for (;TT.T; TT.T = TT.T->next) do_lines(xopenro(TT.T->arg), '\n', do_XT);
 
   // If include file list empty, don't create empty archive
   if (FLAG(c)) {
@@ -704,11 +721,11 @@
 
   // Are we reading?
   if (FLAG(x)||FLAG(t)) {
-    struct tar_hdr *hdr = (void *)(toybuf+sizeof(toybuf)-512);
+    struct tar_hdr *hdr = 0;
 
     // autodetect compression type when not specified
     if (!FLAG(j)&&!FLAG(z)) {
-      len = xread(TT.fd, hdr, 512);
+      len = xread(TT.fd, hdr = (void *)(toybuf+sizeof(toybuf)-512), 512);
       if (len!=512 || strncmp("ustar", hdr->magic, 5)) {
         // detect gzip and bzip signatures
         if (SWAP_BE16(*(short *)hdr)==0x1f8b) toys.optflags |= FLAG_z;
@@ -721,35 +738,48 @@
     }
 
     if (FLAG(j)||FLAG(z)) {
-      int pipefd[2] = {hdr ? -1 : TT.fd, -1}, i;
+      int pipefd[2] = {hdr ? -1 : TT.fd, -1}, i, pid;
 
       xpopen_both((char *[]){FLAG(z)?"gunzip":"bunzip2", "-cf", "-", NULL},
         pipefd);
-      close(TT.fd);
-      TT.fd = pipefd[1];
 
-      // If we autodetected type but then couldn't lseek to put the data back
-      if (hdr) {
-        // dirty trick: move pipefd[0] to 0 so child closes spare copy
+      if (!hdr) {
+        // If we could seek, child gzip inherited fd and we read its output
+        close(TT.fd);
+        TT.fd = pipefd[1];
+
+      } else {
+
+        // If we autodetected type but then couldn't lseek to put the data back
+        // we have to loop reading data from TT.fd and pass it to gzip ourselves
+        // (starting with the block of data we read to autodetect).
+
+        // dirty trick: move gzip input pipe to stdin so child closes spare copy
         dup2(pipefd[0], 0);
         if (pipefd[0]) close(pipefd[0]);
 
-        // Fork a copy of ourselves to handle extraction (reads from zip proc)
-        pipefd[0] = TT.fd;
+        // Fork a copy of ourselves to handle extraction (reads from zip output
+        // pipe, writes to stdout).
+        pipefd[0] = pipefd[1];
         pipefd[1] = 1;
-        xpopen_both(0, pipefd);
-        close(TT.fd);
+        pid = xpopen_both(0, pipefd);
+        close(pipefd[1]);
 
         // loop writing collated data to zip proc
         xwrite(0, hdr, len);
         for (;;) {
-          if ((i = read(0, toybuf, sizeof(toybuf)))<1) return;
+          if ((i = read(TT.fd, toybuf, sizeof(toybuf)))<1) {
+            close(0);
+            xwaitpid(pid);
+            return;
+          }
           xwrite(0, toybuf, i);
         }
-      } else hdr = 0;
+      }
     }
 
     unpack_tar(hdr);
+    dirflush(0);
     if (TT.seen != TT.incl) {
       if (!TT.seen) TT.seen = TT.incl;
       while (TT.incl != TT.seen) {
@@ -787,8 +817,11 @@
   }
 
   if (CFG_TOYBOX_FREE) {
+    llist_traverse(TT.excl, llist_free_double);
+    llist_traverse(TT.incl, llist_free_double);
     while(TT.hlc) free(TT.hlx[--TT.hlc].arg);
     free(TT.hlx);
+    free(TT.cwd);
     close(TT.fd);
   }
 }
diff --git a/toys/pending/vi.c b/toys/pending/vi.c
index c5c8750..31516b5 100644
--- a/toys/pending/vi.c
+++ b/toys/pending/vi.c
@@ -63,12 +63,13 @@
 static int utf8_len(char *str);
 static int utf8_width(char *str, int bytes);
 static int draw_rune(char *c, int x, int y, int highlight);
+static char* utf8_last(char* str, int size);
 
 
-static void cur_left();
-static void cur_right();
-static void cur_up();
-static void cur_down();
+static int cur_left(int count);
+static int cur_right(int count);
+static int cur_up(int count);
+static int cur_down(int count);
 static void check_cursor_bounds();
 static void adjust_screen_buffer();
 
@@ -252,29 +253,57 @@
   return 1;
 }
 
-//does not work with utf8 yet
 int vi_x(int count)
 {
   char *s;
+  char *last;
   int *l;
-  int *p;
+  int length = 0;
+  int width = 0;
+  int remaining = 0;
+  char *end;
+  char *start;
   if (!c_r)
     return 0;
   s = c_r->line->str_data;
   l = &c_r->line->str_len;
-  p = &TT.cur_col;
-  if (!(*l)) return 0;
-  if ((*p) == (*l)-1) {
-    s[*p] = 0;
-    if (*p) (*p)--;
-    (*l)--;
-  } else {
-    memmove(s+(*p), s+(*p)+1, (*l)-(*p));
-    s[*l] = 0;
-    (*l)--;
+
+  last = utf8_last(s,*l);
+  if (last == s+TT.cur_col) {
+    memset(last, 0, (*l)-TT.cur_col);
+    *l = TT.cur_col;
+    if (!TT.cur_col) return 1;
+    last = utf8_last(s, TT.cur_col);
+    TT.cur_col = last-s;
+    return 1;
   }
-  count--;
-  return (count) ? vi_x(count) : 1;
+
+  start = s+TT.cur_col;
+  end = start;
+  remaining = (*l)-TT.cur_col;
+  for (;remaining;) {
+    int next = utf8_lnw(&width, end, remaining);
+    if (next && width) {
+      if (!count) break;
+      count--;
+    } if (!next) break;
+    length += next;
+    end += next;
+    remaining -= next;
+  }
+  if (remaining) {
+    memmove(start, end, remaining);
+    memset(end+remaining,0,end-start);
+  } else {
+    memset(start,0,(*l)-TT.cur_col);
+  }
+  *l -= end-start;
+  if (!TT.cur_col) return 1;
+  if (TT.cur_col == (*l)) {
+    last = utf8_last(s, TT.cur_col);
+    TT.cur_col = last-s;
+  }
+  return 1;
 }
 
 //move commands does not behave correct way yet.
@@ -402,7 +431,7 @@
   int (*vi_cmd_ptr)(int);
 };
 
-struct vi_cmd_param vi_cmds[7] =
+struct vi_cmd_param vi_cmds[11] =
 {
   {"dd", &ex_dd},
   {"dw", &ex_dw},
@@ -411,6 +440,10 @@
   {"b", &vi_movb},
   {"e", &vi_move},
   {"x", &vi_x},
+  {"h", &cur_left},
+  {"j", &cur_down},
+  {"k", &cur_up},
+  {"l", &cur_right},
 };
 
 int run_vi_cmd(char *cmd)
@@ -426,7 +459,7 @@
   else {
     cmd = cmd_e;
   }
-  for (; i<7; i++) {
+  for (; i < 11; i++) {
     if (strstr(cmd, vi_cmds[i].cmd)) {
       return vi_cmds[i].vi_cmd_ptr(val);
     }
@@ -440,7 +473,7 @@
   struct linelist *lst = c_r;
   char *c = strstr(&c_r->line->str_data[TT.cur_col], s);
   if (c) {
-    TT.cur_col = c_r->line->str_data-c;
+    TT.cur_col = c_r->line->str_data-c; //TODO ??
   TT.cur_col = c-c_r->line->str_data;
   }
   else for (; !c;) {
@@ -523,18 +556,6 @@
     }
     if (TT.vi_mode == 1) { //NORMAL
       switch (key) {
-        case 'h':
-          cur_left();
-          break;
-        case 'j':
-          cur_down();
-          break;
-        case 'k':
-          cur_up();
-          break;
-        case 'l':
-          cur_right();
-          break;
         case '/':
         case '?':
         case ':':
@@ -554,7 +575,7 @@
           break;
         default:
           if (key > 0x20 && key < 0x7B) {
-            vi_buf[vi_buf_pos] = key;
+            vi_buf[vi_buf_pos] = key;//TODO handle input better
             vi_buf_pos++;
             if (run_vi_cmd(vi_buf)) {
               memset(vi_buf, 0, 16);
@@ -562,6 +583,7 @@
             }
             else if (vi_buf_pos == 16) {
               vi_buf_pos = 0;
+              memset(vi_buf, 0, 16);
             }
 
           }
@@ -834,12 +856,10 @@
 
 static void check_cursor_bounds()
 {
-  if (c_r->line->str_len-1 < TT.cur_col) {
-    if (c_r->line->str_len == 0)
-      TT.cur_col = 0;
-    else
-      TT.cur_col = c_r->line->str_len-1;
-  }
+  if (c_r->line->str_len == 0) TT.cur_col = 0;
+  else if (c_r->line->str_len-1 < TT.cur_col) TT.cur_col = c_r->line->str_len-1;
+  if(utf8_width(&c_r->line->str_data[TT.cur_col], c_r->line->str_len-TT.cur_col) <= 0)
+    cur_left(1);
 }
 
 static void adjust_screen_buffer()
@@ -963,6 +983,20 @@
   return 0;
 }
 
+static char* utf8_last(char* str, int size)
+{
+  char* end = str+size;
+  int pos = size;
+  int len = 0;
+  int width = 0;
+  while (pos >= 0) {
+    len = utf8_lnw(&width, end, size-pos);
+    if (len && width) return end;
+    end--; pos--;
+  }
+  return 0;
+}
+
 static int draw_str_until(int *drawn, char *str, int width, int bytes)
 {
   int rune_width = 0;
@@ -991,38 +1025,50 @@
   return max_width-width;
 }
 
-static void cur_left()
+static int cur_left(int count)
 {
-  if (!TT.cur_col) return;
-  TT.cur_col--;
+  for (;count--;) {
+    if (!TT.cur_col) return 1;
 
-  if (!utf8_len(&c_r->line->str_data[TT.cur_col])) cur_left();
+    TT.cur_col--;
+    check_cursor_bounds();//has bit ugly recursion hidden here
+  }
+  return 1;
 }
 
-static void cur_right()
+static int cur_right(int count)
 {
-  if (c_r->line->str_len <= 1) return;
-  if (TT.cur_col == c_r->line->str_len-1) return;
-  TT.cur_col++;
-  if (!utf8_len(&c_r->line->str_data[TT.cur_col])) cur_right();
+  for (;count--;) {
+    if (c_r->line->str_len <= 1) return 1;
+    if (TT.cur_col >= c_r->line->str_len-1) {
+      TT.cur_col = utf8_last(c_r->line->str_data, c_r->line->str_len)
+        - c_r->line->str_data;
+      return 1;
+    }
+    TT.cur_col++;
+    if (utf8_width(&c_r->line->str_data[TT.cur_col],
+          c_r->line->str_len-TT.cur_col) <= 0)
+      cur_right(1);
+  }
+  return 1;
 }
 
-static void cur_up()
+static int cur_up(int count)
 {
-  if (c_r->up != 0)
+  for (;count-- && c_r->up;)
     c_r = c_r->up;
 
-  if (!utf8_len(&c_r->line->str_data[TT.cur_col])) cur_left();
   check_cursor_bounds();
   adjust_screen_buffer();
+  return 1;
 }
 
-static void cur_down()
+static int cur_down(int count)
 {
-  if (c_r->down != 0)
+  for (;count-- && c_r->down;)
     c_r = c_r->down;
 
-  if (!utf8_len(&c_r->line->str_data[TT.cur_col])) cur_left();
   check_cursor_bounds();
   adjust_screen_buffer();
+  return 1;
 }