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