chiark / gitweb /
More progress. More work.
authorMark Wooding <mdw@distorted.org.uk>
Thu, 14 Jun 2018 10:26:40 +0000 (11:26 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Thu, 14 Jun 2018 12:33:58 +0000 (13:33 +0100)
Makefile
keys.scala
sys.scala
tar.scala
terminal.scala
util.scala

index c4f471a..6e5ca0e 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -30,8 +30,10 @@ VERSION                      := $(shell ./auto-version)
 ###--------------------------------------------------------------------------
 ### Build parameters.
 
+abs_builddir           := $(shell pwd)
+
 ## Where to put the object files.
-OUTDIR                  = out/
+OUTDIR                  = out
 
 ## Native C compiler.
 CC                      = gcc
@@ -134,14 +136,14 @@ clean::
 ### Native C code.
 
 out/%.o: %.c
-       $(call v_tag,CC)mkdir -p $(OUTDIR) && \
+       $(call v_tag,CC)mkdir -p $(OUTDIR)/ && \
                $(CC) -c $(ALL_CFLAGS) -MMD -o$@ $<
 
 ALL_SOURCES             =
 DISTFILES              += $(ALL_SOURCES)
 
-objects                         = $(patsubst %.c,$(OUTDIR)%$2,$1)
-CLEANFILES             += $(OUTDIR)*.o $(OUTDIR)*.d
+objects                         = $(patsubst %.c,$(OUTDIR)/%$2,$1)
+CLEANFILES             += $(OUTDIR)/*.o $(OUTDIR)/*.d
 
 ###--------------------------------------------------------------------------
 ### Java classes.
@@ -150,25 +152,25 @@ CLEANFILES                += $(OUTDIR)*.o $(OUTDIR)*.d
 ## unpredictable names.  Rather than try to guess stable outputs for these
 ## sources, we make artificial `timestamp' files and uses these in our
 ## dependencies.
-CLASSDIR                = $(OUTDIR)cls/
-stamps                  = $(patsubst %,$(OUTDIR)%.stamp,$1)
+CLASSDIR                = $(OUTDIR)/cls
+stamps                  = $(patsubst %,$(OUTDIR)/%.$1-stamp,$2)
 
 clean::; rm -rf $(CLASSDIR)
-CLEANFILES             += $(OUTDIR)*.stamp
+CLEANFILES             += $(OUTDIR)/*.class-stamp
 
 ## Compiling actual Java code.  Note that this confuses the resident Scala
 ## compiler, so we have to reset it here.
 CLSISH                 += java
-$(OUTDIR)%.stamp: %.java
-       $(call v_tag,JAVAC)mkdir -p $(CLASSDIR) && \
+$(OUTDIR)/%.class-stamp: %.java
+       $(call v_tag,JAVAC)mkdir -p $(CLASSDIR)/ && \
                $(JAVAC) -d $(CLASSDIR) -cp $(CLASSDIR) $(JAVAFLAGS) $< && \
                echo built >$@
        $(V_AT)$(SCALAC_RESET)
 
 ## Compiling Scala code.
 CLSEXT                 += scala
-$(OUTDIR)%.stamp: %.scala
-       $(call v_tag,SCALAC)mkdir -p $(CLASSDIR) && \
+$(OUTDIR)/%.class-stamp: %.scala
+       $(call v_tag,SCALAC)mkdir -p $(CLASSDIR)/ && \
                $(SCALAC) -d $(CLASSDIR) -cp $(CLASSDIR) $(SCALAFLAGS) $< && \
                echo built >$@
 
@@ -178,12 +180,12 @@ $(OUTDIR)%.stamp: %.scala
 SHLIBS                 += toy
 toy_SOURCES             = jni.c
 
-shlibfile               = $(patsubst %,$(OUTDIR)lib%.so,$1)
+shlibfile               = $(patsubst %,$(OUTDIR)/lib%.so,$1)
 SHLIBFILES              = $(call shlibfile,$(SHLIBS))
 TARGETS                        += $(SHLIBFILES)
 ALL_SOURCES            += $(foreach l,$(SHLIBS),$($l_SOURCES))
 
-$(SHLIBFILES): $(OUTDIR)lib%.so: $$(call objects,$$($$*_SOURCES),.o)
+$(SHLIBFILES): $(OUTDIR)/lib%.so: $$(call objects,$$($$*_SOURCES),.o)
        $(call v_tag,LD)$(LD) $(LDFLAGS.so) -o$@ $^ $(LIBS)
 
 ###--------------------------------------------------------------------------
@@ -210,16 +212,59 @@ class-deps                 = $(subst $(COMMA), ,$(word 2,$(subst :, ,$1)))
 
 CLASS_NAMES             = $(foreach c,$(CLASSES),$(call class-name,$c))
 
-all:: $(call stamps,$(CLASS_NAMES))
+all:: $(call stamps,class,$(CLASS_NAMES))
 
-$(foreach c,$(CLASSES),$(eval $(call stamps,$(call class-name,$c)): \
-       $(call stamps,$(call class-deps,$c))))
+$(foreach c,$(CLASSES),$(eval $(call stamps,class,$(call class-name,$c)): \
+       $(call stamps,class,$(call class-deps,$c))))
 
 DISTFILES              += $(foreach c,$(CLASSES),\
                                $(foreach e,$(CLSEXT),\
                                  $(wildcard $(call class-name,$c).$e)))
 
 ###--------------------------------------------------------------------------
+### External packages.
+
+EXTPREFIX               = $(abs_builddir)/$(OUTDIR)/inst
+
+join-paths              = $(if $(filter /%,$2),$2,$1/$2)
+ext-srcdir              = $(or $($1_SRCDIR),../$1)
+
+EXTERNALS              += adns
+adns_CONFIG             = --disable-dynamic
+
+EXTERNALS              += mLib
+mLib_DEPS               = adns
+mLib_CONFIG             = --enable-static --disable-shared --with-adns
+
+EXTERNALS              += catacomb
+catacomb_DEPS           = mLib
+catacomb_CONFIG                 = --enable-static --disable-shared
+
+EXTERNALS              += tripe
+tripe_DEPS              = mLib catacomb
+tripe_CONFIG            = --without-wireshark --with-adns --with-tunnel=slip
+
+all:: $(call stamps,ext,$(EXTERNALS))
+CLEANFILES             += $(OUTDIR)/*.ext-stamp
+clean::; rm -rf $(OUTDIR)/inst $(OUTDIR)/build
+
+$(call stamps,ext,$(EXTERNALS)): \
+               $(OUTDIR)/%.ext-stamp: $$(call stamps,ext,$$($$*_DEPS))
+       $(V_AT)rm -rf $(OUTDIR)/build/$*/
+       $(V_AT)mkdir -p $(OUTDIR)/build/$*/
+       cd $(OUTDIR)/build/$*/ && \
+       $(call join-paths,../../..,$(call ext-srcdir,$*))/configure \
+               --prefix=$(EXTPREFIX) \
+               $($*_CONFIG) \
+               CFLAGS="-O2 -g -fPIC -Wall -I$(EXTPREFIX)/include" \
+               LDFLAGS="-L$(EXTPREFIX)/lib" \
+               PKG_CONFIG="pkg-config --static" \
+               PKG_CONFIG_LIBDIR=$(EXTPREFIX)/lib/pkgconfig
+       $(MAKE) -C$(OUTDIR)/build/$*/
+       $(MAKE) -C$(OUTDIR)/build/$*/ -s install
+       $(V_AT)echo done >$@
+
+###--------------------------------------------------------------------------
 ### Distribution arrangements.
 
 DISTFILES              += COPYING
@@ -230,17 +275,17 @@ distdir                    = $(PACKAGE)-$(VERSION)
 DISTTAR                         = $(distdir).tar.gz
 
 distdir:
-       rm -rf $(OUTDIR)$(distdir)
-       mkdir $(OUTDIR)$(distdir)
-       echo $(VERSION) >$(OUTDIR)$(distdir)/RELEASE
+       rm -rf $(OUTDIR)/$(distdir)
+       mkdir $(OUTDIR)/$(distdir)/
+       echo $(VERSION) >$(OUTDIR)/$(distdir)/RELEASE
        set -e; for i in $(DISTFILES); do \
-         case $$i in */*) mkdir -p $(OUTDIR)$(distdir)/$${i%/*} ;; esac; \
-         cp $$i $(OUTDIR)$(distdir)/; \
+         case $$i in */*) mkdir -p $(OUTDIR)/$(distdir)/$${i%/*}/ ;; esac; \
+         cp $$i $(OUTDIR)/$(distdir)/; \
        done
 .PHONY: distdir
 
 dist: distdir
-       set -e; cd $(OUTDIR); tar chozf ../$(DISTTAR) $(distdir)
+       set -e; cd $(OUTDIR)/; tar chozf ../$(DISTTAR) $(distdir)
        rm -rf $(distdir)
 .PHONY: dist
 
index cec56a9..b9595ec 100644 (file)
@@ -29,13 +29,17 @@ package uk.org.distorted.tripe; package object keys {
 
 import scala.collection.mutable.HashMap;
 
-import java.io.{Closeable, File};
+import java.io.{Closeable, File, IOException};
+import java.lang.{Long => JLong};
 import java.net.{URL, URLConnection};
+import java.text.SimpleDateFormat;
+import java.util.Date;
 import java.util.zip.GZIPInputStream;
 
 import sys.{SystemError, hashsz, runCommand};
 import sys.Errno.EEXIST;
 import sys.FileImplicits._;
+import sys.FileInfo.{DIR, REG};
 
 import progress.{Eyecandy, SimpleModel, DataModel};
 
@@ -49,6 +53,14 @@ private final val RX_KEYVAL = """(?x) ^ \s*
       \s* $""".r;
 private final val RX_DOLLARSUBST = """(?x) \$ \{ ([-\w]+) \}""".r;
 
+private final val RX_PUBKEY = """(?x) ^ peer- (.*) \.pub $""".r;
+
+private final val RX_KEYINFO = """(?x) ^ ([^:]*) : \s* (\S.*) $""".r
+private final val RX_KEYATTR = """(?x) ^ \s*
+       ([^\s=] | [^\s=][^=]*[^\s=])
+       \s* = \s*
+       (\S.*) $""".r;
+
 /*----- Things that go wrong ----------------------------------------------*/
 
 class ConfigSyntaxError(val file: String, val lno: Int, val msg: String)
@@ -108,7 +120,7 @@ private val DEFAULTS: Seq[(String, Config => String)] =
       "sig-fresh" -> { _ => "always" },
       "fingerprint-hash" -> { _("hash") });
 
-private def readConfig(file: File): Config = {
+private def parseConfig(file: File): Config = {
 
   /* Build the new configuration in a temporary place. */
   var m = HashMap[String, String]();
@@ -128,6 +140,13 @@ private def readConfig(file: File): Config = {
     }
   }
 
+  /* Done. */
+  m
+}
+
+private def readConfig(file: File): Config = {
+  var m = parseConfig(file);
+
   /* Fill in defaults where things have been missed out. */
   for ((key, dflt) <- DEFAULTS) {
     if (!(m contains key)) {
@@ -225,10 +244,15 @@ private def keyFingerprint(kr: File, tag: String, hash: String): String = {
   nextToken(out) match {
     case Some((fp, _)) => fp
     case _ =>
-      throw new java.io.IOException("unexpected output from `key fingerprint");
+      throw new IOException("unexpected output from `key fingerprint'");
   }
 }
 
+private def checkIdent(id: String) {
+  if (id exists { ch => ch == ':' || ch == '.' || ch.isWhitespace })
+    throw new IllegalArgumentException(s"bad key tag `$id'");
+}
+
 object Repository {
   object State extends Enumeration {
     val Empty, Pending, Confirmed, Updating, Committing, Live = Value;
@@ -250,24 +274,80 @@ def checkConfigSanity(file: File, ic: Eyecandy) {
   }
 }
 
+private val keydatefmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
+class PrivateKey private[keys](repo: Repository, dir: File) {
+  private[this] lazy val keyring = dir/"keyring";
+  private[this] lazy val meta = parseConfig(dir/"meta");
+  lazy val tag = meta("tag");
+  lazy val time = datefmt synchronized { datefmt.parse(meta("time")); };
+  lazy val fingerprint = keyFingerprint(keyring, tag,
+                                       repo.config("fingerprint-hash"));
+
+  def remove() { dir.rmTree(); }
+
+  private[this] lazy val (info, _attr) = {
+    val m = Map.newBuilder[String, String];
+    val a = Map.newBuilder[String, String];
+    val (out, _) = runCommand("key", "-k", keyring.getPath,
+                             "list", "-vv", tag);
+    val lines = out.lines;
+    while (lines.hasNext) lines.next match {
+      case "attributes:" =>
+       while (lines.hasNext) lines.next match {
+         case RX_KEYATTR(k, v) => a += k -> v;
+         case line => throw new IOException(
+           s"unexpected output from `key list': $line");
+       }
+      case RX_KEYINFO(k, v) =>
+       m += k -> v;
+      case line => throw new IOException(
+       s"unexpected output from `key list': $line");
+    }
+    (m.result, a.result)
+  }
+
+  lazy val expires = info("expiry") match {
+    case "forever" => None
+    case d => Some(keydatefmt synchronized { keydatefmt.parse(d) })
+  }
+  lazy val ty = info("type");
+  lazy val comment = info("comment");
+  lazy val keyid = {
+
+    /* Ugh.  Using `Int' throws an exception on words whose top bit is set
+     * because Java doesn't have proper unsigned integers.  There's
+     * `parseUnsignedInt' in Java 1.8, but that limits our Android targets.
+     * And Scala has put its own `Long' object in the way of Java's so we
+     * need this circumolution.
+     */
+    (JLong.parseLong(info("keyid"), 16)&0xffffffff).toInt;
+  }
+  lazy val attr = _attr;
+}
+
 class Repository(val root: File) extends Closeable {
   import Repository.State.{Value => State, _};
 
   /* Important directories and files. */
-  private[this] val livedir = root/"live";
+  private[this] val configdir = root/"config";
+  private[this] val livedir = configdir/"live";
   private[this] val livereposdir = livedir/"repos";
-  private[this] val newdir = root/"new";
-  private[this] val olddir = root/"old";
-  private[this] val pendingdir = root/"pending";
+  private[this] val newdir = configdir/"new";
+  private[this] val olddir = configdir/"old";
+  private[this] val pendingdir = configdir/"pending";
   private[this] val tmpdir = root/"tmp";
+  private[this] val keysdir = root/"keys";
 
   /* Take out a lock in case of other instances. */
+  private[this] var open = false;
   private[this] val lock = {
-    try { root.mkdir_!(); }
-    catch { case SystemError(EEXIST, _) => ok; }
+     root.mkdirNew_!();
+    open = true;
     (root/"lk").lock_!()
   }
-  def close() { lock.close(); }
+  def close() { lock.close(); open = false; }
+  private[this] def checkLocked()
+    { if (!open) throw new IllegalStateException("repository is unlocked"); }
 
   /* Maintain a cache of some repository state. */
   private var _state: State = null;
@@ -296,6 +376,7 @@ class Repository(val root: File) extends Closeable {
 
   def checkState(wanted: State*) {
     /* Ensure we're in a particular state. */
+    checkLocked();
     val st = state;
     if (wanted.forall(_ != st)) {
       throw new RepositoryStateException(st, s"Repository is $st, not " +
@@ -304,7 +385,7 @@ class Repository(val root: File) extends Closeable {
     }
   }
 
-  def cleanup() {
+  def cleanup(ic: Eyecandy) {
 
     /* If we're part-way through an update then back out or press forward. */
     state match {
@@ -315,7 +396,8 @@ class Repository(val root: File) extends Closeable {
         * either way.
         */
 
-       newdir.rmTree();
+       ic.operation("rolling back failed update")
+         { _ => newdir.rmTree(); }
        invalidate();            // should move back to `Live' or `Confirmed'
 
       case Committing =>
@@ -323,8 +405,9 @@ class Repository(val root: File) extends Closeable {
         * to have to move one of them.  Let's try committing the new tree.
         */
 
-       newdir.rename_!(livedir);       // should move on to `Live'
-       invalidate();
+       ic.operation("committing interrupted update")
+         { _ => newdir.rename_!(livedir); }
+       invalidate();                   // should move on to `Live'
 
       case _ =>
        /* Other states are stable. */
@@ -335,21 +418,26 @@ class Repository(val root: File) extends Closeable {
      * ones which don't belong.  In particular, this will always erase
      * `tmpdir'.
      */
-    val st = state;
-    root.foreachFile { f => (f.getName, st) match {
-      case ("lk", _) => ok;
-      case ("live", Live | Confirmed) => ok;
-      case ("pending", Pending) => ok;
-      case (_, Updating | Committing) =>
-       unreachable(s"unexpectedly still in `$st' state");
-      case _ => f.rmTree();
+    ic.operation("cleaning up configuration area") { or =>
+      val st = state;
+      root foreachFile { f => f.getName match {
+       case "lk" | "keys" => ok;
+       case "config" => configdir foreachFile { f => (f.getName, st) match {
+         case ("live", Live | Confirmed) => ok;
+         case ("pending", Pending) => ok;
+         case (_, Updating | Committing) =>
+           unreachable(s"unexpectedly still in `$st' state");
+         case _ => or.step(s"delete `$f'"); f.rmTree();
+       } }
+       case _ => or.step(s"delete `$f'"); f.rmTree();
+      } }
     }
-  } }
+  }
 
   def destroy(ic: Eyecandy) {
     /* Clear out the entire repository.  Everything.  It's all gone. */
     ic.operation("clearing configuration")
-      { _ => root.foreachFile { f => if (f.getName != "lk") f.rmTree(); } }
+      { _ => root foreachFile { f => if (f.getName != "lk") f.rmTree(); } }
   }
 
   def clearTmp() {
@@ -361,15 +449,15 @@ class Repository(val root: File) extends Closeable {
   def config: Config = {
     /* Return the repository configuration. */
 
+    checkLocked();
     if (_config == null) {
 
       /* Firstly, decide where to find the configuration file. */
-      cleanup();
+      checkState(Pending, Confirmed, Live);
       val dir = state match {
        case Live | Confirmed => livedir
        case Pending => pendingdir
-       case Empty =>
-         throw new RepositoryStateException(Empty, "repository is Empty");
+       case _ => ???
       }
 
       /* And then read the configuration. */
@@ -388,9 +476,11 @@ class Repository(val root: File) extends Closeable {
     val conffile = tmpdir/"tripe-keys.conf";
     downloadToFile(conffile, url, 16*1024, ic);
     checkConfigSanity(conffile, ic);
+    configdir.mkdirNew_!();
     ic.operation("committing configuration")
       { _ => tmpdir.rename_!(pendingdir); }
     invalidate();                      // should move to `Pending'
+    cleanup(ic);
   }
 
   def confirm(ic: Eyecandy) {
@@ -411,6 +501,7 @@ class Repository(val root: File) extends Closeable {
      * against the known fingerprint; and check the signature on the bundle.
      */
 
+    cleanup(ic);
     checkState(Confirmed, Live);
     val conf = config;
     clearTmp();
@@ -435,8 +526,10 @@ class Repository(val root: File) extends Closeable {
        for (e <- tar) {
 
          /* Check the filename to make sure it's not evil. */
-         if (e.name(0) == '/' || e.name.split('/').exists { _ == ".." })
-           throw new KeyConfigException("invalid path in tarball");
+         if (e.name(0) == '/' || e.name.split('/').exists { _ == ".." }) {
+           throw new KeyConfigException(
+             s"invalid path `${e.name}' in tarball");
+         }
 
          /* Report on progress. */
          or.step(s"entry `${e.name}'");
@@ -445,24 +538,26 @@ class Repository(val root: File) extends Closeable {
          val f = unpkdir/e.name;
 
          /* Unpack it. */
-         if (e.isdir) {
-           /* A directory.  Create it if it doesn't exist already. */
+         e.typ match {
+           case DIR =>
+             /* A directory.  Create it if it doesn't exist already. */
+
+             f.mkdirNew_!();
 
-           try { f.mkdir_!(); }
-           catch { case SystemError(EEXIST, _) => ok; }
-         } else if (e.isreg) {
-           /* A regular file.  Write stuff to it. */
+           case REG =>
+             /* A regular file.  Write stuff to it. */
 
-           e.withStream { in =>
-             f.withOutput { out =>
-               for ((b, n) <- blocks(in)) out.write(b, 0, n);
+             e.withStream { in =>
+               f.withOutput { out =>
+                 for ((b, n) <- blocks(in)) out.write(b, 0, n);
+               }
              }
-           }
-         } else {
-           /* Something else.  Be paranoid and reject it. */
 
-           throw new KeyConfigException(
-             s"entry `${e.name}' has unexpected object type");
+           case ty =>
+             /* Something else.  Be paranoid and reject it. */
+
+             throw new KeyConfigException(
+               s"entry `${e.name}' has unexpected object type $ty");
          }
        }
       }
@@ -500,6 +595,19 @@ class Repository(val root: File) extends Closeable {
     /* Confirm that the configuration in the new archive is sane. */
     checkConfigSanity(unpkdir/"tripe-keys.conf", ic);
 
+    /* Build the public keyring.  (Observe the quadratic performance.) */
+    ic.operation("collecting public keys") { or =>
+      val pubkeys = unpkdir/"keyring.pub";
+      pubkeys.remove_!();
+      reposdir foreachFile { file => file.getName match {
+       case RX_PUBKEY(peer) if file.isreg_! =>
+         or.step(peer);
+         runCommand("key", "-k", pubkeys.getPath, "merge", file.getPath);
+       case _ => ok;
+      } }
+      (unpkdir/"keyring.pub.old").remove_!();
+    }
+
     /* Now we just have to juggle the files about. */
     ic.operation("committing new configuration") { _ =>
       unpkdir.rename_!(newdir);
@@ -507,8 +615,56 @@ class Repository(val root: File) extends Closeable {
       newdir.rename_!(livedir);
     }
 
+    /* All done. */
     invalidate();                      // should move to `Live'
+    cleanup(ic);
   }
+
+  def generateKey(tag: String, label: String, ic: Eyecandy) {
+    checkIdent(tag);
+    if (label.exists { _ == '/' })
+      throw new IllegalArgumentException(s"invalid label string `$label'");
+    if ((keysdir/label).isdir_!)
+      throw new IllegalArgumentException(s"key `$label' already exists");
+
+    cleanup(ic);
+    checkState(Live);
+    val conf = config;
+    clearTmp();
+
+    val now = datefmt synchronized { datefmt.format(new Date) };
+    val kr = tmpdir/"keyring";
+    val pub = tmpdir/s"peer-$tag.pub";
+    val param = livereposdir/"param";
+
+    keysdir.mkdirNew_!();
+
+    ic.operation("fetching key-generation parameters") { _ =>
+      runCommand("key", "-k", kr.getPath, "merge", param.getPath);
+    }
+    ic.operation("generating new key") { _ =>
+      runCommand("key", "-k", kr.getPath, "add",
+                "-a", conf("kx-genalg"), "-p", "param",
+                "-e", conf("kx-expire"), "-t", tag, "tripe");
+    }
+    ic.operation("extracting public key") { _ =>
+      runCommand("key", "-k", kr.getPath, "extract",
+                "-f", "-secret", pub.getPath, tag);
+                                        }
+    ic.operation("writing metadata") { _ =>
+      tmpdir/"meta" withWriter { w =>
+       w.write(s"tag = $tag\n");
+       w.write(s"time = $now\n");
+      }
+    }
+    ic.operation("installing new key") { _ =>
+      tmpdir.rename_!(keysdir/label);
+    }
+  }
+
+  def key(label: String): PrivateKey = new PrivateKey(this, keysdir/label);
+  def keyLabels: Seq[String] = (keysdir.files_! map { _.getName }).toStream;
+  def keys: Seq[PrivateKey] = keyLabels map { k => key(k) };
 }
 
 /*----- That's all, folks -------------------------------------------------*/
index 51ac170..6931431 100644 (file)
--- a/sys.scala
+++ b/sys.scala
@@ -438,8 +438,11 @@ def stat(path: String): sys.FileInfo = stat(path.toCString);
 def lstat(path: String): sys.FileInfo = lstat(path.toCString);
 
 object FileInfo extends Enumeration {
-  /* A simple enumeration of things a file might be. */
-  val FIFO, CHR, DIR, BLK, REG, LNK, SOCK, UNK = Value;
+  /* A simple enumeration of things a file might be.
+   *
+   * `HDLNK' is a hard link, used in `tar' files.
+   */
+  val FIFO, CHR, DIR, BLK, REG, LNK, SOCK, HDLNK, UNK = Value;
   type Type = Value;
 }
 import FileInfo._;
@@ -637,19 +640,15 @@ object FileImplicits {
     def islnk_! : Boolean = statish(lstat _, _.ftype == LNK, false);
     def issock_! : Boolean = statish(stat _, _.ftype == SOCK, false);
 
+    /* Slightly more cooked file operations. */
     def remove_!() {
       /* Delete a file, or directory, whatever it is. */
-      while (true) {
-       try { unlink_!(); return; }
-       catch {
-         case SystemError(ENOENT, _) => return;
-         case SystemError(EISDIR, _) => ok;
-       }
-       try { rmdir_!(); return; }
-       catch {
-         case SystemError(ENOENT, _) => return;
-         case SystemError(ENOTDIR, _) => ok;
-       }
+      try { unlink_!(); return; }
+      catch {
+       case SystemError(ENOENT, _) => return;
+       case SystemError(EISDIR, _) =>
+         try { rmdir_!(); return; }
+         catch { case SystemError(ENOENT, _) => return; }
       }
     }
 
@@ -662,6 +661,12 @@ object FileImplicits {
       walk(file);
     }
 
+    def mkdirNew_!() {
+      /* Make a directory if there's nothing there already. */
+      try { mkdir_!(); }
+      catch { case SystemError(EEXIST, _) => ok; }
+    }
+
     /* File locking. */
     def lock_!(flags: Int): FileLock = new FileLock(file.getPath, flags);
     def lock_!(): FileLock = lock_!(LKF_EXCL | 0x1b6);
index 5f20b0a..30a3a4a 100644 (file)
--- a/tar.scala
+++ b/tar.scala
@@ -32,6 +32,9 @@ import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.util.Date;
 
+import sys.FileInfo;
+import sys.FileInfo.{Value, FIFO, CHR, DIR, BLK, REG, LNK, HDLNK, UNK};
+
 /*----- Main code ---------------------------------------------------------*/
 
 class TarFormatError(msg: String) extends Exception(msg);
@@ -45,7 +48,7 @@ trait TarEntry {
   /* Basic facts about the entry. */
   def name: String;
   def size: Long;
-  def typ: Char;
+  def rawtyp: Char;
   def mode: Int;
   def mtime: Date;
   def uid: Int;
@@ -53,17 +56,28 @@ trait TarEntry {
   def link: String;
 
   /* Type predicates (intentionally like `FileInfo'). */
-  def isfifo: Boolean = typ == '6';
-  def ischr: Boolean = typ == '3';
-  def isdir: Boolean = typ == '5';
-  def isblk: Boolean = typ == '4';
-  def isreg: Boolean = typ match {
+  def isfifo: Boolean = rawtyp == '6';
+  def ischr: Boolean = rawtyp == '3';
+  def isdir: Boolean = rawtyp == '5';
+  def isblk: Boolean = rawtyp == '4';
+  def isreg: Boolean = rawtyp match {
     case 0 | '0' | '7' => true
     case _ => false
   }
-  def islnk: Boolean = typ == '2';
+  def islnk: Boolean = rawtyp == '2';
   def issock: Boolean = false;
-  def ishardlink: Boolean = typ == '1';
+  def ishardlink: Boolean = rawtyp == '1';
+
+  def typ: FileInfo.Value = rawtyp match {
+    case 0 | '0' | '7' => REG
+    case '1' => HDLNK
+    case '2' => LNK
+    case '3' => CHR
+    case '4' => BLK
+    case '5' => DIR
+    case '6' => FIFO
+    case _ => UNK
+  }
 
   def verbose: String = {
     /* Encode information about this tar header as a string. */
@@ -71,7 +85,7 @@ trait TarEntry {
     val sb = new StringBuilder;
 
     /* First, the type code. */
-    sb += (typ match {
+    sb += (rawtyp match {
       case 0 | '0' | '7' => '-'
       case '1' => 'L'
       case '2' => 'l'
@@ -274,7 +288,7 @@ class TarFile(in: InputStream)
   }
 
   private[this] class Entry(val name: String, val size: Long,
-                           val typ: Char, val mode: Int,
+                           val rawtyp: Char, val mode: Int,
                            val mtime: Date,
                            val uid: Int, val gid: Int,
                            val link: String,
index 9722e2b..7ca56d3 100644 (file)
@@ -147,7 +147,7 @@ object TerminalEyecandy extends Eyecandy {
       val eta = model.eta(cur);
       if (eta >= 0) {
        sb += ' '; sb += '(';
-       sb ++= formatTime(ceil(eta/1000.0).toInt);
+       sb ++= formatDuration(ceil(eta/1000.0).toInt);
        sb += ')';
       }
 
@@ -156,7 +156,7 @@ object TerminalEyecandy extends Eyecandy {
     }
 
     def done() {
-      val t = formatTime(ceil((currentTimeMillis - t0)/1000.0).toInt);
+      val t = formatDuration(ceil((currentTimeMillis - t0)/1000.0).toInt);
       record(s"${model.what} done ($t)");
     }
 
index ea776e7..8ede691 100644 (file)
@@ -36,6 +36,7 @@ import java.nio.{ByteBuffer, CharBuffer};
 import java.nio.channels.{SelectionKey, Selector};
 import java.nio.channels.spi.{AbstractSelector, AbstractSelectableChannel};
 import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
 import java.util.{Set => JSet};
 import java.util.concurrent.locks.{Lock, ReentrantLock};
 
@@ -561,7 +562,9 @@ def oxford(conj: String, things: Seq[String]): String = things match {
     sb.result
 }
 
-def formatTime(t: Int): String =
+val datefmt = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
+
+def formatDuration(t: Int): String =
   if (t < -1) "???"
   else {
     val (s, t1) = (t%60, t/60);