chiark / gitweb /
Wow, is that a proper Android build system?
[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.text.SimpleDateFormat;
40 import java.util.{Set => JSet};
41 import java.util.concurrent.locks.{Lock, ReentrantLock};
42
43 /*----- Miscellaneous useful things ---------------------------------------*/
44
45 val rng = new java.security.SecureRandom;
46
47 def unreachable(msg: String): Nothing = throw new AssertionError(msg);
48 def unreachable(): Nothing = unreachable("unreachable");
49 final val ok = ();
50 final class Brand;
51
52 /*----- Various pieces of implicit magic ----------------------------------*/
53
54 class InvalidCStringException(msg: String) extends Exception(msg);
55
56 object Implicits {
57
58   /* --- Syntactic sugar for locks --- */
59
60   implicit class LockOps(lk: Lock) {
61     /* LK withLock { BODY }
62      * LK.withLock(INTERRUPT) { BODY }
63      * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
64      * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
65      *
66      * Acquire a lock while executing a BODY.  If a duration or deadline is
67      * given then wait so long for the lock, and then give up and run ALT
68      * instead.
69      */
70
71     def withLock[T](dur: Duration, interrupt: Boolean)
72                    (body: => T): PendingLock[T] =
73       new PendingLock(lk, if (dur > Duration.Zero) dur else Duration.Zero,
74                       interrupt, body);
75     def withLock[T](dur: Duration)(body: => T): PendingLock[T] =
76       withLock(dur, true)(body);
77     def withLock[T](dl: Deadline, interrupt: Boolean)
78                    (body: => T): PendingLock[T] =
79       new PendingLock(lk, dl.timeLeft, interrupt, body);
80     def withLock[T](dl: Deadline)(body: => T): PendingLock[T] =
81       withLock(dl, true)(body);
82     def withLock[T](interrupt: Boolean)(body: => T): T = {
83       if (interrupt) lk.lockInterruptibly();
84       else lk.lock();
85       try { body; } finally lk.unlock();
86     }
87     def withLock[T](body: => T): T = withLock(true)(body);
88   }
89
90   class PendingLock[T] private[Implicits]
91           (val lk: Lock, val dur: Duration,
92            val interrupt: Boolean, body: => T) {
93     /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
94
95     def orElse(alt: => T): T = {
96       val locked = (dur, interrupt) match {
97         case (Duration.Inf, true) => lk.lockInterruptibly(); true
98         case (Duration.Inf, false) => lk.lock(); true
99         case (Duration.Zero, false) => lk.tryLock()
100         case (_, true) => lk.tryLock(dur.length, dur.unit)
101         case _ => unreachable("timed wait is always interruptible");
102       }
103       if (!locked) alt;
104       else try { body; } finally lk.unlock();
105     }
106   }
107 }
108
109 /*----- Cleanup assistant -------------------------------------------------*/
110
111 class Cleaner {
112   /* A helper class for avoiding deep nests of `try'/`finally'.
113    *
114    * Make a `Cleaner' instance CL at the start of your operation.  Apply it
115    * to blocks of code -- as CL { ACTION } -- as you proceed, to accumulate
116    * cleanup actions.   Finally, call CL.cleanup() to invoke the accumulated
117    * actions, in reverse order.
118    */
119
120   var cleanups: List[() => Unit] = Nil;
121   def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
122   def cleanup() { cleanups foreach { _() } }
123 }
124
125 def withCleaner[T](body: Cleaner => T): T = {
126   /* An easier way to use the `Cleaner' class.  Just
127    *
128    *    withCleaner { CL => BODY }
129    *
130    * The BODY can attach cleanup actions to the cleaner CL by saying
131    * CL { ACTION } as usual.  When the BODY exits, normally or otherwise, the
132    * cleanup actions are invoked in reverse order.
133    */
134
135   val cleaner = new Cleaner;
136   try { body(cleaner) }
137   finally { cleaner.cleanup(); }
138 }
139
140 def closing[T, U <: Closeable](thing: U)(body: U => T): T =
141   try { body(thing) }
142   finally { thing.close(); }
143
144 /*----- Control structures ------------------------------------------------*/
145
146 private case class ExitBlock[T](brand: Brand, result: T)
147         extends ControlThrowable;
148
149 def block[T](body: (T => Nothing) => T): T = {
150   /* block { exit[T] => ...; exit(x); ... }
151    *
152    * Execute the body until it calls the `exit' function or finishes.
153    * Annoyingly, Scala isn't clever enough to infer the return type, so
154    * you'll have to write it explicitly.
155    */
156
157   val mybrand = new Brand;
158   try { body { result => throw new ExitBlock(mybrand, result) } }
159   catch {
160     case ExitBlock(brand, result) if brand eq mybrand =>
161       result.asInstanceOf[T]
162   }
163 }
164
165 def blockUnit(body: (=> Nothing) => Unit) {
166   /* blockUnit { exit => ...; exit; ... }
167    *
168    * Like `block'; it just saves you having to write `exit[Unit] => ...;
169    * exit(ok); ...'.
170    */
171
172   val mybrand = new Brand;
173   try { body { throw new ExitBlock(mybrand, null) }; }
174   catch { case ExitBlock(brand, result) if brand eq mybrand => ok; }
175 }
176
177 def loop[T](body: (T => Nothing) => Unit): T = {
178   /* loop { exit[T] => ...; exit(x); ... }
179    *
180    * Repeatedly execute the body until it calls the `exit' function.
181    * Annoyingly, Scala isn't clever enough to infer the return type, so
182    * you'll have to write it explicitly.
183    */
184
185   block { exit => while (true) body(exit); unreachable }
186 }
187
188 def loopUnit(body: (=> Nothing) => Unit): Unit = {
189   /* loopUnit { exit => ...; exit; ... }
190    *
191    * Like `loop'; it just saves you having to write `exit[Unit] => ...;
192    * exit(()); ...'.
193    */
194
195   blockUnit { exit => while (true) body(exit); }
196 }
197
198 val BREAKS = new Breaks;
199 import BREAKS.{breakable, break};
200
201 /*----- Interruptably doing things ----------------------------------------*/
202
203 private class InterruptCatcher[T](body: => T, onWakeup: => Unit)
204         extends AbstractSelector(null) {
205   /* Hook onto the VM's thread interruption machinery.
206    *
207    * The `run' method is the only really interesting one.  It will run the
208    * BODY, returning its result; if the thread is interrupted during this
209    * time, ONWAKEUP is invoked for effect.  The expectation is that ONWAKEUP
210    * will somehow cause BODY to stop early.
211    *
212    * Credit for this hack goes to Nicholas Wilson: see
213    * <https://github.com/NWilson/javaInterruptHook>.
214    */
215
216   private def nope: Nothing =
217     { throw new UnsupportedOperationException("can't do that"); }
218   protected def implCloseSelector() { }
219   protected def register(chan: AbstractSelectableChannel,
220                                   ops: Int, att: Any): SelectionKey = nope;
221   def keys(): JSet[SelectionKey] = nope;
222   def selectedKeys(): JSet[SelectionKey] = nope;
223   def select(): Int = nope;
224   def select(millis: Long): Int = nope;
225   def selectNow(): Int = nope;
226
227   def run(): T = try {
228     begin();
229     val ret = body;
230     if (Thread.interrupted()) throw new InterruptedException;
231     ret
232   } finally {
233     end();
234   }
235   def wakeup(): Selector = { onWakeup; this }
236 }
237
238 class PendingInterruptable[T] private[tripe](body: => T) {
239   /* This class exists to provide the `onInterrupt THUNK' syntax. */
240
241   def onInterrupt(thunk: => Unit): T =
242     new InterruptCatcher(body, thunk).run();
243 }
244 def interruptably[T](body: => T) = {
245   /* interruptably { BODY } onInterrupt { THUNK }
246    *
247    * Execute BODY and return its result.  If the thread receives an
248    * interrupt -- or is already in an interrupted state -- execute THUNK for
249    * effect; it is expected to cause BODY to return expeditiously, and when
250    * the BODY completes, an `InterruptedException' is thrown.
251    */
252
253   new PendingInterruptable(body);
254 }
255
256 /*----- A gadget for fetching URLs ----------------------------------------*/
257
258 class URLFetchException(msg: String) extends Exception(msg);
259
260 trait URLFetchCallbacks {
261   def preflight(conn: URLConnection) { }
262   def write(buf: Array[Byte], n: Int, len: Long): Unit;
263   def done(win: Boolean) { }
264 }
265
266 def fetchURL(url: URL, cb: URLFetchCallbacks) {
267   /* Fetch the URL, feeding the data through the callbacks CB. */
268
269   withCleaner { clean =>
270     var win: Boolean = false; clean { cb.done(win); }
271
272     /* Set up the connection.  This isn't going to block, I think, and we
273      * need to use it in the interrupt handler.
274      */
275     val c = url.openConnection();
276
277     /* Java's default URL handlers don't respond to interrupts, so we have to
278      * take over this duty.
279      */
280     interruptably {
281       /* Run the caller's preflight check.  This must be done here, since it
282        * might well block while it discovers things like the content length.
283        */
284       cb.preflight(c);
285
286       /* Start fetching data. */
287       val in = c.getInputStream; clean { in.close(); }
288       val explen = c.getContentLength;
289
290       /* Read a buffer at a time, and give it to the callback.  Maintain a
291        * running total.
292        */
293       var len: Long = 0;
294       blockUnit { exit =>
295         for ((buf, n) <- blocks(in)) {
296           cb.write(buf, n, len);
297           len += n;
298           if (explen != -1 && len > explen) exit;
299         }
300       }
301
302       /* I can't find it documented anywhere that the existing machinery
303        * checks the received stream against the advertised content length.
304        * It doesn't hurt to check again, anyway.
305        */
306       if (explen != -1 && explen != len) {
307         throw new URLFetchException(
308           s"received $len /= $explen bytes from `$url'");
309       }
310
311       /* Glorious success is ours. */
312       win = true;
313     } onInterrupt {
314       /* Oh.  How do we do this? */
315
316       c match {
317         case c: HttpURLConnection =>
318           /* It's an HTTP connection (what happened to the case here?).
319            * HTTPS connections match too because they're a subclass.  Getting
320            * the input stream will block, but there's an easier way.
321            */
322           c.disconnect();
323
324         case _ =>
325           /* It's something else.  Let's hope that getting the input stream
326            * doesn't block.
327            */
328         c.getInputStream.close();
329       }
330     }
331   }
332 }
333
334 /*----- Threading things --------------------------------------------------*/
335
336 def thread(name: String, run: Boolean = true, daemon: Boolean = true)
337           (f: => Unit): Thread = {
338   /* Make a thread with a given name, and maybe start running it. */
339
340   val t = new Thread(new Runnable { def run() { f; } }, name);
341   if (daemon) t.setDaemon(true);
342   if (run) t.start();
343   t
344 }
345
346 class ValueThread[T](name: String, group: ThreadGroup = null,
347                      stacksz: Long = 0)(body: => T)
348         extends Thread(group, null, name, stacksz) {
349   private[this] var exc: Throwable = _;
350   private[this] var ret: T = _;
351
352   override def run() {
353     try { ret = body; }
354     catch { case e: Throwable => exc = e; }
355   }
356   def get: T =
357     if (isAlive) throw new IllegalArgumentException("still running");
358     else if (exc != null) throw exc;
359     else ret;
360 }
361 def valueThread[T](name: String, run: Boolean = true)
362                   (body: => T): ValueThread[T] = {
363   val t = new ValueThread(name)(body);
364   if (run) t.start();
365   t
366 }
367
368 /*----- Quoting and parsing tokens ----------------------------------------*/
369
370 def quoteTokens(v: Seq[String]): String = {
371   /* Return a string representing the token sequence V.
372    *
373    * The tokens are quoted as necessary.
374    */
375
376   val b = new StringBuilder;
377   var sep = false;
378   for (s <- v) {
379
380     /* If this isn't the first word, then write a separating space. */
381     if (!sep) sep = true;
382     else b += ' ';
383
384     /* Decide how to handle this token. */
385     if (s.length > 0 &&
386         (s forall { ch => (ch != ''' && ch != '"' && ch != '\\' &&
387                            !ch.isWhitespace) })) {
388       /* If this word is nonempty and contains no problematic characters,
389        * we can write it literally.
390        */
391
392       b ++= s;
393     } else {
394       /* Otherwise, we shall have to do this the hard way.  We could be
395        * cleverer about this, but it's not worth the effort.
396        */
397
398       b += '"';
399       s foreach { ch =>
400         if (ch == '"' || ch == '\\') b += '\\';
401         b += ch;
402       }
403       b += '"';
404     }
405   }
406   b.result
407 }
408
409 class InvalidQuotingException(msg: String) extends Exception(msg);
410
411 def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
412   /* Parse the next token from a string S.
413    *
414    * If there is a token in S starting at or after index POS, then return
415    * it, and the index for the following token; otherwise return `None'.
416    */
417
418   val b = new StringBuilder;
419   val n = s.length;
420   var i = pos;
421   var q = 0;
422
423   /* Skip whitespace while we find the next token. */
424   while (i < n && s(i).isWhitespace) i += 1;
425
426   /* Maybe there just isn't anything to find. */
427   if (i >= n) return None;
428
429   /* There is something there.  Unpick the quoting and escaping. */
430   while (i < n && (q != 0 || !s(i).isWhitespace)) {
431     s(i) match {
432       case '\\' =>
433         if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
434         b += s(i + 1); i += 2;
435       case ch@('"' | ''') =>
436         if (q == 0) q = ch;
437         else if (q == ch) q = 0;
438         else b += ch;
439         i += 1;
440       case ch =>
441         b += ch;
442         i += 1;
443     }
444   }
445
446   /* Check that the quoting was valid. */
447   if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
448
449   /* Skip whitespace before the next token. */
450   while (i < n && s(i).isWhitespace) i += 1;
451
452   /* We're done. */
453   Some((b.result, i))
454 }
455
456 def splitTokens(s: String, pos: Int = 0): Seq[String] = {
457   /* Return all of the tokens in string S into tokens, starting at POS. */
458
459   val b = List.newBuilder[String];
460   var i = pos;
461
462   loopUnit { exit => nextToken(s, i) match {
463     case Some((w, j)) => b += w; i = j;
464     case None => exit;
465   } }
466   b.result
467 }
468
469 /*----- Other random things -----------------------------------------------*/
470
471 trait LookaheadIterator[T] extends BufferedIterator[T] {
472   /* An iterator in terms of a single `maybe there's another item' function.
473    *
474    * It seems like every time I write an iterator in Scala, the only way to
475    * find out whether there's a next item, for `hasNext', is to actually try
476    * to fetch it.  So here's an iterator in terms of a function which goes
477    * off and maybe returns a next thing.  It turns out to be easy to satisfy
478    * the additional requirements for `BufferedIterator', so why not?
479    */
480
481   /* Subclass responsibility. */
482   protected def fetch(): Option[T];
483
484   /* The machinery.  `st' is `None' if there's no current item, null if we've
485    * actually hit the end, or `Some(x)' if the current item is x.
486    */
487   private[this] var st: Option[T] = None;
488   private[this] def peek() {
489     /* Arrange to have a current item. */
490     if (st == None) fetch() match {
491       case None => st = null;
492       case x@Some(_) => st = x;
493     }
494   }
495
496   /* The `BufferedIterator' protocol. */
497   override def hasNext: Boolean = { peek(); st != null }
498   override def head: T =
499     { peek(); if (st == null) throw new NoSuchElementException; st.get }
500   override def next(): T = { val it = head; st = None; it }
501 }
502
503 def bufferedReader(r: Reader): BufferedReader = r match {
504   case br: BufferedReader => br
505   case _ => new BufferedReader(r)
506 }
507
508 def lines(r: BufferedReader): BufferedIterator[String] =
509   new LookaheadIterator[String] {
510     /* Iterates over the lines of text in a `Reader' object. */
511     override protected def fetch() = Option(r.readLine());
512   }
513 def lines(r: Reader): BufferedIterator[String] = lines(bufferedReader(r));
514
515 def blocks(in: InputStream, blksz: Int):
516         BufferedIterator[(Array[Byte], Int)] =
517   /* Iterates over (possibly irregularly sized) blocks in a stream. */
518   new LookaheadIterator[(Array[Byte], Int)] {
519     val buf = new Array[Byte](blksz)
520     override protected def fetch() = {
521       val n = in.read(buf);
522       if (n < 0) None
523       else Some((buf, n))
524     }
525   }
526 def blocks(in: InputStream):
527         BufferedIterator[(Array[Byte], Int)] = blocks(in, 65536);
528
529 def blocks(in: BufferedReader, blksz: Int):
530         BufferedIterator[(Array[Char], Int)] =
531   /* Iterates over (possibly irregularly sized) blocks in a reader. */
532   new LookaheadIterator[(Array[Char], Int)] {
533     val buf = new Array[Char](blksz)
534     override protected def fetch() = {
535       val n = in.read(buf);
536       if (n < 0) None
537       else Some((buf, n))
538     }
539   }
540 def blocks(in: BufferedReader):
541         BufferedIterator[(Array[Char], Int)] = blocks(in, 65536);
542 def blocks(r: Reader, blksz: Int): BufferedIterator[(Array[Char], Int)] =
543   blocks(bufferedReader(r), blksz);
544 def blocks(r: Reader): BufferedIterator[(Array[Char], Int)] =
545   blocks(bufferedReader(r));
546
547 def oxford(conj: String, things: Seq[String]): String = things match {
548   case Seq() => "<nothing>"
549   case Seq(a) => a
550   case Seq(a, b) => s"$a $conj $b"
551   case Seq(a, tail@_*) =>
552     val sb = new StringBuilder;
553     sb ++= a; sb ++= ", ";
554     def iter(rest: Seq[String]) {
555       rest match {
556         case Seq() => unreachable;
557         case Seq(a) => sb ++= conj; sb += ' '; sb ++= a;
558         case Seq(a, tail@_*) => sb ++= a; sb ++= ", "; iter(tail);
559       }
560     }
561     iter(tail);
562     sb.result
563 }
564
565 val datefmt = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
566
567 def formatDuration(t: Int): String =
568   if (t < -1) "???"
569   else {
570     val (s, t1) = (t%60, t/60);
571     val (m, h) = (t1%60, t1/60);
572     if (h > 0) f"$h%d:$m%02d:$s%02d"
573     else f"$m%02d:$s%02d"
574   }
575
576 /*----- That's all, folks -------------------------------------------------*/
577
578 }