chiark / gitweb /
progress.scala: Miscellaneous WIP.
[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.{URL, URLConnection};
35 import java.nio.{ByteBuffer, CharBuffer};
36 import java.nio.charset.Charset;
37 import java.util.concurrent.locks.{Lock, ReentrantLock};
38
39 /*----- Miscellaneous useful things ---------------------------------------*/
40
41 val rng = new java.security.SecureRandom;
42
43 def unreachable(msg: String): Nothing = throw new AssertionError(msg);
44 def unreachable(): Nothing = unreachable("unreachable");
45 final val ok = ();
46 final class Brand;
47
48 /*----- Various pieces of implicit magic ----------------------------------*/
49
50 class InvalidCStringException(msg: String) extends Exception(msg);
51
52 object Implicits {
53
54   /* --- Syntactic sugar for locks --- */
55
56   implicit class LockOps(lk: Lock) {
57     /* LK withLock { BODY }
58      * LK.withLock(INTERRUPT) { BODY }
59      * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
60      * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
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
86   class PendingLock[T] private[Implicits]
87           (val lk: Lock, val dur: Duration,
88            val interrupt: Boolean, body: => T) {
89     /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
90
91     def orElse(alt: => T): T = {
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   }
103 }
104
105 /*----- Cleanup assistant -------------------------------------------------*/
106
107 class 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
121 def 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
136 def closing[T, U <: Closeable](thing: U)(body: U => T): T =
137   try { body(thing) }
138   finally { thing.close(); }
139
140 /*----- Control structures ------------------------------------------------*/
141
142 private case class ExitBlock[T](brand: Brand, result: T)
143         extends ControlThrowable;
144
145 def 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
161 def 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
173 def 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
184 def 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
194 val BREAKS = new Breaks;
195 import BREAKS.{breakable, break};
196
197 /*----- A gadget for fetching URLs ----------------------------------------*/
198
199 class URLFetchException(msg: String) extends Exception(msg);
200
201 trait URLFetchCallbacks {
202   def preflight(conn: URLConnection) { }
203   def write(buf: Array[Byte], n: Int, len: Long): Unit;
204   def done(win: Boolean) { }
205 }
206
207 def 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(); }
220     val explen = c.getContentLength;
221
222     /* Read a buffer at a time, and give it to the callback.  Maintain a
223      * running total.
224      */
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       }
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
248 /*----- Threading things --------------------------------------------------*/
249
250 def 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
262 def 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
301 class InvalidQuotingException(msg: String) extends Exception(msg);
302
303 def 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
348 def 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
354   loopUnit { exit => nextToken(s, i) match {
355     case Some((w, j)) => b += w; i = j;
356     case None => exit;
357   } }
358   b.result
359 }
360
361 /*----- Other random things -----------------------------------------------*/
362
363 trait LookaheadIterator[T] extends BufferedIterator[T] {
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. */
374   protected def fetch(): Option[T];
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;
380   private[this] def peek() {
381     /* Arrange to have a current item. */
382     if (st == None) fetch() match {
383       case None => st = null;
384       case x@Some(_) => st = x;
385     }
386   }
387
388   /* The `BufferedIterator' protocol. */
389   override def hasNext: Boolean = { peek(); st != null }
390   override def head: T =
391     { peek(); if (st == null) throw new NoSuchElementException; st.get }
392   override def next(): T = { val it = head; st = None; it }
393 }
394
395 def bufferedReader(r: Reader): BufferedReader = r match {
396   case br: BufferedReader => br
397   case _ => new BufferedReader(r)
398 }
399
400 def 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   }
405 def lines(r: Reader): BufferedIterator[String] = lines(bufferedReader(r));
406
407 def 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   }
418 def blocks(in: InputStream):
419         BufferedIterator[(Array[Byte], Int)] = blocks(in, 4096);
420
421 def 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     }
431   }
432 def blocks(in: BufferedReader):
433         BufferedIterator[(Array[Char], Int)] = blocks(in, 4096);
434 def blocks(r: Reader, blksz: Int): BufferedIterator[(Array[Char], Int)] =
435   blocks(bufferedReader(r), blksz);
436 def blocks(r: Reader): BufferedIterator[(Array[Char], Int)] =
437   blocks(bufferedReader(r));
438
439 def 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
455 }
456
457 /*----- That's all, folks -------------------------------------------------*/
458
459 }