From: Mark Wooding Date: Thu, 14 Jun 2018 10:26:40 +0000 (+0100) Subject: More progress. More work. X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~mdw/git/tripe-android/commitdiff_plain/a5ec891a1f3cbe4ae9aad5d2252f39b483ed543e More progress. More work. --- diff --git a/Makefile b/Makefile index c4f471a..6e5ca0e 100644 --- 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,15 +212,58 @@ 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. @@ -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 diff --git a/keys.scala b/keys.scala index cec56a9..b9595ec 100644 --- a/keys.scala +++ b/keys.scala @@ -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 -------------------------------------------------*/ diff --git a/sys.scala b/sys.scala index 51ac170..6931431 100644 --- 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); diff --git a/tar.scala b/tar.scala index 5f20b0a..30a3a4a 100644 --- 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, diff --git a/terminal.scala b/terminal.scala index 9722e2b..7ca56d3 100644 --- a/terminal.scala +++ b/terminal.scala @@ -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)"); } diff --git a/util.scala b/util.scala index ea776e7..8ede691 100644 --- a/util.scala +++ b/util.scala @@ -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);