chiark / gitweb /
wip
authorMark Wooding <mdw@distorted.org.uk>
Thu, 31 May 2018 13:11:37 +0000 (14:11 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Thu, 31 May 2018 13:11:49 +0000 (14:11 +0100)
12 files changed:
.skelrc [new file with mode: 0644]
Makefile
admin.scala
jni.c
jni.java [deleted file]
jni.scala [new file with mode: 0644]
keys.scala [new file with mode: 0644]
main.scala
peers.scala [new file with mode: 0644]
sock.scala
sys.scala [new file with mode: 0644]
util.scala [new file with mode: 0644]

diff --git a/.skelrc b/.skelrc
new file mode 100644 (file)
index 0000000..87645b6
--- /dev/null
+++ b/.skelrc
@@ -0,0 +1,9 @@
+;;; -*-emacs-lisp-*-
+
+(setq skel-alist
+      (append
+       '((author . "Straylight/Edgeware")
+        (full-title . "the Trivial IP Encryption (TrIPE) Android app")
+        (program . "TrIPE")
+        (licence-text . "[[gpl-3]]"))
+       skel-alist))
index cd8e4a40874306da18187ced090b52c0b537aa8b..a03ba642a673753adaf02ebf142f6e6bd8ff0428 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -10,10 +10,16 @@ JDK                  = /usr/lib/jvm/default-java
 JDK_PLAT                = linux
 INCLUDES                = $(JDK)/include $(JDK)/include/$(JDK_PLAT)
 
+PKGS                    = mLib catacomb
+PKGS_CFLAGS            := $(foreach p,$(PKGS),$(shell pkg-config --cflags $p))
+PKGS_LIBS              := $(foreach p,$(PKGS),$(shell pkg-config --libs $p))
+
 CC                      = gcc
-CFLAGS                  = -O0 -g -Wall -fPIC $(addprefix -I,$(INCLUDES))
+CFLAGS                  = -O2 -g -Wall -fPIC $(addprefix -I,$(INCLUDES))
+CFLAGS                 += $(PKGS_CFLAGS)
 
 LD                      = gcc
+LIBS                    = $(PKGS_LIBS)
 LDFLAGS.so              = -shared
 
 JAVAC                   = javac
@@ -22,6 +28,9 @@ JAVAFLAGS              =
 SCALAC                  = fsc
 SCALAFLAGS              = -optimise
 
+## Hack around https://issues.scala-lang.org/browse/SI-9689
+SCALAFLAGS             += -Yno-load-impl-class
+
 all::
 .PHONY: all
 
@@ -34,6 +43,7 @@ CLASSDIR               = cls/
        $(call v_tag,JAVAC)mkdir -p $(CLASSDIR) && \
                $(JAVAC) -d $(CLASSDIR) -cp $(CLASSDIR) $(JAVAFLAGS) $< && \
                echo built >$@
+       $(V_AT)$(SCALAC) -reset
 %.stamp: %.scala
        $(call v_tag,SCALAC)mkdir -p $(CLASSDIR) && \
                $(SCALAC) -d $(CLASSDIR) -cp $(CLASSDIR) $(SCALAFLAGS) $< && \
@@ -46,17 +56,21 @@ objects                      = $(patsubst %.c,%$2,$1)
 TARGETS += libtoy.so
 libtoy.so_SOURCES       = jni.c
 libtoy.so: $(call objects,$(libtoy.so_SOURCES),.o)
-       $(call v_tag,LD)$(LD) $(LDFLAGS.so) -o$@ $^
+       $(call v_tag,LD)$(LD) $(LDFLAGS.so) -o$@ $^ $(LIBS)
+
+TARGETS                        += util.stamp
 
 TARGETS                        += jni.stamp
+jni.stamp: util.stamp
 
-TARGETS                        += sock.stamp
-sock.stamp: jni.stamp
+TARGETS                        += sys.stamp
+sys.stamp: jni.stamp util.stamp
 
 TARGETS                        += admin.stamp
+admin.stamp: util.stamp
 
 TARGETS                        += main.stamp
-main.stamp: jni.stamp sock.stamp
+main.stamp: jni.stamp
 
 all:: $(TARGETS)
 ALLSOURCES             += $(foreach t,$(TARGETS),$($t_SOURCES))
index 85978fecce24a0d854448d6c3022450f2673a95a..cc8218687a368363e59746617ab1197562f807dc 100644 (file)
-package uk.org.distorted.tripe;
-
-import scala.collection.mutable.ArrayBuffer;
-
-object Admin {
-  val RX_ORDINARY = "^[^\\\\'\"\\s]+$".r;
-  val RX_WEIRD = "[\\\\'\"]".r;
-
-  def quote(v: Seq[String]) = {
-    val b = new StringBuilder;
-    var sep = false;
-    for (s <- v) {
-      if (!sep) sep = true;
-      else b.append(' ');
-      s match {
-       case RX_ORDINARY() => b.append(s);
-       case _ =>
-         b.append('"');
-         b.append(RX_WEIRD.replaceAllIn(s, "\\\\$0"));
-         b.append('"');
+/* -*-scala-*-
+ *
+ * Managing TrIPE administration connections
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted.tripe; package object admin {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import java.io.{BufferedReader, Reader, Writer};
+import java.util.concurrent.locks.{Condition, ReentrantLock => Lock};
+
+import scala.collection.mutable.{HashMap, Publisher};
+import scala.concurrent.Channel;
+import scala.util.control.Breaks;
+
+import Magic._;
+
+/*----- Classification of server messages ---------------------------------*/
+
+sealed abstract class Message;
+
+sealed abstract class JobMessage extends Message;
+case object JobOK extends JobMessage;
+case class JobInfo(info: Seq[String]) extends JobMessage;
+case class JobFail(err: Seq[String]) extends JobMessage;
+case object JobLostConnection extends JobMessage;
+
+case class BackgroundJobMessage(tag: String, msg: JobMessage)
+       extends Message;
+case class JobDetached(tag: String) extends Message;
+
+sealed abstract class AsyncMessage extends Message;
+case class Trace(msg: String) extends AsyncMessage;
+case class Warning(err: Seq[String]) extends AsyncMessage;
+case class Notify(note: Seq[String]) extends AsyncMessage;
+case object ConnectionLost extends AsyncMessage;
+
+sealed abstract class ServiceMessage extends Message;
+case class ServiceCancel(jobid: String) extends ServiceMessage;
+case class ServiceClaim(svc: String, version: String)
+       extends ServiceMessage;
+case class ServiceJob(jobid: String, svc: String,
+                     cmd: String, args: Seq[String])
+       extends ServiceMessage;
+
+/*----- Main code ---------------------------------------------------------*/
+
+object Connection {
+}
+
+class ConnectionClosed extends Exception;
+
+class ServerFailed(msg: String) extends Exception(msg);
+
+class CommandFailed(val msg: Seq[String]) extends Exception {
+  override def getMessage(): String =
+    "%s(%s)".format(getClass.getName, quoteTokens(msg));
+}
+
+class ConnectionLostException extends Exception;
+
+class Connection(val in: Reader, val out: Writer)
+       extends Publisher[AsyncMessage]
+{
+  /* Synchronization.
+   *
+   * This class is complicatedly multithreaded.  The following fields must
+   * only be accessed while the instance is locked.  To prevent deadlocks,
+   * hold the `Connection' lock before locking any individual `Job' objects.
+   */
+
+  var livep: Boolean = true;           // Is this connection still alive?
+  var fgjob: Option[this.Job] = None;  // Foreground job, if there is one.
+  val jobmap = new HashMap[String, this.Job]; // Maps tags to extant jobs.
+  var bgseq = 0;                       // Next background job tag.
+
+  class Job extends Iterator[Seq[String]] {
+    private[Connection] val ch = new Channel[JobMessage];
+    private[this] var nextmsg: Option[JobMessage] = None;
+
+    private[this] def fetchNext()
+      { if (nextmsg == None) nextmsg = Some(ch.read); }
+    override def hasNext: Boolean = {
+      fetchNext();
+      nextmsg match {
+       case Some(JobOK) => false
+       case _ => true
+      }
+    }
+    override def next(): Seq[String] = {
+      fetchNext();
+      nextmsg match {
+       case None => ???
+       case Some(JobOK) => throw new NoSuchElementException
+       case Some(JobFail(msg)) => throw new CommandFailed(msg)
+       case Some(JobLostConnection) => throw new ConnectionLostException
+       case Some(JobInfo(msg)) => nextmsg = None; msg
       }
     }
-    b.mkString
-  }
 
-  class InvalidQuotingException(msg: String) extends Exception(msg);
+    def keyvals(): Map[String, String] = {
+      val b = Map.newBuilder[String, String];
+      for (line <- this; token <- line) {
+       token.indexOf('=') match {
+         case -1 => throw new ServerFailed("missing `=' in key-value list");
+         case eq =>
+           val k = token.substring(0, eq);
+           val v = token.substring(eq + 1);
+           b += k -> v;
+       }
+      }
+      b.result
+    }
 
-  def split(s: String): Array[String] = {
-    val ab = new ArrayBuffer[String]();
-    val sb = new StringBuilder;
+    def traceish(): Seq[(Char, Boolean, String)] = {
+      val b = Seq.newBuilder[(Char, Boolean, String)];
+      for (line <- this) line match {
+       case List(key, desc@_*) =>
+         val live = if (key.length == 1) false
+                    else if (key.length == 2 && key(1) == '+') true
+                    else throw new ServerFailed(
+                      s"incomprehensible traceish key `$key'");
+         b += ((key(0), live, desc.mkString(" ")));
+       case _ => throw new ServerFailed("empty line in traceish output");
+      }
+      b.result
+    }
 
-    object State extends Enumeration {
-      val BETWEEN, WORD, SQUOTE, DQUOTE = Value;
+    def expectEmpty() {
+      if (hasNext) throw new ServerFailed("no output expected");
     }
-    import State.{Value => _, _};
-
-    val n = s.length;
-
-    def scan(pos: Int, st: State.Value, bs: Boolean)
-    {
-      if (pos >= n) {
-       if (bs)
-         throw new InvalidQuotingException("trailing `\\'");
-       else if (st == SQUOTE || st == DQUOTE)
-         throw new InvalidQuotingException("unmatched quote");
-       if (st != BETWEEN) ab += sb.mkString;
-      } else (st, bs, s(pos)) match {
-       case (BETWEEN, false, '\\') => scan(pos + 1, WORD, true);
-       case (_, false, '\\') => scan(pos + 1, st, true);
-       case (SQUOTE, false, ''') | (DQUOTE, false, '"') =>
-         scan(pos + 1, WORD, false);
-       case (BETWEEN | WORD, false, ''') => scan(pos + 1, SQUOTE, false);
-       case (BETWEEN | WORD, false, '"') => scan(pos + 1, DQUOTE, false);
-       case (BETWEEN, false, ch) if ch.isWhitespace =>
-         scan(pos + 1, st, false);
-       case (WORD, false, ch) if ch.isWhitespace =>
-         ab += sb.mkString; sb.clear();
-         scan(pos + 1, BETWEEN, false);
-       case (BETWEEN, _, ch) => sb.append(ch); scan(pos + 1, WORD, false);
-       case (_, _, ch) => sb.append(ch); scan(pos + 1, st, false);
+
+    def oneLine(): Seq[String] = {
+      if (hasNext) {
+       val line = next();
+       if (!hasNext) return line;
       }
+      throw new ServerFailed("exactly one line expected");
+    }
+  }
+
+  def submit(bg: Boolean, toks: String*): this.Job = {
+    var cmd = toks;
+println(";; wait for lock");
+    synchronized {
+      if (bg) {
+       val tag = bgseq formatted "J%05d"; bgseq += 1;
+       cmd = toks match {
+         case Seq(cmd, tail@_*) => cmd +: "-background" +: tag +: tail;
+       }
+      }
+println(";; wait for foreground");
+      while (livep && fgjob != None) wait();
+      if (!livep) throw new ConnectionClosed;
+println(";; write command");
+      try { out.write(quoteTokens(cmd)); out.write('\n'); out.flush(); }
+      catch { case e: Throwable => notify(); throw e; }
+      val j = new Job;
+      fgjob = Some(j);
+      j
+    }
+  }
+
+  def submit(toks: String*): this.Job = submit(false, toks: _*);
+
+  def close() { synchronized { out.close(); } }
+
+  /* These two expect the connection lock to be held. */
+  def foregroundJob: Job =
+    fgjob.getOrElse { throw new ServerFailed("no foreground job"); }
+  def releaseForegroundJob() { fgjob = None; notify(); }
+
+  def parseServerLine(s: String): Message = nextToken(s) match {
+    case None => throw new ServerFailed("empty line from server")
+    case Some(("TRACE", next)) => Trace(s.substring(next))
+    case Some((code, next)) => (code, splitTokens(s, next)) match {
+      case ("OK", Seq()) => JobOK
+      case ("INFO", tail) => JobInfo(tail)
+      case ("FAIL", tail) => JobFail(tail)
+      case ("BGDETACH", Seq(tag)) => JobDetached(tag)
+      case ("BGOK", Seq(tag)) => BackgroundJobMessage(tag, JobOK)
+      case ("BGINFO", Seq(tag, tail@_*)) =>
+       BackgroundJobMessage(tag, JobInfo(tail))
+      case ("BGFAIL", Seq(tag, tail@_*)) =>
+       BackgroundJobMessage(tag, JobFail(tail))
+      case ("WARN", tail) => Warning(tail)
+      case ("NOTE", tail) => Notify(tail)
+      case ("SVCCLAIM", Seq(svc, ver)) => ServiceClaim(svc, ver)
+      case ("SVCJOB", Seq(tag, svc, cmd, args@_*)) =>
+       ServiceJob(tag, svc, cmd, args)
+      case ("SVCCANCEL", Seq(tag)) => ServiceCancel(tag)
+      case (_, tail) => throw new ServerFailed(
+       "incomprehensible line from server: " + quoteTokens(code +: tail))
     }
-    scan(0, BETWEEN, false);
-    ab.toArray
   }
 
-  def main(args: Array[String])
-  {
-    if (args.length != 1) println(quote(args));
-    else for (s <- split(args(0))) println(s);
+  def processJobMessage(msg: JobMessage)
+                      (getjob: (Boolean) => Job) {
+    synchronized { getjob(msg.isInstanceOf[JobInfo]); }.ch.write(msg);
   }
+
+  /* Reading lines from the server. */
+  val readthr = thread("admin reader") {
+println(";; readthr running");
+    val bin = in match {
+      case br: BufferedReader => br;
+      case _ => new BufferedReader(in)
+    }
+    var line: String = null;
+
+    try {
+println(";; wait for line");
+      while ({line = bin.readLine; line != null}) {
+println(s";; line: $line");
+       parseServerLine(line) match {
+         case JobDetached(tag) => synchronized {
+           jobmap(tag) = foregroundJob; releaseForegroundJob();
+         }
+         case msg: JobMessage => processJobMessage(msg) { keep =>
+           val j = foregroundJob; if (!keep) releaseForegroundJob(); j
+         }
+         case BackgroundJobMessage(tag, msg) =>
+           processJobMessage(msg) { keep =>
+             val j = jobmap.getOrElse(tag, throw new ServerFailed(
+               s"no job with tag `${tag}'"));
+             if (!keep) jobmap.remove(tag);
+             j
+           }
+         case msg: AsyncMessage =>
+           publish(msg);
+         case _: ServiceMessage =>
+           ();
+       }
+      }
+    } catch {
+      case e: Throwable => e.printStackTrace();
+    } finally {
+      synchronized {
+       livep = false;
+       for ((_, j) <- jobmap) j.ch.write(JobLostConnection);
+       fgjob match {
+         case Some(j) =>
+           j.ch.write(JobLostConnection);
+           fgjob = None;
+           notifyAll();
+         case None => ();
+       }
+      }
+      publish(ConnectionLost);
+      in.close(); out.close();
+    }
+  }
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+
 }
diff --git a/jni.c b/jni.c
index 9f9e44d5b4f05ca5c958e0f8b56a5147a9603e94..fd91d0c4ec40b335ecd80de510e80a0dcfbcf5a3 100644 (file)
--- a/jni.c
+++ b/jni.c
@@ -1,7 +1,37 @@
+/* -*-c-*-
+ *
+ * Native-code portions of the project
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#define _FILE_OFFSET_BITS 64
+
 #include <assert.h>
+#include <ctype.h>
 #include <errno.h>
 #include <inttypes.h>
-#include <stdint.h>
+#include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include <sys/types.h>
 #include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/sysmacros.h>
 #include <sys/un.h>
+#include <fcntl.h>
 #include <unistd.h>
+#include <dirent.h>
+
+#include <mLib/align.h>
+#include <mLib/bits.h>
+#include <mLib/dstr.h>
+#include <mLib/macros.h>
+
+#include <catacomb/ghash.h>
 
 #undef sun
 
-union align {
-  int i;
-  long l;
-  double d;
-  void *p;
-  void (*f)(void *);
-  struct notexist *s;
-};
+/*----- Magic class names and similar -------------------------------------*/
+
+/* The name decoration is horrific.  Hide it. */
+#define JNIFUNC(f) Java_uk_org_distorted_tripe_jni_package_00024_##f
+
+/* The little class for bundling up error codes. */
+#define ERRENTRY "uk/org/distorted/tripe/jni/package$ErrorEntry"
+
+/* The `stat' class. */
+#define STAT "uk/org/distorted/tripe/jni/package$FileInfo"
+
+/* Exception class names. */
+#define NULLERR "java/lang/NullPointerException"
+#define TYPEERR "uk/org/distorted/tripe/jni/package$NativeObjectTypeException"
+#define SYSERR "uk/org/distorted/tripe/sys/package$SystemError"
+#define ARGERR "java/lang/IllegalArgumentException"
+#define BOUNDSERR "java/lang/IndexOutOfBoundsException"
+
+/*----- Miscellaneous utilities -------------------------------------------*/
+
+static void put_cstring(JNIEnv *jni, jbyteArray v, const char *p)
+  { if (p) (*jni)->ReleaseByteArrayElements(jni, v, (jbyte *)p, JNI_ABORT); }
+
+static void vexcept(JNIEnv *jni, const char *clsname,
+                   const char *msg, va_list ap)
+{
+  jclass cls;
+  int rc;
+  dstr d = DSTR_INIT;
+
+  cls = (*jni)->FindClass(jni, clsname); assert(cls);
+  if (!msg)
+    rc = (*jni)->ThrowNew(jni, cls, 0);
+  else {
+    dstr_vputf(&d, msg, &ap);
+    rc = (*jni)->ThrowNew(jni, cls, d.buf);
+    assert(!rc);
+    dstr_destroy(&d);
+  }
+  assert(!rc);
+}
+
+static void except(JNIEnv *jni, const char *clsname, const char *msg, ...)
+{
+  va_list ap;
+
+  va_start(ap, msg);
+  vexcept(jni, clsname, msg, ap);
+  va_end(ap);
+}
+
+#ifdef DEBUG
+static void dump_bytes(const void *p, size_t n, size_t o)
+{
+  const unsigned char *q = p;
+  size_t i;
+
+  if (!n) return;
+  for (;;) {
+    fprintf(stderr, ";;   %08zx\n", o);
+    for (i = 0; i < 8; i++)
+      if (i < n) fprintf(stderr, "%02x ", q[i]);
+      else fprintf(stderr, "** ");
+    fprintf(stderr, ": ");
+    for (i = 0; i < 8; i++)
+      fputc(i >= n ? '*' : isprint(q[i]) ? q[i] : '.', stderr);
+    fputc('\n', stderr);
+    if (n <= 8) break;
+    q += 8; n -= 8;
+  }
+}
+
+static void dump_byte_array(JNIEnv *jni, const char *what, jbyteArray v)
+{
+  jsize n;
+  jbyte *p;
+
+  fprintf(stderr, ";; %s\n", what);
+  if (!v) { fprintf(stderr, ";;   <null>\n"); return; }
+  n = (*jni)->GetArrayLength(jni, v);
+  p = (*jni)->GetByteArrayElements(jni, v, 0);
+  dump_bytes(p, n, 0);
+  (*jni)->ReleaseByteArrayElements(jni, v, p, JNI_ABORT);
+}
+#endif
+
+static jbyteArray wrap_cstring(JNIEnv *jni, const char *p)
+{
+  size_t n;
+  jbyteArray v;
+  jbyte *q;
+
+  if (!p) return (0);
+  n = strlen(p) + 1;
+  v = (*jni)->NewByteArray(jni, n); if (!v) return (0);
+  q = (*jni)->GetByteArrayElements(jni, v, 0); if (!q) return (0);
+  memcpy(q, p, n);
+  (*jni)->ReleaseByteArrayElements(jni, v, q, 0);
+  return (v);
+}
+
+static const char *get_cstring(JNIEnv *jni, jbyteArray v)
+{
+  if (!v) { except(jni, NULLERR, 0); return (0); }
+  return ((const char *)(*jni)->GetByteArrayElements(jni, v, 0));
+}
+
+static void vexcept_syserror(JNIEnv *jni, const char *clsname,
+                            int err, const char *msg, va_list ap)
+{
+  jclass cls;
+  int rc;
+  dstr d = DSTR_INIT;
+  jbyteArray msgstr;
+  jthrowable e;
+  jmethodID init;
+
+  cls = (*jni)->FindClass(jni, clsname); assert(cls);
+  init = (*jni)->GetMethodID(jni, cls, "<init>", "(I[B)V"); assert(init);
+  dstr_vputf(&d, msg, &ap);
+  msgstr = wrap_cstring(jni, d.buf); assert(msgstr);
+  dstr_destroy(&d);
+  e = (*jni)->NewObject(jni, cls, init, err, msgstr); assert(e);
+  rc = (*jni)->Throw(jni, e); assert(!rc);
+}
+
+static void except_syserror(JNIEnv *jni, const char *clsname,
+                           int err, const char *msg, ...)
+{
+  va_list ap;
+
+  va_start(ap, msg);
+  vexcept_syserror(jni, clsname, err, msg, ap);
+  va_end(ap);
+}
+
+/*----- Wrapping native types ---------------------------------------------*/
+
+/* There's no way defined in the JNI to stash a C pointer in a Java object.
+ * It seems that the usual approach is to cast to `jlong', but this is
+ * clearly unsatisfactory.  Instead, we store structures as Java byte arrays,
+ * with a 32-bit tag on the front.
+ */
 
 struct native_type {
   const char *name;
   size_t sz;
-  uint32_t tag;
+  uint32 tag;
 };
 
-typedef jbyteArray wrapped;
+typedef jbyteArray wrapper;
 
-struct open {
-  wrapped obj;
-  jbyte *arr;
+struct native_base {
+  uint32 tag;
 };
 
-struct base {
-  uint32_t tag;
+static int unwrap(JNIEnv *jni, void *p,
+                 const struct native_type *ty, wrapper w)
+{
+  jbyte *q;
+  jclass cls;
+  struct native_base *b = p;
+  jsize n;
+
+  if (!w) { except(jni, NULLERR, 0); return (-1); }
+  cls = (*jni)->FindClass(jni, "[B"); assert(cls);
+  if (!(*jni)->IsInstanceOf(jni, w, cls)) {
+    except(jni, TYPEERR,
+          "corrupted native object wrapper: expected a byte array");
+    return (-1);
+  }
+  n = (*jni)->GetArrayLength(jni, w);
+  if (n != ty->sz) {
+    except(jni, TYPEERR,
+          "corrupted native object wrapper: wrong size for `%s'",
+          ty->name);
+    return (-1);
+  }
+  q = (*jni)->GetByteArrayElements(jni, w, 0); if (!q) return (-1);
+  memcpy(b, q, ty->sz);
+  (*jni)->ReleaseByteArrayElements(jni, w, q, JNI_ABORT);
+  if (b->tag != ty->tag) {
+    except(jni, TYPEERR,
+          "corrupted native object wrapper: expected tag for `%s'",
+          ty->name);
+    return (-1);
+  }
+  return (0);
+}
+
+static int update_wrapper(JNIEnv *jni, const struct native_type *ty,
+                         wrapper w, const void *p)
+{
+  jbyte *q;
+
+  q = (*jni)->GetByteArrayElements(jni, w, 0); if (!q) return (-1);
+  memcpy(q, p, ty->sz);
+  (*jni)->ReleaseByteArrayElements(jni, w, q, 0);
+  return (0);
+}
+
+static wrapper wrap(JNIEnv *jni, const struct native_type *ty, const void *p)
+{
+  wrapper w;
+
+  w = (*jni)->NewByteArray(jni, ty->sz); if (!w) return (0);
+  if (update_wrapper(jni, ty, w, p)) return (0);
+  return (w);
+}
+
+#define INIT_NATIVE(type, p) do (p)->_base.tag = type##_type.tag; while (0)
+
+/*----- Crypto information ------------------------------------------------*/
+
+JNIEXPORT jint JNICALL JNIFUNC(hashsz)(JNIEnv *jni, jobject cls,
+                                      jstring hnamestr)
+{
+  jint rc = -1;
+  const char *hname;
+  const gchash *hc;
+
+  hname = (*jni)->GetStringUTFChars(jni, hnamestr, 0);
+  if (!hname) goto end;
+  hc = ghash_byname(hname); if (!hc) goto end;
+  rc = hc->hashsz;
+
+end:
+  if (hname) (*jni)->ReleaseStringUTFChars(jni, hnamestr, hname);
+  return (rc);
+}
+
+/*----- System errors -----------------------------------------------------*/
+
+static const struct errtab { const char *tag; int err; } errtab[] = {
+  /*
+     ;;; The errno name table is very boring to type.  To make life less
+     ;;; awful, put the errno names in this list and evaluate the code to
+     ;;; get Emacs to regenerate it.
+
+     (let ((errors '(EPERM ENOENT ESRCH EINTR EIO ENXIO E2BIG ENOEXEC EBADF
+                    ECHILD EAGAIN ENOMEM EACCES EFAULT ENOTBLK EBUSY EEXIST
+                    EXDEV ENODEV ENOTDIR EISDIR EINVAL ENFILE EMFILE ENOTTY
+                    ETXTBSY EFBIG ENOSPC ESPIPE EROFS EMLINK EPIPE EDOM
+                    ERANGE
+
+                    EDEADLK ENAMETOOLONG ENOLCK ENOSYS ENOTEMPTY ELOOP
+                    EWOULDBLOCK ENOMSG EIDRM ECHRNG EL2NSYNC EL3HLT EL3RST
+                    ELNRNG EUNATCH ENOCSI EL2HLT EBADE EBADR EXFULL ENOANO
+                    EBADRQC EBADSLT EDEADLOCK EBFONT ENOSTR ENODATA ETIME
+                    ENOSR ENONET ENOPKG EREMOTE ENOLINK EADV ESRMNT ECOMM
+                    EPROTO EMULTIHOP EDOTDOT EBADMSG EOVERFLOW ENOTUNIQ
+                    EBADFD EREMCHG ELIBACC ELIBBAD ELIBSCN ELIBMAX ELIBEXEC
+                    EILSEQ ERESTART ESTRPIPE EUSERS ENOTSOCK EDESTADDRREQ
+                    EMSGSIZE EPROTOTYPE ENOPROTOOPT EPROTONOSUPPORT
+                    ESOCKTNOSUPPORT EOPNOTSUPP EPFNOSUPPORT EAFNOSUPPORT
+                    EADDRINUSE EADDRNOTAVAIL ENETDOWN ENETUNREACH ENETRESET
+                    ECONNABORTED ECONNRESET ENOBUFS EISCONN ENOTCONN
+                    ESHUTDOWN ETOOMANYREFS ETIMEDOUT ECONNREFUSED EHOSTDOWN
+                    EHOSTUNREACH EALREADY EINPROGRESS ESTALE EUCLEAN ENOTNAM
+                    ENAVAIL EISNAM EREMOTEIO EDQUOT ENOMEDIUM EMEDIUMTYPE
+                    ECANCELED ENOKEY EKEYEXPIRED EKEYREVOKED EKEYREJECTED
+                    EOWNERDEAD ENOTRECOVERABLE ERFKILL EHWPOISON)))
+       (save-excursion
+         (goto-char (point-min))
+         (search-forward (concat "***" "BEGIN errtab" "***"))
+         (beginning-of-line 2)
+         (delete-region (point)
+                        (progn
+                          (search-forward "***END***")
+                          (beginning-of-line)
+                          (point)))
+         (dolist (err errors)
+           (insert (format "#ifdef %s\n  { \"%s\", %s },\n#endif\n"
+                           err err err)))))
+  */
+  /***BEGIN errtab***/
+#ifdef EPERM
+  { "EPERM", EPERM },
+#endif
+#ifdef ENOENT
+  { "ENOENT", ENOENT },
+#endif
+#ifdef ESRCH
+  { "ESRCH", ESRCH },
+#endif
+#ifdef EINTR
+  { "EINTR", EINTR },
+#endif
+#ifdef EIO
+  { "EIO", EIO },
+#endif
+#ifdef ENXIO
+  { "ENXIO", ENXIO },
+#endif
+#ifdef E2BIG
+  { "E2BIG", E2BIG },
+#endif
+#ifdef ENOEXEC
+  { "ENOEXEC", ENOEXEC },
+#endif
+#ifdef EBADF
+  { "EBADF", EBADF },
+#endif
+#ifdef ECHILD
+  { "ECHILD", ECHILD },
+#endif
+#ifdef EAGAIN
+  { "EAGAIN", EAGAIN },
+#endif
+#ifdef ENOMEM
+  { "ENOMEM", ENOMEM },
+#endif
+#ifdef EACCES
+  { "EACCES", EACCES },
+#endif
+#ifdef EFAULT
+  { "EFAULT", EFAULT },
+#endif
+#ifdef ENOTBLK
+  { "ENOTBLK", ENOTBLK },
+#endif
+#ifdef EBUSY
+  { "EBUSY", EBUSY },
+#endif
+#ifdef EEXIST
+  { "EEXIST", EEXIST },
+#endif
+#ifdef EXDEV
+  { "EXDEV", EXDEV },
+#endif
+#ifdef ENODEV
+  { "ENODEV", ENODEV },
+#endif
+#ifdef ENOTDIR
+  { "ENOTDIR", ENOTDIR },
+#endif
+#ifdef EISDIR
+  { "EISDIR", EISDIR },
+#endif
+#ifdef EINVAL
+  { "EINVAL", EINVAL },
+#endif
+#ifdef ENFILE
+  { "ENFILE", ENFILE },
+#endif
+#ifdef EMFILE
+  { "EMFILE", EMFILE },
+#endif
+#ifdef ENOTTY
+  { "ENOTTY", ENOTTY },
+#endif
+#ifdef ETXTBSY
+  { "ETXTBSY", ETXTBSY },
+#endif
+#ifdef EFBIG
+  { "EFBIG", EFBIG },
+#endif
+#ifdef ENOSPC
+  { "ENOSPC", ENOSPC },
+#endif
+#ifdef ESPIPE
+  { "ESPIPE", ESPIPE },
+#endif
+#ifdef EROFS
+  { "EROFS", EROFS },
+#endif
+#ifdef EMLINK
+  { "EMLINK", EMLINK },
+#endif
+#ifdef EPIPE
+  { "EPIPE", EPIPE },
+#endif
+#ifdef EDOM
+  { "EDOM", EDOM },
+#endif
+#ifdef ERANGE
+  { "ERANGE", ERANGE },
+#endif
+#ifdef EDEADLK
+  { "EDEADLK", EDEADLK },
+#endif
+#ifdef ENAMETOOLONG
+  { "ENAMETOOLONG", ENAMETOOLONG },
+#endif
+#ifdef ENOLCK
+  { "ENOLCK", ENOLCK },
+#endif
+#ifdef ENOSYS
+  { "ENOSYS", ENOSYS },
+#endif
+#ifdef ENOTEMPTY
+  { "ENOTEMPTY", ENOTEMPTY },
+#endif
+#ifdef ELOOP
+  { "ELOOP", ELOOP },
+#endif
+#ifdef EWOULDBLOCK
+  { "EWOULDBLOCK", EWOULDBLOCK },
+#endif
+#ifdef ENOMSG
+  { "ENOMSG", ENOMSG },
+#endif
+#ifdef EIDRM
+  { "EIDRM", EIDRM },
+#endif
+#ifdef ECHRNG
+  { "ECHRNG", ECHRNG },
+#endif
+#ifdef EL2NSYNC
+  { "EL2NSYNC", EL2NSYNC },
+#endif
+#ifdef EL3HLT
+  { "EL3HLT", EL3HLT },
+#endif
+#ifdef EL3RST
+  { "EL3RST", EL3RST },
+#endif
+#ifdef ELNRNG
+  { "ELNRNG", ELNRNG },
+#endif
+#ifdef EUNATCH
+  { "EUNATCH", EUNATCH },
+#endif
+#ifdef ENOCSI
+  { "ENOCSI", ENOCSI },
+#endif
+#ifdef EL2HLT
+  { "EL2HLT", EL2HLT },
+#endif
+#ifdef EBADE
+  { "EBADE", EBADE },
+#endif
+#ifdef EBADR
+  { "EBADR", EBADR },
+#endif
+#ifdef EXFULL
+  { "EXFULL", EXFULL },
+#endif
+#ifdef ENOANO
+  { "ENOANO", ENOANO },
+#endif
+#ifdef EBADRQC
+  { "EBADRQC", EBADRQC },
+#endif
+#ifdef EBADSLT
+  { "EBADSLT", EBADSLT },
+#endif
+#ifdef EDEADLOCK
+  { "EDEADLOCK", EDEADLOCK },
+#endif
+#ifdef EBFONT
+  { "EBFONT", EBFONT },
+#endif
+#ifdef ENOSTR
+  { "ENOSTR", ENOSTR },
+#endif
+#ifdef ENODATA
+  { "ENODATA", ENODATA },
+#endif
+#ifdef ETIME
+  { "ETIME", ETIME },
+#endif
+#ifdef ENOSR
+  { "ENOSR", ENOSR },
+#endif
+#ifdef ENONET
+  { "ENONET", ENONET },
+#endif
+#ifdef ENOPKG
+  { "ENOPKG", ENOPKG },
+#endif
+#ifdef EREMOTE
+  { "EREMOTE", EREMOTE },
+#endif
+#ifdef ENOLINK
+  { "ENOLINK", ENOLINK },
+#endif
+#ifdef EADV
+  { "EADV", EADV },
+#endif
+#ifdef ESRMNT
+  { "ESRMNT", ESRMNT },
+#endif
+#ifdef ECOMM
+  { "ECOMM", ECOMM },
+#endif
+#ifdef EPROTO
+  { "EPROTO", EPROTO },
+#endif
+#ifdef EMULTIHOP
+  { "EMULTIHOP", EMULTIHOP },
+#endif
+#ifdef EDOTDOT
+  { "EDOTDOT", EDOTDOT },
+#endif
+#ifdef EBADMSG
+  { "EBADMSG", EBADMSG },
+#endif
+#ifdef EOVERFLOW
+  { "EOVERFLOW", EOVERFLOW },
+#endif
+#ifdef ENOTUNIQ
+  { "ENOTUNIQ", ENOTUNIQ },
+#endif
+#ifdef EBADFD
+  { "EBADFD", EBADFD },
+#endif
+#ifdef EREMCHG
+  { "EREMCHG", EREMCHG },
+#endif
+#ifdef ELIBACC
+  { "ELIBACC", ELIBACC },
+#endif
+#ifdef ELIBBAD
+  { "ELIBBAD", ELIBBAD },
+#endif
+#ifdef ELIBSCN
+  { "ELIBSCN", ELIBSCN },
+#endif
+#ifdef ELIBMAX
+  { "ELIBMAX", ELIBMAX },
+#endif
+#ifdef ELIBEXEC
+  { "ELIBEXEC", ELIBEXEC },
+#endif
+#ifdef EILSEQ
+  { "EILSEQ", EILSEQ },
+#endif
+#ifdef ERESTART
+  { "ERESTART", ERESTART },
+#endif
+#ifdef ESTRPIPE
+  { "ESTRPIPE", ESTRPIPE },
+#endif
+#ifdef EUSERS
+  { "EUSERS", EUSERS },
+#endif
+#ifdef ENOTSOCK
+  { "ENOTSOCK", ENOTSOCK },
+#endif
+#ifdef EDESTADDRREQ
+  { "EDESTADDRREQ", EDESTADDRREQ },
+#endif
+#ifdef EMSGSIZE
+  { "EMSGSIZE", EMSGSIZE },
+#endif
+#ifdef EPROTOTYPE
+  { "EPROTOTYPE", EPROTOTYPE },
+#endif
+#ifdef ENOPROTOOPT
+  { "ENOPROTOOPT", ENOPROTOOPT },
+#endif
+#ifdef EPROTONOSUPPORT
+  { "EPROTONOSUPPORT", EPROTONOSUPPORT },
+#endif
+#ifdef ESOCKTNOSUPPORT
+  { "ESOCKTNOSUPPORT", ESOCKTNOSUPPORT },
+#endif
+#ifdef EOPNOTSUPP
+  { "EOPNOTSUPP", EOPNOTSUPP },
+#endif
+#ifdef EPFNOSUPPORT
+  { "EPFNOSUPPORT", EPFNOSUPPORT },
+#endif
+#ifdef EAFNOSUPPORT
+  { "EAFNOSUPPORT", EAFNOSUPPORT },
+#endif
+#ifdef EADDRINUSE
+  { "EADDRINUSE", EADDRINUSE },
+#endif
+#ifdef EADDRNOTAVAIL
+  { "EADDRNOTAVAIL", EADDRNOTAVAIL },
+#endif
+#ifdef ENETDOWN
+  { "ENETDOWN", ENETDOWN },
+#endif
+#ifdef ENETUNREACH
+  { "ENETUNREACH", ENETUNREACH },
+#endif
+#ifdef ENETRESET
+  { "ENETRESET", ENETRESET },
+#endif
+#ifdef ECONNABORTED
+  { "ECONNABORTED", ECONNABORTED },
+#endif
+#ifdef ECONNRESET
+  { "ECONNRESET", ECONNRESET },
+#endif
+#ifdef ENOBUFS
+  { "ENOBUFS", ENOBUFS },
+#endif
+#ifdef EISCONN
+  { "EISCONN", EISCONN },
+#endif
+#ifdef ENOTCONN
+  { "ENOTCONN", ENOTCONN },
+#endif
+#ifdef ESHUTDOWN
+  { "ESHUTDOWN", ESHUTDOWN },
+#endif
+#ifdef ETOOMANYREFS
+  { "ETOOMANYREFS", ETOOMANYREFS },
+#endif
+#ifdef ETIMEDOUT
+  { "ETIMEDOUT", ETIMEDOUT },
+#endif
+#ifdef ECONNREFUSED
+  { "ECONNREFUSED", ECONNREFUSED },
+#endif
+#ifdef EHOSTDOWN
+  { "EHOSTDOWN", EHOSTDOWN },
+#endif
+#ifdef EHOSTUNREACH
+  { "EHOSTUNREACH", EHOSTUNREACH },
+#endif
+#ifdef EALREADY
+  { "EALREADY", EALREADY },
+#endif
+#ifdef EINPROGRESS
+  { "EINPROGRESS", EINPROGRESS },
+#endif
+#ifdef ESTALE
+  { "ESTALE", ESTALE },
+#endif
+#ifdef EUCLEAN
+  { "EUCLEAN", EUCLEAN },
+#endif
+#ifdef ENOTNAM
+  { "ENOTNAM", ENOTNAM },
+#endif
+#ifdef ENAVAIL
+  { "ENAVAIL", ENAVAIL },
+#endif
+#ifdef EISNAM
+  { "EISNAM", EISNAM },
+#endif
+#ifdef EREMOTEIO
+  { "EREMOTEIO", EREMOTEIO },
+#endif
+#ifdef EDQUOT
+  { "EDQUOT", EDQUOT },
+#endif
+#ifdef ENOMEDIUM
+  { "ENOMEDIUM", ENOMEDIUM },
+#endif
+#ifdef EMEDIUMTYPE
+  { "EMEDIUMTYPE", EMEDIUMTYPE },
+#endif
+#ifdef ECANCELED
+  { "ECANCELED", ECANCELED },
+#endif
+#ifdef ENOKEY
+  { "ENOKEY", ENOKEY },
+#endif
+#ifdef EKEYEXPIRED
+  { "EKEYEXPIRED", EKEYEXPIRED },
+#endif
+#ifdef EKEYREVOKED
+  { "EKEYREVOKED", EKEYREVOKED },
+#endif
+#ifdef EKEYREJECTED
+  { "EKEYREJECTED", EKEYREJECTED },
+#endif
+#ifdef EOWNERDEAD
+  { "EOWNERDEAD", EOWNERDEAD },
+#endif
+#ifdef ENOTRECOVERABLE
+  { "ENOTRECOVERABLE", ENOTRECOVERABLE },
+#endif
+#ifdef ERFKILL
+  { "ERFKILL", ERFKILL },
+#endif
+#ifdef EHWPOISON
+  { "EHWPOISON", EHWPOISON },
+#endif
+  /***END***/
 };
 
-static void except(JNIEnv *jni, const char *clsname, const char *msg)
+JNIEXPORT jobject JNIFUNC(errtab)(JNIEnv *jni, jobject cls)
 {
-  jclass cls;
-  int rc;
+  size_t i;
+  jclass eltcls;
+  jarray v;
+  jmethodID init;
+  jobject e;
 
-  cls = (*jni)->FindClass(jni, clsname); assert(cls);
-  rc = (*jni)->ThrowNew(jni, cls, msg); assert(!rc);
+  eltcls =
+    (*jni)->FindClass(jni, ERRENTRY);
+  assert(eltcls);
+  v = (*jni)->NewObjectArray(jni, N(errtab), eltcls, 0); if (!v) return (0);
+  init = (*jni)->GetMethodID(jni, eltcls, "<init>",
+                            "(Ljava/lang/String;I)V");
+  assert(init);
+
+  for (i = 0; i < N(errtab); i++) {
+    e = (*jni)->NewObject(jni, eltcls, init,
+                         (*jni)->NewStringUTF(jni, errtab[i].tag),
+                         errtab[i].err);
+    (*jni)->SetObjectArrayElement(jni, v, i, e);
+  }
+  return (v);
+}
+
+JNIEXPORT jobject JNIFUNC(strerror)(JNIEnv *jni, jobject cls, jint err)
+  { return (wrap_cstring(jni, strerror(err))); }
+
+/*----- Low-level file operations -----------------------------------------*/
+
+/* Java has these already, as methods on `java.io.File' objects.  Alas, these
+ * methods are useless at reporting errors: they tend to return a `boolean'
+ * success/ fail indicator, and throw away any more detailed information.
+ * There's better functionality in `java.nio.file.Files', but that only turns
+ * up in Android API 26 (in 7.0 Nougat).  There's `android.system.Os', which
+ * has a bunch of POSIX-shaped functions -- but they're only in Android API
+ * 21 (in 5.0 Lollipop), and there's nothing in the support library to help.
+ *
+ * So the other option is to implement them ourselves.
+ */
+
+JNIEXPORT void JNIFUNC(unlink)(JNIEnv *jni, jobject cls, jobject path)
+{
+  const char *pathstr = 0;
+
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  if (unlink(pathstr)) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to delete file `%s'", pathstr);
+    goto end;
+  }
+end:
+  put_cstring(jni, path, pathstr);
 }
 
-static void except_errno(JNIEnv *jni, const char *clsname, int err)
-  { except(jni, clsname, strerror(err)); }
+JNIEXPORT void JNIFUNC(rmdir)(JNIEnv *jni, jobject cls, jobject path)
+{
+  const char *pathstr = 0;
 
-static void *open_struct_unchecked(JNIEnv *jni, wrapped obj, struct open *op)
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  if (rmdir(pathstr)) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to delete directory `%s'", pathstr);
+    goto end;
+  }
+end:
+  put_cstring(jni, path, pathstr);
+}
+
+JNIEXPORT void JNIFUNC(mkdir)(JNIEnv *jni, jobject cls,
+                             jobject path, jint mode)
 {
-  jboolean copyp;
-  uintptr_t p, q;
-
-  op->obj = obj;
-  op->arr = (*jni)->GetByteArrayElements(jni, obj, &copyp);
-  if (!op->arr) return (0);
-  p = (uintptr_t)op->arr;
-  q = p + sizeof(union align) - 1;
-  q -= q%sizeof(union align);
-  fprintf(stderr, ";; offset = %"PRIuPTR"\n", q - p);
-  return (op->arr + (q - p));
+  const char *pathstr = 0;
+
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  if (mkdir(pathstr, mode)) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to create directory `%s'", pathstr);
+    goto end;
+  }
+end:
+  put_cstring(jni, path, pathstr);
 }
 
-static void *open_struct(JNIEnv *jni, wrapped obj,
-                        const struct native_type *ty, struct open *op)
+JNIEXPORT void JNIFUNC(mkfile)(JNIEnv *jni, jobject cls,
+                           jobject path, jint mode)
 {
-  struct base *p;
-  jsize n;
+  const char *pathstr = 0;
+  int fd = -1;
 
-  if (!obj) { except(jni, "java/lang/NullPointerException", 0); return (0); }
-  n = (*jni)->GetArrayLength(jni, obj);
-  if ((*jni)->ExceptionOccurred(jni)) return (0);
-  p = open_struct_unchecked(jni, obj, op);
-  if (!p) return (0);
-  if (n < ty->sz + sizeof(union align) - 1 || p->tag != ty->tag)
-  {
-    (*jni)->ReleaseByteArrayElements(jni, obj, op->arr, JNI_ABORT);
-    except(jni, "uk/org/distorted/tripe/JNI$NativeObjectTypeException", 0);
-    return (0);
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  fd = open(pathstr, O_WRONLY | O_CREAT | O_EXCL, mode);
+  if (fd < 0) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to create fresh file `%s'", pathstr);
+    goto end;
   }
-  return (p);
+end:
+  if (fd != -1) close(fd);
+  put_cstring(jni, path, pathstr);
 }
 
-static wrapped close_struct(JNIEnv *jni, struct open *op)
+JNIEXPORT void JNIFUNC(rename)(JNIEnv *jni, jobject cls,
+                              jobject from, jobject to)
 {
-  (*jni)->ReleaseByteArrayElements(jni, op->obj, op->arr, 0);
-  return (op->obj);
+  const char *fromstr = 0, *tostr = 0;
+
+  fromstr = get_cstring(jni, from); if (!fromstr) goto end;
+  tostr = get_cstring(jni, to); if (!tostr) goto end;
+  if (rename(fromstr, tostr)) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to rename `%s' as `%s'", fromstr, tostr);
+    goto end;
+  }
+end:
+  put_cstring(jni, from, fromstr);
+  put_cstring(jni, to, tostr);
 }
 
-static void *alloc_struct(JNIEnv *jni, const struct native_type *ty,
-                         struct open *op)
+#define LKF_EXCL 1u
+#define LKF_WAIT 2u
+struct lockf {
+  struct native_base _base;
+  int fd;
+};
+static struct native_type lockf_type =
+       { "lock", sizeof(struct lockf), 0xb2648926};
+JNIEXPORT wrapper JNIFUNC(lock)(JNIEnv *jni, jobject cls,
+                               jobject path, jint flags)
+{
+  const char *pathstr = 0;
+  int fd = -1;
+  struct flock l;
+  struct lockf lk;
+  struct stat st0, st1;
+  int f;
+  wrapper r = 0;
+
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+
+again:
+  fd = open(pathstr, O_RDWR | O_CREAT); if (fd < 0) goto err;
+  if (fstat(fd, &st0)) goto err;
+  f = fcntl(fd, F_GETFD); if (f < 0) goto err;
+  if (fcntl(fd, F_SETFD, f | FD_CLOEXEC)) goto err;
+  l.l_type = (flags&LKF_EXCL) ? F_WRLCK : F_RDLCK;
+  l.l_whence = SEEK_SET;
+  l.l_start = 0;
+  l.l_len = 0;
+  if (fcntl(fd, (flags&LKF_WAIT) ? F_SETLKW : F_SETLK, &l)) goto err;
+  if (stat(pathstr, &st1))
+    { if (errno == ENOENT) goto again; else goto err; }
+  if (st0.st_dev != st1.st_dev || st0.st_ino != st1.st_ino)
+    { close(fd); fd = -1; goto again; }
+
+  INIT_NATIVE(lockf, &lk); lk.fd = fd; fd = -1;
+  r = wrap(jni, &lockf_type, &lk);
+  goto end;
+
+err:
+  except_syserror(jni, SYSERR, errno, "failed to lock file `%s'", pathstr);
+end:
+  if (fd != -1) close(fd);
+  put_cstring(jni, path, pathstr);
+  return (r);
+}
+
+JNIEXPORT void JNIFUNC(unlock)(JNIEnv *jni, jobject cls, wrapper wlk)
+{
+  struct lockf lk;
+  struct flock l;
+  int rc;
+
+  if (unwrap(jni, &lk, &lockf_type, wlk)) goto end;
+  if (lk.fd == -1) goto end;
+  l.l_type = F_UNLCK;
+  l.l_whence = SEEK_SET;
+  l.l_start = 0;
+  l.l_len = 0;
+  if (fcntl(lk.fd, F_SETLK, &l)) goto end;
+  close(lk.fd); lk.fd = -1;
+  rc = update_wrapper(jni, &lockf_type, wlk, &lk); assert(!rc);
+end:;
+}
+
+static jlong xlttimespec(const struct timespec *ts)
+  { return (1000*(jlong)ts->tv_sec + ts->tv_nsec/1000000); }
+
+static jobject xltstat(JNIEnv *jni, const struct stat *st)
+{
+  jclass cls;
+  jmethodID init;
+  jint modehack;
+  modehack = st->st_mode&07777;
+  if (S_ISFIFO(st->st_mode)) modehack |= 0010000;
+  else if (S_ISCHR(st->st_mode)) modehack |= 0020000;
+  else if (S_ISDIR(st->st_mode)) modehack |= 0040000;
+  else if (S_ISBLK(st->st_mode)) modehack |= 0060000;
+  else if (S_ISREG(st->st_mode)) modehack |= 0100000;
+  else if (S_ISLNK(st->st_mode)) modehack |= 0120000;
+  else if (S_ISSOCK(st->st_mode)) modehack |= 0140000;
+
+  cls = (*jni)->FindClass(jni, STAT); assert(cls);
+  init = (*jni)->GetMethodID(jni, cls, "<init>", "(IIJIIIIIIJIJJJJ)V");
+  assert(init);
+  return ((*jni)->NewObject(jni, cls, init,
+                           (jint)major(st->st_dev), (jint)minor(st->st_dev),
+                           (jlong)st->st_ino,
+                           modehack,
+                           (jint)st->st_nlink,
+                           (jint)st->st_uid, (jint)st->st_gid,
+                           (jint)major(st->st_rdev), (jint)minor(st->st_rdev),
+                           (jlong)st->st_size,
+                           (jint)st->st_blksize, (jlong)st->st_blocks,
+                           xlttimespec(&st->st_atim),
+                           xlttimespec(&st->st_mtim),
+                           xlttimespec(&st->st_ctim)));
+}
+
+JNIEXPORT jobject JNIFUNC(stat)(JNIEnv *jni, jobject cls, jobject path)
 {
-  wrapped obj;
-  struct base *p;
-
-  obj = (*jni)->NewByteArray(jni, ty->sz + sizeof(union align) - 1);
-  if (!obj) return (0);
-  p = open_struct_unchecked(jni, obj, op);
-  if (!p) { (*jni)->DeleteLocalRef(jni, obj); return (0); }
-  p->tag = ty->tag;
-  return (p);
+  jobject r = 0;
+  const char *pathstr = 0;
+  struct stat st;
+
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  if (stat(pathstr, &st)) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to read information about `%s'", pathstr);
+    goto end;
+  }
+  r = xltstat(jni, &st);
+end:
+  put_cstring(jni, path, pathstr);
+  return (r);
 }
 
-JNIEXPORT void JNICALL Java_uk_org_distorted_tripe_JNI_test
-       (JNIEnv *jni, jobject cls)
-  { printf("Hello from C!\n"); }
+JNIEXPORT jobject JNIFUNC(lstat)(JNIEnv *jni, jobject cls, jobject path)
+{
+  jobject r = 0;
+  const char *pathstr = 0;
+  struct stat st;
 
-struct toy {
-  struct base _base;
-  const char *p;
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  if (lstat(pathstr, &st)) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to read information about `%s'", pathstr);
+    goto end;
+  }
+  r = xltstat(jni, &st);
+end:
+  put_cstring(jni, path, pathstr);
+  return (r);
+}
+
+struct dir {
+  struct native_base _base;
+  DIR *d;
 };
-static const struct native_type toy_type =
-       { "toy", sizeof(struct toy), 0x58008918 };
+static const struct native_type dir_type =
+       { "dir", sizeof(struct dir), 0x0f5ca477 };
 
-JNIEXPORT wrapped JNICALL Java_uk_org_distorted_tripe_JNI_make
-       (JNIEnv *jni, jobject cls)
+JNIEXPORT jobject JNIFUNC(opendir)(JNIEnv *jni, jobject cls, jobject path)
 {
-  struct open op_toy;
-  struct toy *toy;
+  const char *pathstr = 0;
+  struct dir dir;
+  wrapper r = 0;
 
-  toy = alloc_struct(jni, &toy_type, &op_toy);
-  if (!toy) return (0);
-  toy->p = "A working thing";
-  return (close_struct(jni, &op_toy));
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  INIT_NATIVE(dir, &dir);
+  dir.d = opendir(pathstr);
+  if (!dir.d) {
+    except_syserror(jni, SYSERR, errno,
+                   "failed to open directory `%s'", pathstr);
+    goto end;
+  }
+  r = wrap(jni, &dir_type, &dir);
+end:
+  put_cstring(jni, path, pathstr);
+  return (r);
 }
 
-JNIEXPORT void JNICALL Java_uk_org_distorted_tripe_JNI_check
-       (JNIEnv *jni, jobject cls, wrapped wtoy)
+JNIEXPORT jbyteArray JNIFUNC(readdir)(JNIEnv *jni, jobject cls,
+                                     jobject path, jobject wdir)
 {
-  struct toy *toy;
-  struct open op_toy;
+  const char *pathstr = 0;
+  struct dir dir;
+  struct dirent *d;
+  jbyteArray r = 0;
 
-  toy = open_struct(jni, wtoy, &toy_type, &op_toy);
-  if (!toy) return;
-  printf("Toy says: %s\n", toy->p);
-  close_struct(jni, &op_toy);
+  if (unwrap(jni, &dir, &dir_type, wdir)) goto end;
+  if (!dir.d) { except(jni, ARGERR, "directory has been closed"); goto end; }
+  errno = 0; d = readdir(dir.d);
+  if (errno) {
+    pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+    except_syserror(jni, SYSERR, errno,
+                   "failed to read directory `%s'", pathstr);
+    goto end;
+  }
+  if (d) r = wrap_cstring(jni, d->d_name);
+end:
+  put_cstring(jni, path, pathstr);
+  return (r);
+}
+
+JNIEXPORT void JNIFUNC(closedir)(JNIEnv *jni, jobject cls,
+                                jobject path, jobject wdir)
+{
+  const char *pathstr = 0;
+  struct dir dir;
+
+  if (unwrap(jni, &dir, &dir_type, wdir)) goto end;
+  if (!dir.d) goto end;
+  if (closedir(dir.d)) {
+    pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+    except_syserror(jni, SYSERR, errno,
+                   "failed to close directory `%s'", pathstr);
+    goto end;
+  }
+  dir.d = 0;
+  if (update_wrapper(jni, &dir_type, wdir, &dir)) goto end;
+end:
+  put_cstring(jni, path, pathstr);
 }
 
+/*----- A server connection, using a Unix-domain socket -------------------*/
+
 struct conn {
-  struct base _base;
+  struct native_base _base;
   int fd;
   unsigned f;
 #define CF_CLOSERD 1u
 #define CF_CLOSEWR 2u
 #define CF_CLOSEMASK (CF_CLOSERD | CF_CLOSEWR)
 };
-static const struct native_type conn_type
-       { "conn", sizeof(struct conn), 0xed030167 };
+static const struct native_type conn_type =
+       { "conn", sizeof(struct conn), 0xed030167 };
 
-JNIEXPORT wrapped JNICALL Java_uk_org_distorted_tripe_JNI_connect
-       (JNIEnv *jni, jobject cls)
+JNIEXPORT wrapper JNICALL JNIFUNC(connect)(JNIEnv *jni, jobject cls,
+                                          jobject path)
 {
-  struct conn *conn;
-  struct open op;
+  struct conn conn;
   struct sockaddr_un sun;
+  const char *pathstr = 0;
+  jobject ret = 0;
   int fd = -1;
 
-  conn = alloc_struct(jni, &conn_type, &op);
-  if (!conn) goto err;
+  pathstr = get_cstring(jni, path); if (!pathstr) goto end;
+  if (strlen(pathstr) >= sizeof(sun.sun_path)) {
+    except(jni, ARGERR,
+          "Unix-domain socket path `%s' too long", pathstr);
+    goto end;
+  }
 
-  fd = socket(SOCK_STREAM, PF_UNIX, 0);
-  if (!fd) goto err_except;
+  INIT_NATIVE(conn, &conn);
+  fd = socket(SOCK_STREAM, PF_UNIX, 0); if (fd < 0) goto err;
 
   sun.sun_family = AF_UNIX;
-  strcpy(sun.sun_path, "/tmp/mdw/sk");
-  if (connect(fd, (struct sockaddr *)&sun, sizeof(sun))) goto err_except;
+  strcpy(sun.sun_path, (char *)pathstr);
+  if (connect(fd, (struct sockaddr *)&sun, sizeof(sun))) goto err;
 
-  conn->fd = fd;
-  return (close_struct(jni, &op));
+  conn.fd = fd; fd = -1;
+  conn.f = 0;
+  ret = wrap(jni, &conn_type, &conn);
+  goto end;
 
-err_except:
-  except_errno(jni, "java/io/IOException", errno);
 err:
-  if (fd) close(fd);
-  return (0);
+  except_syserror(jni, SYSERR, errno,
+                 "failed to connect to Unix-domain socket `%s'", pathstr);
+end:
+  if (fd == -1) close(fd);
+  put_cstring(jni, path, pathstr);
+  return (ret);
 }
 
-JNIEXPORT void JNICALL Java_uk_org_distorted_tripe_JNI_send
-       (JNIEnv *jni, jobject cls, wrapped wconn, jbyteArray buf,
-        jint start, jint len)
+static int check_buffer_bounds(JNIEnv *jni, const char *what,
+                              jbyteArray buf, jint start, jint len)
 {
-  struct conn *conn = 0;
-  struct open op;
-  jboolean copyp;
   jsize bufsz;
-  ssize_t n;
-  jbyte *p = 0;
-
-  conn = open_struct(jni, wconn, &conn_type, &op);
-  if (!conn) goto end;
+  jclass cls;
 
+  cls = (*jni)->FindClass(jni, "[B"); assert(cls);
+  if (!(*jni)->IsInstanceOf(jni, buf, cls)) {
+    except(jni, ARGERR,
+          "expected a byte array");
+    return (-1);
+  }
   bufsz = (*jni)->GetArrayLength(jni, buf);
-  if ((*jni)->ExceptionOccurred(jni)) goto end;
-  if (bufsz < start || bufsz - start < len) {
-    except(jni, "java/lang/IndexOutOfBoundsException",
-          "bad send-buffer bounds");
-    goto end;
+  if (start > bufsz) {
+    except(jni, BOUNDSERR,
+          "bad %s buffer bounds: start %d > buffer size %d", start, bufsz);
+    return (-1);
+  }
+  if (len > bufsz - start) {
+    except(jni, BOUNDSERR,
+          "bad %s buffer bounds: length %d > remaining buffer size %d",
+          len, bufsz - start);
+    return (-1);
   }
+  return (0);
+}
+
+JNIEXPORT void JNICALL JNIFUNC(send)(JNIEnv *jni, jobject cls,
+                                    wrapper wconn, jbyteArray buf,
+                                    jint start, jint len)
+{
+  struct conn conn;
+  ssize_t n;
+  jbyte *p = 0;
+
+  if (unwrap(jni, &conn, &conn_type, wconn)) goto end;
+  if (check_buffer_bounds(jni, "send", buf, start, len)) goto end;
 
-  p = (*jni)->GetByteArrayElements(jni, buf, &copyp);
+  p = (*jni)->GetByteArrayElements(jni, buf, 0);
   if (!p) goto end;
 
   while (len) {
-    n = send(conn->fd, p + start, len, 0);
+    n = send(conn.fd, p + start, len, 0);
     if (n < 0) {
-      except_errno(jni, "java/io/IOException", errno);
+      except_syserror(jni, SYSERR,
+                     errno, "failed to send on connection");
       goto end;
     }
     start += n; len -= n;
@@ -218,69 +1163,58 @@ JNIEXPORT void JNICALL Java_uk_org_distorted_tripe_JNI_send
 
 end:
   if (p) (*jni)->ReleaseByteArrayElements(jni, buf, p, JNI_ABORT);
-  if (conn) close_struct(jni, &op);
   return;
 }
 
-JNIEXPORT jint JNICALL Java_uk_org_distorted_tripe_JNI_recv
-       (JNIEnv *jni, jobject cls, wrapped wconn, jbyteArray buf,
-        jint start, jint len)
+JNIEXPORT jint JNICALL JNIFUNC(recv)(JNIEnv *jni, jobject cls,
+                                    wrapper wconn, jbyteArray buf,
+                                    jint start, jint len)
 {
-  struct conn *conn = 0;
-  struct open op;
-  jboolean copyp;
-  jsize bufsz;
+  struct conn conn;
   jbyte *p = 0;
   jint rc = -1;
 
-  conn = open_struct(jni, wconn, &conn_type, &op);
-  if (!conn) goto end;
-
-  bufsz = (*jni)->GetArrayLength(jni, buf);
-  if ((*jni)->ExceptionOccurred(jni)) goto end;
-  if (bufsz < start || bufsz - start < len) {
-    except(jni, "java/lang/IndexOutOfBoundsException",
-          "bad receive-buffer bounds");
-    goto end;
-  }
+  if (unwrap(jni, &conn, &conn_type, wconn)) goto end;
+  if (check_buffer_bounds(jni, "send", buf, start, len)) goto end;
 
-  p = (*jni)->GetByteArrayElements(jni, buf, &copyp);
+  p = (*jni)->GetByteArrayElements(jni, buf, 0);
   if (!p) goto end;
 
-  rc = recv(conn->fd, p + start, len, 0);
+  rc = recv(conn.fd, p + start, len, 0);
   if (rc < 0) {
-    except_errno(jni, "java/io/IOException", errno);
+    except_syserror(jni, SYSERR,
+                   errno, "failed to read from connection");
     goto end;
   }
   if (!rc) rc = -1;
 
 end:
   if (p) (*jni)->ReleaseByteArrayElements(jni, buf, p, 0);
-  if (conn) close_struct(jni, &op);
   return (rc);
 }
 
-JNIEXPORT void JNICALL Java_uk_org_distorted_tripe_JNI_close
-       (JNIEnv *jni, jobject cls, wrapped wconn, jint how)
+JNIEXPORT void JNICALL JNIFUNC(close)(JNIEnv *jni, jobject cls,
+                                     wrapper wconn, jint how)
 {
-  struct conn *conn = 0;
-  struct open op;
-
-  conn = open_struct(jni, wconn, &conn_type, &op);
-  if (!conn || conn->fd == -1) goto end;
-
-  how &= CF_CLOSEMASK&~conn->f;
-  conn->f |= how;
-fprintf(stderr, ";; closing %u\n", how);
-  if ((conn->f&CF_CLOSEMASK) == CF_CLOSEMASK) {
-    close(conn->fd);
-    conn->fd = -1;
+  struct conn conn;
+  int rc;
+
+  if (unwrap(jni, &conn, &conn_type, wconn)) goto end;
+  if (conn.fd == -1) goto end;
+
+  how &= CF_CLOSEMASK&~conn.f;
+  conn.f |= how;
+  if ((conn.f&CF_CLOSEMASK) == CF_CLOSEMASK) {
+    close(conn.fd);
+    conn.fd = -1;
   } else {
-    if (how&CF_CLOSERD) shutdown(conn->fd, SHUT_RD);
-    if (how&CF_CLOSEWR) shutdown(conn->fd, SHUT_WR);
+    if (how&CF_CLOSERD) shutdown(conn.fd, SHUT_RD);
+    if (how&CF_CLOSEWR) shutdown(conn.fd, SHUT_WR);
   }
+  rc = update_wrapper(jni, &conn_type, wconn, &conn); assert(!rc);
 
 end:
-  if (conn) close_struct(jni, &op);
   return;
 }
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/jni.java b/jni.java
deleted file mode 100644 (file)
index 03630a8..0000000
--- a/jni.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package uk.org.distorted.tripe;
-
-class JNI {
-  static { System.loadLibrary("toy"); }
-  static class NativeObjectTypeException extends RuntimeException {
-    NativeObjectTypeException() { super(); }
-    NativeObjectTypeException(String msg) { super(msg); }
-  }
-
-  static final int CF_CLOSERD = 1, CF_CLOSEWR = 2;
-  static final int CF_CLOSEMASK = CF_CLOSERD | CF_CLOSEWR;
-
-  static native void test();
-
-  static native Object make();
-  static native void check(Object toy);
-
-  static native Object connect();
-  static native void send(Object conn, byte[] buf, int start, int len);
-  static native int recv(Object conn, byte[] buf, int start, int len);
-  static native void close(Object conn, int how);
-}
diff --git a/jni.scala b/jni.scala
new file mode 100644 (file)
index 0000000..ea6ae76
--- /dev/null
+++ b/jni.scala
@@ -0,0 +1,246 @@
+/* -*-java-*-
+ *
+ * Declarations of C functions
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted.tripe; package object jni {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import java.io.{Closeable, File};
+import java.util.Date;
+import Magic._;
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* Import the native code library. */
+System.loadLibrary("toy");
+
+/* Exception indicating that a wrapped native object has been clobbered. */
+class NativeObjectTypeException(msg: String) extends RuntimeException(msg);
+type Wrapper = Array[Byte];
+
+case class ErrorEntry(val tag: String, val err: Int);
+@native def errtab: Array[ErrorEntry];
+@native def strerror(err: Int): CString;
+
+@native def hashsz(hash: String): Int;
+      /* Return the output hash size for the named HASH function, or -1. */
+
+/* Flags for `close'. */
+val CF_CLOSERD = 1;
+val CF_CLOSEWR = 2;
+val CF_CLOSEMASK = CF_CLOSERD | CF_CLOSEWR;
+
+/* Flags for `lock'. */
+val LKF_EXCL = 1;
+val LKF_WAIT = 2;
+
+/* Flags for `stat'. */
+val S_IFMT = 0xf000;
+val S_IFIFO = 0x1000;
+val S_IFCHR = 0x2000;
+val S_IFDIR = 0x4000;
+val S_IFBLK = 0x6000;
+val S_IFREG = 0x8000;
+val S_IFLNK = 0xa000;
+val S_IFSOCK = 0xc000;
+
+object FileType extends Enumeration {
+  val FIFO, CHR, DIR, BLK, REG, LNK, SOCK, UNK = Value;
+}
+import FileType.{Value => _, _};
+
+class FileInfo private[this](val devMajor: Int, val devMinor: Int,
+                            val ino: Long, val mode: Int, val nlink: Int,
+                            val uid: Int, val gid: Int,
+                            _rdevMinor: Int, _rdevMajor: Int,
+                            val size: Long,
+                            val blksize: Int, val blocks: Long,
+                            val atime: Date, val mtime: Date,
+                            val ctime: Date) {
+  def this(devMajor: Int, devMinor: Int, ino: Long,
+          mode: Int, nlink: Int, uid: Int, gid: Int,
+          rdevMinor: Int, rdevMajor: Int,
+          size: Long, blksize: Int, blocks: Long,
+          atime: Long, mtime: Long, ctime: Long) {
+    this(devMajor, devMinor, ino, mode, nlink, uid, gid,
+        rdevMajor, rdevMinor, size, blksize, blocks,
+        new Date(atime), new Date(mtime), new Date(ctime));
+  }
+  def perms: Int = mode&0xfff;
+  def ftype: FileType.Value = (mode&S_IFMT) match {
+    case S_IFIFO => FIFO
+    case S_IFCHR => CHR
+    case S_IFDIR => DIR
+    case S_IFBLK => BLK
+    case S_IFREG => REG
+    case S_IFLNK => LNK
+    case S_IFSOCK => SOCK
+    case _ => UNK
+  }
+  def isfifo: Boolean = ftype == FIFO
+  def ischr: Boolean = ftype == CHR
+  def isdir: Boolean = ftype == DIR
+  def isblk: Boolean = ftype == BLK
+  def isreg: Boolean = ftype == REG
+  def islnk: Boolean = ftype == LNK
+  def issock: Boolean = ftype == SOCK
+  def isdev: Boolean = ischr || isblk;
+  private[this] def mustBeDevice() {
+    if (!isdev) throw new IllegalArgumentException("Object is not a device");
+  }
+  def rdevMajor: Int = { mustBeDevice(); _rdevMajor }
+  def rdevMinor: Int = { mustBeDevice(); _rdevMinor }
+}
+@native protected def unlink(path: CString);
+def unlink(path: String) { unlink(path.toCString); }
+def unlink(file: File) { unlink(file.getPath); }
+@native protected def rmdir(path: CString);
+def rmdir(path: String) { rmdir(path.toCString); }
+def rmdir(file: File) { rmdir(file.getPath); }
+@native protected def mkdir(path: CString, mode: Int);
+def mkdir(path: String, mode: Int) { mkdir(path.toCString, mode); }
+def mkdir(path: String) { mkdir(path, 0x1ff); }
+def mkdir(file: File, mode: Int) { mkdir(file.getPath, mode); }
+def mkdir(file: File) { mkdir(file.getPath); }
+@native protected def mkfile(path: CString, mode: Int);
+def mkfile(path: String, mode: Int) { mkfile(path.toCString, mode); }
+def mkfile(path: String) { mkfile(path, 0x1b6); }
+def mkfile(file: File, mode: Int) { mkfile(file.getPath, mode); }
+def mkfile(file: File) { mkfile(file.getPath); }
+@native protected def rename(from: CString, to: CString);
+def rename(from: String, to: String) 
+  { rename(from.toCString, to.toCString); }
+def rename(from: File, to: File) 
+  { rename(from.getPath, to.getPath); }
+@native protected def stat(path: CString): FileInfo;
+def stat(path: String): FileInfo = stat(path.toCString);
+def stat(file: File): FileInfo = stat(file.getPath);
+@native protected def lstat(path: CString): FileInfo;
+def lstat(path: String): FileInfo = lstat(path.toCString);
+def lstat(file: File): FileInfo = lstat(file.getPath);
+
+@native protected def opendir(path: CString): Wrapper;
+@native protected def readdir(path: CString, dir: Wrapper): CString;
+@native protected def closedir(path: CString, dir: Wrapper);
+
+abstract class BaseDirIterator[T](cpath: CString)
+       extends LookaheadIterator[T] with Closeable {
+  def this(path: String) { this(path.toCString); }
+  def this(dir: File) { this(dir.getPath); }
+  override def close() { closedir(cpath, dir); }
+  override protected def finalize() { super.finalize(); close(); }
+  private[this] val dir = opendir(cpath);
+  protected def mangle(file: String): T;
+  override protected def fetch(): Option[T] = readdir(cpath, dir) match {
+    case null => None
+    case f => f.toJString match {
+      case "." | ".." => fetch()
+      case jf => Some(mangle(jf))
+    }
+  }
+}
+
+class DirIterator(val path: String) extends BaseDirIterator[String](path) {
+  def this(dir: File) { this(dir.getPath); }
+  override protected def mangle(file: String): String = file;
+}
+def listDir(path: String): List[String] = {
+  val iter = new DirIterator(path);
+  try { iter.toList }
+  finally { iter.close(); }
+}
+def listDir(dir: File): List[String] = listDir(dir.getPath);
+
+class DirFilesIterator private[this](val dir: File, cpath: CString)
+       extends BaseDirIterator[File](cpath) {
+  def this(dir: File) { this(dir, dir.getPath.toCString); }
+  def this(path: String) { this(new File(path), path.toCString); }
+  override protected def mangle(file: String): File = new File(dir, file);
+}
+def listDirFiles(path: String): List[File] = {
+  val iter = new DirFilesIterator(path);
+  try { iter.toList }
+  finally { iter.close(); }
+}
+def listDirFiles(dir: File): List[File] = listDirFiles(dir.getPath);
+
+@native protected def lock(path: CString, flags: Int): Wrapper;
+@native protected def unlock(lock: Wrapper);
+class FileLock(path: String, flags: Int) extends Closeable {
+  def this(file: File, flags: Int) { this(file.getPath, flags); }
+  def this(path: String) { this(path, LKF_EXCL); }
+  def this(file: File) { this(file.getPath, LKF_EXCL); }
+  private[this] val lk = lock(path.toCString, flags);
+  override def close() { unlock(lk); }
+  override protected def finalize() { super.finalize(); close(); }
+}
+def withLock[T](path: String, flags: Int)(body: => T): T = {
+  val lk = new FileLock(path, flags);
+  try { body; } finally { lk.close(); }
+}
+def withLock[T](file: File, flags: Int)(body: => T): T =
+  withLock(file.getPath, flags) { body }
+def withLock[T](path: String)(body: => T): T =
+  withLock(path, LKF_EXCL) { body }
+def withLock[T](file: File)(body: => T): T =
+  withLock(file.getPath, LKF_EXCL) { body }
+
+@native protected def connect(path: CString): Wrapper;
+@native def send(conn: Wrapper, buf: CString,
+                start: Int, len: Int);
+@native def recv(conn: Wrapper, buf: CString,
+                start: Int, len: Int): Int;
+@native def close(conn: Wrapper, how: Int);
+class Connection(path: String) extends Closeable {
+  def this(file: File) { this(file.getPath); }
+  private[this] val conn = connect(path.toCString);
+  override def close() { jni.close(conn, CF_CLOSEMASK); }
+  override protected def finalize() { super.finalize(); close(); }
+  class InputStream private[Connection] extends java.io.InputStream {
+    override def read(): Int = {
+      val buf = new Array[Byte](1);
+      val n = read(buf, 0, 1);
+      if (n < 0) -1 else buf(0)&0xff;
+    }
+    override def read(buf: Array[Byte]): Int =
+      read(buf, 0, buf.length);
+    override def read(buf: Array[Byte], start: Int, len: Int) =
+      recv(conn, buf, start, len);
+    override def close() { jni.close(conn, CF_CLOSERD); }
+  }
+  lazy val input = new InputStream;
+  class OutputStream private[Connection] extends java.io.OutputStream {
+    override def write(b: Int) { write(Array[Byte](b.toByte), 0, 1); }
+    override def write(buf: Array[Byte]) { write(buf, 0, buf.length); }
+    override def write(buf: Array[Byte], start: Int, len: Int)
+      { send(conn, buf, start, len); }
+    override def close() { jni.close(conn, CF_CLOSEWR); }
+  }
+  lazy val output = new OutputStream;
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+
+}
diff --git a/keys.scala b/keys.scala
new file mode 100644 (file)
index 0000000..f075159
--- /dev/null
@@ -0,0 +1,223 @@
+/* -*-scala-*-
+ *
+ * Key distribution
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted.tripe; package object keys {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import java.io.{Closeable, File, FileOutputStream, FileReader, IOException};
+
+import scala.collection.mutable.HashMap;
+
+/*----- Useful regular expressions ----------------------------------------*/
+
+val RX_COMMENT = """(?x) ^ \s* (?: \# .* )? $""".r;
+val RX_KEYVAL = """(?x) ^ \s*
+      ([-\w]+)
+      (?:\s+(?!=)|\s*=\s*)
+      (|\S|\S.*\S)
+      \s* $""".r;
+val RX_DOLLARSUBST = """(?x) \$ \{ ([-\w]+) \}""".r;
+
+/*----- Things that go wrong ----------------------------------------------*/
+
+class ConfigSyntaxError(val file: String, val lno: Int, val msg: String)
+       extends Exception {
+  override def getMessage(): String = s"$file:$lno: $msg";
+}
+
+class ConfigDefaultFailed(val file: String, val dfltkey: String,
+                         val badkey: String, val badval: String)
+       extends Exception {
+  override def getMessage(): String =
+    s"$file: can't default `$dfltkey' because " +
+         s"`$badval' is not a recognized value for `$badkey'";
+}
+
+class DefaultFailed(val key: String) extends Exception;
+
+/*----- Parsing a configuration -------------------------------------------*/
+
+type Config = scala.collection.Map[String, String];
+
+val DEFAULTS: Seq[(String, Config => String)] =
+  Seq("repos-base" -> { _ => "tripe-keys.tar.gz" },
+      "sig-base" -> { _ => "tripe-keys.sig-<SEQ>" },
+      "repos-url" -> { conf => conf("base-url") + conf("repos-base") },
+      "sig-url" -> { conf => conf("base-url") + conf("sig-base") },
+      "kx" -> { _ => "dh" },
+      "kx-genalg" -> { conf => conf("kx") match {
+       case alg@("dh" | "ec" | "x25519" | "x448") => alg
+       case _ => throw new DefaultFailed("kx")
+      } },
+      "kx-expire" -> { _ => "now + 1 year" },
+      "kx-warn-days" -> { _ => "28" },
+      "bulk" -> { _ => "iiv" },
+      "cipher" -> { conf => conf("bulk") match {
+       case "naclbox" => "salsa20"
+       case _ => "rijndael-cbc"
+      } },
+      "hash" -> { _ => "sha256" },
+      "mgf" -> { conf => conf("hash") + "-mgf" },
+      "mac" -> { conf => conf("bulk") match {
+       case "naclbox" => "poly1305/128"
+       case _ =>
+         val h = conf("hash");
+         JNI.hashsz(h) match {
+           case -1 => throw new DefaultFailed("hash")
+           case hsz => s"${h}-hmac/${4*hsz}"
+         }
+      } },
+      "sig" -> { conf => conf("kx") match {
+       case "dh" => "dsa"
+       case "ec" => "ecdsa"
+       case "x25519" => "ed25519"
+       case "x448" => "ed448"
+       case _ => throw new DefaultFailed("kx")
+      } },
+      "sig-fresh" -> { _ => "always" },
+      "fingerprint-hash" -> { _("hash") });
+
+def readConfig(path: String): Config = {
+  var m = HashMap[String, String]();
+  withCleaner { clean =>
+    var in = new FileReader(path); clean { in.close(); }
+    var lno = 1;
+    for (line <- lines(in)) {
+      line match {
+       case RX_COMMENT() => ();
+       case RX_KEYVAL(key, value) => m += key -> value;
+       case _ =>
+         throw new ConfigSyntaxError(path, lno, "failed to parse line");
+      }
+      lno += 1;
+    }
+  }
+
+  for ((key, dflt) <- DEFAULTS) {
+    if (!(m contains key)) {
+      try { m += key -> dflt(m); }
+      catch {
+       case e: DefaultFailed =>
+         throw new ConfigDefaultFailed(path, key, e.key, m(e.key));
+      }
+    }
+  }
+  m
+}
+
+/*----- Managing a key repository -----------------------------------------*/
+
+/* Lifecycle notes
+ *
+ *   -> empty
+ *
+ * insert config file via URL or something
+ *
+ *   -> pending (pending/tripe-keys.conf)
+ *
+ * verify master key fingerprint (against barcode?)
+ *
+ *   -> confirmed (live/tripe-keys.conf; no live/repos)
+ *   -> live (live/...)
+ *
+ * download package
+ * extract contents
+ * verify signature
+ * build keyrings
+ * build peer config
+ * rename tmp -> new
+ *
+ *   -> updating (live/...; new/...)
+ *
+ * rename old repository aside
+ *
+ *   -> committing (old/...; new/...)
+ *
+ * rename verified repository
+ *
+ *   -> live (live/...)
+ *
+ * (delete old/)
+ */
+
+object Repository {
+  object State extends Enumeration {
+    val Empty, Pending, Confirmed, Updating, Committing, Live = Value;
+  }
+
+}
+
+class Repository(val root: File) extends Closeable {
+  import Repository.State.{Value => State, _};
+
+  val livedir = new File(root, "live");
+  val livereposdir = new File(livedir, "repos");
+  val newdir = new File(root, "new");
+  val olddir = new File(root, "old");
+  val pendingdir = new File(root, "pending");
+  val tmpdir = new File(root, "tmp");
+
+  val lock = {
+    if (!root.isDirectory && !root.mkdir()) ???;
+    val chan = new FileOutputStream(new File(root, "lk")).getChannel;
+    chan.tryLock() match {
+      case null =>
+       throw new IOException(s"repository `${root.getPath}' locked")
+      case lk => lk
+    }
+  }
+
+  def close() {
+    lock.release();
+    lock.channel.close();
+  }
+
+  def state: State =
+    if (livedir.isDirectory) {
+      if (!livereposdir.isDirectory) Confirmed
+      else if (newdir.isDirectory && olddir.isDirectory) Committing
+      else Live
+    } else {
+      if (newdir.isDirectory) Updating
+      else if (pendingdir.isDirectory) Pending
+      else Empty
+    }
+
+  def commitState(): State = state match {
+    case Updating => rmTree(newdir); state
+    case Committing =>
+      if (!newdir.renameTo(livedir) && !olddir.renameTo(livedir))
+       throw new IOException("failed to commit update");
+      state
+    case st => st;
+
+  def clean() {
+       
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+
+}
index 456063895c1ad1f6ffa3c3d36e3a131ea201f88b..c4b82848e87bd6b50ad22101eb20e2fb5bfa54ae 100644 (file)
@@ -1,64 +1,25 @@
-package uk.org.distorted;
+package uk.org.distorted.tripe; package object test {
 
-import java.io.{InputStreamReader, OutputStreamWriter};
+import java.io.{BufferedReader, BufferedWriter,
+               InputStreamReader, OutputStreamWriter};
 import scala.collection.mutable.StringBuilder;
 import scala.util.control.Breaks;
 
-package object tripe {
-  def main(args: Array[String])
-  {
-    println("Hello from Scala");
-    JNI.test();
-    val toy = JNI.make();
-    for (i <- 0 until args.length) println(f"$i%2d: ${args(i)}%s");
-    //toy match { case toy: Array[Byte] => toy(1) = -1; case _ => () }
-    JNI.check(toy);
+def main(args: Array[String])
+{
+  val conn = new jni.Connection(args(0));
+  try {
+    val rd = new BufferedReader(new InputStreamReader(conn.input));
+    val wr = new BufferedWriter(new OutputStreamWriter(conn.output));
 
-    val conn = new Connection;
-    try {
-      val rd = new InputStreamReader(new ConnectionInputStream(conn));
-      val wr = new OutputStreamWriter(new ConnectionOutputStream(conn));
+    wr.write("Hello, world!\n"); wr.flush();
 
-      wr.write("Hello, world!\n"); wr.flush();
-
-      val buf = new Array[Char](4096);
-      val line = new StringBuilder;
-
-      val R = new Breaks;
-      val L = new Breaks;
-      var any = false;
-      R.breakable {
-       while (true) {
-         val n = rd.read(buf);
-         if (n <= 0) R.break;
-         var pos = 0;
-         L.breakable {
-           while (true) {
-             val nl = buf.indexOf('\n', pos);
-             if (nl == -1 || nl >= n) {
-               if (pos < n)
-                 { line.appendAll(buf, pos, n - pos); any = true; }
-               L.break;
-             }
-             val s = if (!any)
-               new String(buf, pos, nl - pos);
-             else {
-               line.appendAll(buf, pos, nl - pos);
-               val s = line.mkString;
-               line.clear(); any = false;
-               s
-             };
-             println(s"found line `$s'");
-             pos = nl + 1;
-           }
-         }
-       }
-      }
-
-      rd.close();
-      wr.close();
-    } finally {
-      conn.close();
-    }
+    for (line <- lines(rd)) println(s"found line `$line'");
+    rd.close();
+    wr.close();
+  } finally {
+    conn.close();
   }
 }
+
+}
diff --git a/peers.scala b/peers.scala
new file mode 100644 (file)
index 0000000..931c8f4
--- /dev/null
@@ -0,0 +1,359 @@
+package uk.org.distorted.tripe; package object peers {
+
+import java.io.{BufferedReader, File, FileReader, Reader};
+import java.net.{InetAddress, Inet4Address, Inet6Address,
+                UnknownHostException};
+
+import scala.collection.mutable.{HashMap, HashSet};
+import scala.concurrent.Channel;
+import scala.util.control.Breaks;
+import scala.util.matching.Regex;
+
+val RX_COMMENT = """(?x) ^ \s* (?: [;\#] .* )? $""".r;
+val RX_GRPHDR = """(?x) ^ \s* \[ (.*) \] \s* $""".r;
+val RX_ASSGN = """(?x) ^
+       ([^\s:=] (?: [^:=]* [^\s:=])?)
+       \s* [:=] \s*
+       (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
+       (?: \s+ (?: [;\#].*)? )? $""".r;
+val RX_CONT = """(?x) ^ \s+
+       (| [^\s\#;]\S* (?: \s+ [^\s\#;]\S*)*)
+       (?: \s+ (?: [;\#].*)? )? $""".r;
+val RX_REF = """(?x) \$ \( ([^)]+) \)""".r;
+val RX_RESOLVE = """(?x) \$ ([46*]*) \[ ([^\]]+) \]""".r;
+val RX_PARENT = """(?x) [^\s,]+""".r
+
+def with_cleaner[T](body: Cleaner => T): T = {
+  val cleaner = new Cleaner;
+  try { body(cleaner) }
+  finally { cleaner.cleanup(); }
+}
+
+class Cleaner {
+  var cleanups: List[() => Unit] = Nil;
+  def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
+  def cleanup() { cleanups foreach { _() } }
+}
+
+def lines(r: Reader) = new Traversable[String] {
+  val in: BufferedReader = new BufferedReader(r);
+  override def foreach[T](f: String => T) {
+    while (true) in.readLine match {
+      case null => return;
+      case line => f(line);
+    }
+  }
+}
+
+def thread(name: String, run: Boolean = true, daemon: Boolean = true)
+         (body: => Unit): Thread = {
+  val t = new Thread(new Runnable { override def run() { body } }, name);
+  t.setDaemon(daemon);
+  if (run) t.start();
+  t
+}
+
+object BulkResolver {
+  val BREAK = new Breaks;
+}
+
+class BulkResolver(val nthreads: Int = 8) {
+  import BulkResolver.BREAK._;
+  class Host(val name: String) {
+    var a4, a6: Seq[InetAddress] = Seq.empty;
+
+    def addaddr(a: InetAddress) { a match {
+      case _: Inet4Address => a4 +:= a;
+      case _: Inet6Address => a6 +:= a;
+      case _ => ();
+    } }
+
+    def get(flags: String): Seq[InetAddress] = {
+      var wanta4, wanta6, any, all = false;
+      var b = Seq.newBuilder[InetAddress];
+      for (ch <- flags) ch match {
+       case '*' => all = true;
+       case '4' => wanta4 = true; any = true;
+       case '6' => wanta6 = true; any = true;
+       case _ => ???
+      }
+      if (!any) { wanta4 = true; wanta6 = true; }
+      if (wanta4) b ++= a4;
+      if (wanta6) b ++= a6;
+      (all, b.result) match {
+       case (true, aa) => aa
+       case (false, aa@(Nil | Seq(_))) => aa
+       case (false, Seq(a, _*)) => Seq(a)
+      }
+    }
+  }
+  val ch = new Channel[Host];
+  val map = HashMap[String, Host]();
+  var preparing = true;
+
+  val workers = Array.tabulate(nthreads) { i =>
+    thread(s"resolver worker #$i") {
+      breakable {
+       while (true) {
+         val host = ch.read; if (host == null) break;
+println(s";; ${Thread.currentThread.getName} resolving `${host.name}'");
+         try {
+           for (a <- InetAddress.getAllByName(host.name)) host.addaddr(a);
+         } catch { case e: UnknownHostException => () }
+       }
+      }
+println(s";; ${Thread.currentThread.getName} done'");
+      ch.write(null);
+    }
+  }
+
+  def prepare(name: String) {
+println(s";; prepare host `$name'");
+    assert(preparing);
+    if (!(map contains name)) {
+      val host = new Host(name);
+      map(name) = host;
+      ch.write(host);
+    }
+  }
+
+  def finish() {
+    assert(preparing);
+    preparing = false;
+    ch.write(null);
+    for (t <- workers) t.join();
+  }
+
+  def resolve(name: String, flags: String): Seq[InetAddress] =
+    map(name).get(flags);
+}
+
+def fmtpath(path: Seq[String]) =
+  path.reverse map { i => s"`$i'" } mkString " -> ";
+
+class ConfigSyntaxError(val file: File, val lno: Int, val msg: String)
+       extends Exception {
+  override def getMessage(): String = s"$file:$lno: $msg";
+}
+class MissingConfigSection(val sect: String) extends Exception {
+  override def getMessage(): String =
+    s"missing configuration section `$sect'";
+}
+class MissingConfigItem(val sect: String, val key: String,
+                       val path: Seq[(String)]) extends Exception {
+  override def getMessage(): String = {
+    val msg = s"missing configuration item `$key' in section `$sect'";
+    if (path == Nil) msg
+    else msg + s" (wanted while expanding ${fmtpath(path)})"
+  }
+}
+class AmbiguousConfig(val key: String,
+                     val v0: String, val p0: Seq[String],
+                     val v1: String, val p1: Seq[String])
+       extends Exception {
+  override def getMessage(): String =
+    s"ambiguous answer resolving key `$key': " +
+    s"path ${fmtpath(p0)} yields `$v0', but ${fmtpath(p1)} yields `$v1'";
+}
+
+class ConfigCycle(val key: String, path: Seq[String]) extends Exception {
+  override def getMessage(): String =
+    s"found a cycle ${fmtpath(path)} looking up key `$key'";
+}
+class NoHostAddresses(val sect: String, val key: String, val host: String)
+       extends Exception {
+  override def getMessage(): String =
+    s"no addresses found for `$host' (key `$key' in section `$sect')";
+}
+
+object Config {
+  sealed abstract class ConfigCacheEntry;
+  case object StillLooking extends ConfigCacheEntry;
+  case object NotFound extends ConfigCacheEntry;
+  case class Found(value: String, path: Seq[String])
+    extends ConfigCacheEntry;
+}
+
+class Config { conf =>
+  import Config._;
+  class Section(val name: String) {
+    val itemmap = HashMap[String, String]();
+    val cache = HashMap[String, ConfigCacheEntry]();
+    override def toString: String = s"${getClass.getName}($name)";
+    def parents: Seq[Section] =
+      (itemmap.get("@inherit")
+       map { pp => (RX_PARENT.findAllIn(pp) map { conf.section _ }).toList }
+       getOrElse Nil);
+
+    def get_internal(key: String, path: Seq[String] = Nil):
+             Option[(String, Seq[String])] = {
+      val incpath = name +: path;
+
+      for (r <- cache.get(key)) r match {
+       case StillLooking => throw new ConfigCycle(key, incpath)
+       case NotFound => return None
+       case Found(v, p) => return Some((v, p ++ path));
+      }
+
+      for (v <- itemmap.get(key)) {
+       cache(key) = Found(v, Seq(name));
+       return Some((v, incpath));
+      }
+
+      cache(key) = StillLooking;
+
+      ((None: Option[(String, Seq[String])]) /: parents) { (st, parent) =>
+       parent.get_internal(key, incpath) match {
+         case None => st;
+         case newst@Some((v, p)) => st match {
+           case None => newst
+           case Some((vv, _)) if v == vv => st
+           case Some((vv, pp)) =>
+             throw new AmbiguousConfig(key, v, p, vv, pp)
+         }
+       }
+      } match {
+       case None => cache(key) = NotFound; None
+       case Some((v, p)) =>
+         cache(key) = Found(v, p dropRight path.length);
+         Some((v, p))
+      }
+    }
+
+    def get(key: String, resolve: Boolean = true,
+           path: Seq[String] = Nil): String = {
+      val v0 = key match {
+       case "name" => itemmap.getOrElse("name", name)
+       case _ => get_internal(key).
+         getOrElse(throw new MissingConfigItem(name, key, path))._1
+      }
+      expand(key, v0, resolve, path)
+    }
+
+    def expand(key: String, value: String, resolve: Boolean,
+              path: Seq[String]): String = {
+      val v1 = RX_REF.replaceAllIn(value, { m =>
+       Regex.quoteReplacement(get(m.group(1), resolve, path))
+      });
+      val v2 = if (!resolve) v1
+              else RX_RESOLVE.replaceAllIn(v1, { m =>
+                resolver.resolve(m.group(2), m.group(1)) match {
+                  case Nil =>
+                    throw new NoHostAddresses(name, key, m.group(2));
+                  case addrs =>
+                    Regex.quoteReplacement((addrs map { _.getHostAddress }).
+                                           mkString(" "));
+                }
+              })
+      v2
+    }
+
+    def items: Seq[String] = {
+      val b = Seq.newBuilder[String];
+      val seen = HashSet[String]();
+      val visiting = HashSet[String](name);
+      var stack = List(this);
+
+      while (stack != Nil) {
+       val sect = stack.head; stack = stack.tail;
+       for (k <- sect.itemmap.keys)
+         if (!(seen contains k)) { b += k; seen += k; }
+       for (p <- sect.parents)
+         if (!(visiting contains p.name))
+           { stack ::= p; visiting += p.name; }
+      }
+      b.result
+    }
+  }
+  val sectmap = new HashMap[String, Section];
+  def sections: Iterator[Section] = sectmap.values.iterator;
+  def section(name: String): Section =
+    sectmap.getOrElse(name, throw new MissingConfigSection(name));
+
+  val resolver = new BulkResolver;
+
+  def parseFile(path: File): this.type = {
+println(s";; parse ${path.getPath}");
+    with_cleaner { clean =>
+      val in = new FileReader(path); clean { in.close(); }
+
+      val lno = 1;
+      val b = new StringBuilder;
+      var key: String = null;
+      var sect: Section = null;
+      def flush() {
+       if (key != null) {
+         sect.itemmap(key) = b.result;
+println(s";; in `${sect.name}', set `$key' to `${b.result}'");
+         b.clear();
+         key = null;
+       }
+      }
+      for (line <- lines(in)) line match {
+       case RX_COMMENT() =>
+         ();
+       case RX_GRPHDR(grp) =>
+         flush();
+         sect = sectmap.getOrElseUpdate(grp, new Section(grp));
+       case RX_CONT(v) =>
+         if (key == null) {
+           throw new ConfigSyntaxError(
+             path, lno, "no config value to continue");
+         }
+         b += '\n'; b ++= v;
+       case RX_ASSGN(k, v) =>
+         if (sect == null) {
+           throw new ConfigSyntaxError(
+             path, lno, "no active section to update");
+         }
+         flush();
+         key = k; b ++= v;
+       case _ =>
+         throw new ConfigSyntaxError(path, lno, "incomprehensible line");
+      }
+      flush();
+    }
+    this
+  }
+  def parse(path: File): this.type = {
+    if (!path.isDirectory) parseFile(path);
+    else for {
+      f <- path.listFiles sortBy { _.getName };
+      name = f.getName;
+      if name.length > 0;
+      tail = name(name.length - 1);
+      if tail != '#' && tail != '~'
+    } parseFile(f);
+    this
+  }
+  def parse(path: String): this.type = parse(new File(path));
+
+  def analyse() {
+println(";; resolving all...");
+    for ((_, sect) <- sectmap) {
+println(s";; resolving in section `${sect.name}'...");
+      for (key <- sect.items) {
+println(s";;   resolving in key `$key'...");
+       val mm = RX_RESOLVE.findAllIn(sect.get(key, false));
+       for (host <- mm) { resolver.prepare(mm.group(2)); }
+      }
+    }
+    resolver.finish();
+
+    def dumpsect(sect: Section) {
+      for (k <- sect.items.filterNot(_.startsWith("@")).sorted)
+       println(s";;    `$k' -> `${sect.get(k)}'")
+    }
+    for (sect <- sectmap.values.toSeq sortBy { _.name })
+      if (sect.name.startsWith("@")) ();
+      else if (sect.name.startsWith("$")) {
+       println(s";; special section `${sect.name}'");
+       dumpsect(sect);
+      } else {
+       println(s";; peer section `${sect.name}'");
+       dumpsect(sect);
+      }
+  }
+}
+
+}
index 773b0af07b63a111e41080f93f2ba4b88b64b62d..1b52bf1d07060bd6e2fa971dbe1c3b2524ec616b 100644 (file)
@@ -1,11 +1,15 @@
 package uk.org.distorted.tripe;
 
-import java.io.{InputStream, OutputStream};
+import java.io.{Closeable, File, InputStream, OutputStream};
+import jni.Constants._;
 
-class Connection {
-  val conn = JNI.connect();
-  def close() { JNI.close(conn, JNI.CF_CLOSEMASK); }
-  override protected def finalize() { close(); }
+class Connection(path: File) extends Closeable {
+  def this(path: String) { this(new File(path)); }
+  val conn = jni.connect(path.getPath);
+  override def close() { jni.close(conn, CF_CLOSEMASK); }
+  lazy val input = new ConnectionInputStream(this);
+  lazy val output = new ConnectionOutputStream(this);
+  override protected def finalize() { super.finalize(); close(); }
 }
 
 class ConnectionInputStream(val conn: Connection) extends InputStream {
@@ -17,8 +21,8 @@ class ConnectionInputStream(val conn: Connection) extends InputStream {
   override def read(buf: Array[Byte]): Int =
     read(buf, 0, buf.length);
   override def read(buf: Array[Byte], start: Int, len: Int) =
-    JNI.recv(conn.conn, buf, start, len);
-  override def close() { JNI.close(conn.conn, JNI.CF_CLOSERD); }
+    jni.recv(conn.conn, buf, start, len);
+  override def close() { jni.close(conn.conn, CF_CLOSERD); }
 }
 
 class ConnectionOutputStream(val conn: Connection) extends OutputStream {
@@ -28,6 +32,6 @@ class ConnectionOutputStream(val conn: Connection) extends OutputStream {
   }
   override def write(buf: Array[Byte]) { write(buf, 0, buf.length); }
   override def write(buf: Array[Byte], start: Int, len: Int) =
-    JNI.send(conn.conn, buf, start, len);
-  override def close() { JNI.close(conn.conn, JNI.CF_CLOSEWR); }
+    jni.send(conn.conn, buf, start, len);
+  override def close() { jni.close(conn.conn, CF_CLOSEWR); }
 }
diff --git a/sys.scala b/sys.scala
new file mode 100644 (file)
index 0000000..a6c00c3
--- /dev/null
+++ b/sys.scala
@@ -0,0 +1,327 @@
+/* -*-scala-*-
+ *
+ * System calls and errors
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted.tripe; package object sys {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import scala.collection.mutable.HashSet;
+
+import java.io.File;
+
+import Magic._;
+
+/*----- Error codes -------------------------------------------------------*/
+
+object Errno extends Enumeration {
+  private[this] val tagmap = {
+    val b = Map.newBuilder[String, Int];
+    for (jni.ErrorEntry(tag, err) <- jni.errtab) b += tag -> err;
+    b.result
+  }
+  private[this] var wrong = -255;
+  private[this] val seen = HashSet[Int]();
+
+  class ErrnoVal private[Errno](tag: String, val code: Int, id: Int)
+       extends Val(id, tag) {
+    def message: String = jni.strerror(code).toJString;
+  }
+
+  private[this] def err(tag: String, code: Int): ErrnoVal = {
+    if (seen contains code) { wrong -= 1; new ErrnoVal(tag, code, wrong) }
+    else { seen += code; new ErrnoVal(tag, code, code) }
+  }
+  private[this] def err(tag: String): ErrnoVal = err(tag, tagmap(tag));
+
+  val OK = err("OK", 0);
+
+  /*
+     ;;; The errno name table is very boring to type.  To make life less
+     ;;; awful, put the errno names in this list and evaluate the code to
+     ;;; get Emacs to regenerate it.
+
+     (let ((errors '(EPERM ENOENT ESRCH EINTR EIO ENXIO E2BIG ENOEXEC EBADF
+                    ECHILD EAGAIN ENOMEM EACCES EFAULT ENOTBLK EBUSY EEXIST
+                    EXDEV ENODEV ENOTDIR EISDIR EINVAL ENFILE EMFILE ENOTTY
+                    ETXTBSY EFBIG ENOSPC ESPIPE EROFS EMLINK EPIPE EDOM
+                    ERANGE
+
+                    EDEADLK ENAMETOOLONG ENOLCK ENOSYS ENOTEMPTY ELOOP
+                    EWOULDBLOCK ENOMSG EIDRM ECHRNG EL2NSYNC EL3HLT EL3RST
+                    ELNRNG EUNATCH ENOCSI EL2HLT EBADE EBADR EXFULL ENOANO
+                    EBADRQC EBADSLT EDEADLOCK EBFONT ENOSTR ENODATA ETIME
+                    ENOSR ENONET ENOPKG EREMOTE ENOLINK EADV ESRMNT ECOMM
+                    EPROTO EMULTIHOP EDOTDOT EBADMSG EOVERFLOW ENOTUNIQ
+                    EBADFD EREMCHG ELIBACC ELIBBAD ELIBSCN ELIBMAX ELIBEXEC
+                    EILSEQ ERESTART ESTRPIPE EUSERS ENOTSOCK EDESTADDRREQ
+                    EMSGSIZE EPROTOTYPE ENOPROTOOPT EPROTONOSUPPORT
+                    ESOCKTNOSUPPORT EOPNOTSUPP EPFNOSUPPORT EAFNOSUPPORT
+                    EADDRINUSE EADDRNOTAVAIL ENETDOWN ENETUNREACH ENETRESET
+                    ECONNABORTED ECONNRESET ENOBUFS EISCONN ENOTCONN
+                    ESHUTDOWN ETOOMANYREFS ETIMEDOUT ECONNREFUSED EHOSTDOWN
+                    EHOSTUNREACH EALREADY EINPROGRESS ESTALE EUCLEAN ENOTNAM
+                    ENAVAIL EISNAM EREMOTEIO EDQUOT ENOMEDIUM EMEDIUMTYPE
+                    ECANCELED ENOKEY EKEYEXPIRED EKEYREVOKED EKEYREJECTED
+                    EOWNERDEAD ENOTRECOVERABLE ERFKILL EHWPOISON)))
+       (save-excursion
+        (goto-char (point-min))
+        (search-forward (concat "***" "BEGIN errtab" "***"))
+        (beginning-of-line 2)
+        (delete-region (point)
+                       (progn
+                         (search-forward "***END***")
+                         (beginning-of-line)
+                         (point)))
+        (dolist (err errors)
+          (insert (format "  val %s = err(\"%s\");\n" err err)))))
+  */
+  /***BEGIN errtab***/
+  val EPERM = err("EPERM");
+  val ENOENT = err("ENOENT");
+  val ESRCH = err("ESRCH");
+  val EINTR = err("EINTR");
+  val EIO = err("EIO");
+  val ENXIO = err("ENXIO");
+  val E2BIG = err("E2BIG");
+  val ENOEXEC = err("ENOEXEC");
+  val EBADF = err("EBADF");
+  val ECHILD = err("ECHILD");
+  val EAGAIN = err("EAGAIN");
+  val ENOMEM = err("ENOMEM");
+  val EACCES = err("EACCES");
+  val EFAULT = err("EFAULT");
+  val ENOTBLK = err("ENOTBLK");
+  val EBUSY = err("EBUSY");
+  val EEXIST = err("EEXIST");
+  val EXDEV = err("EXDEV");
+  val ENODEV = err("ENODEV");
+  val ENOTDIR = err("ENOTDIR");
+  val EISDIR = err("EISDIR");
+  val EINVAL = err("EINVAL");
+  val ENFILE = err("ENFILE");
+  val EMFILE = err("EMFILE");
+  val ENOTTY = err("ENOTTY");
+  val ETXTBSY = err("ETXTBSY");
+  val EFBIG = err("EFBIG");
+  val ENOSPC = err("ENOSPC");
+  val ESPIPE = err("ESPIPE");
+  val EROFS = err("EROFS");
+  val EMLINK = err("EMLINK");
+  val EPIPE = err("EPIPE");
+  val EDOM = err("EDOM");
+  val ERANGE = err("ERANGE");
+  val EDEADLK = err("EDEADLK");
+  val ENAMETOOLONG = err("ENAMETOOLONG");
+  val ENOLCK = err("ENOLCK");
+  val ENOSYS = err("ENOSYS");
+  val ENOTEMPTY = err("ENOTEMPTY");
+  val ELOOP = err("ELOOP");
+  val EWOULDBLOCK = err("EWOULDBLOCK");
+  val ENOMSG = err("ENOMSG");
+  val EIDRM = err("EIDRM");
+  val ECHRNG = err("ECHRNG");
+  val EL2NSYNC = err("EL2NSYNC");
+  val EL3HLT = err("EL3HLT");
+  val EL3RST = err("EL3RST");
+  val ELNRNG = err("ELNRNG");
+  val EUNATCH = err("EUNATCH");
+  val ENOCSI = err("ENOCSI");
+  val EL2HLT = err("EL2HLT");
+  val EBADE = err("EBADE");
+  val EBADR = err("EBADR");
+  val EXFULL = err("EXFULL");
+  val ENOANO = err("ENOANO");
+  val EBADRQC = err("EBADRQC");
+  val EBADSLT = err("EBADSLT");
+  val EDEADLOCK = err("EDEADLOCK");
+  val EBFONT = err("EBFONT");
+  val ENOSTR = err("ENOSTR");
+  val ENODATA = err("ENODATA");
+  val ETIME = err("ETIME");
+  val ENOSR = err("ENOSR");
+  val ENONET = err("ENONET");
+  val ENOPKG = err("ENOPKG");
+  val EREMOTE = err("EREMOTE");
+  val ENOLINK = err("ENOLINK");
+  val EADV = err("EADV");
+  val ESRMNT = err("ESRMNT");
+  val ECOMM = err("ECOMM");
+  val EPROTO = err("EPROTO");
+  val EMULTIHOP = err("EMULTIHOP");
+  val EDOTDOT = err("EDOTDOT");
+  val EBADMSG = err("EBADMSG");
+  val EOVERFLOW = err("EOVERFLOW");
+  val ENOTUNIQ = err("ENOTUNIQ");
+  val EBADFD = err("EBADFD");
+  val EREMCHG = err("EREMCHG");
+  val ELIBACC = err("ELIBACC");
+  val ELIBBAD = err("ELIBBAD");
+  val ELIBSCN = err("ELIBSCN");
+  val ELIBMAX = err("ELIBMAX");
+  val ELIBEXEC = err("ELIBEXEC");
+  val EILSEQ = err("EILSEQ");
+  val ERESTART = err("ERESTART");
+  val ESTRPIPE = err("ESTRPIPE");
+  val EUSERS = err("EUSERS");
+  val ENOTSOCK = err("ENOTSOCK");
+  val EDESTADDRREQ = err("EDESTADDRREQ");
+  val EMSGSIZE = err("EMSGSIZE");
+  val EPROTOTYPE = err("EPROTOTYPE");
+  val ENOPROTOOPT = err("ENOPROTOOPT");
+  val EPROTONOSUPPORT = err("EPROTONOSUPPORT");
+  val ESOCKTNOSUPPORT = err("ESOCKTNOSUPPORT");
+  val EOPNOTSUPP = err("EOPNOTSUPP");
+  val EPFNOSUPPORT = err("EPFNOSUPPORT");
+  val EAFNOSUPPORT = err("EAFNOSUPPORT");
+  val EADDRINUSE = err("EADDRINUSE");
+  val EADDRNOTAVAIL = err("EADDRNOTAVAIL");
+  val ENETDOWN = err("ENETDOWN");
+  val ENETUNREACH = err("ENETUNREACH");
+  val ENETRESET = err("ENETRESET");
+  val ECONNABORTED = err("ECONNABORTED");
+  val ECONNRESET = err("ECONNRESET");
+  val ENOBUFS = err("ENOBUFS");
+  val EISCONN = err("EISCONN");
+  val ENOTCONN = err("ENOTCONN");
+  val ESHUTDOWN = err("ESHUTDOWN");
+  val ETOOMANYREFS = err("ETOOMANYREFS");
+  val ETIMEDOUT = err("ETIMEDOUT");
+  val ECONNREFUSED = err("ECONNREFUSED");
+  val EHOSTDOWN = err("EHOSTDOWN");
+  val EHOSTUNREACH = err("EHOSTUNREACH");
+  val EALREADY = err("EALREADY");
+  val EINPROGRESS = err("EINPROGRESS");
+  val ESTALE = err("ESTALE");
+  val EUCLEAN = err("EUCLEAN");
+  val ENOTNAM = err("ENOTNAM");
+  val ENAVAIL = err("ENAVAIL");
+  val EISNAM = err("EISNAM");
+  val EREMOTEIO = err("EREMOTEIO");
+  val EDQUOT = err("EDQUOT");
+  val ENOMEDIUM = err("ENOMEDIUM");
+  val EMEDIUMTYPE = err("EMEDIUMTYPE");
+  val ECANCELED = err("ECANCELED");
+  val ENOKEY = err("ENOKEY");
+  val EKEYEXPIRED = err("EKEYEXPIRED");
+  val EKEYREVOKED = err("EKEYREVOKED");
+  val EKEYREJECTED = err("EKEYREJECTED");
+  val EOWNERDEAD = err("EOWNERDEAD");
+  val ENOTRECOVERABLE = err("ENOTRECOVERABLE");
+  val ERFKILL = err("ERFKILL");
+  val EHWPOISON = err("EHWPOISON");
+  /***end***/
+}
+import Errno.{Value => _, _};
+
+object SystemError {
+  def apply(err: Errno.Value, what: String): SystemError =
+    new SystemError(err, what);
+  def unapply(e: Exception): Option[(Errno.Value, String)] = e match {
+    case e: SystemError => Some((e.err, e.what))
+    case _ => None
+  }
+}
+
+class SystemError private[this](val err: Errno.ErrnoVal, val what: String)
+       extends Exception {
+  def this(err: Errno.Value, what: String)
+    { this(err.asInstanceOf[Errno.ErrnoVal], what); }
+  private[tripe] def this(err: Int, what: CString)
+    { this(Errno(err), what.toJString); }
+  override def getMessage(): String = s"$what: ${err.message}";
+}
+
+/*----- Filesystem hacks --------------------------------------------------*/
+
+def freshFile(d: File): File = {
+  /* Return the name of a freshly created file in directory D. */
+
+  val buf = new Array[Byte](6);
+  val b = new StringBuilder;
+
+  while (true) {
+    /* Keep going until we find a fresh one. */
+
+    /* Provide a prefix.  Mostly this is to prevent the file starting with
+     * an unfortunate character like `-'.
+     */
+    b ++= "tmp.";
+
+    /* Generate some random bytes. */
+    rng.nextBytes(buf);
+
+    /* Now turn the bytes into a filename.  This is a cheesy implementation
+     * of Base64 encoding.
+     */
+    var a = 0;
+    var n = 0;
+
+    for (x <- buf) {
+      a = (a << 8) | x; n += 8;
+      while (n >= 6) {
+       val y = (a >> n - 6)&0x3f; n -= 6;
+       b += (if (y < 26) 'A' + y
+             else if (y < 52) 'a' + (y - 26)
+             else if (y < 62) '0' + (y - 52)
+             else if (y == 62) '+'
+             else '-').toChar;
+      }
+    }
+
+    /* Make the filename, and try to create the file.  If we succeed, we
+     * win.
+     */
+    val f = new File(d, b.result); b.clear();
+    try { jni.mkfile(f); return f; }
+    catch { case SystemError(EEXIST, _) => (); }
+  }
+
+  /* We shouldn't get here, but the type checker needs placating. */
+  unreachable("unreachable");
+}
+
+def rmTree(f: File) {
+  def walk(f: File) {
+    if (jni.stat(f).isdir) {
+      closing(new jni.DirFilesIterator(f)) { _ foreach(walk _) }
+      try { jni.rmdir(f); }
+      catch { case SystemError(ENOENT, _) => (); }
+    } else {
+      try { jni.unlink(f); }
+      catch { case SystemError(ENOENT, _) => (); }
+    }
+  }
+  walk(f);
+}
+def rmTree(path: String) { rmTree(new File(path)); }
+
+def fileExists(path: String): Boolean =
+  try { jni.stat(path); true }
+  catch { case SystemError(ENOENT, _) => false };
+def fileExists(file: File): Boolean = fileExists(file.getPath);
+
+/*----- That's all, folks -------------------------------------------------*/
+
+}
diff --git a/util.scala b/util.scala
new file mode 100644 (file)
index 0000000..8eba6f8
--- /dev/null
@@ -0,0 +1,412 @@
+/* -*-scala-*-
+ *
+ * Miscellaneous utilities
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted; package object tripe {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import scala.concurrent.duration.{Deadline, Duration};
+import scala.util.control.Breaks;
+
+import java.io.{BufferedReader, Closeable, File, Reader};
+import java.net.{URL, URLConnection};
+import java.nio.{ByteBuffer, CharBuffer};
+import java.nio.charset.Charset;
+import java.util.concurrent.locks.{Lock, ReentrantLock};
+
+/*----- Miscellaneous useful things ---------------------------------------*/
+
+val rng = new java.security.SecureRandom;
+
+def unreachable(msg: String): Nothing = throw new AssertionError(msg);
+
+/*----- Various pieces of implicit magic ----------------------------------*/
+
+class InvalidCStringException(msg: String) extends Exception(msg);
+type CString = Array[Byte];
+
+object Magic {
+
+  /* --- Syntactic sugar for locks --- */
+
+  implicit class LockOps(lk: Lock) {
+    /* LK withLock { BODY }
+     * LK.withLock(INTERRUPT) { BODY }
+     * LK.withLock(DUR, [INTERRUPT]) { BODY } orelse { ALT }
+     * LK.withLock(DL, [INTERRUPT]) { BODY } orelse { ALT }
+     *
+     * Acquire a lock while executing a BODY.  If a duration or deadline is
+     * given then wait so long for the lock, and then give up and run ALT
+     * instead.
+     */
+
+    def withLock[T](dur: Duration, interrupt: Boolean)
+                  (body: => T): PendingLock[T] =
+      new PendingLock(lk, if (dur > Duration.Zero) dur else Duration.Zero,
+                     interrupt, body);
+    def withLock[T](dur: Duration)(body: => T): PendingLock[T] =
+      withLock(dur, true)(body);
+    def withLock[T](dl: Deadline, interrupt: Boolean)
+                  (body: => T): PendingLock[T] =
+      new PendingLock(lk, dl.timeLeft, interrupt, body);
+    def withLock[T](dl: Deadline)(body: => T): PendingLock[T] =
+      withLock(dl, true)(body);
+    def withLock[T](interrupt: Boolean)(body: => T): T = {
+      if (interrupt) lk.lockInterruptibly();
+      else lk.lock();
+      try { body; } finally lk.unlock();
+    }
+    def withLock[T](body: => T): T = withLock(true)(body);
+  }
+
+  class PendingLock[T] private[Magic]
+         (val lk: Lock, val dur: Duration,
+          val interrupt: Boolean, body: => T) {
+    /* An auxiliary class for LockOps; provides the `orelse' qualifier. */
+
+    def orelse(alt: => T): T = {
+      val locked = (dur, interrupt) match {
+       case (Duration.Inf, true) => lk.lockInterruptibly(); true
+       case (Duration.Inf, false) => lk.lock(); true
+       case (Duration.Zero, false) => lk.tryLock()
+       case (_, true) => lk.tryLock(dur.length, dur.unit)
+       case _ => unreachable("timed wait is always interruptible");
+      }
+      if (!locked) alt;
+      else try { body; } finally lk.unlock();
+    }
+  }
+
+  /* --- Conversion to/from C strings --- */
+
+  implicit class ConvertJStringToCString(s: String) {
+    /* Magic to convert a string into a C string (null-terminated bytes). */
+
+    def toCString: CString = {
+      /* Convert the receiver to a C string.
+       *
+       * We do this by hand, rather than relying on the JNI's built-in
+       * conversions, because we use the default encoding taken from the
+       * locale settings, rather than the ridiculous `modified UTF-8' which
+       * is (a) insensitive to the user's chosen locale and (b) not actually
+       * UTF-8 either.
+       */
+
+      val enc = Charset.defaultCharset.newEncoder;
+      val in = CharBuffer.wrap(s);
+      var sz: Int = (s.length*enc.averageBytesPerChar + 1).toInt;
+      var out = ByteBuffer.allocate(sz);
+
+      while (true) {
+       /* If there's still stuff to encode, then encode it.  Otherwise,
+        * there must be some dregs left in the encoder, so flush them out.
+        */
+       val r = if (in.hasRemaining) enc.encode(in, out, true)
+               else enc.flush(out);
+
+       /* Sift through the wreckage to figure out what to do. */
+       if (r.isError) r.throwException();
+       else if (r.isOverflow) {
+         /* No space in the buffer.  Make it bigger. */
+
+         sz *= 2;
+         val newout = ByteBuffer.allocate(sz);
+         out.flip(); newout.put(out);
+         out = newout;
+       } else if (r.isUnderflow) {
+         /* All done.  Check that there are no unexpected zero bytes -- so
+          * this will indeed be a valid C string -- and convert into a byte
+          * array that the C code will be able to pick apart.
+          */
+
+         out.flip(); val n = out.limit; val u = out.array;
+         if ({val z = u.indexOf(0); 0 <= z && z < n})
+           throw new InvalidCStringException("null byte in encoding");
+         val v = new Array[Byte](n + 1);
+         out.array.copyToArray(v, 0, n);
+         v(n) = 0;
+         return v;
+       }
+      }
+
+      /* Placate the type checker. */
+      unreachable("unreachable");
+    }
+  }
+
+  implicit class ConvertCStringToJString(v: CString) {
+    /* Magic to convert a C string into a `proper' string. */
+
+    def toJString: String = {
+      /* Convert the receiver to a C string.
+       *
+       * We do this by hand, rather than relying on the JNI's built-in
+       * conversions, because we use the default encoding taken from the
+       * locale settings, rather than the ridiculous `modified UTF-8' which
+       * is (a) insensitive to the user's chosen locale and (b) not actually
+       * UTF-8 either.
+       */
+
+      val inlen = v.indexOf(0) match {
+       case -1 => v.length
+       case n => n
+      }
+      val dec = Charset.defaultCharset.newDecoder;
+      val in = ByteBuffer.wrap(v, 0, inlen);
+      dec.decode(in).toString
+    }
+  }
+}
+
+/*----- Cleanup assistant -------------------------------------------------*/
+
+class Cleaner {
+  /* A helper class for avoiding deep nests of `try'/`finally'.
+   *
+   * Make a `Cleaner' instance CL at the start of your operation.  Apply it
+   * to blocks of code -- as CL { ACTION } -- as you proceed, to accumulate
+   * cleanup actions.   Finally, call CL.cleanup() to invoke the accumulated
+   * actions, in reverse order.
+   */
+
+  var cleanups: List[() => Unit] = Nil;
+  def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
+  def cleanup() { cleanups foreach { _() } }
+}
+
+def withCleaner[T](body: Cleaner => T): T = {
+  /* An easier way to use the `Cleaner' class.  Just
+   *
+   *   withCleaner { CL => BODY }
+   *
+   * The BODY can attach cleanup actions to the cleaner CL by saying
+   * CL { ACTION } as usual.  When the BODY exits, normally or otherwise, the
+   * cleanup actions are invoked in reverse order.
+   */
+
+  val cleaner = new Cleaner;
+  try { body(cleaner) }
+  finally { cleaner.cleanup(); }
+}
+
+def closing[T, U <: Closeable](thing: U)(body: U => T): T =
+  try { body(thing) }
+  finally { thing.close(); }
+
+/*----- A gadget for fetching URLs ----------------------------------------*/
+
+class URLFetchException(msg: String) extends Exception(msg);
+
+trait URLFetchCallbacks {
+  def preflight(conn: URLConnection) { }
+  def write(buf: Array[Byte], n: Int, len: Int): Unit;
+  def done(win: Boolean) { }
+}
+
+def fetchURL(url: URL, cb: URLFetchCallbacks) {
+  /* Fetch the URL, feeding the data through the callbacks CB. */
+
+  withCleaner { clean =>
+    var win: Boolean = false;
+    clean { cb.done(win); }
+
+    /* Set up the connection, and run a preflight check. */
+    val c = url.openConnection();
+    cb.preflight(c);
+
+    /* Start fetching data. */
+    val in = c.getInputStream; clean { in.close(); }
+    val explen = c.getContentLength();
+
+    /* Read a buffer at a time, and give it to the callback.  Maintain a
+     * running total.
+     */
+    val buf = new Array[Byte](4096);
+    var n = 0;
+    var len = 0;
+    while ({n = in.read(buf); n >= 0 && (explen == -1 || len <= explen)}) {
+      cb.write(buf, n, len);
+      len += n;
+    }
+
+    /* I can't find it documented anywhere that the existing machinery
+     * checks the received stream against the advertised content length.
+     * It doesn't hurt to check again, anyway.
+     */
+    if (explen != -1 && explen != len) {
+      throw new URLFetchException(
+       s"received $len /= $explen bytes from `$url'");
+    }
+
+    /* Glorious success is ours. */
+    win = true;
+  }
+}
+
+/*----- Running processes -------------------------------------------------*/
+
+//def runProgram(
+
+/*----- Threading things --------------------------------------------------*/
+
+def thread[T](name: String, run: Boolean = true, daemon: Boolean = true)
+            (f: => T): Thread = {
+  /* Make a thread with a given name, and maybe start running it. */
+
+  val t = new Thread(new Runnable { def run() { f; } }, name);
+  if (daemon) t.setDaemon(true);
+  if (run) t.start();
+  t
+}
+
+/*----- Quoting and parsing tokens ----------------------------------------*/
+
+def quoteTokens(v: Seq[String]): String = {
+  /* Return a string representing the token sequence V.
+   *
+   * The tokens are quoted as necessary.
+   */
+
+  val b = new StringBuilder;
+  var sep = false;
+  for (s <- v) {
+
+    /* If this isn't the first word, then write a separating space. */
+    if (!sep) sep = true;
+    else b += ' ';
+
+    /* Decide how to handle this token. */
+    if (s.length > 0 &&
+       (s forall { ch => (ch != ''' && ch != '"' && ch != '\\' &&
+                          !ch.isWhitespace) })) {
+      /* If this word is nonempty and contains no problematic characters,
+       * we can write it literally.
+       */
+
+      b ++= s;
+    } else {
+      /* Otherwise, we shall have to do this the hard way.  We could be
+       * cleverer about this, but it's not worth the effort.
+       */
+
+      b += '"';
+      s foreach { ch =>
+       if (ch == '"' || ch == '\\') b += '\\';
+       b += ch;
+      }
+      b += '"';
+    }
+  }
+  b.result
+}
+
+class InvalidQuotingException(msg: String) extends Exception(msg);
+
+def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
+  /* Parse the next token from a string S.
+   *
+   * If there is a token in S starting at or after index POS, then return
+   * it, and the index for the following token; otherwise return `None'.
+   */
+
+  val b = new StringBuilder;
+  val n = s.length;
+  var i = pos;
+  var q = 0;
+
+  /* Skip whitespace while we find the next token. */
+  while (i < n && s(i).isWhitespace) i += 1;
+
+  /* Maybe there just isn't anything to find. */
+  if (i >= n) return None;
+
+  /* There is something there.  Unpick the quoting and escaping. */
+  while (i < n && (q != 0 || !s(i).isWhitespace)) {
+    s(i) match {
+      case '\\' =>
+       if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
+       b += s(i + 1); i += 2;
+      case ch@('"' | ''') =>
+       if (q == 0) q = ch;
+       else if (q == ch) q = 0;
+       else b += ch;
+       i += 1;
+      case ch =>
+       b += ch;
+       i += 1;
+    }
+  }
+
+  /* Check that the quoting was valid. */
+  if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
+
+  /* Skip whitespace before the next token. */
+  while (i < n && s(i).isWhitespace) i += 1;
+
+  /* We're done. */
+  Some((b.result, i))
+}
+
+def splitTokens(s: String, pos: Int = 0): Seq[String] = {
+  /* Return all of the tokens in string S into tokens, starting at POS. */
+
+  val b = List.newBuilder[String];
+  var i = pos;
+
+  while (nextToken(s, i) match {
+    case Some((w, j)) => b += w; i = j; true
+    case None => false
+  }) ();
+  b.result
+}
+
+trait LookaheadIterator[T] extends BufferedIterator[T] {
+  private[this] var st: Option[T] = None;
+  protected def fetch(): Option[T];
+  private[this] def peek() {
+    if (st == None) fetch() match {
+      case None => st = null;
+      case x@Some(_) => st = x;
+    }
+  }
+  override def hasNext: Boolean = { peek(); st != null }
+  override def head(): T =
+    { peek(); if (st == null) throw new NoSuchElementException; st.get }
+  override def next(): T = { val it = head(); st = None; it }
+}
+
+def lines(r: Reader) = new LookaheadIterator[String] {
+  /* Iterates over the lines of text in a `Reader' object. */
+
+  private[this] val in = r match {
+    case br: BufferedReader => br;
+    case _ => new BufferedReader(r);
+  }
+  protected override def fetch(): Option[String] = Option(in.readLine);
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+
+}