From 25c3546915ef2105c0d53983939da840ddbde795 Mon Sep 17 00:00:00 2001 Message-Id: <25c3546915ef2105c0d53983939da840ddbde795.1714901792.git.mdw@distorted.org.uk> From: Mark Wooding Date: Thu, 31 May 2018 19:29:23 +0100 Subject: [PATCH] shake it all up Organization: Straylight/Edgeware From: Mark Wooding --- Makefile | 9 +- admin.scala | 2 +- jni.c | 24 ++-- jni.scala | 246 --------------------------------- main.scala | 2 +- sys.scala | 385 ++++++++++++++++++++++++++++++++++++++++++++++++---- util.scala | 93 +------------ 7 files changed, 380 insertions(+), 381 deletions(-) delete mode 100644 jni.scala diff --git a/Makefile b/Makefile index a03ba64..fb3c83a 100644 --- 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)) diff --git a/admin.scala b/admin.scala index cc82186..01d7995 100644 --- a/admin.scala +++ b/admin.scala @@ -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 --- a/jni.c +++ b/jni.c @@ -59,17 +59,17 @@ /*----- 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, "", "(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 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 . - */ - -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 -------------------------------------------------*/ - -} diff --git a/main.scala b/main.scala index c4b8284..16d2b0a 100644 --- a/main.scala +++ b/main.scala @@ -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)); diff --git a/sys.scala b/sys.scala index a6c00c3..c012d1f 100644 --- 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 -------------------------------------------------*/ diff --git a/util.scala b/util.scala index 8eba6f8..79ac861 100644 --- a/util.scala +++ b/util.scala @@ -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 -------------------------------------------------*/ -- [mdw]