/* -*-scala-*-
*
- * System calls and errors
+ * System-level hacking
*
* (c) 2018 Straylight/Edgeware
*/
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);
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. */
* 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, _) => (); }
}
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 -------------------------------------------------*/