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};
31import scala.util.control.Breaks;
32
33import java.io.{BufferedReader, Closeable, File, Reader};
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);
44
45/*----- Various pieces of implicit magic ----------------------------------*/
46
47class InvalidCStringException(msg: String) extends Exception(msg);
48type CString = Array[Byte];
49
50object 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
185class 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
199def 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
214def 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
220class URLFetchException(msg: String) extends Exception(msg);
221
222trait URLFetchCallbacks {
223 def preflight(conn: URLConnection) { }
224 def write(buf: Array[Byte], n: Int, len: Int): Unit;
225 def done(win: Boolean) { }
226}
227
228def 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
274def 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
286def 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
325class InvalidQuotingException(msg: String) extends Exception(msg);
326
327def 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
372def 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
385trait 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
400def 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}