chiark / gitweb /
More progress. More work.
[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;
a5ec891a 39import java.text.SimpleDateFormat;
04a5abae 40import java.util.{Set => JSet};
8eabb4ff
MW
41import java.util.concurrent.locks.{Lock, ReentrantLock};
42
43/*----- Miscellaneous useful things ---------------------------------------*/
44
45val rng = new java.security.SecureRandom;
46
47def unreachable(msg: String): Nothing = throw new AssertionError(msg);
c8292b34
MW
48def unreachable(): Nothing = unreachable("unreachable");
49final val ok = ();
50final class Brand;
8eabb4ff
MW
51
52/*----- Various pieces of implicit magic ----------------------------------*/
53
54class InvalidCStringException(msg: String) extends Exception(msg);
8eabb4ff 55
25c35469 56object Implicits {
8eabb4ff
MW
57
58 /* --- Syntactic sugar for locks --- */
59
60 implicit class LockOps(lk: Lock) {
61 /* LK withLock { BODY }
62 * LK.withLock(INTERRUPT) { BODY }
25c35469
MW
63 * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
64 * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
8eabb4ff
MW
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
25c35469 90 class PendingLock[T] private[Implicits]
8eabb4ff
MW
91 (val lk: Lock, val dur: Duration,
92 val interrupt: Boolean, body: => T) {
25c35469 93 /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
8eabb4ff 94
25c35469 95 def orElse(alt: => T): T = {
8eabb4ff
MW
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 }
8eabb4ff
MW
107}
108
109/*----- Cleanup assistant -------------------------------------------------*/
110
111class 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
125def 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
140def closing[T, U <: Closeable](thing: U)(body: U => T): T =
141 try { body(thing) }
142 finally { thing.close(); }
143
c8292b34
MW
144/*----- Control structures ------------------------------------------------*/
145
146private case class ExitBlock[T](brand: Brand, result: T)
147 extends ControlThrowable;
148
149def 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
165def 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
177def 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
188def 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
198val BREAKS = new Breaks;
199import BREAKS.{breakable, break};
200
04a5abae
MW
201/*----- Interruptably doing things ----------------------------------------*/
202
203private 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
238class 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}
244def 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
8eabb4ff
MW
256/*----- A gadget for fetching URLs ----------------------------------------*/
257
258class URLFetchException(msg: String) extends Exception(msg);
259
260trait URLFetchCallbacks {
261 def preflight(conn: URLConnection) { }
c8292b34 262 def write(buf: Array[Byte], n: Int, len: Long): Unit;
8eabb4ff
MW
263 def done(win: Boolean) { }
264}
265
266def fetchURL(url: URL, cb: URLFetchCallbacks) {
267 /* Fetch the URL, feeding the data through the callbacks CB. */
268
269 withCleaner { clean =>
04a5abae 270 var win: Boolean = false; clean { cb.done(win); }
8eabb4ff 271
04a5abae
MW
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 */
8eabb4ff 275 val c = url.openConnection();
8eabb4ff 276
04a5abae
MW
277 /* Java's default URL handlers don't respond to interrupts, so we have to
278 * take over this duty.
8eabb4ff 279 */
04a5abae
MW
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 }
c8292b34 300 }
8eabb4ff 301
04a5abae
MW
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 }
8eabb4ff 310
04a5abae
MW
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 }
8eabb4ff
MW
331 }
332}
333
8eabb4ff
MW
334/*----- Threading things --------------------------------------------------*/
335
04a5abae
MW
336def thread(name: String, run: Boolean = true, daemon: Boolean = true)
337 (f: => Unit): Thread = {
8eabb4ff
MW
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
04a5abae
MW
346class 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}
361def 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
8eabb4ff
MW
368/*----- Quoting and parsing tokens ----------------------------------------*/
369
370def 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
409class InvalidQuotingException(msg: String) extends Exception(msg);
410
411def 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
456def 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
c8292b34
MW
462 loopUnit { exit => nextToken(s, i) match {
463 case Some((w, j)) => b += w; i = j;
464 case None => exit;
465 } }
8eabb4ff
MW
466 b.result
467}
468
c8292b34
MW
469/*----- Other random things -----------------------------------------------*/
470
8eabb4ff 471trait LookaheadIterator[T] extends BufferedIterator[T] {
c8292b34
MW
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. */
8eabb4ff 482 protected def fetch(): Option[T];
c8292b34
MW
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;
8eabb4ff 488 private[this] def peek() {
c8292b34 489 /* Arrange to have a current item. */
8eabb4ff
MW
490 if (st == None) fetch() match {
491 case None => st = null;
492 case x@Some(_) => st = x;
493 }
494 }
c8292b34
MW
495
496 /* The `BufferedIterator' protocol. */
8eabb4ff 497 override def hasNext: Boolean = { peek(); st != null }
c8292b34 498 override def head: T =
8eabb4ff 499 { peek(); if (st == null) throw new NoSuchElementException; st.get }
c8292b34 500 override def next(): T = { val it = head; st = None; it }
8eabb4ff
MW
501}
502
c8292b34
MW
503def bufferedReader(r: Reader): BufferedReader = r match {
504 case br: BufferedReader => br
505 case _ => new BufferedReader(r)
506}
8eabb4ff 507
c8292b34
MW
508def 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 }
513def lines(r: Reader): BufferedIterator[String] = lines(bufferedReader(r));
514
515def 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 }
526def blocks(in: InputStream):
04a5abae 527 BufferedIterator[(Array[Byte], Int)] = blocks(in, 65536);
c8292b34
MW
528
529def 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 }
8eabb4ff 539 }
c8292b34 540def blocks(in: BufferedReader):
04a5abae 541 BufferedIterator[(Array[Char], Int)] = blocks(in, 65536);
c8292b34
MW
542def blocks(r: Reader, blksz: Int): BufferedIterator[(Array[Char], Int)] =
543 blocks(bufferedReader(r), blksz);
544def blocks(r: Reader): BufferedIterator[(Array[Char], Int)] =
545 blocks(bufferedReader(r));
546
547def 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
8eabb4ff
MW
563}
564
a5ec891a
MW
565val datefmt = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
566
567def formatDuration(t: Int): String =
04a5abae
MW
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
8eabb4ff
MW
576/*----- That's all, folks -------------------------------------------------*/
577
578}