chiark / gitweb /
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;
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 type CString = Array[Byte];
49
50 object Magic {
51
52   /* --- Syntactic sugar for locks --- */
53
54   implicit class LockOps(lk: Lock) {
55     /* LK withLock { BODY }
56      * LK.withLock(INTERRUPT) { BODY }
57      * LK.withLock(DUR, [INTERRUPT]) { BODY } orelse { ALT }
58      * LK.withLock(DL, [INTERRUPT]) { BODY } orelse { ALT }
59      *
60      * Acquire a lock while executing a BODY.  If a duration or deadline is
61      * given then wait so long for the lock, and then give up and run ALT
62      * instead.
63      */
64
65     def withLock[T](dur: Duration, interrupt: Boolean)
66                    (body: => T): PendingLock[T] =
67       new PendingLock(lk, if (dur > Duration.Zero) dur else Duration.Zero,
68                       interrupt, body);
69     def withLock[T](dur: Duration)(body: => T): PendingLock[T] =
70       withLock(dur, true)(body);
71     def withLock[T](dl: Deadline, interrupt: Boolean)
72                    (body: => T): PendingLock[T] =
73       new PendingLock(lk, dl.timeLeft, interrupt, body);
74     def withLock[T](dl: Deadline)(body: => T): PendingLock[T] =
75       withLock(dl, true)(body);
76     def withLock[T](interrupt: Boolean)(body: => T): T = {
77       if (interrupt) lk.lockInterruptibly();
78       else lk.lock();
79       try { body; } finally lk.unlock();
80     }
81     def withLock[T](body: => T): T = withLock(true)(body);
82   }
83
84   class PendingLock[T] private[Magic]
85           (val lk: Lock, val dur: Duration,
86            val interrupt: Boolean, body: => T) {
87     /* An auxiliary class for LockOps; provides the `orelse' qualifier. */
88
89     def orelse(alt: => T): T = {
90       val locked = (dur, interrupt) match {
91         case (Duration.Inf, true) => lk.lockInterruptibly(); true
92         case (Duration.Inf, false) => lk.lock(); true
93         case (Duration.Zero, false) => lk.tryLock()
94         case (_, true) => lk.tryLock(dur.length, dur.unit)
95         case _ => unreachable("timed wait is always interruptible");
96       }
97       if (!locked) alt;
98       else try { body; } finally lk.unlock();
99     }
100   }
101
102   /* --- Conversion to/from C strings --- */
103
104   implicit class ConvertJStringToCString(s: String) {
105     /* Magic to convert a string into a C string (null-terminated bytes). */
106
107     def toCString: CString = {
108       /* Convert the receiver to a C string.
109        *
110        * We do this by hand, rather than relying on the JNI's built-in
111        * conversions, because we use the default encoding taken from the
112        * locale settings, rather than the ridiculous `modified UTF-8' which
113        * is (a) insensitive to the user's chosen locale and (b) not actually
114        * UTF-8 either.
115        */
116
117       val enc = Charset.defaultCharset.newEncoder;
118       val in = CharBuffer.wrap(s);
119       var sz: Int = (s.length*enc.averageBytesPerChar + 1).toInt;
120       var out = ByteBuffer.allocate(sz);
121
122       while (true) {
123         /* If there's still stuff to encode, then encode it.  Otherwise,
124          * there must be some dregs left in the encoder, so flush them out.
125          */
126         val r = if (in.hasRemaining) enc.encode(in, out, true)
127                 else enc.flush(out);
128
129         /* Sift through the wreckage to figure out what to do. */
130         if (r.isError) r.throwException();
131         else if (r.isOverflow) {
132           /* No space in the buffer.  Make it bigger. */
133
134           sz *= 2;
135           val newout = ByteBuffer.allocate(sz);
136           out.flip(); newout.put(out);
137           out = newout;
138         } else if (r.isUnderflow) {
139           /* All done.  Check that there are no unexpected zero bytes -- so
140            * this will indeed be a valid C string -- and convert into a byte
141            * array that the C code will be able to pick apart.
142            */
143
144           out.flip(); val n = out.limit; val u = out.array;
145           if ({val z = u.indexOf(0); 0 <= z && z < n})
146             throw new InvalidCStringException("null byte in encoding");
147           val v = new Array[Byte](n + 1);
148           out.array.copyToArray(v, 0, n);
149           v(n) = 0;
150           return v;
151         }
152       }
153
154       /* Placate the type checker. */
155       unreachable("unreachable");
156     }
157   }
158
159   implicit class ConvertCStringToJString(v: CString) {
160     /* Magic to convert a C string into a `proper' string. */
161
162     def toJString: String = {
163       /* Convert the receiver to a C string.
164        *
165        * We do this by hand, rather than relying on the JNI's built-in
166        * conversions, because we use the default encoding taken from the
167        * locale settings, rather than the ridiculous `modified UTF-8' which
168        * is (a) insensitive to the user's chosen locale and (b) not actually
169        * UTF-8 either.
170        */
171
172       val inlen = v.indexOf(0) match {
173         case -1 => v.length
174         case n => n
175       }
176       val dec = Charset.defaultCharset.newDecoder;
177       val in = ByteBuffer.wrap(v, 0, inlen);
178       dec.decode(in).toString
179     }
180   }
181 }
182
183 /*----- Cleanup assistant -------------------------------------------------*/
184
185 class Cleaner {
186   /* A helper class for avoiding deep nests of `try'/`finally'.
187    *
188    * Make a `Cleaner' instance CL at the start of your operation.  Apply it
189    * to blocks of code -- as CL { ACTION } -- as you proceed, to accumulate
190    * cleanup actions.   Finally, call CL.cleanup() to invoke the accumulated
191    * actions, in reverse order.
192    */
193
194   var cleanups: List[() => Unit] = Nil;
195   def apply(cleanup: => Unit) { cleanups +:= { () => cleanup; } }
196   def cleanup() { cleanups foreach { _() } }
197 }
198
199 def withCleaner[T](body: Cleaner => T): T = {
200   /* An easier way to use the `Cleaner' class.  Just
201    *
202    *    withCleaner { CL => BODY }
203    *
204    * The BODY can attach cleanup actions to the cleaner CL by saying
205    * CL { ACTION } as usual.  When the BODY exits, normally or otherwise, the
206    * cleanup actions are invoked in reverse order.
207    */
208
209   val cleaner = new Cleaner;
210   try { body(cleaner) }
211   finally { cleaner.cleanup(); }
212 }
213
214 def closing[T, U <: Closeable](thing: U)(body: U => T): T =
215   try { body(thing) }
216   finally { thing.close(); }
217
218 /*----- A gadget for fetching URLs ----------------------------------------*/
219
220 class URLFetchException(msg: String) extends Exception(msg);
221
222 trait URLFetchCallbacks {
223   def preflight(conn: URLConnection) { }
224   def write(buf: Array[Byte], n: Int, len: Int): Unit;
225   def done(win: Boolean) { }
226 }
227
228 def fetchURL(url: URL, cb: URLFetchCallbacks) {
229   /* Fetch the URL, feeding the data through the callbacks CB. */
230
231   withCleaner { clean =>
232     var win: Boolean = false;
233     clean { cb.done(win); }
234
235     /* Set up the connection, and run a preflight check. */
236     val c = url.openConnection();
237     cb.preflight(c);
238
239     /* Start fetching data. */
240     val in = c.getInputStream; clean { in.close(); }
241     val explen = c.getContentLength();
242
243     /* Read a buffer at a time, and give it to the callback.  Maintain a
244      * running total.
245      */
246     val buf = new Array[Byte](4096);
247     var n = 0;
248     var len = 0;
249     while ({n = in.read(buf); n >= 0 && (explen == -1 || len <= explen)}) {
250       cb.write(buf, n, len);
251       len += n;
252     }
253
254     /* I can't find it documented anywhere that the existing machinery
255      * checks the received stream against the advertised content length.
256      * It doesn't hurt to check again, anyway.
257      */
258     if (explen != -1 && explen != len) {
259       throw new URLFetchException(
260         s"received $len /= $explen bytes from `$url'");
261     }
262
263     /* Glorious success is ours. */
264     win = true;
265   }
266 }
267
268 /*----- Running processes -------------------------------------------------*/
269
270 //def runProgram(
271
272 /*----- Threading things --------------------------------------------------*/
273
274 def thread[T](name: String, run: Boolean = true, daemon: Boolean = true)
275              (f: => T): Thread = {
276   /* Make a thread with a given name, and maybe start running it. */
277
278   val t = new Thread(new Runnable { def run() { f; } }, name);
279   if (daemon) t.setDaemon(true);
280   if (run) t.start();
281   t
282 }
283
284 /*----- Quoting and parsing tokens ----------------------------------------*/
285
286 def quoteTokens(v: Seq[String]): String = {
287   /* Return a string representing the token sequence V.
288    *
289    * The tokens are quoted as necessary.
290    */
291
292   val b = new StringBuilder;
293   var sep = false;
294   for (s <- v) {
295
296     /* If this isn't the first word, then write a separating space. */
297     if (!sep) sep = true;
298     else b += ' ';
299
300     /* Decide how to handle this token. */
301     if (s.length > 0 &&
302         (s forall { ch => (ch != ''' && ch != '"' && ch != '\\' &&
303                            !ch.isWhitespace) })) {
304       /* If this word is nonempty and contains no problematic characters,
305        * we can write it literally.
306        */
307
308       b ++= s;
309     } else {
310       /* Otherwise, we shall have to do this the hard way.  We could be
311        * cleverer about this, but it's not worth the effort.
312        */
313
314       b += '"';
315       s foreach { ch =>
316         if (ch == '"' || ch == '\\') b += '\\';
317         b += ch;
318       }
319       b += '"';
320     }
321   }
322   b.result
323 }
324
325 class InvalidQuotingException(msg: String) extends Exception(msg);
326
327 def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
328   /* Parse the next token from a string S.
329    *
330    * If there is a token in S starting at or after index POS, then return
331    * it, and the index for the following token; otherwise return `None'.
332    */
333
334   val b = new StringBuilder;
335   val n = s.length;
336   var i = pos;
337   var q = 0;
338
339   /* Skip whitespace while we find the next token. */
340   while (i < n && s(i).isWhitespace) i += 1;
341
342   /* Maybe there just isn't anything to find. */
343   if (i >= n) return None;
344
345   /* There is something there.  Unpick the quoting and escaping. */
346   while (i < n && (q != 0 || !s(i).isWhitespace)) {
347     s(i) match {
348       case '\\' =>
349         if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
350         b += s(i + 1); i += 2;
351       case ch@('"' | ''') =>
352         if (q == 0) q = ch;
353         else if (q == ch) q = 0;
354         else b += ch;
355         i += 1;
356       case ch =>
357         b += ch;
358         i += 1;
359     }
360   }
361
362   /* Check that the quoting was valid. */
363   if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
364
365   /* Skip whitespace before the next token. */
366   while (i < n && s(i).isWhitespace) i += 1;
367
368   /* We're done. */
369   Some((b.result, i))
370 }
371
372 def splitTokens(s: String, pos: Int = 0): Seq[String] = {
373   /* Return all of the tokens in string S into tokens, starting at POS. */
374
375   val b = List.newBuilder[String];
376   var i = pos;
377
378   while (nextToken(s, i) match {
379     case Some((w, j)) => b += w; i = j; true
380     case None => false
381   }) ();
382   b.result
383 }
384
385 trait LookaheadIterator[T] extends BufferedIterator[T] {
386   private[this] var st: Option[T] = None;
387   protected def fetch(): Option[T];
388   private[this] def peek() {
389     if (st == None) fetch() match {
390       case None => st = null;
391       case x@Some(_) => st = x;
392     }
393   }
394   override def hasNext: Boolean = { peek(); st != null }
395   override def head(): T =
396     { peek(); if (st == null) throw new NoSuchElementException; st.get }
397   override def next(): T = { val it = head(); st = None; it }
398 }
399
400 def lines(r: Reader) = new LookaheadIterator[String] {
401   /* Iterates over the lines of text in a `Reader' object. */
402
403   private[this] val in = r match {
404     case br: BufferedReader => br;
405     case _ => new BufferedReader(r);
406   }
407   protected override def fetch(): Option[String] = Option(in.readLine);
408 }
409
410 /*----- That's all, folks -------------------------------------------------*/
411
412 }