From 8eabb4ff13562f3550499ee599297f7e97fa8754 Mon Sep 17 00:00:00 2001 From: Mark Wooding Date: Thu, 31 May 2018 14:11:37 +0100 Subject: [PATCH] wip --- .skelrc | 9 + Makefile | 24 +- admin.scala | 326 ++++++++++++--- jni.c | 1264 +++++++++++++++++++++++++++++++++++++++++++++++++++-------- jni.java | 22 -- jni.scala | 246 ++++++++++++ keys.scala | 223 +++++++++++ main.scala | 73 +--- peers.scala | 359 +++++++++++++++++ sock.scala | 22 +- sys.scala | 327 ++++++++++++++++ util.scala | 412 +++++++++++++++++++ 12 files changed, 2990 insertions(+), 317 deletions(-) create mode 100644 .skelrc delete mode 100644 jni.java create mode 100644 jni.scala create mode 100644 keys.scala create mode 100644 peers.scala create mode 100644 sys.scala create mode 100644 util.scala diff --git a/.skelrc b/.skelrc new file mode 100644 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)) diff --git a/Makefile b/Makefile index cd8e4a4..a03ba64 100644 --- 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)) diff --git a/admin.scala b/admin.scala index 85978fe..cc82186 100644 --- a/admin.scala +++ b/admin.scala @@ -1,72 +1,278 @@ -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 . + */ + +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 9f9e44d..fd91d0c 100644 --- 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 . + */ + +/*----- Header files ------------------------------------------------------*/ + +#define _FILE_OFFSET_BITS 64 + #include +#include #include #include -#include +#include #include #include #include @@ -10,207 +40,1122 @@ #include #include +#include +#include #include +#include #include +#include + +#include +#include +#include +#include + +#include #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, ";; \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, "", "(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, "", + "(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, ©p); - 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, "", "(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, ©p); + 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, ©p); + 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 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 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 . + */ + +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 index 0000000..f075159 --- /dev/null +++ b/keys.scala @@ -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 . + */ + +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-" }, + "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 -------------------------------------------------*/ + +} diff --git a/main.scala b/main.scala index 4560638..c4b8284 100644 --- a/main.scala +++ b/main.scala @@ -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 index 0000000..931c8f4 --- /dev/null +++ b/peers.scala @@ -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); + } + } +} + +} diff --git a/sock.scala b/sock.scala index 773b0af..1b52bf1 100644 --- a/sock.scala +++ b/sock.scala @@ -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 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 . + */ + +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 index 0000000..8eba6f8 --- /dev/null +++ b/util.scala @@ -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 . + */ + +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 -------------------------------------------------*/ + +} -- 2.11.0