shake it all up
[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);
8eabb4ff 48
25c35469 49object Implicits {
8eabb4ff
MW
50
51 /* --- Syntactic sugar for locks --- */
52
53 implicit class LockOps(lk: Lock) {
54 /* LK withLock { BODY }
55 * LK.withLock(INTERRUPT) { BODY }
25c35469
MW
56 * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
57 * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
8eabb4ff
MW
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
25c35469 83 class PendingLock[T] private[Implicits]
8eabb4ff
MW
84 (val lk: Lock, val dur: Duration,
85 val interrupt: Boolean, body: => T) {
25c35469 86 /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
8eabb4ff 87
25c35469 88 def orElse(alt: => T): T = {
8eabb4ff
MW
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 }
8eabb4ff
MW
100}
101
102/*----- Cleanup assistant -------------------------------------------------*/
103
104class 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
118def 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
133def 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
139class URLFetchException(msg: String) extends Exception(msg);
140
141trait URLFetchCallbacks {
142 def preflight(conn: URLConnection) { }
143 def write(buf: Array[Byte], n: Int, len: Int): Unit;
144 def done(win: Boolean) { }
145}
146
147def 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
193def 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
205def 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
244class InvalidQuotingException(msg: String) extends Exception(msg);
245
246def 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
291def 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
304trait 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
319def 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}