--- /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 -------------------------------------------------*/
+
+}