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