3 * Miscellaneous utilities
5 * (c) 2018 Straylight/Edgeware
8 /*----- Licensing notice --------------------------------------------------*
10 * This file is part of the Trivial IP Encryption (TrIPE) Android app.
12 * TrIPE is free software: you can redistribute it and/or modify it under
13 * the terms of the GNU General Public License as published by the Free
14 * Software Foundation; either version 3 of the License, or (at your
15 * option) any later version.
17 * TrIPE is distributed in the hope that it will be useful, but WITHOUT
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
22 * You should have received a copy of the GNU General Public License
23 * along with TrIPE. If not, see <https://www.gnu.org/licenses/>.
26 package uk.org.distorted; package object tripe {
28 /*----- Imports -----------------------------------------------------------*/
30 import scala.concurrent.duration.{Deadline, Duration};
31 import scala.util.control.{Breaks, ControlThrowable};
33 import java.io.{BufferedReader, Closeable, File, InputStream, Reader};
34 import java.net.{HttpURLConnection, URL, URLConnection};
35 import java.nio.{ByteBuffer, CharBuffer};
36 import java.nio.channels.{SelectionKey, Selector};
37 import java.nio.channels.spi.{AbstractSelector, AbstractSelectableChannel};
38 import java.nio.charset.Charset;
39 import java.util.{Set => JSet};
40 import java.util.concurrent.locks.{Lock, ReentrantLock};
42 /*----- Miscellaneous useful things ---------------------------------------*/
44 val rng = new java.security.SecureRandom;
46 def unreachable(msg: String): Nothing = throw new AssertionError(msg);
47 def unreachable(): Nothing = unreachable("unreachable");
51 /*----- Various pieces of implicit magic ----------------------------------*/
53 class InvalidCStringException(msg: String) extends Exception(msg);
57 /* --- Syntactic sugar for locks --- */
59 implicit class LockOps(lk: Lock) {
60 /* LK withLock { BODY }
61 * LK.withLock(INTERRUPT) { BODY }
62 * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
63 * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
65 * Acquire a lock while executing a BODY. If a duration or deadline is
66 * given then wait so long for the lock, and then give up and run ALT
70 def withLock[T](dur: Duration, interrupt: Boolean)
71 (body: => T): PendingLock[T] =
72 new PendingLock(lk, if (dur > Duration.Zero) dur else Duration.Zero,
74 def withLock[T](dur: Duration)(body: => T): PendingLock[T] =
75 withLock(dur, true)(body);
76 def withLock[T](dl: Deadline, interrupt: Boolean)
77 (body: => T): PendingLock[T] =
78 new PendingLock(lk, dl.timeLeft, interrupt, body);
79 def withLock[T](dl: Deadline)(body: => T): PendingLock[T] =
80 withLock(dl, true)(body);
81 def withLock[T](interrupt: Boolean)(body: => T): T = {
82 if (interrupt) lk.lockInterruptibly();
84 try { body; } finally lk.unlock();
86 def withLock[T](body: => T): T = withLock(true)(body);
89 class PendingLock[T] private[Implicits]
90 (val lk: Lock, val dur: Duration,
91 val interrupt: Boolean, body: => T) {
92 /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
94 def orElse(alt: => T): T = {
95 val locked = (dur, interrupt) match {
96 case (Duration.Inf, true) => lk.lockInterruptibly(); true
97 case (Duration.Inf, false) => lk.lock(); true
98 case (Duration.Zero, false) => lk.tryLock()
99 case (_, true) => lk.tryLock(dur.length, dur.unit)
100 case _ => unreachable("timed wait is always interruptible");
103 else try { body; } finally lk.unlock();
108 /*----- Cleanup assistant -------------------------------------------------*/
111 /* A helper class for avoiding deep nests of `try'/`finally'.
113 * Make a `Cleaner' instance CL at the start of your operation. Apply it
114 * to blocks of code -- as CL { ACTION } -- as you proceed, to accumulate
115 * cleanup actions. Finally, call CL.cleanup() to invoke the accumulated
116 * actions, in reverse order.
119 var cleanups: List[() => Unit] = Nil;
120 def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
121 def cleanup() { cleanups foreach { _() } }
124 def withCleaner[T](body: Cleaner => T): T = {
125 /* An easier way to use the `Cleaner' class. Just
127 * withCleaner { CL => BODY }
129 * The BODY can attach cleanup actions to the cleaner CL by saying
130 * CL { ACTION } as usual. When the BODY exits, normally or otherwise, the
131 * cleanup actions are invoked in reverse order.
134 val cleaner = new Cleaner;
135 try { body(cleaner) }
136 finally { cleaner.cleanup(); }
139 def closing[T, U <: Closeable](thing: U)(body: U => T): T =
141 finally { thing.close(); }
143 /*----- Control structures ------------------------------------------------*/
145 private case class ExitBlock[T](brand: Brand, result: T)
146 extends ControlThrowable;
148 def block[T](body: (T => Nothing) => T): T = {
149 /* block { exit[T] => ...; exit(x); ... }
151 * Execute the body until it calls the `exit' function or finishes.
152 * Annoyingly, Scala isn't clever enough to infer the return type, so
153 * you'll have to write it explicitly.
156 val mybrand = new Brand;
157 try { body { result => throw new ExitBlock(mybrand, result) } }
159 case ExitBlock(brand, result) if brand eq mybrand =>
160 result.asInstanceOf[T]
164 def blockUnit(body: (=> Nothing) => Unit) {
165 /* blockUnit { exit => ...; exit; ... }
167 * Like `block'; it just saves you having to write `exit[Unit] => ...;
171 val mybrand = new Brand;
172 try { body { throw new ExitBlock(mybrand, null) }; }
173 catch { case ExitBlock(brand, result) if brand eq mybrand => ok; }
176 def loop[T](body: (T => Nothing) => Unit): T = {
177 /* loop { exit[T] => ...; exit(x); ... }
179 * Repeatedly execute the body until it calls the `exit' function.
180 * Annoyingly, Scala isn't clever enough to infer the return type, so
181 * you'll have to write it explicitly.
184 block { exit => while (true) body(exit); unreachable }
187 def loopUnit(body: (=> Nothing) => Unit): Unit = {
188 /* loopUnit { exit => ...; exit; ... }
190 * Like `loop'; it just saves you having to write `exit[Unit] => ...;
194 blockUnit { exit => while (true) body(exit); }
197 val BREAKS = new Breaks;
198 import BREAKS.{breakable, break};
200 /*----- Interruptably doing things ----------------------------------------*/
202 private class InterruptCatcher[T](body: => T, onWakeup: => Unit)
203 extends AbstractSelector(null) {
204 /* Hook onto the VM's thread interruption machinery.
206 * The `run' method is the only really interesting one. It will run the
207 * BODY, returning its result; if the thread is interrupted during this
208 * time, ONWAKEUP is invoked for effect. The expectation is that ONWAKEUP
209 * will somehow cause BODY to stop early.
211 * Credit for this hack goes to Nicholas Wilson: see
212 * <https://github.com/NWilson/javaInterruptHook>.
215 private def nope: Nothing =
216 { throw new UnsupportedOperationException("can't do that"); }
217 protected def implCloseSelector() { }
218 protected def register(chan: AbstractSelectableChannel,
219 ops: Int, att: Any): SelectionKey = nope;
220 def keys(): JSet[SelectionKey] = nope;
221 def selectedKeys(): JSet[SelectionKey] = nope;
222 def select(): Int = nope;
223 def select(millis: Long): Int = nope;
224 def selectNow(): Int = nope;
229 if (Thread.interrupted()) throw new InterruptedException;
234 def wakeup(): Selector = { onWakeup; this }
237 class PendingInterruptable[T] private[tripe](body: => T) {
238 /* This class exists to provide the `onInterrupt THUNK' syntax. */
240 def onInterrupt(thunk: => Unit): T =
241 new InterruptCatcher(body, thunk).run();
243 def interruptably[T](body: => T) = {
244 /* interruptably { BODY } onInterrupt { THUNK }
246 * Execute BODY and return its result. If the thread receives an
247 * interrupt -- or is already in an interrupted state -- execute THUNK for
248 * effect; it is expected to cause BODY to return expeditiously, and when
249 * the BODY completes, an `InterruptedException' is thrown.
252 new PendingInterruptable(body);
255 /*----- A gadget for fetching URLs ----------------------------------------*/
257 class URLFetchException(msg: String) extends Exception(msg);
259 trait URLFetchCallbacks {
260 def preflight(conn: URLConnection) { }
261 def write(buf: Array[Byte], n: Int, len: Long): Unit;
262 def done(win: Boolean) { }
265 def fetchURL(url: URL, cb: URLFetchCallbacks) {
266 /* Fetch the URL, feeding the data through the callbacks CB. */
268 withCleaner { clean =>
269 var win: Boolean = false; clean { cb.done(win); }
271 /* Set up the connection. This isn't going to block, I think, and we
272 * need to use it in the interrupt handler.
274 val c = url.openConnection();
276 /* Java's default URL handlers don't respond to interrupts, so we have to
277 * take over this duty.
280 /* Run the caller's preflight check. This must be done here, since it
281 * might well block while it discovers things like the content length.
285 /* Start fetching data. */
286 val in = c.getInputStream; clean { in.close(); }
287 val explen = c.getContentLength;
289 /* Read a buffer at a time, and give it to the callback. Maintain a
294 for ((buf, n) <- blocks(in)) {
295 cb.write(buf, n, len);
297 if (explen != -1 && len > explen) exit;
301 /* I can't find it documented anywhere that the existing machinery
302 * checks the received stream against the advertised content length.
303 * It doesn't hurt to check again, anyway.
305 if (explen != -1 && explen != len) {
306 throw new URLFetchException(
307 s"received $len /= $explen bytes from `$url'");
310 /* Glorious success is ours. */
313 /* Oh. How do we do this? */
316 case c: HttpURLConnection =>
317 /* It's an HTTP connection (what happened to the case here?).
318 * HTTPS connections match too because they're a subclass. Getting
319 * the input stream will block, but there's an easier way.
324 /* It's something else. Let's hope that getting the input stream
327 c.getInputStream.close();
333 /*----- Threading things --------------------------------------------------*/
335 def thread(name: String, run: Boolean = true, daemon: Boolean = true)
336 (f: => Unit): Thread = {
337 /* Make a thread with a given name, and maybe start running it. */
339 val t = new Thread(new Runnable { def run() { f; } }, name);
340 if (daemon) t.setDaemon(true);
345 class ValueThread[T](name: String, group: ThreadGroup = null,
346 stacksz: Long = 0)(body: => T)
347 extends Thread(group, null, name, stacksz) {
348 private[this] var exc: Throwable = _;
349 private[this] var ret: T = _;
353 catch { case e: Throwable => exc = e; }
356 if (isAlive) throw new IllegalArgumentException("still running");
357 else if (exc != null) throw exc;
360 def valueThread[T](name: String, run: Boolean = true)
361 (body: => T): ValueThread[T] = {
362 val t = new ValueThread(name)(body);
367 /*----- Quoting and parsing tokens ----------------------------------------*/
369 def quoteTokens(v: Seq[String]): String = {
370 /* Return a string representing the token sequence V.
372 * The tokens are quoted as necessary.
375 val b = new StringBuilder;
379 /* If this isn't the first word, then write a separating space. */
380 if (!sep) sep = true;
383 /* Decide how to handle this token. */
385 (s forall { ch => (ch != ''' && ch != '"' && ch != '\\' &&
386 !ch.isWhitespace) })) {
387 /* If this word is nonempty and contains no problematic characters,
388 * we can write it literally.
393 /* Otherwise, we shall have to do this the hard way. We could be
394 * cleverer about this, but it's not worth the effort.
399 if (ch == '"' || ch == '\\') b += '\\';
408 class InvalidQuotingException(msg: String) extends Exception(msg);
410 def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
411 /* Parse the next token from a string S.
413 * If there is a token in S starting at or after index POS, then return
414 * it, and the index for the following token; otherwise return `None'.
417 val b = new StringBuilder;
422 /* Skip whitespace while we find the next token. */
423 while (i < n && s(i).isWhitespace) i += 1;
425 /* Maybe there just isn't anything to find. */
426 if (i >= n) return None;
428 /* There is something there. Unpick the quoting and escaping. */
429 while (i < n && (q != 0 || !s(i).isWhitespace)) {
432 if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
433 b += s(i + 1); i += 2;
434 case ch@('"' | ''') =>
436 else if (q == ch) q = 0;
445 /* Check that the quoting was valid. */
446 if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
448 /* Skip whitespace before the next token. */
449 while (i < n && s(i).isWhitespace) i += 1;
455 def splitTokens(s: String, pos: Int = 0): Seq[String] = {
456 /* Return all of the tokens in string S into tokens, starting at POS. */
458 val b = List.newBuilder[String];
461 loopUnit { exit => nextToken(s, i) match {
462 case Some((w, j)) => b += w; i = j;
468 /*----- Other random things -----------------------------------------------*/
470 trait LookaheadIterator[T] extends BufferedIterator[T] {
471 /* An iterator in terms of a single `maybe there's another item' function.
473 * It seems like every time I write an iterator in Scala, the only way to
474 * find out whether there's a next item, for `hasNext', is to actually try
475 * to fetch it. So here's an iterator in terms of a function which goes
476 * off and maybe returns a next thing. It turns out to be easy to satisfy
477 * the additional requirements for `BufferedIterator', so why not?
480 /* Subclass responsibility. */
481 protected def fetch(): Option[T];
483 /* The machinery. `st' is `None' if there's no current item, null if we've
484 * actually hit the end, or `Some(x)' if the current item is x.
486 private[this] var st: Option[T] = None;
487 private[this] def peek() {
488 /* Arrange to have a current item. */
489 if (st == None) fetch() match {
490 case None => st = null;
491 case x@Some(_) => st = x;
495 /* The `BufferedIterator' protocol. */
496 override def hasNext: Boolean = { peek(); st != null }
497 override def head: T =
498 { peek(); if (st == null) throw new NoSuchElementException; st.get }
499 override def next(): T = { val it = head; st = None; it }
502 def bufferedReader(r: Reader): BufferedReader = r match {
503 case br: BufferedReader => br
504 case _ => new BufferedReader(r)
507 def lines(r: BufferedReader): BufferedIterator[String] =
508 new LookaheadIterator[String] {
509 /* Iterates over the lines of text in a `Reader' object. */
510 override protected def fetch() = Option(r.readLine());
512 def lines(r: Reader): BufferedIterator[String] = lines(bufferedReader(r));
514 def blocks(in: InputStream, blksz: Int):
515 BufferedIterator[(Array[Byte], Int)] =
516 /* Iterates over (possibly irregularly sized) blocks in a stream. */
517 new LookaheadIterator[(Array[Byte], Int)] {
518 val buf = new Array[Byte](blksz)
519 override protected def fetch() = {
520 val n = in.read(buf);
525 def blocks(in: InputStream):
526 BufferedIterator[(Array[Byte], Int)] = blocks(in, 65536);
528 def blocks(in: BufferedReader, blksz: Int):
529 BufferedIterator[(Array[Char], Int)] =
530 /* Iterates over (possibly irregularly sized) blocks in a reader. */
531 new LookaheadIterator[(Array[Char], Int)] {
532 val buf = new Array[Char](blksz)
533 override protected def fetch() = {
534 val n = in.read(buf);
539 def blocks(in: BufferedReader):
540 BufferedIterator[(Array[Char], Int)] = blocks(in, 65536);
541 def blocks(r: Reader, blksz: Int): BufferedIterator[(Array[Char], Int)] =
542 blocks(bufferedReader(r), blksz);
543 def blocks(r: Reader): BufferedIterator[(Array[Char], Int)] =
544 blocks(bufferedReader(r));
546 def oxford(conj: String, things: Seq[String]): String = things match {
547 case Seq() => "<nothing>"
549 case Seq(a, b) => s"$a $conj $b"
550 case Seq(a, tail@_*) =>
551 val sb = new StringBuilder;
552 sb ++= a; sb ++= ", ";
553 def iter(rest: Seq[String]) {
555 case Seq() => unreachable;
556 case Seq(a) => sb ++= conj; sb += ' '; sb ++= a;
557 case Seq(a, tail@_*) => sb ++= a; sb ++= ", "; iter(tail);
564 def formatTime(t: Int): String =
567 val (s, t1) = (t%60, t/60);
568 val (m, h) = (t1%60, t1/60);
569 if (h > 0) f"$h%d:$m%02d:$s%02d"
570 else f"$m%02d:$s%02d"
573 /*----- That's all, folks -------------------------------------------------*/