chiark / gitweb /
progress.scala: Miscellaneous WIP.
[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};
8eabb4ff
MW
34import java.net.{URL, URLConnection};
35import java.nio.{ByteBuffer, CharBuffer};
36import java.nio.charset.Charset;
37import java.util.concurrent.locks.{Lock, ReentrantLock};
38
39/*----- Miscellaneous useful things ---------------------------------------*/
40
41val rng = new java.security.SecureRandom;
42
43def unreachable(msg: String): Nothing = throw new AssertionError(msg);
c8292b34
MW
44def unreachable(): Nothing = unreachable("unreachable");
45final val ok = ();
46final class Brand;
8eabb4ff
MW
47
48/*----- Various pieces of implicit magic ----------------------------------*/
49
50class InvalidCStringException(msg: String) extends Exception(msg);
8eabb4ff 51
25c35469 52object Implicits {
8eabb4ff
MW
53
54 /* --- Syntactic sugar for locks --- */
55
56 implicit class LockOps(lk: Lock) {
57 /* LK withLock { BODY }
58 * LK.withLock(INTERRUPT) { BODY }
25c35469
MW
59 * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
60 * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
8eabb4ff
MW
61 *
62 * Acquire a lock while executing a BODY. If a duration or deadline is
63 * given then wait so long for the lock, and then give up and run ALT
64 * instead.
65 */
66
67 def withLock[T](dur: Duration, interrupt: Boolean)
68 (body: => T): PendingLock[T] =
69 new PendingLock(lk, if (dur > Duration.Zero) dur else Duration.Zero,
70 interrupt, body);
71 def withLock[T](dur: Duration)(body: => T): PendingLock[T] =
72 withLock(dur, true)(body);
73 def withLock[T](dl: Deadline, interrupt: Boolean)
74 (body: => T): PendingLock[T] =
75 new PendingLock(lk, dl.timeLeft, interrupt, body);
76 def withLock[T](dl: Deadline)(body: => T): PendingLock[T] =
77 withLock(dl, true)(body);
78 def withLock[T](interrupt: Boolean)(body: => T): T = {
79 if (interrupt) lk.lockInterruptibly();
80 else lk.lock();
81 try { body; } finally lk.unlock();
82 }
83 def withLock[T](body: => T): T = withLock(true)(body);
84 }
85
25c35469 86 class PendingLock[T] private[Implicits]
8eabb4ff
MW
87 (val lk: Lock, val dur: Duration,
88 val interrupt: Boolean, body: => T) {
25c35469 89 /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
8eabb4ff 90
25c35469 91 def orElse(alt: => T): T = {
8eabb4ff
MW
92 val locked = (dur, interrupt) match {
93 case (Duration.Inf, true) => lk.lockInterruptibly(); true
94 case (Duration.Inf, false) => lk.lock(); true
95 case (Duration.Zero, false) => lk.tryLock()
96 case (_, true) => lk.tryLock(dur.length, dur.unit)
97 case _ => unreachable("timed wait is always interruptible");
98 }
99 if (!locked) alt;
100 else try { body; } finally lk.unlock();
101 }
102 }
8eabb4ff
MW
103}
104
105/*----- Cleanup assistant -------------------------------------------------*/
106
107class Cleaner {
108 /* A helper class for avoiding deep nests of `try'/`finally'.
109 *
110 * Make a `Cleaner' instance CL at the start of your operation. Apply it
111 * to blocks of code -- as CL { ACTION } -- as you proceed, to accumulate
112 * cleanup actions. Finally, call CL.cleanup() to invoke the accumulated
113 * actions, in reverse order.
114 */
115
116 var cleanups: List[() => Unit] = Nil;
117 def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
118 def cleanup() { cleanups foreach { _() } }
119}
120
121def withCleaner[T](body: Cleaner => T): T = {
122 /* An easier way to use the `Cleaner' class. Just
123 *
124 * withCleaner { CL => BODY }
125 *
126 * The BODY can attach cleanup actions to the cleaner CL by saying
127 * CL { ACTION } as usual. When the BODY exits, normally or otherwise, the
128 * cleanup actions are invoked in reverse order.
129 */
130
131 val cleaner = new Cleaner;
132 try { body(cleaner) }
133 finally { cleaner.cleanup(); }
134}
135
136def closing[T, U <: Closeable](thing: U)(body: U => T): T =
137 try { body(thing) }
138 finally { thing.close(); }
139
c8292b34
MW
140/*----- Control structures ------------------------------------------------*/
141
142private case class ExitBlock[T](brand: Brand, result: T)
143 extends ControlThrowable;
144
145def block[T](body: (T => Nothing) => T): T = {
146 /* block { exit[T] => ...; exit(x); ... }
147 *
148 * Execute the body until it calls the `exit' function or finishes.
149 * Annoyingly, Scala isn't clever enough to infer the return type, so
150 * you'll have to write it explicitly.
151 */
152
153 val mybrand = new Brand;
154 try { body { result => throw new ExitBlock(mybrand, result) } }
155 catch {
156 case ExitBlock(brand, result) if brand eq mybrand =>
157 result.asInstanceOf[T]
158 }
159}
160
161def blockUnit(body: (=> Nothing) => Unit) {
162 /* blockUnit { exit => ...; exit; ... }
163 *
164 * Like `block'; it just saves you having to write `exit[Unit] => ...;
165 * exit(ok); ...'.
166 */
167
168 val mybrand = new Brand;
169 try { body { throw new ExitBlock(mybrand, null) }; }
170 catch { case ExitBlock(brand, result) if brand eq mybrand => ok; }
171}
172
173def loop[T](body: (T => Nothing) => Unit): T = {
174 /* loop { exit[T] => ...; exit(x); ... }
175 *
176 * Repeatedly execute the body until it calls the `exit' function.
177 * Annoyingly, Scala isn't clever enough to infer the return type, so
178 * you'll have to write it explicitly.
179 */
180
181 block { exit => while (true) body(exit); unreachable }
182}
183
184def loopUnit(body: (=> Nothing) => Unit): Unit = {
185 /* loopUnit { exit => ...; exit; ... }
186 *
187 * Like `loop'; it just saves you having to write `exit[Unit] => ...;
188 * exit(()); ...'.
189 */
190
191 blockUnit { exit => while (true) body(exit); }
192}
193
194val BREAKS = new Breaks;
195import BREAKS.{breakable, break};
196
8eabb4ff
MW
197/*----- A gadget for fetching URLs ----------------------------------------*/
198
199class URLFetchException(msg: String) extends Exception(msg);
200
201trait URLFetchCallbacks {
202 def preflight(conn: URLConnection) { }
c8292b34 203 def write(buf: Array[Byte], n: Int, len: Long): Unit;
8eabb4ff
MW
204 def done(win: Boolean) { }
205}
206
207def fetchURL(url: URL, cb: URLFetchCallbacks) {
208 /* Fetch the URL, feeding the data through the callbacks CB. */
209
210 withCleaner { clean =>
211 var win: Boolean = false;
212 clean { cb.done(win); }
213
214 /* Set up the connection, and run a preflight check. */
215 val c = url.openConnection();
216 cb.preflight(c);
217
218 /* Start fetching data. */
219 val in = c.getInputStream; clean { in.close(); }
c8292b34 220 val explen = c.getContentLength;
8eabb4ff
MW
221
222 /* Read a buffer at a time, and give it to the callback. Maintain a
223 * running total.
224 */
c8292b34
MW
225 var len: Long = 0;
226 blockUnit { exit =>
227 for ((buf, n) <- blocks(in)) {
228 cb.write(buf, n, len);
229 len += n;
230 if (explen != -1 && len > explen) exit;
231 }
8eabb4ff
MW
232 }
233
234 /* I can't find it documented anywhere that the existing machinery
235 * checks the received stream against the advertised content length.
236 * It doesn't hurt to check again, anyway.
237 */
238 if (explen != -1 && explen != len) {
239 throw new URLFetchException(
240 s"received $len /= $explen bytes from `$url'");
241 }
242
243 /* Glorious success is ours. */
244 win = true;
245 }
246}
247
8eabb4ff
MW
248/*----- Threading things --------------------------------------------------*/
249
250def thread[T](name: String, run: Boolean = true, daemon: Boolean = true)
251 (f: => T): Thread = {
252 /* Make a thread with a given name, and maybe start running it. */
253
254 val t = new Thread(new Runnable { def run() { f; } }, name);
255 if (daemon) t.setDaemon(true);
256 if (run) t.start();
257 t
258}
259
260/*----- Quoting and parsing tokens ----------------------------------------*/
261
262def quoteTokens(v: Seq[String]): String = {
263 /* Return a string representing the token sequence V.
264 *
265 * The tokens are quoted as necessary.
266 */
267
268 val b = new StringBuilder;
269 var sep = false;
270 for (s <- v) {
271
272 /* If this isn't the first word, then write a separating space. */
273 if (!sep) sep = true;
274 else b += ' ';
275
276 /* Decide how to handle this token. */
277 if (s.length > 0 &&
278 (s forall { ch => (ch != ''' && ch != '"' && ch != '\\' &&
279 !ch.isWhitespace) })) {
280 /* If this word is nonempty and contains no problematic characters,
281 * we can write it literally.
282 */
283
284 b ++= s;
285 } else {
286 /* Otherwise, we shall have to do this the hard way. We could be
287 * cleverer about this, but it's not worth the effort.
288 */
289
290 b += '"';
291 s foreach { ch =>
292 if (ch == '"' || ch == '\\') b += '\\';
293 b += ch;
294 }
295 b += '"';
296 }
297 }
298 b.result
299}
300
301class InvalidQuotingException(msg: String) extends Exception(msg);
302
303def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
304 /* Parse the next token from a string S.
305 *
306 * If there is a token in S starting at or after index POS, then return
307 * it, and the index for the following token; otherwise return `None'.
308 */
309
310 val b = new StringBuilder;
311 val n = s.length;
312 var i = pos;
313 var q = 0;
314
315 /* Skip whitespace while we find the next token. */
316 while (i < n && s(i).isWhitespace) i += 1;
317
318 /* Maybe there just isn't anything to find. */
319 if (i >= n) return None;
320
321 /* There is something there. Unpick the quoting and escaping. */
322 while (i < n && (q != 0 || !s(i).isWhitespace)) {
323 s(i) match {
324 case '\\' =>
325 if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
326 b += s(i + 1); i += 2;
327 case ch@('"' | ''') =>
328 if (q == 0) q = ch;
329 else if (q == ch) q = 0;
330 else b += ch;
331 i += 1;
332 case ch =>
333 b += ch;
334 i += 1;
335 }
336 }
337
338 /* Check that the quoting was valid. */
339 if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
340
341 /* Skip whitespace before the next token. */
342 while (i < n && s(i).isWhitespace) i += 1;
343
344 /* We're done. */
345 Some((b.result, i))
346}
347
348def splitTokens(s: String, pos: Int = 0): Seq[String] = {
349 /* Return all of the tokens in string S into tokens, starting at POS. */
350
351 val b = List.newBuilder[String];
352 var i = pos;
353
c8292b34
MW
354 loopUnit { exit => nextToken(s, i) match {
355 case Some((w, j)) => b += w; i = j;
356 case None => exit;
357 } }
8eabb4ff
MW
358 b.result
359}
360
c8292b34
MW
361/*----- Other random things -----------------------------------------------*/
362
8eabb4ff 363trait LookaheadIterator[T] extends BufferedIterator[T] {
c8292b34
MW
364 /* An iterator in terms of a single `maybe there's another item' function.
365 *
366 * It seems like every time I write an iterator in Scala, the only way to
367 * find out whether there's a next item, for `hasNext', is to actually try
368 * to fetch it. So here's an iterator in terms of a function which goes
369 * off and maybe returns a next thing. It turns out to be easy to satisfy
370 * the additional requirements for `BufferedIterator', so why not?
371 */
372
373 /* Subclass responsibility. */
8eabb4ff 374 protected def fetch(): Option[T];
c8292b34
MW
375
376 /* The machinery. `st' is `None' if there's no current item, null if we've
377 * actually hit the end, or `Some(x)' if the current item is x.
378 */
379 private[this] var st: Option[T] = None;
8eabb4ff 380 private[this] def peek() {
c8292b34 381 /* Arrange to have a current item. */
8eabb4ff
MW
382 if (st == None) fetch() match {
383 case None => st = null;
384 case x@Some(_) => st = x;
385 }
386 }
c8292b34
MW
387
388 /* The `BufferedIterator' protocol. */
8eabb4ff 389 override def hasNext: Boolean = { peek(); st != null }
c8292b34 390 override def head: T =
8eabb4ff 391 { peek(); if (st == null) throw new NoSuchElementException; st.get }
c8292b34 392 override def next(): T = { val it = head; st = None; it }
8eabb4ff
MW
393}
394
c8292b34
MW
395def bufferedReader(r: Reader): BufferedReader = r match {
396 case br: BufferedReader => br
397 case _ => new BufferedReader(r)
398}
8eabb4ff 399
c8292b34
MW
400def lines(r: BufferedReader): BufferedIterator[String] =
401 new LookaheadIterator[String] {
402 /* Iterates over the lines of text in a `Reader' object. */
403 override protected def fetch() = Option(r.readLine());
404 }
405def lines(r: Reader): BufferedIterator[String] = lines(bufferedReader(r));
406
407def blocks(in: InputStream, blksz: Int):
408 BufferedIterator[(Array[Byte], Int)] =
409 /* Iterates over (possibly irregularly sized) blocks in a stream. */
410 new LookaheadIterator[(Array[Byte], Int)] {
411 val buf = new Array[Byte](blksz)
412 override protected def fetch() = {
413 val n = in.read(buf);
414 if (n < 0) None
415 else Some((buf, n))
416 }
417 }
418def blocks(in: InputStream):
419 BufferedIterator[(Array[Byte], Int)] = blocks(in, 4096);
420
421def blocks(in: BufferedReader, blksz: Int):
422 BufferedIterator[(Array[Char], Int)] =
423 /* Iterates over (possibly irregularly sized) blocks in a reader. */
424 new LookaheadIterator[(Array[Char], Int)] {
425 val buf = new Array[Char](blksz)
426 override protected def fetch() = {
427 val n = in.read(buf);
428 if (n < 0) None
429 else Some((buf, n))
430 }
8eabb4ff 431 }
c8292b34
MW
432def blocks(in: BufferedReader):
433 BufferedIterator[(Array[Char], Int)] = blocks(in, 4096);
434def blocks(r: Reader, blksz: Int): BufferedIterator[(Array[Char], Int)] =
435 blocks(bufferedReader(r), blksz);
436def blocks(r: Reader): BufferedIterator[(Array[Char], Int)] =
437 blocks(bufferedReader(r));
438
439def oxford(conj: String, things: Seq[String]): String = things match {
440 case Seq() => "<nothing>"
441 case Seq(a) => a
442 case Seq(a, b) => s"$a $conj $b"
443 case Seq(a, tail@_*) =>
444 val sb = new StringBuilder;
445 sb ++= a; sb ++= ", ";
446 def iter(rest: Seq[String]) {
447 rest match {
448 case Seq() => unreachable;
449 case Seq(a) => sb ++= conj; sb += ' '; sb ++= a;
450 case Seq(a, tail@_*) => sb ++= a; sb ++= ", "; iter(tail);
451 }
452 }
453 iter(tail);
454 sb.result
8eabb4ff
MW
455}
456
457/*----- That's all, folks -------------------------------------------------*/
458
459}