chiark / gitweb /
More work in progress.
[tripe-android] / util.scala
1 /* -*-scala-*-
2  *
3  * Miscellaneous utilities
4  *
5  * (c) 2018 Straylight/Edgeware
6  */
7
8 /*----- Licensing notice --------------------------------------------------*
9  *
10  * This file is part of the Trivial IP Encryption (TrIPE) Android app.
11  *
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.
16  *
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
20  * for more details.
21  *
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/>.
24  */
25
26 package uk.org.distorted; package object tripe {
27
28 /*----- Imports -----------------------------------------------------------*/
29
30 import scala.concurrent.duration.{Deadline, Duration};
31 import scala.util.control.{Breaks, ControlThrowable};
32
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};
41
42 /*----- Miscellaneous useful things ---------------------------------------*/
43
44 val rng = new java.security.SecureRandom;
45
46 def unreachable(msg: String): Nothing = throw new AssertionError(msg);
47 def unreachable(): Nothing = unreachable("unreachable");
48 final val ok = ();
49 final class Brand;
50
51 /*----- Various pieces of implicit magic ----------------------------------*/
52
53 class InvalidCStringException(msg: String) extends Exception(msg);
54
55 object Implicits {
56
57   /* --- Syntactic sugar for locks --- */
58
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 }
64      *
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
67      * instead.
68      */
69
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,
73                       interrupt, body);
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();
83       else lk.lock();
84       try { body; } finally lk.unlock();
85     }
86     def withLock[T](body: => T): T = withLock(true)(body);
87   }
88
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. */
93
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");
101       }
102       if (!locked) alt;
103       else try { body; } finally lk.unlock();
104     }
105   }
106 }
107
108 /*----- Cleanup assistant -------------------------------------------------*/
109
110 class Cleaner {
111   /* A helper class for avoiding deep nests of `try'/`finally'.
112    *
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.
117    */
118
119   var cleanups: List[() => Unit] = Nil;
120   def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
121   def cleanup() { cleanups foreach { _() } }
122 }
123
124 def withCleaner[T](body: Cleaner => T): T = {
125   /* An easier way to use the `Cleaner' class.  Just
126    *
127    *    withCleaner { CL => BODY }
128    *
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.
132    */
133
134   val cleaner = new Cleaner;
135   try { body(cleaner) }
136   finally { cleaner.cleanup(); }
137 }
138
139 def closing[T, U <: Closeable](thing: U)(body: U => T): T =
140   try { body(thing) }
141   finally { thing.close(); }
142
143 /*----- Control structures ------------------------------------------------*/
144
145 private case class ExitBlock[T](brand: Brand, result: T)
146         extends ControlThrowable;
147
148 def block[T](body: (T => Nothing) => T): T = {
149   /* block { exit[T] => ...; exit(x); ... }
150    *
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.
154    */
155
156   val mybrand = new Brand;
157   try { body { result => throw new ExitBlock(mybrand, result) } }
158   catch {
159     case ExitBlock(brand, result) if brand eq mybrand =>
160       result.asInstanceOf[T]
161   }
162 }
163
164 def blockUnit(body: (=> Nothing) => Unit) {
165   /* blockUnit { exit => ...; exit; ... }
166    *
167    * Like `block'; it just saves you having to write `exit[Unit] => ...;
168    * exit(ok); ...'.
169    */
170
171   val mybrand = new Brand;
172   try { body { throw new ExitBlock(mybrand, null) }; }
173   catch { case ExitBlock(brand, result) if brand eq mybrand => ok; }
174 }
175
176 def loop[T](body: (T => Nothing) => Unit): T = {
177   /* loop { exit[T] => ...; exit(x); ... }
178    *
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.
182    */
183
184   block { exit => while (true) body(exit); unreachable }
185 }
186
187 def loopUnit(body: (=> Nothing) => Unit): Unit = {
188   /* loopUnit { exit => ...; exit; ... }
189    *
190    * Like `loop'; it just saves you having to write `exit[Unit] => ...;
191    * exit(()); ...'.
192    */
193
194   blockUnit { exit => while (true) body(exit); }
195 }
196
197 val BREAKS = new Breaks;
198 import BREAKS.{breakable, break};
199
200 /*----- Interruptably doing things ----------------------------------------*/
201
202 private class InterruptCatcher[T](body: => T, onWakeup: => Unit)
203         extends AbstractSelector(null) {
204   /* Hook onto the VM's thread interruption machinery.
205    *
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.
210    *
211    * Credit for this hack goes to Nicholas Wilson: see
212    * <https://github.com/NWilson/javaInterruptHook>.
213    */
214
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;
225
226   def run(): T = try {
227     begin();
228     val ret = body;
229     if (Thread.interrupted()) throw new InterruptedException;
230     ret
231   } finally {
232     end();
233   }
234   def wakeup(): Selector = { onWakeup; this }
235 }
236
237 class PendingInterruptable[T] private[tripe](body: => T) {
238   /* This class exists to provide the `onInterrupt THUNK' syntax. */
239
240   def onInterrupt(thunk: => Unit): T =
241     new InterruptCatcher(body, thunk).run();
242 }
243 def interruptably[T](body: => T) = {
244   /* interruptably { BODY } onInterrupt { THUNK }
245    *
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.
250    */
251
252   new PendingInterruptable(body);
253 }
254
255 /*----- A gadget for fetching URLs ----------------------------------------*/
256
257 class URLFetchException(msg: String) extends Exception(msg);
258
259 trait URLFetchCallbacks {
260   def preflight(conn: URLConnection) { }
261   def write(buf: Array[Byte], n: Int, len: Long): Unit;
262   def done(win: Boolean) { }
263 }
264
265 def fetchURL(url: URL, cb: URLFetchCallbacks) {
266   /* Fetch the URL, feeding the data through the callbacks CB. */
267
268   withCleaner { clean =>
269     var win: Boolean = false; clean { cb.done(win); }
270
271     /* Set up the connection.  This isn't going to block, I think, and we
272      * need to use it in the interrupt handler.
273      */
274     val c = url.openConnection();
275
276     /* Java's default URL handlers don't respond to interrupts, so we have to
277      * take over this duty.
278      */
279     interruptably {
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.
282        */
283       cb.preflight(c);
284
285       /* Start fetching data. */
286       val in = c.getInputStream; clean { in.close(); }
287       val explen = c.getContentLength;
288
289       /* Read a buffer at a time, and give it to the callback.  Maintain a
290        * running total.
291        */
292       var len: Long = 0;
293       blockUnit { exit =>
294         for ((buf, n) <- blocks(in)) {
295           cb.write(buf, n, len);
296           len += n;
297           if (explen != -1 && len > explen) exit;
298         }
299       }
300
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.
304        */
305       if (explen != -1 && explen != len) {
306         throw new URLFetchException(
307           s"received $len /= $explen bytes from `$url'");
308       }
309
310       /* Glorious success is ours. */
311       win = true;
312     } onInterrupt {
313       /* Oh.  How do we do this? */
314
315       c match {
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.
320            */
321           c.disconnect();
322
323         case _ =>
324           /* It's something else.  Let's hope that getting the input stream
325            * doesn't block.
326            */
327         c.getInputStream.close();
328       }
329     }
330   }
331 }
332
333 /*----- Threading things --------------------------------------------------*/
334
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. */
338
339   val t = new Thread(new Runnable { def run() { f; } }, name);
340   if (daemon) t.setDaemon(true);
341   if (run) t.start();
342   t
343 }
344
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 = _;
350
351   override def run() {
352     try { ret = body; }
353     catch { case e: Throwable => exc = e; }
354   }
355   def get: T =
356     if (isAlive) throw new IllegalArgumentException("still running");
357     else if (exc != null) throw exc;
358     else ret;
359 }
360 def valueThread[T](name: String, run: Boolean = true)
361                   (body: => T): ValueThread[T] = {
362   val t = new ValueThread(name)(body);
363   if (run) t.start();
364   t
365 }
366
367 /*----- Quoting and parsing tokens ----------------------------------------*/
368
369 def quoteTokens(v: Seq[String]): String = {
370   /* Return a string representing the token sequence V.
371    *
372    * The tokens are quoted as necessary.
373    */
374
375   val b = new StringBuilder;
376   var sep = false;
377   for (s <- v) {
378
379     /* If this isn't the first word, then write a separating space. */
380     if (!sep) sep = true;
381     else b += ' ';
382
383     /* Decide how to handle this token. */
384     if (s.length > 0 &&
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.
389        */
390
391       b ++= s;
392     } else {
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.
395        */
396
397       b += '"';
398       s foreach { ch =>
399         if (ch == '"' || ch == '\\') b += '\\';
400         b += ch;
401       }
402       b += '"';
403     }
404   }
405   b.result
406 }
407
408 class InvalidQuotingException(msg: String) extends Exception(msg);
409
410 def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
411   /* Parse the next token from a string S.
412    *
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'.
415    */
416
417   val b = new StringBuilder;
418   val n = s.length;
419   var i = pos;
420   var q = 0;
421
422   /* Skip whitespace while we find the next token. */
423   while (i < n && s(i).isWhitespace) i += 1;
424
425   /* Maybe there just isn't anything to find. */
426   if (i >= n) return None;
427
428   /* There is something there.  Unpick the quoting and escaping. */
429   while (i < n && (q != 0 || !s(i).isWhitespace)) {
430     s(i) match {
431       case '\\' =>
432         if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
433         b += s(i + 1); i += 2;
434       case ch@('"' | ''') =>
435         if (q == 0) q = ch;
436         else if (q == ch) q = 0;
437         else b += ch;
438         i += 1;
439       case ch =>
440         b += ch;
441         i += 1;
442     }
443   }
444
445   /* Check that the quoting was valid. */
446   if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
447
448   /* Skip whitespace before the next token. */
449   while (i < n && s(i).isWhitespace) i += 1;
450
451   /* We're done. */
452   Some((b.result, i))
453 }
454
455 def splitTokens(s: String, pos: Int = 0): Seq[String] = {
456   /* Return all of the tokens in string S into tokens, starting at POS. */
457
458   val b = List.newBuilder[String];
459   var i = pos;
460
461   loopUnit { exit => nextToken(s, i) match {
462     case Some((w, j)) => b += w; i = j;
463     case None => exit;
464   } }
465   b.result
466 }
467
468 /*----- Other random things -----------------------------------------------*/
469
470 trait LookaheadIterator[T] extends BufferedIterator[T] {
471   /* An iterator in terms of a single `maybe there's another item' function.
472    *
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?
478    */
479
480   /* Subclass responsibility. */
481   protected def fetch(): Option[T];
482
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.
485    */
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;
492     }
493   }
494
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 }
500 }
501
502 def bufferedReader(r: Reader): BufferedReader = r match {
503   case br: BufferedReader => br
504   case _ => new BufferedReader(r)
505 }
506
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());
511   }
512 def lines(r: Reader): BufferedIterator[String] = lines(bufferedReader(r));
513
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);
521       if (n < 0) None
522       else Some((buf, n))
523     }
524   }
525 def blocks(in: InputStream):
526         BufferedIterator[(Array[Byte], Int)] = blocks(in, 65536);
527
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);
535       if (n < 0) None
536       else Some((buf, n))
537     }
538   }
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));
545
546 def oxford(conj: String, things: Seq[String]): String = things match {
547   case Seq() => "<nothing>"
548   case Seq(a) => a
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]) {
554       rest match {
555         case Seq() => unreachable;
556         case Seq(a) => sb ++= conj; sb += ' '; sb ++= a;
557         case Seq(a, tail@_*) => sb ++= a; sb ++= ", "; iter(tail);
558       }
559     }
560     iter(tail);
561     sb.result
562 }
563
564 def formatTime(t: Int): String =
565   if (t < -1) "???"
566   else {
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"
571   }
572
573 /*----- That's all, folks -------------------------------------------------*/
574
575 }