More work in progress.
[tripe-android] / util.scala
CommitLineData
8eabb4ff
MW
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
26package uk.org.distorted; package object tripe {
27
28/*----- Imports -----------------------------------------------------------*/
29
30import scala.concurrent.duration.{Deadline, Duration};
c8292b34 31import scala.util.control.{Breaks, ControlThrowable};
8eabb4ff 32
c8292b34 33import java.io.{BufferedReader, Closeable, File, InputStream, Reader};
04a5abae 34import java.net.{HttpURLConnection, URL, URLConnection};
8eabb4ff 35import java.nio.{ByteBuffer, CharBuffer};
04a5abae
MW
36import java.nio.channels.{SelectionKey, Selector};
37import java.nio.channels.spi.{AbstractSelector, AbstractSelectableChannel};
8eabb4ff 38import java.nio.charset.Charset;
04a5abae 39import java.util.{Set => JSet};
8eabb4ff
MW
40import java.util.concurrent.locks.{Lock, ReentrantLock};
41
42/*----- Miscellaneous useful things ---------------------------------------*/
43
44val rng = new java.security.SecureRandom;
45
46def unreachable(msg: String): Nothing = throw new AssertionError(msg);
c8292b34
MW
47def unreachable(): Nothing = unreachable("unreachable");
48final val ok = ();
49final class Brand;
8eabb4ff
MW
50
51/*----- Various pieces of implicit magic ----------------------------------*/
52
53class InvalidCStringException(msg: String) extends Exception(msg);
8eabb4ff 54
25c35469 55object Implicits {
8eabb4ff
MW
56
57 /* --- Syntactic sugar for locks --- */
58
59 implicit class LockOps(lk: Lock) {
60 /* LK withLock { BODY }
61 * LK.withLock(INTERRUPT) { BODY }
25c35469
MW
62 * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
63 * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
8eabb4ff
MW
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
25c35469 89 class PendingLock[T] private[Implicits]
8eabb4ff
MW
90 (val lk: Lock, val dur: Duration,
91 val interrupt: Boolean, body: => T) {
25c35469 92 /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
8eabb4ff 93
25c35469 94 def orElse(alt: => T): T = {
8eabb4ff
MW
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 }
8eabb4ff
MW
106}
107
108/*----- Cleanup assistant -------------------------------------------------*/
109
110class 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
124def 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
139def closing[T, U <: Closeable](thing: U)(body: U => T): T =
140 try { body(thing) }
141 finally { thing.close(); }
142
c8292b34
MW
143/*----- Control structures ------------------------------------------------*/
144
145private case class ExitBlock[T](brand: Brand, result: T)
146 extends ControlThrowable;
147
148def 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
164def 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
176def 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
187def 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
197val BREAKS = new Breaks;
198import BREAKS.{breakable, break};
199
04a5abae
MW
200/*----- Interruptably doing things ----------------------------------------*/
201
202private 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
237class 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}
243def 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
8eabb4ff
MW
255/*----- A gadget for fetching URLs ----------------------------------------*/
256
257class URLFetchException(msg: String) extends Exception(msg);
258
259trait URLFetchCallbacks {
260 def preflight(conn: URLConnection) { }
c8292b34 261 def write(buf: Array[Byte], n: Int, len: Long): Unit;
8eabb4ff
MW
262 def done(win: Boolean) { }
263}
264
265def fetchURL(url: URL, cb: URLFetchCallbacks) {
266 /* Fetch the URL, feeding the data through the callbacks CB. */
267
268 withCleaner { clean =>
04a5abae 269 var win: Boolean = false; clean { cb.done(win); }
8eabb4ff 270
04a5abae
MW
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 */
8eabb4ff 274 val c = url.openConnection();
8eabb4ff 275
04a5abae
MW
276 /* Java's default URL handlers don't respond to interrupts, so we have to
277 * take over this duty.
8eabb4ff 278 */
04a5abae
MW
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 }
c8292b34 299 }
8eabb4ff 300
04a5abae
MW
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 }
8eabb4ff 309
04a5abae
MW
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 }
8eabb4ff
MW
330 }
331}
332
8eabb4ff
MW
333/*----- Threading things --------------------------------------------------*/
334
04a5abae
MW
335def thread(name: String, run: Boolean = true, daemon: Boolean = true)
336 (f: => Unit): Thread = {
8eabb4ff
MW
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
04a5abae
MW
345class 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}
360def 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
8eabb4ff
MW
367/*----- Quoting and parsing tokens ----------------------------------------*/
368
369def 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
408class InvalidQuotingException(msg: String) extends Exception(msg);
409
410def 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
455def 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
c8292b34
MW
461 loopUnit { exit => nextToken(s, i) match {
462 case Some((w, j)) => b += w; i = j;
463 case None => exit;
464 } }
8eabb4ff
MW
465 b.result
466}
467
c8292b34
MW
468/*----- Other random things -----------------------------------------------*/
469
8eabb4ff 470trait LookaheadIterator[T] extends BufferedIterator[T] {
c8292b34
MW
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. */
8eabb4ff 481 protected def fetch(): Option[T];
c8292b34
MW
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;
8eabb4ff 487 private[this] def peek() {
c8292b34 488 /* Arrange to have a current item. */
8eabb4ff
MW
489 if (st == None) fetch() match {
490 case None => st = null;
491 case x@Some(_) => st = x;
492 }
493 }
c8292b34
MW
494
495 /* The `BufferedIterator' protocol. */
8eabb4ff 496 override def hasNext: Boolean = { peek(); st != null }
c8292b34 497 override def head: T =
8eabb4ff 498 { peek(); if (st == null) throw new NoSuchElementException; st.get }
c8292b34 499 override def next(): T = { val it = head; st = None; it }
8eabb4ff
MW
500}
501
c8292b34
MW
502def bufferedReader(r: Reader): BufferedReader = r match {
503 case br: BufferedReader => br
504 case _ => new BufferedReader(r)
505}
8eabb4ff 506
c8292b34
MW
507def 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 }
512def lines(r: Reader): BufferedIterator[String] = lines(bufferedReader(r));
513
514def 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 }
525def blocks(in: InputStream):
04a5abae 526 BufferedIterator[(Array[Byte], Int)] = blocks(in, 65536);
c8292b34
MW
527
528def 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 }
8eabb4ff 538 }
c8292b34 539def blocks(in: BufferedReader):
04a5abae 540 BufferedIterator[(Array[Char], Int)] = blocks(in, 65536);
c8292b34
MW
541def blocks(r: Reader, blksz: Int): BufferedIterator[(Array[Char], Int)] =
542 blocks(bufferedReader(r), blksz);
543def blocks(r: Reader): BufferedIterator[(Array[Char], Int)] =
544 blocks(bufferedReader(r));
545
546def 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
8eabb4ff
MW
562}
563
04a5abae
MW
564def 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
8eabb4ff
MW
573/*----- That's all, folks -------------------------------------------------*/
574
575}