chiark / gitweb /
79ac861d7d6afcd3388d5eb7fae1a3802b76bf67
[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;
32
33 import java.io.{BufferedReader, Closeable, File, 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
45 /*----- Various pieces of implicit magic ----------------------------------*/
46
47 class InvalidCStringException(msg: String) extends Exception(msg);
48
49 object Implicits {
50
51   /* --- Syntactic sugar for locks --- */
52
53   implicit class LockOps(lk: Lock) {
54     /* LK withLock { BODY }
55      * LK.withLock(INTERRUPT) { BODY }
56      * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
57      * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
58      *
59      * Acquire a lock while executing a BODY.  If a duration or deadline is
60      * given then wait so long for the lock, and then give up and run ALT
61      * instead.
62      */
63
64     def withLock[T](dur: Duration, interrupt: Boolean)
65                    (body: => T): PendingLock[T] =
66       new PendingLock(lk, if (dur > Duration.Zero) dur else Duration.Zero,
67                       interrupt, body);
68     def withLock[T](dur: Duration)(body: => T): PendingLock[T] =
69       withLock(dur, true)(body);
70     def withLock[T](dl: Deadline, interrupt: Boolean)
71                    (body: => T): PendingLock[T] =
72       new PendingLock(lk, dl.timeLeft, interrupt, body);
73     def withLock[T](dl: Deadline)(body: => T): PendingLock[T] =
74       withLock(dl, true)(body);
75     def withLock[T](interrupt: Boolean)(body: => T): T = {
76       if (interrupt) lk.lockInterruptibly();
77       else lk.lock();
78       try { body; } finally lk.unlock();
79     }
80     def withLock[T](body: => T): T = withLock(true)(body);
81   }
82
83   class PendingLock[T] private[Implicits]
84           (val lk: Lock, val dur: Duration,
85            val interrupt: Boolean, body: => T) {
86     /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
87
88     def orElse(alt: => T): T = {
89       val locked = (dur, interrupt) match {
90         case (Duration.Inf, true) => lk.lockInterruptibly(); true
91         case (Duration.Inf, false) => lk.lock(); true
92         case (Duration.Zero, false) => lk.tryLock()
93         case (_, true) => lk.tryLock(dur.length, dur.unit)
94         case _ => unreachable("timed wait is always interruptible");
95       }
96       if (!locked) alt;
97       else try { body; } finally lk.unlock();
98     }
99   }
100 }
101
102 /*----- Cleanup assistant -------------------------------------------------*/
103
104 class Cleaner {
105   /* A helper class for avoiding deep nests of `try'/`finally'.
106    *
107    * Make a `Cleaner' instance CL at the start of your operation.  Apply it
108    * to blocks of code -- as CL { ACTION } -- as you proceed, to accumulate
109    * cleanup actions.   Finally, call CL.cleanup() to invoke the accumulated
110    * actions, in reverse order.
111    */
112
113   var cleanups: List[() => Unit] = Nil;
114   def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
115   def cleanup() { cleanups foreach { _() } }
116 }
117
118 def withCleaner[T](body: Cleaner => T): T = {
119   /* An easier way to use the `Cleaner' class.  Just
120    *
121    *    withCleaner { CL => BODY }
122    *
123    * The BODY can attach cleanup actions to the cleaner CL by saying
124    * CL { ACTION } as usual.  When the BODY exits, normally or otherwise, the
125    * cleanup actions are invoked in reverse order.
126    */
127
128   val cleaner = new Cleaner;
129   try { body(cleaner) }
130   finally { cleaner.cleanup(); }
131 }
132
133 def closing[T, U <: Closeable](thing: U)(body: U => T): T =
134   try { body(thing) }
135   finally { thing.close(); }
136
137 /*----- A gadget for fetching URLs ----------------------------------------*/
138
139 class URLFetchException(msg: String) extends Exception(msg);
140
141 trait URLFetchCallbacks {
142   def preflight(conn: URLConnection) { }
143   def write(buf: Array[Byte], n: Int, len: Int): Unit;
144   def done(win: Boolean) { }
145 }
146
147 def fetchURL(url: URL, cb: URLFetchCallbacks) {
148   /* Fetch the URL, feeding the data through the callbacks CB. */
149
150   withCleaner { clean =>
151     var win: Boolean = false;
152     clean { cb.done(win); }
153
154     /* Set up the connection, and run a preflight check. */
155     val c = url.openConnection();
156     cb.preflight(c);
157
158     /* Start fetching data. */
159     val in = c.getInputStream; clean { in.close(); }
160     val explen = c.getContentLength();
161
162     /* Read a buffer at a time, and give it to the callback.  Maintain a
163      * running total.
164      */
165     val buf = new Array[Byte](4096);
166     var n = 0;
167     var len = 0;
168     while ({n = in.read(buf); n >= 0 && (explen == -1 || len <= explen)}) {
169       cb.write(buf, n, len);
170       len += n;
171     }
172
173     /* I can't find it documented anywhere that the existing machinery
174      * checks the received stream against the advertised content length.
175      * It doesn't hurt to check again, anyway.
176      */
177     if (explen != -1 && explen != len) {
178       throw new URLFetchException(
179         s"received $len /= $explen bytes from `$url'");
180     }
181
182     /* Glorious success is ours. */
183     win = true;
184   }
185 }
186
187 /*----- Running processes -------------------------------------------------*/
188
189 //def runProgram(
190
191 /*----- Threading things --------------------------------------------------*/
192
193 def thread[T](name: String, run: Boolean = true, daemon: Boolean = true)
194              (f: => T): Thread = {
195   /* Make a thread with a given name, and maybe start running it. */
196
197   val t = new Thread(new Runnable { def run() { f; } }, name);
198   if (daemon) t.setDaemon(true);
199   if (run) t.start();
200   t
201 }
202
203 /*----- Quoting and parsing tokens ----------------------------------------*/
204
205 def quoteTokens(v: Seq[String]): String = {
206   /* Return a string representing the token sequence V.
207    *
208    * The tokens are quoted as necessary.
209    */
210
211   val b = new StringBuilder;
212   var sep = false;
213   for (s <- v) {
214
215     /* If this isn't the first word, then write a separating space. */
216     if (!sep) sep = true;
217     else b += ' ';
218
219     /* Decide how to handle this token. */
220     if (s.length > 0 &&
221         (s forall { ch => (ch != ''' && ch != '"' && ch != '\\' &&
222                            !ch.isWhitespace) })) {
223       /* If this word is nonempty and contains no problematic characters,
224        * we can write it literally.
225        */
226
227       b ++= s;
228     } else {
229       /* Otherwise, we shall have to do this the hard way.  We could be
230        * cleverer about this, but it's not worth the effort.
231        */
232
233       b += '"';
234       s foreach { ch =>
235         if (ch == '"' || ch == '\\') b += '\\';
236         b += ch;
237       }
238       b += '"';
239     }
240   }
241   b.result
242 }
243
244 class InvalidQuotingException(msg: String) extends Exception(msg);
245
246 def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
247   /* Parse the next token from a string S.
248    *
249    * If there is a token in S starting at or after index POS, then return
250    * it, and the index for the following token; otherwise return `None'.
251    */
252
253   val b = new StringBuilder;
254   val n = s.length;
255   var i = pos;
256   var q = 0;
257
258   /* Skip whitespace while we find the next token. */
259   while (i < n && s(i).isWhitespace) i += 1;
260
261   /* Maybe there just isn't anything to find. */
262   if (i >= n) return None;
263
264   /* There is something there.  Unpick the quoting and escaping. */
265   while (i < n && (q != 0 || !s(i).isWhitespace)) {
266     s(i) match {
267       case '\\' =>
268         if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
269         b += s(i + 1); i += 2;
270       case ch@('"' | ''') =>
271         if (q == 0) q = ch;
272         else if (q == ch) q = 0;
273         else b += ch;
274         i += 1;
275       case ch =>
276         b += ch;
277         i += 1;
278     }
279   }
280
281   /* Check that the quoting was valid. */
282   if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
283
284   /* Skip whitespace before the next token. */
285   while (i < n && s(i).isWhitespace) i += 1;
286
287   /* We're done. */
288   Some((b.result, i))
289 }
290
291 def splitTokens(s: String, pos: Int = 0): Seq[String] = {
292   /* Return all of the tokens in string S into tokens, starting at POS. */
293
294   val b = List.newBuilder[String];
295   var i = pos;
296
297   while (nextToken(s, i) match {
298     case Some((w, j)) => b += w; i = j; true
299     case None => false
300   }) ();
301   b.result
302 }
303
304 trait LookaheadIterator[T] extends BufferedIterator[T] {
305   private[this] var st: Option[T] = None;
306   protected def fetch(): Option[T];
307   private[this] def peek() {
308     if (st == None) fetch() match {
309       case None => st = null;
310       case x@Some(_) => st = x;
311     }
312   }
313   override def hasNext: Boolean = { peek(); st != null }
314   override def head(): T =
315     { peek(); if (st == null) throw new NoSuchElementException; st.get }
316   override def next(): T = { val it = head(); st = None; it }
317 }
318
319 def lines(r: Reader) = new LookaheadIterator[String] {
320   /* Iterates over the lines of text in a `Reader' object. */
321
322   private[this] val in = r match {
323     case br: BufferedReader => br;
324     case _ => new BufferedReader(r);
325   }
326   protected override def fetch(): Option[String] = Option(in.readLine);
327 }
328
329 /*----- That's all, folks -------------------------------------------------*/
330
331 }