chiark / gitweb /
wip
[tripe-android] / util.scala
diff --git a/util.scala b/util.scala
new file mode 100644 (file)
index 0000000..8eba6f8
--- /dev/null
@@ -0,0 +1,412 @@
+/* -*-scala-*-
+ *
+ * Miscellaneous utilities
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted; package object tripe {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import scala.concurrent.duration.{Deadline, Duration};
+import scala.util.control.Breaks;
+
+import java.io.{BufferedReader, Closeable, File, Reader};
+import java.net.{URL, URLConnection};
+import java.nio.{ByteBuffer, CharBuffer};
+import java.nio.charset.Charset;
+import java.util.concurrent.locks.{Lock, ReentrantLock};
+
+/*----- Miscellaneous useful things ---------------------------------------*/
+
+val rng = new java.security.SecureRandom;
+
+def unreachable(msg: String): Nothing = throw new AssertionError(msg);
+
+/*----- Various pieces of implicit magic ----------------------------------*/
+
+class InvalidCStringException(msg: String) extends Exception(msg);
+type CString = Array[Byte];
+
+object Magic {
+
+  /* --- Syntactic sugar for locks --- */
+
+  implicit class LockOps(lk: Lock) {
+    /* LK withLock { BODY }
+     * LK.withLock(INTERRUPT) { BODY }
+     * LK.withLock(DUR, [INTERRUPT]) { BODY } orelse { ALT }
+     * LK.withLock(DL, [INTERRUPT]) { BODY } orelse { ALT }
+     *
+     * Acquire a lock while executing a BODY.  If a duration or deadline is
+     * given then wait so long for the lock, and then give up and run ALT
+     * instead.
+     */
+
+    def withLock[T](dur: Duration, interrupt: Boolean)
+                  (body: => T): PendingLock[T] =
+      new PendingLock(lk, if (dur > Duration.Zero) dur else Duration.Zero,
+                     interrupt, body);
+    def withLock[T](dur: Duration)(body: => T): PendingLock[T] =
+      withLock(dur, true)(body);
+    def withLock[T](dl: Deadline, interrupt: Boolean)
+                  (body: => T): PendingLock[T] =
+      new PendingLock(lk, dl.timeLeft, interrupt, body);
+    def withLock[T](dl: Deadline)(body: => T): PendingLock[T] =
+      withLock(dl, true)(body);
+    def withLock[T](interrupt: Boolean)(body: => T): T = {
+      if (interrupt) lk.lockInterruptibly();
+      else lk.lock();
+      try { body; } finally lk.unlock();
+    }
+    def withLock[T](body: => T): T = withLock(true)(body);
+  }
+
+  class PendingLock[T] private[Magic]
+         (val lk: Lock, val dur: Duration,
+          val interrupt: Boolean, body: => T) {
+    /* An auxiliary class for LockOps; provides the `orelse' qualifier. */
+
+    def orelse(alt: => T): T = {
+      val locked = (dur, interrupt) match {
+       case (Duration.Inf, true) => lk.lockInterruptibly(); true
+       case (Duration.Inf, false) => lk.lock(); true
+       case (Duration.Zero, false) => lk.tryLock()
+       case (_, true) => lk.tryLock(dur.length, dur.unit)
+       case _ => unreachable("timed wait is always interruptible");
+      }
+      if (!locked) alt;
+      else try { body; } finally lk.unlock();
+    }
+  }
+
+  /* --- Conversion to/from C strings --- */
+
+  implicit class ConvertJStringToCString(s: String) {
+    /* Magic to convert a string into a C string (null-terminated bytes). */
+
+    def toCString: CString = {
+      /* Convert the receiver to a C string.
+       *
+       * We do this by hand, rather than relying on the JNI's built-in
+       * conversions, because we use the default encoding taken from the
+       * locale settings, rather than the ridiculous `modified UTF-8' which
+       * is (a) insensitive to the user's chosen locale and (b) not actually
+       * UTF-8 either.
+       */
+
+      val enc = Charset.defaultCharset.newEncoder;
+      val in = CharBuffer.wrap(s);
+      var sz: Int = (s.length*enc.averageBytesPerChar + 1).toInt;
+      var out = ByteBuffer.allocate(sz);
+
+      while (true) {
+       /* If there's still stuff to encode, then encode it.  Otherwise,
+        * there must be some dregs left in the encoder, so flush them out.
+        */
+       val r = if (in.hasRemaining) enc.encode(in, out, true)
+               else enc.flush(out);
+
+       /* Sift through the wreckage to figure out what to do. */
+       if (r.isError) r.throwException();
+       else if (r.isOverflow) {
+         /* No space in the buffer.  Make it bigger. */
+
+         sz *= 2;
+         val newout = ByteBuffer.allocate(sz);
+         out.flip(); newout.put(out);
+         out = newout;
+       } else if (r.isUnderflow) {
+         /* All done.  Check that there are no unexpected zero bytes -- so
+          * this will indeed be a valid C string -- and convert into a byte
+          * array that the C code will be able to pick apart.
+          */
+
+         out.flip(); val n = out.limit; val u = out.array;
+         if ({val z = u.indexOf(0); 0 <= z && z < n})
+           throw new InvalidCStringException("null byte in encoding");
+         val v = new Array[Byte](n + 1);
+         out.array.copyToArray(v, 0, n);
+         v(n) = 0;
+         return v;
+       }
+      }
+
+      /* Placate the type checker. */
+      unreachable("unreachable");
+    }
+  }
+
+  implicit class ConvertCStringToJString(v: CString) {
+    /* Magic to convert a C string into a `proper' string. */
+
+    def toJString: String = {
+      /* Convert the receiver to a C string.
+       *
+       * We do this by hand, rather than relying on the JNI's built-in
+       * conversions, because we use the default encoding taken from the
+       * locale settings, rather than the ridiculous `modified UTF-8' which
+       * is (a) insensitive to the user's chosen locale and (b) not actually
+       * UTF-8 either.
+       */
+
+      val inlen = v.indexOf(0) match {
+       case -1 => v.length
+       case n => n
+      }
+      val dec = Charset.defaultCharset.newDecoder;
+      val in = ByteBuffer.wrap(v, 0, inlen);
+      dec.decode(in).toString
+    }
+  }
+}
+
+/*----- Cleanup assistant -------------------------------------------------*/
+
+class Cleaner {
+  /* A helper class for avoiding deep nests of `try'/`finally'.
+   *
+   * Make a `Cleaner' instance CL at the start of your operation.  Apply it
+   * to blocks of code -- as CL { ACTION } -- as you proceed, to accumulate
+   * cleanup actions.   Finally, call CL.cleanup() to invoke the accumulated
+   * actions, in reverse order.
+   */
+
+  var cleanups: List[() => Unit] = Nil;
+  def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
+  def cleanup() { cleanups foreach { _() } }
+}
+
+def withCleaner[T](body: Cleaner => T): T = {
+  /* An easier way to use the `Cleaner' class.  Just
+   *
+   *   withCleaner { CL => BODY }
+   *
+   * The BODY can attach cleanup actions to the cleaner CL by saying
+   * CL { ACTION } as usual.  When the BODY exits, normally or otherwise, the
+   * cleanup actions are invoked in reverse order.
+   */
+
+  val cleaner = new Cleaner;
+  try { body(cleaner) }
+  finally { cleaner.cleanup(); }
+}
+
+def closing[T, U <: Closeable](thing: U)(body: U => T): T =
+  try { body(thing) }
+  finally { thing.close(); }
+
+/*----- A gadget for fetching URLs ----------------------------------------*/
+
+class URLFetchException(msg: String) extends Exception(msg);
+
+trait URLFetchCallbacks {
+  def preflight(conn: URLConnection) { }
+  def write(buf: Array[Byte], n: Int, len: Int): Unit;
+  def done(win: Boolean) { }
+}
+
+def fetchURL(url: URL, cb: URLFetchCallbacks) {
+  /* Fetch the URL, feeding the data through the callbacks CB. */
+
+  withCleaner { clean =>
+    var win: Boolean = false;
+    clean { cb.done(win); }
+
+    /* Set up the connection, and run a preflight check. */
+    val c = url.openConnection();
+    cb.preflight(c);
+
+    /* Start fetching data. */
+    val in = c.getInputStream; clean { in.close(); }
+    val explen = c.getContentLength();
+
+    /* Read a buffer at a time, and give it to the callback.  Maintain a
+     * running total.
+     */
+    val buf = new Array[Byte](4096);
+    var n = 0;
+    var len = 0;
+    while ({n = in.read(buf); n >= 0 && (explen == -1 || len <= explen)}) {
+      cb.write(buf, n, len);
+      len += n;
+    }
+
+    /* I can't find it documented anywhere that the existing machinery
+     * checks the received stream against the advertised content length.
+     * It doesn't hurt to check again, anyway.
+     */
+    if (explen != -1 && explen != len) {
+      throw new URLFetchException(
+       s"received $len /= $explen bytes from `$url'");
+    }
+
+    /* Glorious success is ours. */
+    win = true;
+  }
+}
+
+/*----- Running processes -------------------------------------------------*/
+
+//def runProgram(
+
+/*----- Threading things --------------------------------------------------*/
+
+def thread[T](name: String, run: Boolean = true, daemon: Boolean = true)
+            (f: => T): Thread = {
+  /* Make a thread with a given name, and maybe start running it. */
+
+  val t = new Thread(new Runnable { def run() { f; } }, name);
+  if (daemon) t.setDaemon(true);
+  if (run) t.start();
+  t
+}
+
+/*----- Quoting and parsing tokens ----------------------------------------*/
+
+def quoteTokens(v: Seq[String]): String = {
+  /* Return a string representing the token sequence V.
+   *
+   * The tokens are quoted as necessary.
+   */
+
+  val b = new StringBuilder;
+  var sep = false;
+  for (s <- v) {
+
+    /* If this isn't the first word, then write a separating space. */
+    if (!sep) sep = true;
+    else b += ' ';
+
+    /* Decide how to handle this token. */
+    if (s.length > 0 &&
+       (s forall { ch => (ch != ''' && ch != '"' && ch != '\\' &&
+                          !ch.isWhitespace) })) {
+      /* If this word is nonempty and contains no problematic characters,
+       * we can write it literally.
+       */
+
+      b ++= s;
+    } else {
+      /* Otherwise, we shall have to do this the hard way.  We could be
+       * cleverer about this, but it's not worth the effort.
+       */
+
+      b += '"';
+      s foreach { ch =>
+       if (ch == '"' || ch == '\\') b += '\\';
+       b += ch;
+      }
+      b += '"';
+    }
+  }
+  b.result
+}
+
+class InvalidQuotingException(msg: String) extends Exception(msg);
+
+def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
+  /* Parse the next token from a string S.
+   *
+   * If there is a token in S starting at or after index POS, then return
+   * it, and the index for the following token; otherwise return `None'.
+   */
+
+  val b = new StringBuilder;
+  val n = s.length;
+  var i = pos;
+  var q = 0;
+
+  /* Skip whitespace while we find the next token. */
+  while (i < n && s(i).isWhitespace) i += 1;
+
+  /* Maybe there just isn't anything to find. */
+  if (i >= n) return None;
+
+  /* There is something there.  Unpick the quoting and escaping. */
+  while (i < n && (q != 0 || !s(i).isWhitespace)) {
+    s(i) match {
+      case '\\' =>
+       if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
+       b += s(i + 1); i += 2;
+      case ch@('"' | ''') =>
+       if (q == 0) q = ch;
+       else if (q == ch) q = 0;
+       else b += ch;
+       i += 1;
+      case ch =>
+       b += ch;
+       i += 1;
+    }
+  }
+
+  /* Check that the quoting was valid. */
+  if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
+
+  /* Skip whitespace before the next token. */
+  while (i < n && s(i).isWhitespace) i += 1;
+
+  /* We're done. */
+  Some((b.result, i))
+}
+
+def splitTokens(s: String, pos: Int = 0): Seq[String] = {
+  /* Return all of the tokens in string S into tokens, starting at POS. */
+
+  val b = List.newBuilder[String];
+  var i = pos;
+
+  while (nextToken(s, i) match {
+    case Some((w, j)) => b += w; i = j; true
+    case None => false
+  }) ();
+  b.result
+}
+
+trait LookaheadIterator[T] extends BufferedIterator[T] {
+  private[this] var st: Option[T] = None;
+  protected def fetch(): Option[T];
+  private[this] def peek() {
+    if (st == None) fetch() match {
+      case None => st = null;
+      case x@Some(_) => st = x;
+    }
+  }
+  override def hasNext: Boolean = { peek(); st != null }
+  override def head(): T =
+    { peek(); if (st == null) throw new NoSuchElementException; st.get }
+  override def next(): T = { val it = head(); st = None; it }
+}
+
+def lines(r: Reader) = new LookaheadIterator[String] {
+  /* Iterates over the lines of text in a `Reader' object. */
+
+  private[this] val in = r match {
+    case br: BufferedReader => br;
+    case _ => new BufferedReader(r);
+  }
+  protected override def fetch(): Option[String] = Option(in.readLine);
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+
+}