shake it all up
authorMark Wooding <mdw@distorted.org.uk>
Thu, 31 May 2018 18:29:23 +0000 (19:29 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Thu, 31 May 2018 18:29:23 +0000 (19:29 +0100)
Makefile
admin.scala
jni.c
jni.scala [deleted file]
main.scala
sys.scala
util.scala

index a03ba64..fb3c83a 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -60,17 +60,14 @@ libtoy.so: $(call objects,$(libtoy.so_SOURCES),.o)
 
 TARGETS                        += util.stamp
 
-TARGETS                        += jni.stamp
-jni.stamp: util.stamp
-
 TARGETS                        += sys.stamp
-sys.stamp: jni.stamp util.stamp
+sys.stamp: util.stamp
 
 TARGETS                        += admin.stamp
-admin.stamp: util.stamp
+admin.stamp: util.stamp sys.stamp
 
 TARGETS                        += main.stamp
-main.stamp: jni.stamp
+main.stamp: sys.stamp
 
 all:: $(TARGETS)
 ALLSOURCES             += $(foreach t,$(TARGETS),$($t_SOURCES))
index cc82186..01d7995 100644 (file)
@@ -34,7 +34,7 @@ import scala.collection.mutable.{HashMap, Publisher};
 import scala.concurrent.Channel;
 import scala.util.control.Breaks;
 
-import Magic._;
+import Implicits._;
 
 /*----- Classification of server messages ---------------------------------*/
 
diff --git a/jni.c b/jni.c
index fd91d0c..934be91 100644 (file)
--- a/jni.c
+++ b/jni.c
 /*----- Magic class names and similar -------------------------------------*/
 
 /* The name decoration is horrific.  Hide it. */
-#define JNIFUNC(f) Java_uk_org_distorted_tripe_jni_package_00024_##f
+#define JNIFUNC(f) Java_uk_org_distorted_tripe_sys_package_00024_##f
 
 /* The little class for bundling up error codes. */
-#define ERRENTRY "uk/org/distorted/tripe/jni/package$ErrorEntry"
+#define ERRENTRY "uk/org/distorted/tripe/sys/package$ErrorEntry"
 
 /* The `stat' class. */
-#define STAT "uk/org/distorted/tripe/jni/package$FileInfo"
+#define STAT "uk/org/distorted/tripe/sys/package$FileInfo"
 
 /* Exception class names. */
 #define NULLERR "java/lang/NullPointerException"
-#define TYPEERR "uk/org/distorted/tripe/jni/package$NativeObjectTypeException"
+#define TYPEERR "uk/org/distorted/tripe/sys/package$NativeObjectTypeException"
 #define SYSERR "uk/org/distorted/tripe/sys/package$SystemError"
 #define ARGERR "java/lang/IllegalArgumentException"
 #define BOUNDSERR "java/lang/IndexOutOfBoundsException"
@@ -80,7 +80,7 @@ static void put_cstring(JNIEnv *jni, jbyteArray v, const char *p)
   { if (p) (*jni)->ReleaseByteArrayElements(jni, v, (jbyte *)p, JNI_ABORT); }
 
 static void vexcept(JNIEnv *jni, const char *clsname,
-                   const char *msg, va_list ap)
+                   const char *msg, va_list *ap)
 {
   jclass cls;
   int rc;
@@ -90,7 +90,7 @@ static void vexcept(JNIEnv *jni, const char *clsname,
   if (!msg)
     rc = (*jni)->ThrowNew(jni, cls, 0);
   else {
-    dstr_vputf(&d, msg, &ap);
+    dstr_vputf(&d, msg, ap);
     rc = (*jni)->ThrowNew(jni, cls, d.buf);
     assert(!rc);
     dstr_destroy(&d);
@@ -103,7 +103,7 @@ static void except(JNIEnv *jni, const char *clsname, const char *msg, ...)
   va_list ap;
 
   va_start(ap, msg);
-  vexcept(jni, clsname, msg, ap);
+  vexcept(jni, clsname, msg, &ap);
   va_end(ap);
 }
 
@@ -164,7 +164,7 @@ static const char *get_cstring(JNIEnv *jni, jbyteArray v)
 }
 
 static void vexcept_syserror(JNIEnv *jni, const char *clsname,
-                            int err, const char *msg, va_list ap)
+                            int err, const char *msg, va_list *ap)
 {
   jclass cls;
   int rc;
@@ -175,7 +175,7 @@ static void vexcept_syserror(JNIEnv *jni, const char *clsname,
 
   cls = (*jni)->FindClass(jni, clsname); assert(cls);
   init = (*jni)->GetMethodID(jni, cls, "<init>", "(I[B)V"); assert(init);
-  dstr_vputf(&d, msg, &ap);
+  dstr_vputf(&d, msg, ap);
   msgstr = wrap_cstring(jni, d.buf); assert(msgstr);
   dstr_destroy(&d);
   e = (*jni)->NewObject(jni, cls, init, err, msgstr); assert(e);
@@ -188,7 +188,7 @@ static void except_syserror(JNIEnv *jni, const char *clsname,
   va_list ap;
 
   va_start(ap, msg);
-  vexcept_syserror(jni, clsname, err, msg, ap);
+  vexcept_syserror(jni, clsname, err, msg, &ap);
   va_end(ap);
 }
 
@@ -1193,8 +1193,8 @@ end:
   return (rc);
 }
 
-JNIEXPORT void JNICALL JNIFUNC(close)(JNIEnv *jni, jobject cls,
-                                     wrapper wconn, jint how)
+JNIEXPORT void JNICALL JNIFUNC(closeconn)(JNIEnv *jni, jobject cls,
+                                         wrapper wconn, jint how)
 {
   struct conn conn;
   int rc;
diff --git a/jni.scala b/jni.scala
deleted file mode 100644 (file)
index ea6ae76..0000000
--- a/jni.scala
+++ /dev/null
@@ -1,246 +0,0 @@
-/* -*-java-*-
- *
- * Declarations of C functions
- *
- * (c) 2018 Straylight/Edgeware
- */
-
-/*----- Licensing notice --------------------------------------------------*
- *
- * This file is part of the Trivial IP Encryption (TrIPE) Android app.
- *
- * TrIPE is free software: you can redistribute it and/or modify it under
- * the terms of the GNU General Public License as published by the Free
- * Software Foundation; either version 3 of the License, or (at your
- * option) any later version.
- *
- * TrIPE is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-package uk.org.distorted.tripe; package object jni {
-
-/*----- Imports -----------------------------------------------------------*/
-
-import java.io.{Closeable, File};
-import java.util.Date;
-import Magic._;
-
-/*----- Main code ---------------------------------------------------------*/
-
-/* Import the native code library. */
-System.loadLibrary("toy");
-
-/* Exception indicating that a wrapped native object has been clobbered. */
-class NativeObjectTypeException(msg: String) extends RuntimeException(msg);
-type Wrapper = Array[Byte];
-
-case class ErrorEntry(val tag: String, val err: Int);
-@native def errtab: Array[ErrorEntry];
-@native def strerror(err: Int): CString;
-
-@native def hashsz(hash: String): Int;
-      /* Return the output hash size for the named HASH function, or -1. */
-
-/* Flags for `close'. */
-val CF_CLOSERD = 1;
-val CF_CLOSEWR = 2;
-val CF_CLOSEMASK = CF_CLOSERD | CF_CLOSEWR;
-
-/* Flags for `lock'. */
-val LKF_EXCL = 1;
-val LKF_WAIT = 2;
-
-/* Flags for `stat'. */
-val S_IFMT = 0xf000;
-val S_IFIFO = 0x1000;
-val S_IFCHR = 0x2000;
-val S_IFDIR = 0x4000;
-val S_IFBLK = 0x6000;
-val S_IFREG = 0x8000;
-val S_IFLNK = 0xa000;
-val S_IFSOCK = 0xc000;
-
-object FileType extends Enumeration {
-  val FIFO, CHR, DIR, BLK, REG, LNK, SOCK, UNK = Value;
-}
-import FileType.{Value => _, _};
-
-class FileInfo private[this](val devMajor: Int, val devMinor: Int,
-                            val ino: Long, val mode: Int, val nlink: Int,
-                            val uid: Int, val gid: Int,
-                            _rdevMinor: Int, _rdevMajor: Int,
-                            val size: Long,
-                            val blksize: Int, val blocks: Long,
-                            val atime: Date, val mtime: Date,
-                            val ctime: Date) {
-  def this(devMajor: Int, devMinor: Int, ino: Long,
-          mode: Int, nlink: Int, uid: Int, gid: Int,
-          rdevMinor: Int, rdevMajor: Int,
-          size: Long, blksize: Int, blocks: Long,
-          atime: Long, mtime: Long, ctime: Long) {
-    this(devMajor, devMinor, ino, mode, nlink, uid, gid,
-        rdevMajor, rdevMinor, size, blksize, blocks,
-        new Date(atime), new Date(mtime), new Date(ctime));
-  }
-  def perms: Int = mode&0xfff;
-  def ftype: FileType.Value = (mode&S_IFMT) match {
-    case S_IFIFO => FIFO
-    case S_IFCHR => CHR
-    case S_IFDIR => DIR
-    case S_IFBLK => BLK
-    case S_IFREG => REG
-    case S_IFLNK => LNK
-    case S_IFSOCK => SOCK
-    case _ => UNK
-  }
-  def isfifo: Boolean = ftype == FIFO
-  def ischr: Boolean = ftype == CHR
-  def isdir: Boolean = ftype == DIR
-  def isblk: Boolean = ftype == BLK
-  def isreg: Boolean = ftype == REG
-  def islnk: Boolean = ftype == LNK
-  def issock: Boolean = ftype == SOCK
-  def isdev: Boolean = ischr || isblk;
-  private[this] def mustBeDevice() {
-    if (!isdev) throw new IllegalArgumentException("Object is not a device");
-  }
-  def rdevMajor: Int = { mustBeDevice(); _rdevMajor }
-  def rdevMinor: Int = { mustBeDevice(); _rdevMinor }
-}
-@native protected def unlink(path: CString);
-def unlink(path: String) { unlink(path.toCString); }
-def unlink(file: File) { unlink(file.getPath); }
-@native protected def rmdir(path: CString);
-def rmdir(path: String) { rmdir(path.toCString); }
-def rmdir(file: File) { rmdir(file.getPath); }
-@native protected def mkdir(path: CString, mode: Int);
-def mkdir(path: String, mode: Int) { mkdir(path.toCString, mode); }
-def mkdir(path: String) { mkdir(path, 0x1ff); }
-def mkdir(file: File, mode: Int) { mkdir(file.getPath, mode); }
-def mkdir(file: File) { mkdir(file.getPath); }
-@native protected def mkfile(path: CString, mode: Int);
-def mkfile(path: String, mode: Int) { mkfile(path.toCString, mode); }
-def mkfile(path: String) { mkfile(path, 0x1b6); }
-def mkfile(file: File, mode: Int) { mkfile(file.getPath, mode); }
-def mkfile(file: File) { mkfile(file.getPath); }
-@native protected def rename(from: CString, to: CString);
-def rename(from: String, to: String) 
-  { rename(from.toCString, to.toCString); }
-def rename(from: File, to: File) 
-  { rename(from.getPath, to.getPath); }
-@native protected def stat(path: CString): FileInfo;
-def stat(path: String): FileInfo = stat(path.toCString);
-def stat(file: File): FileInfo = stat(file.getPath);
-@native protected def lstat(path: CString): FileInfo;
-def lstat(path: String): FileInfo = lstat(path.toCString);
-def lstat(file: File): FileInfo = lstat(file.getPath);
-
-@native protected def opendir(path: CString): Wrapper;
-@native protected def readdir(path: CString, dir: Wrapper): CString;
-@native protected def closedir(path: CString, dir: Wrapper);
-
-abstract class BaseDirIterator[T](cpath: CString)
-       extends LookaheadIterator[T] with Closeable {
-  def this(path: String) { this(path.toCString); }
-  def this(dir: File) { this(dir.getPath); }
-  override def close() { closedir(cpath, dir); }
-  override protected def finalize() { super.finalize(); close(); }
-  private[this] val dir = opendir(cpath);
-  protected def mangle(file: String): T;
-  override protected def fetch(): Option[T] = readdir(cpath, dir) match {
-    case null => None
-    case f => f.toJString match {
-      case "." | ".." => fetch()
-      case jf => Some(mangle(jf))
-    }
-  }
-}
-
-class DirIterator(val path: String) extends BaseDirIterator[String](path) {
-  def this(dir: File) { this(dir.getPath); }
-  override protected def mangle(file: String): String = file;
-}
-def listDir(path: String): List[String] = {
-  val iter = new DirIterator(path);
-  try { iter.toList }
-  finally { iter.close(); }
-}
-def listDir(dir: File): List[String] = listDir(dir.getPath);
-
-class DirFilesIterator private[this](val dir: File, cpath: CString)
-       extends BaseDirIterator[File](cpath) {
-  def this(dir: File) { this(dir, dir.getPath.toCString); }
-  def this(path: String) { this(new File(path), path.toCString); }
-  override protected def mangle(file: String): File = new File(dir, file);
-}
-def listDirFiles(path: String): List[File] = {
-  val iter = new DirFilesIterator(path);
-  try { iter.toList }
-  finally { iter.close(); }
-}
-def listDirFiles(dir: File): List[File] = listDirFiles(dir.getPath);
-
-@native protected def lock(path: CString, flags: Int): Wrapper;
-@native protected def unlock(lock: Wrapper);
-class FileLock(path: String, flags: Int) extends Closeable {
-  def this(file: File, flags: Int) { this(file.getPath, flags); }
-  def this(path: String) { this(path, LKF_EXCL); }
-  def this(file: File) { this(file.getPath, LKF_EXCL); }
-  private[this] val lk = lock(path.toCString, flags);
-  override def close() { unlock(lk); }
-  override protected def finalize() { super.finalize(); close(); }
-}
-def withLock[T](path: String, flags: Int)(body: => T): T = {
-  val lk = new FileLock(path, flags);
-  try { body; } finally { lk.close(); }
-}
-def withLock[T](file: File, flags: Int)(body: => T): T =
-  withLock(file.getPath, flags) { body }
-def withLock[T](path: String)(body: => T): T =
-  withLock(path, LKF_EXCL) { body }
-def withLock[T](file: File)(body: => T): T =
-  withLock(file.getPath, LKF_EXCL) { body }
-
-@native protected def connect(path: CString): Wrapper;
-@native def send(conn: Wrapper, buf: CString,
-                start: Int, len: Int);
-@native def recv(conn: Wrapper, buf: CString,
-                start: Int, len: Int): Int;
-@native def close(conn: Wrapper, how: Int);
-class Connection(path: String) extends Closeable {
-  def this(file: File) { this(file.getPath); }
-  private[this] val conn = connect(path.toCString);
-  override def close() { jni.close(conn, CF_CLOSEMASK); }
-  override protected def finalize() { super.finalize(); close(); }
-  class InputStream private[Connection] extends java.io.InputStream {
-    override def read(): Int = {
-      val buf = new Array[Byte](1);
-      val n = read(buf, 0, 1);
-      if (n < 0) -1 else buf(0)&0xff;
-    }
-    override def read(buf: Array[Byte]): Int =
-      read(buf, 0, buf.length);
-    override def read(buf: Array[Byte], start: Int, len: Int) =
-      recv(conn, buf, start, len);
-    override def close() { jni.close(conn, CF_CLOSERD); }
-  }
-  lazy val input = new InputStream;
-  class OutputStream private[Connection] extends java.io.OutputStream {
-    override def write(b: Int) { write(Array[Byte](b.toByte), 0, 1); }
-    override def write(buf: Array[Byte]) { write(buf, 0, buf.length); }
-    override def write(buf: Array[Byte], start: Int, len: Int)
-      { send(conn, buf, start, len); }
-    override def close() { jni.close(conn, CF_CLOSEWR); }
-  }
-  lazy val output = new OutputStream;
-}
-
-/*----- That's all, folks -------------------------------------------------*/
-
-}
index c4b8284..16d2b0a 100644 (file)
@@ -7,7 +7,7 @@ import scala.util.control.Breaks;
 
 def main(args: Array[String])
 {
-  val conn = new jni.Connection(args(0));
+  val conn = new sys.Connection(args(0));
   try {
     val rd = new BufferedReader(new InputStreamReader(conn.input));
     val wr = new BufferedWriter(new OutputStreamWriter(conn.output));
index a6c00c3..c012d1f 100644 (file)
--- a/sys.scala
+++ b/sys.scala
@@ -1,6 +1,6 @@
 /* -*-scala-*-
  *
- * System calls and errors
+ * System-level hacking
  *
  * (c) 2018 Straylight/Edgeware
  */
@@ -29,31 +29,131 @@ package uk.org.distorted.tripe; package object sys {
 
 import scala.collection.mutable.HashSet;
 
-import java.io.File;
+import java.io.{Closeable, File};
+import java.nio.{ByteBuffer, CharBuffer};
+import java.nio.charset.Charset;
+import java.util.Date;
 
-import Magic._;
+/*----- Some magic for C strings ------------------------------------------*/
+
+type CString = Array[Byte];
+
+/* We do this by hand, rather than relying on the JNI's built-in conversions,
+ * because we use the default encoding taken from the locale settings, rather
+ * than the ridiculous `modified UTF-8' which is (a) insensitive to the
+ * user's chosen locale and (b) not actually UTF-8 either.
+ */
+
+class InvalidCStringException(msg: String) extends Exception(msg);
+
+object StringImplicits {
+  implicit class ConvertJStringToCString(s: String) {
+    /* Magic to convert a string into a C string (null-terminated bytes). */
+
+    def toCString: CString = {
+      /* Convert the receiver to a C string. */
+
+      val enc = Charset.defaultCharset.newEncoder;
+      val in = CharBuffer.wrap(s);
+      var sz: Int = (s.length*enc.averageBytesPerChar + 1).toInt;
+      var out = ByteBuffer.allocate(sz);
+
+      while (true) {
+       /* If there's still stuff to encode, then encode it.  Otherwise,
+        * there must be some dregs left in the encoder, so flush them out.
+        */
+       val r = if (in.hasRemaining) enc.encode(in, out, true)
+               else enc.flush(out);
+
+       /* Sift through the wreckage to figure out what to do. */
+       if (r.isError) r.throwException();
+       else if (r.isOverflow) {
+         /* No space in the buffer.  Make it bigger. */
+
+         sz *= 2;
+         val newout = ByteBuffer.allocate(sz);
+         out.flip(); newout.put(out);
+         out = newout;
+       } else if (r.isUnderflow) {
+         /* All done.  Check that there are no unexpected zero bytes -- so
+          * this will indeed be a valid C string -- and convert into a byte
+          * array that the C code will be able to pick apart.
+          */
+
+         out.flip(); val n = out.limit; val u = out.array;
+         if ({val z = u.indexOf(0); 0 <= z && z < n})
+           throw new InvalidCStringException("null byte in encoding");
+         val v = new Array[Byte](n + 1);
+         out.array.copyToArray(v, 0, n);
+         v(n) = 0;
+         return v;
+       }
+      }
+
+      /* Placate the type checker. */
+      unreachable("unreachable");
+    }
+  }
+
+  implicit class ConvertCStringToJString(v: CString) {
+    /* Magic to convert a C string into a `proper' string. */
+
+    def toJString: String = {
+      /* Convert the receiver to a C string.
+       *
+       * We do this by hand, rather than relying on the JNI's built-in
+       * conversions, because we use the default encoding taken from the
+       * locale settings, rather than the ridiculous `modified UTF-8' which
+       * is (a) insensitive to the user's chosen locale and (b) not actually
+       * UTF-8 either.
+       */
+
+      val inlen = v.indexOf(0) match {
+       case -1 => v.length
+       case n => n
+      }
+      val dec = Charset.defaultCharset.newDecoder;
+      val in = ByteBuffer.wrap(v, 0, inlen);
+      dec.decode(in).toString
+    }
+  }
+}
+import StringImplicits._;
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* Import the native code library. */
+System.loadLibrary("toy");
+
+/* Exception indicating that a wrapped native object has been clobbered. */
+class NativeObjectTypeException(msg: String) extends RuntimeException(msg);
+type Wrapper = Array[Byte];
 
 /*----- Error codes -------------------------------------------------------*/
 
+protected case class ErrorEntry(val tag: String, val err: Int);
+@native protected def errtab: Array[ErrorEntry];
+@native protected def strerror(err: Int): CString;
+
 object Errno extends Enumeration {
-  private[this] val tagmap = {
+  private val tagmap = {
     val b = Map.newBuilder[String, Int];
-    for (jni.ErrorEntry(tag, err) <- jni.errtab) b += tag -> err;
+    for (ErrorEntry(tag, err) <- errtab) b += tag -> err;
     b.result
   }
-  private[this] var wrong = -255;
-  private[this] val seen = HashSet[Int]();
+  private var wrong = -255;
+  private val seen = HashSet[Int]();
 
   class ErrnoVal private[Errno](tag: String, val code: Int, id: Int)
        extends Val(id, tag) {
-    def message: String = jni.strerror(code).toJString;
+    def message: String = strerror(code).toJString;
   }
 
-  private[this] def err(tag: String, code: Int): ErrnoVal = {
+  private def err(tag: String, code: Int): ErrnoVal = {
     if (seen contains code) { wrong -= 1; new ErrnoVal(tag, code, wrong) }
     else { seen += code; new ErrnoVal(tag, code, code) }
   }
-  private[this] def err(tag: String): ErrnoVal = err(tag, tagmap(tag));
+  private def err(tag: String): ErrnoVal = err(tag, tagmap(tag));
 
   val OK = err("OK", 0);
 
@@ -248,12 +348,214 @@ class SystemError private[this](val err: Errno.ErrnoVal, val what: String)
        extends Exception {
   def this(err: Errno.Value, what: String)
     { this(err.asInstanceOf[Errno.ErrnoVal], what); }
-  private[tripe] def this(err: Int, what: CString)
+  private def this(err: Int, what: CString)
     { this(Errno(err), what.toJString); }
   override def getMessage(): String = s"$what: ${err.message}";
 }
 
-/*----- Filesystem hacks --------------------------------------------------*/
+/*----- Basic file operations ---------------------------------------------*/
+
+@native protected def unlink(path: CString);
+def unlink(path: String) { unlink(path.toCString); }
+@native protected def rmdir(path: CString);
+def rmdir(path: String) { rmdir(path.toCString); }
+@native protected def mkdir(path: CString, mode: Int);
+def mkdir(path: String, mode: Int) { mkdir(path.toCString, mode); }
+@native protected def mkfile(path: CString, mode: Int);
+def mkfile(path: String, mode: Int) { mkfile(path.toCString, mode); }
+@native protected def rename(from: CString, to: CString);
+def rename(from: String, to: String)
+  { rename(from.toCString, to.toCString); }
+
+/*----- File status information -------------------------------------------*/
+
+/* These are the traditional values, but the C code carefully arranges to
+ * return them regardless of what your kernel actually thinks.
+ */
+val S_IFMT = 0xf000;
+val S_IFIFO = 0x1000;
+val S_IFCHR = 0x2000;
+val S_IFDIR = 0x4000;
+val S_IFBLK = 0x6000;
+val S_IFREG = 0x8000;
+val S_IFLNK = 0xa000;
+val S_IFSOCK = 0xc000;
+
+@native protected def stat(path: CString): sys.FileInfo;
+def stat(path: String): sys.FileInfo = stat(path.toCString);
+@native protected def lstat(path: CString): sys.FileInfo;
+def lstat(path: String): sys.FileInfo = lstat(path.toCString);
+
+object FileInfo extends Enumeration {
+  val FIFO, CHR, DIR, BLK, REG, LNK, SOCK, UNK = Value;
+  type Type = Value;
+}
+import FileInfo._;
+
+
+class FileInfo private[this](val devMajor: Int, val devMinor: Int,
+                            val ino: Long, val mode: Int, val nlink: Int,
+                            val uid: Int, val gid: Int,
+                            _rdevMinor: Int, _rdevMajor: Int,
+                            val size: Long,
+                            val blksize: Int, val blocks: Long,
+                            val atime: Date, val mtime: Date,
+                            val ctime: Date) {
+  private def this(devMajor: Int, devMinor: Int, ino: Long,
+                  mode: Int, nlink: Int, uid: Int, gid: Int,
+                  rdevMinor: Int, rdevMajor: Int,
+                  size: Long, blksize: Int, blocks: Long,
+                  atime: Long, mtime: Long, ctime: Long) {
+    this(devMajor, devMinor, ino, mode, nlink, uid, gid,
+        rdevMajor, rdevMinor, size, blksize, blocks,
+        new Date(atime), new Date(mtime), new Date(ctime));
+  }
+
+  def perms: Int = mode&0xfff;
+
+  def ftype: Type = (mode&S_IFMT) match {
+    case S_IFIFO => FIFO
+    case S_IFCHR => CHR
+    case S_IFDIR => DIR
+    case S_IFBLK => BLK
+    case S_IFREG => REG
+    case S_IFLNK => LNK
+    case S_IFSOCK => SOCK
+    case _ => UNK
+  }
+
+  private[this] def mustBeDevice() {
+    ftype match {
+      case CHR | BLK => ();
+      case _ => throw new IllegalArgumentException("Object is not a device");
+    }
+  }
+  def rdevMajor: Int = { mustBeDevice(); _rdevMajor }
+  def rdevMinor: Int = { mustBeDevice(); _rdevMinor }
+}
+
+/*----- Listing directories -----------------------------------------------*/
+
+@native protected def opendir(path: CString): Wrapper;
+@native protected def readdir(path: CString, dir: Wrapper): CString;
+@native protected def closedir(path: CString, dir: Wrapper);
+
+protected abstract class BaseDirIterator[T](cpath: CString)
+       extends LookaheadIterator[T] with Closeable {
+  def this(path: String) { this(path.toCString); }
+  def this(dir: File) { this(dir.getPath); }
+  override def close() { closedir(cpath, dir); }
+  override protected def finalize() { super.finalize(); close(); }
+  private[this] val dir = opendir(cpath);
+  protected def mangle(file: String): T;
+  override protected def fetch(): Option[T] = readdir(cpath, dir) match {
+    case null => None
+    case f => f.toJString match {
+      case "." | ".." => fetch()
+      case jf => Some(mangle(jf))
+    }
+  }
+}
+
+class DirIterator(val path: String) extends BaseDirIterator[String](path) {
+  def this(dir: File) { this(dir.getPath); }
+  override protected def mangle(file: String): String = file;
+}
+
+class DirFilesIterator private[this](val dir: File, cpath: CString)
+       extends BaseDirIterator[File](cpath) {
+  def this(dir: File) { this(dir, dir.getPath.toCString); }
+  def this(path: String) { this(new File(path), path.toCString); }
+  override protected def mangle(file: String): File = new File(dir, file);
+}
+
+/*----- File locking ------------------------------------------------------*/
+
+val LKF_EXCL = 1;
+val LKF_WAIT = 2;
+@native protected def lock(path: CString, flags: Int): Wrapper;
+@native protected def unlock(lock: Wrapper);
+
+class FileLock(path: String, flags: Int) extends Closeable {
+  def this(file: File, flags: Int) { this(file.getPath, flags); }
+  def this(path: String) { this(path, LKF_EXCL); }
+  def this(file: File) { this(file.getPath, LKF_EXCL); }
+  private[this] val lk = lock(path.toCString, flags);
+  override def close() { unlock(lk); }
+  override protected def finalize() { super.finalize(); close(); }
+}
+
+/*----- File implicits ----------------------------------------------------*/
+
+object FileImplicits {
+  implicit class FileOps(file: File) {
+
+    def unlink_!() { unlink(file.getPath); }
+    def rmdir_!() { rmdir(file.getPath); }
+    def mkdir_!(mode: Int) { mkdir(file.getPath, mode); }
+    def mkdir_!() { mkdir_!(0x1ff); }
+    def mkfile_!(mode: Int) { mkfile(file.getPath, mode); }
+    def mkfile_!() { mkfile_!(0x1b6); }
+
+    def withFilesIterator[T](body: DirFilesIterator => T): T = {
+      val iter = new DirFilesIterator(file.getPath);
+      try { body(iter) } finally { iter.close(); }
+    }
+    def files_! : Seq[File] = withFilesIterator { _.toSeq };
+
+    def stat_! : FileInfo = stat(file.getPath);
+    def lstat_! : FileInfo = lstat(file.getPath);
+
+    private[this] def statish[T](statfn: String => FileInfo,
+                                ifexists: FileInfo => T,
+                                ifmissing: => T): T =
+      (try { statfn(file.getPath) }
+       catch { case SystemError(ENOENT, _) => null }) match {
+       case null => ifmissing
+       case st => ifexists(st)
+      };
+    def exists_! : Boolean = statish(stat _, _ => true, false);
+    def isfifo_! : Boolean = statish(stat _, _.ftype == FIFO, false);
+    def isblk_! : Boolean = statish(stat _, _.ftype == BLK, false);
+    def isdir_! : Boolean = statish(stat _, _.ftype == DIR, false);
+    def ischr_! : Boolean = statish(stat _, _.ftype == CHR, false);
+    def isreg_! : Boolean = statish(stat _, _.ftype == REG, false);
+    def islnk_! : Boolean = statish(lstat _, _.ftype == LNK, false);
+    def issock_! : Boolean = statish(stat _, _.ftype == SOCK, false);
+
+    def remove_!() {
+      while (true) {
+       try { unlink_!(); return }
+       catch {
+         case SystemError(ENOENT, _) => return;
+         case SystemError(EISDIR, _) => ();
+       }
+       try { rmdir_!(); return }
+       catch {
+         case SystemError(ENOENT, _) => return;
+         case SystemError(ENOTDIR, _) => ();
+       }
+      }
+    }
+
+    def rmTree() {
+      def walk(f: File) {
+       if (f.isdir_!) f.withFilesIterator { _ foreach(walk _) };
+       f.remove_!();
+      }
+      walk(file);
+    }
+
+    def withLock[T](flags: Int)(body: => T): T = {
+      val lk = new FileLock(file.getPath, flags);
+      try { body } finally { lk.close(); }
+    }
+    def withLock[T](body: => T): T = withLock(LKF_EXCL) { body };
+  }
+}
+import FileImplicits._;
+
+/*----- Miscellaneous file hacks ------------------------------------------*/
 
 def freshFile(d: File): File = {
   /* Return the name of a freshly created file in directory D. */
@@ -294,7 +596,7 @@ def freshFile(d: File): File = {
      * win.
      */
     val f = new File(d, b.result); b.clear();
-    try { jni.mkfile(f); return f; }
+    try { f.mkfile_!(); return f; }
     catch { case SystemError(EEXIST, _) => (); }
   }
 
@@ -302,25 +604,52 @@ def freshFile(d: File): File = {
   unreachable("unreachable");
 }
 
-def rmTree(f: File) {
-  def walk(f: File) {
-    if (jni.stat(f).isdir) {
-      closing(new jni.DirFilesIterator(f)) { _ foreach(walk _) }
-      try { jni.rmdir(f); }
-      catch { case SystemError(ENOENT, _) => (); }
-    } else {
-      try { jni.unlink(f); }
-      catch { case SystemError(ENOENT, _) => (); }
+/*----- Connecting to a server --------------------------------------------*/
+
+val CF_CLOSERD = 1;
+val CF_CLOSEWR = 2;
+val CF_CLOSEMASK = CF_CLOSERD | CF_CLOSEWR;
+@native protected def connect(path: CString): Wrapper;
+@native protected def send(conn: Wrapper, buf: CString,
+                          start: Int, len: Int);
+@native protected def recv(conn: Wrapper, buf: CString,
+                          start: Int, len: Int): Int;
+@native def closeconn(conn: Wrapper, how: Int);
+
+class Connection(path: String) extends Closeable {
+  def this(file: File) { this(file.getPath); }
+  private[this] val conn = connect(path.toCString);
+  override def close() { closeconn(conn, CF_CLOSEMASK); }
+  override protected def finalize() { super.finalize(); close(); }
+
+  class InputStream private[Connection] extends java.io.InputStream {
+    override def read(): Int = {
+      val buf = new Array[Byte](1);
+      val n = read(buf, 0, 1);
+      if (n < 0) -1 else buf(0)&0xff;
     }
+    override def read(buf: Array[Byte]): Int =
+      read(buf, 0, buf.length);
+    override def read(buf: Array[Byte], start: Int, len: Int) =
+      recv(conn, buf, start, len);
+    override def close() { closeconn(conn, CF_CLOSERD); }
   }
-  walk(f);
+  lazy val input = new InputStream;
+
+  class OutputStream private[Connection] extends java.io.OutputStream {
+    override def write(b: Int) { write(Array[Byte](b.toByte), 0, 1); }
+    override def write(buf: Array[Byte]) { write(buf, 0, buf.length); }
+    override def write(buf: Array[Byte], start: Int, len: Int)
+      { send(conn, buf, start, len); }
+    override def close() { closeconn(conn, CF_CLOSEWR); }
+  }
+  lazy val output = new OutputStream;
 }
-def rmTree(path: String) { rmTree(new File(path)); }
 
-def fileExists(path: String): Boolean =
-  try { jni.stat(path); true }
-  catch { case SystemError(ENOENT, _) => false };
-def fileExists(file: File): Boolean = fileExists(file.getPath);
+/*----- Crypto-library hacks ----------------------------------------------*/
+
+@native def hashsz(hash: String): Int;
+      /* Return the output hash size for the named HASH function, or -1. */
 
 /*----- That's all, folks -------------------------------------------------*/
 
index 8eba6f8..79ac861 100644 (file)
@@ -45,17 +45,16 @@ def unreachable(msg: String): Nothing = throw new AssertionError(msg);
 /*----- Various pieces of implicit magic ----------------------------------*/
 
 class InvalidCStringException(msg: String) extends Exception(msg);
-type CString = Array[Byte];
 
-object Magic {
+object Implicits {
 
   /* --- Syntactic sugar for locks --- */
 
   implicit class LockOps(lk: Lock) {
     /* LK withLock { BODY }
      * LK.withLock(INTERRUPT) { BODY }
-     * LK.withLock(DUR, [INTERRUPT]) { BODY } orelse { ALT }
-     * LK.withLock(DL, [INTERRUPT]) { BODY } orelse { ALT }
+     * LK.withLock(DUR, [INTERRUPT]) { BODY } orElse { ALT }
+     * LK.withLock(DL, [INTERRUPT]) { BODY } orElse { ALT }
      *
      * Acquire a lock while executing a BODY.  If a duration or deadline is
      * given then wait so long for the lock, and then give up and run ALT
@@ -81,12 +80,12 @@ object Magic {
     def withLock[T](body: => T): T = withLock(true)(body);
   }
 
-  class PendingLock[T] private[Magic]
+  class PendingLock[T] private[Implicits]
          (val lk: Lock, val dur: Duration,
           val interrupt: Boolean, body: => T) {
-    /* An auxiliary class for LockOps; provides the `orelse' qualifier. */
+    /* An auxiliary class for LockOps; provides the `orElse' qualifier. */
 
-    def orelse(alt: => T): T = {
+    def orElse(alt: => T): T = {
       val locked = (dur, interrupt) match {
        case (Duration.Inf, true) => lk.lockInterruptibly(); true
        case (Duration.Inf, false) => lk.lock(); true
@@ -98,86 +97,6 @@ object Magic {
       else try { body; } finally lk.unlock();
     }
   }
-
-  /* --- Conversion to/from C strings --- */
-
-  implicit class ConvertJStringToCString(s: String) {
-    /* Magic to convert a string into a C string (null-terminated bytes). */
-
-    def toCString: CString = {
-      /* Convert the receiver to a C string.
-       *
-       * We do this by hand, rather than relying on the JNI's built-in
-       * conversions, because we use the default encoding taken from the
-       * locale settings, rather than the ridiculous `modified UTF-8' which
-       * is (a) insensitive to the user's chosen locale and (b) not actually
-       * UTF-8 either.
-       */
-
-      val enc = Charset.defaultCharset.newEncoder;
-      val in = CharBuffer.wrap(s);
-      var sz: Int = (s.length*enc.averageBytesPerChar + 1).toInt;
-      var out = ByteBuffer.allocate(sz);
-
-      while (true) {
-       /* If there's still stuff to encode, then encode it.  Otherwise,
-        * there must be some dregs left in the encoder, so flush them out.
-        */
-       val r = if (in.hasRemaining) enc.encode(in, out, true)
-               else enc.flush(out);
-
-       /* Sift through the wreckage to figure out what to do. */
-       if (r.isError) r.throwException();
-       else if (r.isOverflow) {
-         /* No space in the buffer.  Make it bigger. */
-
-         sz *= 2;
-         val newout = ByteBuffer.allocate(sz);
-         out.flip(); newout.put(out);
-         out = newout;
-       } else if (r.isUnderflow) {
-         /* All done.  Check that there are no unexpected zero bytes -- so
-          * this will indeed be a valid C string -- and convert into a byte
-          * array that the C code will be able to pick apart.
-          */
-
-         out.flip(); val n = out.limit; val u = out.array;
-         if ({val z = u.indexOf(0); 0 <= z && z < n})
-           throw new InvalidCStringException("null byte in encoding");
-         val v = new Array[Byte](n + 1);
-         out.array.copyToArray(v, 0, n);
-         v(n) = 0;
-         return v;
-       }
-      }
-
-      /* Placate the type checker. */
-      unreachable("unreachable");
-    }
-  }
-
-  implicit class ConvertCStringToJString(v: CString) {
-    /* Magic to convert a C string into a `proper' string. */
-
-    def toJString: String = {
-      /* Convert the receiver to a C string.
-       *
-       * We do this by hand, rather than relying on the JNI's built-in
-       * conversions, because we use the default encoding taken from the
-       * locale settings, rather than the ridiculous `modified UTF-8' which
-       * is (a) insensitive to the user's chosen locale and (b) not actually
-       * UTF-8 either.
-       */
-
-      val inlen = v.indexOf(0) match {
-       case -1 => v.length
-       case n => n
-      }
-      val dec = Charset.defaultCharset.newDecoder;
-      val in = ByteBuffer.wrap(v, 0, inlen);
-      dec.decode(in).toString
-    }
-  }
 }
 
 /*----- Cleanup assistant -------------------------------------------------*/