changeset 136:4084f07f2c7a

Added simpler mergetag readers and writers, with unittest.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sun, 01 Feb 2009 12:36:21 +0000
parents bc697a218716
children 9f035cd139c6
files mde/file/mergetag/MTTagReader.d mde/file/mergetag/MTTagUnittest.d mde/file/mergetag/MTTagWriter.d mde/file/mergetag/Reader.d mde/file/mergetag/Writer.d mde/file/mergetag/internal.d mde/file/mergetag/mdeUT.d mde/file/paths.d mde/gui/widget/layout.d mde/input/Input.d mde/mde.d mde/setup/Init.d
diffstat 12 files changed, 557 insertions(+), 86 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/MTTagReader.d	Sun Feb 01 12:36:21 2009 +0000
@@ -0,0 +1,284 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program 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 2 of the License, or (at your option) any later version.
+
+This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/******************************************************************************
+ * This module contains a simpler, easier to use, mergetag reader.
+ *****************************************************************************/
+module mde.file.mergetag.MTTagReader;
+
+import mde.file.mergetag.internal;
+import mde.file.mergetag.exception;
+
+import tango.io.UnicodeFile;
+import tango.io.FilePath;
+import Util = tango.text.Util;
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this() {
+    logger = Log.getLogger ("mde.file.mergetag.MTTagReader");
+}
+
+/** Make an TagReader class.
+*
+* Create an appropriate reader: MTTTagReader or MTBTagReader.
+*
+* Throws:
+*  $(TABLE
+*  $(TR $(TH Exception) $(TH Thrown when))
+*  $(TR $(TD MTFileIOException) $(TD When extension given is neither mtt nor mtb))
+*  )
+*
+*/
+MTTagReader makeMTTagReader (FilePath path) {
+    if      (path.ext == "mtb") return new MTBTagReader (path.toString);
+    else if (path.ext == "mtt") return new MTTTagReader (path.toString);
+    else throw new MTFileIOException ("Invalid mergetag extension");
+}
+
+abstract class MTTagReader
+{
+    /** The current section (null for header), tag type, tag id, and tag data.
+     *
+     * If the last call to readTag returned true, these are valid. */
+    char[] section, tagType, tagID, tagData;
+    
+    /** Read the next tag.
+     * 
+     * The variables section, tagType, tagID and tagData are updated, dependant
+     * on the type of tag read.
+     * 
+     * Params:
+     * sectionTag = If sectionTag is true, the read tag is a section marker,
+     * not a data tag.
+     *
+     * Returns:
+     * True if a tag has been read, false at EOF.
+     * 
+     * Example usage:
+     * ------
+     * TagReader reader = ...;
+     * while (reader.readTag (sectionTag)) {
+     *     if (sectionTag) {
+     *         // section has changed, potentially do something...
+     *     } else {
+     *         // do something with the tag data...
+     *     }
+     * }
+     * ------ */
+    abstract bool readTag (out bool sectionTag);
+}
+
+class MTTTagReader : MTTagReader
+{
+private:
+    // Non-static symbols:
+    final char[] ErrInFile;		// something like "in \"path/file.mtt\""
+    
+    final char[] fbuf;			// file is read into this
+    size_t pos;				// position within fbuf
+    MTFormat fileVer = MTFormat.INVALID;	// Remains INVALID until set otherwise by CTOR.
+    
+    bool fatal = false;			// a fatal file error occured; don't try to recover
+//END DATA
+    
+    /** Tries to read file path and checks its header.
+     *
+     * Params:
+     * path     = The name or FilePath of the file to open.
+     *     Standard extensions are .mtt and .mtb for text and binary files respectively.
+     * 
+     * No need to close later; the whole file is read within this(). */
+    public this (char[] path) {
+        scope (failure)
+            logger.warn ("Failure reading {}", path);
+        
+        // Open & read the file
+        try {	// Supports unicode files with a BOM; defaults to UTF8 when there isn't a BOM:
+            scope file = new UnicodeFile!(char) (path, Encoding.Unknown);
+            fbuf = cast(char[]) file.read();
+        } catch (Exception e) {
+            throwMTErr ("Error reading file: " ~ e.msg, new MTFileIOException);
+        }
+        ErrInFile = " in \"" ~ path ~ '"';
+        
+        // Version checking & matching header section tag:
+        if (checkHeader(fbuf) != MTFormat.MT01)
+            throwMTErr("Not a valid (known) MergeTag text file" ~ ErrInFile, new MTFileFormatException);
+    }
+    
+    public bool readTag (out bool sectionTag) {
+        debug scope (failure)
+                logger.trace ("MTTTagReader.readTag: failure");
+        if (fatal) return false;
+        
+        /* Searches fbuf starting from start to find one of <=>| and stops at its index.
+    
+        If quotable then be quote-aware for single and double quotes.
+        Note: there's no length restriction for the content of the quote since it could be a single
+        non-ascii UTF-8 char which would look like several chars.
+        */
+        void fbufLocateDataTagChar (ref size_t pos, bool quotable) {
+            while (true) {
+                fbufIncrement (pos);
+                
+                if ((fbuf[pos] >= '<' && fbuf[pos] <= '>') || fbuf[pos] == '|') return;
+                else if (quotable) {
+                    char c = fbuf[pos];
+                    if (c == '\'' || c == '"') {
+                        fbufIncrement(pos);
+                        while (fbuf[pos] != c) {
+                            if (fbuf[pos] == '\\') ++pos;	// escape seq.
+                            fbufIncrement(pos);
+                        }
+                    }
+                }
+            }
+        }
+        
+        // Used to ignore a tag (if it starts !< or !{ or should otherwise be ignored):
+        bool comment = false;
+        while (pos < fbuf.length) {
+            if (Util.isSpace(fbuf[pos])) {
+                ++pos;
+                continue;
+            }
+            else if (fbuf[pos] == '<') {		// data tag
+                char[] ErrDTAG = "Bad data tag format: not <type|id=data>" ~ ErrInFile;
+                
+                // Type section of tag:
+                size_t pos_s = pos + 1;
+                fbufLocateDataTagChar (pos, false);	// find end of type section
+                if (fbuf[pos] != '|') throwMTErr (ErrDTAG, new MTSyntaxException);
+                tagType = Util.trim (fbuf[pos_s..pos]);
+                
+                // char[] section of tag:
+                pos_s = pos + 1;
+                fbufLocateDataTagChar (pos, false);	// find end of type section
+                if (fbuf[pos] != '=') throwMTErr (ErrDTAG, new MTSyntaxException);
+                tagID = cast(char[]) fbuf[pos_s..pos];
+                
+                // Data section of tag:
+                pos_s = pos + 1;
+                fbufLocateDataTagChar (pos, true);      // find end of data section
+                if (fbuf[pos] != '>') throwMTErr (ErrDTAG, new MTSyntaxException);
+                tagData = fbuf[pos_s..pos];
+                ++pos;
+                
+                if (!comment) {
+                    return true;			// got a tag
+                } else comment = false;			// cancel comment status now
+            }
+            else if (fbuf[pos] == '{') {
+                fbufIncrement (pos);
+                size_t start = pos;
+                uint depth = 0;				// depth of embedded {} blocks
+                while (true) {
+                    if (fbuf[pos] == '}') {
+                        if (depth == 0) break;
+                        else --depth;
+                    } else if (fbuf[pos] == '{')
+                        ++depth;
+                    fbufIncrement (pos);
+                }
+                fbufIncrement(pos);
+                if (comment) {				// simple block comment
+                    comment = false;			// end of this comment
+                } else {
+                    section = cast(char[]) fbuf[start..pos];
+                    sectionTag = true;
+                    return true;
+                }
+            }
+            else if (fbuf[pos] == '!') {		// possibly a comment; check next char
+                comment = true;				// starting a comment (or an error)
+                					// variable is reset at end of comment
+            } else					// must be an error
+            throwMTErr ("Invalid character '"~fbuf[pos..pos+1]~"' (or sequence starting \"!\") outside of tag" ~ ErrInFile, new MTSyntaxException);
+        }
+        return false;					// EOF
+    }
+    
+    /* Increments pos and checks it hasn't hit fbuf.length . */
+    private void fbufIncrement(ref size_t pos) {
+        ++pos;
+        if (pos >= fbuf.length) throwMTErr("Unexpected EOF" ~ ErrInFile, new MTSyntaxException);
+    }
+    
+    private void throwMTErr (char[] msg, MTException exc = new MTException) {
+        fatal = true;	// if anyone catches the error and tries to do anything --- we're dead now
+        logger.error (msg);	// report the error
+        throw exc;		// and signal our error
+    }
+}
+
+
+/**
+* Class for reading a mergetag text file.
+*
+* Currently only a dummy class: a MTNotImplementedException will be thrown if created.
+*/
+class MTBTagReader : MTTagReader
+{
+    public this (char[] path) {
+        throw new MTNotImplementedException;
+    }
+        
+    bool readTag (out bool sectionTag) {
+        return false;
+    }
+}
+
+
+/** A special adapter for reading from multiple mergetag files. */
+class MTMultiTagReader : MTTagReader
+{
+    this (FilePath[] files)
+    in {
+        assert (files !is null, "MTMultiTagReader.this: files is null");
+    } body {
+        Exception exc;
+        foreach (file; files) {
+            try {   // try reading each file
+                MTTagReader r = makeMTTagReader (file);
+                readers ~= r;
+            } catch (Exception e) {
+                exc = e;
+            }
+        }
+        if (readers.length == 0)	// no files have valid headers
+            throw exc;			// fail: re-throw last exception
+    }
+    
+    bool readTag (out bool sectionTag) {
+        while (readers.length) {	// something left to read
+            MTTagReader r = readers[0];
+            bool ret = r.readTag (sectionTag);
+            if (ret) {
+                section = r.section;
+                tagType = r.tagType;
+                tagID   = r.tagID;
+                tagData = r.tagData;
+                return true;
+            }
+            delete r;
+            readers = readers[1..$];	// done with that reader
+        }
+        return false;
+    }
+    
+private:
+    MTTagReader[] readers;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/MTTagUnittest.d	Sun Feb 01 12:36:21 2009 +0000
@@ -0,0 +1,73 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program 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 2 of the License, or (at your option) any later version.
+
+This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/******************************************************************************
+ * A unittest for the tag reader and writer.
+ *****************************************************************************/
+module mde.file.mergetag.MTTagUnittest;
+
+debug (mdeUnitTest) {
+    import mde.file.mergetag.MTTagReader;
+    import mde.file.mergetag.MTTagWriter;
+    import tango.io.FilePath;
+    import tango.util.log.Log : Log, Logger;
+    
+    private Logger logger;
+    static this() {
+        logger = Log.getLogger ("mde.file.mergetag.MTTagUnittest");
+    }
+    
+    unittest {
+        auto file = FilePath("unittest.mtt");
+        struct S {
+            char[] type, id, data;
+	}
+        static S tag1 = { type:"t1", id:"i1", data:"123"};
+        static S tag2 = { type:"t2", id:"i2", data:"abc"};
+        static S tag3 = { type:"t3", id:"i3", data:"\" a string \""};
+        static S tag4 = { type:"t1", id:"i1", data:"5.-98"};
+        
+        MTTagWriter w = makeMTTagWriter (file.toString);
+        w.dataTag (tag2.type, tag2.id, tag2.data);
+        w.sectionTag ("one");
+        w.dataTag (tag1.type, tag1.id, tag1.data);
+        w.sectionTag ("three");
+        w.dataTag (tag3.type, tag3.id, tag3.data);
+        w.writeTag ("one", tag4.type, tag4.id, tag4.data);
+        w.close;
+        
+        MTTagReader r = makeMTTagReader (file);
+        bool isSecTag;
+        while (r.readTag (isSecTag)) {
+            if (isSecTag) continue;
+            if (r.tagID == tag1.id) {
+                assert (r.tagType == tag1.type, r.tagID);
+                assert (r.tagData == tag1.data || r.tagData == tag4.data, r.tagID);
+            } else if (r.tagID == tag2.id) {
+                assert (r.tagType == tag2.type, r.tagID);
+                assert (r.tagData == tag2.data, r.tagID);
+            } else if (r.tagID == tag3.id) {
+                assert (r.tagType == tag3.type, r.tagID);
+                assert (r.tagData == tag3.data, r.tagID);
+            } else assert (false, "extra tag: "~r.tagID);
+        }
+        
+        // Delete the unittest file now
+        file.remove;
+        
+        logger.info ("Unittest complete.");
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/file/mergetag/MTTagWriter.d	Sun Feb 01 12:36:21 2009 +0000
@@ -0,0 +1,102 @@
+/* LICENSE BLOCK
+Part of mde: a Modular D game-oriented Engine
+Copyright © 2007-2008 Diggory Hardy
+
+This program 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 2 of the License, or (at your option) any later version.
+
+This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. */
+
+/******************************************************************************
+ * This module contains a simpler, easier to use, mergetag writer.
+ *****************************************************************************/
+module mde.file.mergetag.MTTagWriter;
+
+import mde.file.mergetag.internal;
+import mde.file.mergetag.exception;
+
+import tango.io.device.File;
+import tango.io.stream.Buffer;
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this() {
+    logger = Log.getLogger ("mde.file.mergetag.MTTagWriter");
+}
+
+MTTagWriter makeMTTagWriter (char[] path) {
+    if (path.length > 4 && path[$-4..$] == ".mtt")
+        return new MTTTagWriter (path);
+    else if (path.length > 4 && path[$-4..$] == ".mtb")
+        return new MTBTagWriter (path);
+    else {
+        logger.error ("Unable to determine writing format: text or binary");
+        throw new MTFileFormatException;
+    }
+}
+
+abstract class MTTagWriter
+{
+    /// Set the current section
+    void sectionTag (char[] section);
+    
+    /// Write a data tag
+    void dataTag (char[] type, char[] id, char[] data);
+    
+    /// Change the section if necessary and write a data tag
+    void writeTag (char[] section, char[] type, char[] id, char[] data) {
+        if (section != sec)
+            sectionTag (section);
+        dataTag (type, id, data);
+    }
+    
+    /// Close the file
+    void close ();
+    
+protected:
+    char[] sec;		// current section
+}
+
+class MTTTagWriter : MTTagWriter
+{
+    /** Opens the file path for writing. Call close() when done! */
+    this (char[] path) {
+        buffer = new BufferOutput (new File (path, File.WriteCreate));
+        
+        buffer.append ("{" ~ CurrentVersionString ~ "}" ~ Eol);
+    }
+    
+    void sectionTag (char[] section) {
+        sec = section;
+        buffer.append ("{" ~ section ~ "}" ~ Eol);
+    }
+    
+    void dataTag (char[] type, char[] id, char[] data) {
+        buffer.append ("<" ~ type ~ "|" ~ id ~"=" ~ data ~ ">" ~ Eol);
+    }
+    
+    void close () {
+        buffer.flush;
+        buffer.close;
+    }
+    
+private:
+    scope BufferOutput buffer;
+}
+
+class MTBTagWriter : MTTagWriter
+{
+    this (char[] path) {
+        throw new MTNotImplementedException;
+    }
+    
+    void sectionTag (char[] section) {}
+    void dataTag (char[] type, char[] id, char[] data) {}
+    void close () {}
+}
--- a/mde/file/mergetag/Reader.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/file/mergetag/Reader.d	Sun Feb 01 12:36:21 2009 +0000
@@ -22,8 +22,8 @@
 public import mde.file.mergetag.iface.IReader;
 import mde.file.mergetag.DataSet;
 import mde.file.mergetag.DefaultData;
+import mde.file.mergetag.internal;
 import mde.file.mergetag.exception;
-import mde.file.mergetag.internal;
 
 import tango.core.Exception;
 
@@ -55,8 +55,8 @@
 *
 */
 IReader makeReader (FilePath path, DataSet ds = null, bool rdHeader = false) {
-    if      (path.ext == "mtb") return new MTBReader (path, ds, rdHeader);
-    else if (path.ext == "mtt") return new MTTReader (path, ds, rdHeader);
+    if      (path.ext == "mtb") return new MTBReader (path.toString, ds, rdHeader);
+    else if (path.ext == "mtt") return new MTTReader (path.toString, ds, rdHeader);
     else throw new MTFileIOException ("Invalid mergetag extension");
 }
 
@@ -154,7 +154,6 @@
     final char[] ErrInFile;		// something like "in \"path/file.mtt\""
     
     final char[] fbuf;			// file is read into this
-    MTFormatVersion.VERS fileVer = MTFormatVersion.VERS.INVALID;	// Remains INVALID until set otherwise by CTOR.
     
     IDataSection delegate (ID) _dataSecCreator = null;   // see property setter above
     
@@ -205,31 +204,24 @@
      * would no longer be possible.
      */
     public this (char[] path, DataSet ds = null, bool rdHeader = false) {
-        this (new FilePath (path), ds, rdHeader);
-    }
-    /** ditto */
-    public this (FilePath path, DataSet ds = null, bool rdHeader = false) {
         // Create a dataset or use an existing one
         if (ds !is null) _dataset = ds;
         else _dataset = new DataSet();
         
         // Open & read the file
         try {	// Supports unicode files with a BOM; defaults to UTF8 when there isn't a BOM:
-            scope file = new UnicodeFile!(char) (path.toString, Encoding.Unknown);
+            scope file = new UnicodeFile!(char) (path, Encoding.Unknown);
             fbuf = cast(char[]) file.read();
         } catch (Exception e) {
             throwMTErr ("Error reading file: " ~ e.msg, new MTFileIOException);
         }
         // Remember the file name so that we can report errors (somewhat) informatively:
-        ErrFile = path.path ~ path.file;
+        ErrFile = path;
         ErrInFile = " in \"" ~ ErrFile ~ '"';
         
         // Version checking & matching header section tag:
-        if (fbuf.length < 6 || fbuf[0] != '{' || fbuf[1] != 'M' || fbuf[2] != 'T' || fbuf[5] != '}')
-            throwMTErr("Not a valid MergeTag text file" ~ ErrInFile, new MTFileFormatException);
-        fileVer = MTFormatVersion.parseString (fbuf[3..5]);
-        if (fileVer == MTFormatVersion.VERS.INVALID)
-            throwMTErr("Unrecognised MergeTag version: MT" ~ fbuf[3..5] ~ ErrInFile, new MTFileFormatException);
+        if (checkHeader(fbuf) != MTFormat.MT01)
+            throwMTErr("Not a valid (known) MergeTag text file" ~ ErrInFile, new MTFileFormatException);
         
         // Header reading/skipping:
         if (rdHeader) {	// only bother actually reading it if it was requested
@@ -520,14 +512,12 @@
 }
 
 
-/** A special adapter for reading from multiple mergetag files.
- *
- * The number of files $(B must not) exceed MAX_PATHS. */
+/** A special adapter for reading from multiple mergetag files. */
 class MTMultiReader : IReader
 {
     this (FilePath[] files, DataSet ds, bool rdHeader)
     in {
-        assert (files !is null, "mdeReader.this: files is null");
+        assert (files !is null, "MTMultiReader.this: files is null");
     } body {
         // Don't let sub-readers create their own, separate, datasets:
         if (ds is null) ds = new DataSet;
@@ -536,12 +526,12 @@
         foreach (file; files) {
             try {   // try reading header of each file
                 IReader r = makeReader (file, ds, rdHeader);
-                readers[readersLen++] = r;
+                readers ~= r;
             } catch (Exception e) {
                 exc = e;
             }
         }
-        if (readersLen == 0)        // no files have valid headers
+        if (readers.length == 0)    // no files have valid headers
             throw exc;              // fail: re-throw last exception
     }
     
@@ -549,11 +539,11 @@
         return readers[0].dataset;      // all readers share the same dataset
     }
     void dataset (DataSet ds) {         /// Set the DataSet
-        for (uint i = 0; i < readersLen; ++i) readers[i].dataset (ds);
+        foreach (reader; readers) reader.dataset (ds);
     }
     
     void dataSecCreator (IDataSection delegate (ID) dsC) {  /// Set the dataSecCreator
-        for (uint i = 0; i < readersLen; ++i) readers[i].dataSecCreator = dsC;
+        foreach (reader; readers) reader.dataSecCreator = dsC;
     }
     
     /** Get identifiers for all sections.
@@ -562,23 +552,20 @@
      * together, starting with the highest-priority file. */
     ID[] getSectionNames () {
         ID[] names;
-        for (int i = readersLen-1; i >= 0; --i)
-            names ~= readers[i].getSectionNames;
+        foreach_reverse (reader; readers)
+            names ~= reader.getSectionNames;
         return names;
     }
     void read () {                      /// Commence reading
-        for (uint i = 0; i < readersLen; ++i) readers[i].read();
+        foreach (reader; readers) reader.read();
     }
     void read (ID[] secSet) {           /// ditto
-        for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
+        foreach (reader; readers) reader.read(secSet);
     }
     void read (IContainer   !(ID) secSet) {      /// ditto
-        for (uint i = 0; i < readersLen; ++i) readers[i].read(secSet);
+        foreach (reader; readers) reader.read(secSet);
     }
     
-    const MAX_READERS = 4;
 private:
-    // Use a simpler static array:
-    IReader[MAX_READERS] readers;
-    ubyte readersLen = 0;
+    IReader[] readers;
 }
--- a/mde/file/mergetag/Writer.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/file/mergetag/Writer.d	Sun Feb 01 12:36:21 2009 +0000
@@ -38,7 +38,6 @@
 import tango.core.Exception;
 import tango.io.device.File;
 import tango.io.stream.Buffer;
-import convInt = tango.text.convert.Integer;
 import tango.util.log.Log : Log, Logger;
 
 private Logger logger;
@@ -135,12 +134,6 @@
         
     
 private:
-    // taken from tango.io.Console, mostly to make sure notepad can read our files:
-    version (Win32)
-        const char[] Eol = "\r\n";
-    else
-        const char[] Eol = "\n";
-
     /* The container where data is written from. */
     DataSet _dataset;
     
@@ -188,7 +181,7 @@
             scope(exit) buffer.flush();
             
             // Write the header:
-            buffer.append ("{MT" ~ MTFormatVersion.CurrentString ~ "}" ~ Eol);
+            buffer.append ("{" ~ CurrentVersionString ~ "}" ~ Eol);
             if (_dataset.header !is null) writeSection (buffer, _dataset.header);
         
             // Write the rest:
@@ -209,13 +202,12 @@
     }
         
     private void writeSectionIdentifier (BufferOutput buffer, ID id) {
-        char[] tp = "{" ~ cast(char[])id ~ "}" ~ Eol;
-        buffer.append (tp);
+        buffer.append ("{" ~ id ~ "}" ~ Eol);
     }
     
     private void writeSection (BufferOutput buffer, IDataSection sec) {
         void writeItem (char[] tp, ID id, char[] dt) {	// actually writes an item
-            buffer.append ("<" ~ tp ~ "|" ~ cast(char[])id ~"=" ~ dt ~ ">" ~ Eol);
+            buffer.append ("<" ~ tp ~ "|" ~ id ~"=" ~ dt ~ ">" ~ Eol);
         }
         sec.writeAll (&writeItem);
         
--- a/mde/file/mergetag/internal.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/file/mergetag/internal.d	Sun Feb 01 12:36:21 2009 +0000
@@ -16,20 +16,33 @@
 /// Contains functions/data structures used internally by mergetag.
 module mde.file.mergetag.internal;
 
-package abstract class MTFormatVersion {
-    enum VERS : ubyte {	// convenient list of all known file format versions
+    enum MTFormat : ubyte {	// known formats
         INVALID	= 0x00,
         MT01	= 0x01,		// not yet final
     }
     /// The current MergeTag version
-    static const VERS Current = VERS.MT01;
-    static const char[2] CurrentString = "01";
+    static const MTFormat CurrentVersion = MTFormat.MT01;
+    static const char[] CurrentVersionString = "MT01";
     
-    static VERS parseString (char[] str)
+    static MTFormat checkVersion (char[] str)
     in {
             assert (str.length == 2);
     } body {
-        if (str[0] == '0' && str[1] == '1') return VERS.MT01;
-        else return VERS.INVALID;
+        if (str[0] == '0' && str[1] == '1') return MTFormat.MT01;
+        else return MTFormat.INVALID;
     }
-}
+    
+    /// Check the header (first 6 bytes) and return version
+    static MTFormat checkHeader (char[] fbuf) {
+        if (fbuf.length < 6 || fbuf[0] != '{' || fbuf[1] != 'M' || fbuf[2] != 'T' || fbuf[5] != '}')
+            return MTFormat.INVALID;
+        else
+            return checkVersion (fbuf[3..5]);
+    }
+    
+    // taken from tango.io.Console, mostly to make sure notepad can read our files:
+    version (Win32)
+            const char[] Eol = "\r\n";
+    else
+        const char[] Eol = "\n";
+
--- a/mde/file/mergetag/mdeUT.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/file/mergetag/mdeUT.d	Sun Feb 01 12:36:21 2009 +0000
@@ -39,7 +39,7 @@
         const file = "unittest";
         const ID UT_ID = cast (ID) "mdeUT";
         const headInfo = "mde Unit Test";
-                
+        
         DataSet dsW = new DataSet();
         
         dsW.header = new DefaultData();
--- a/mde/file/paths.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/file/paths.d	Sun Feb 01 12:36:21 2009 +0000
@@ -36,7 +36,9 @@
 
 import mde.exception;
 import mde.file.mergetag.Reader;
+import mde.file.mergetag.MTTagReader;
 import mde.file.mergetag.Writer;
+import mde.file.mergetag.MTTagWriter;
 import mde.file.mergetag.DataSet;
 import mde.file.mergetag.exception;
 
@@ -69,28 +71,33 @@
 * In the case of confDir, the user path is guaranteed to exist (as highest priority path). */
 struct mdeDirectory
 {
-    /** Creates an MT reader for each file.
+    /** Creates an MT IReader for each file (using MTMultiReader).
     *
     * Params:
-    *   file      = The file path and name relative to the mdeDirectory, without a suffix
-    *               (e.g. "options")
-    *   readOrder = Read the highest priority or lowest priority files first? For correct merging,
-    *               this should be LOW_HIGH when newly-read items override old ones (as is the case
-    *               with DefaultData) and HIGH_LOW when the first-read items survive. Thus override
-    *               order needs to be the same for each section, except the header which is always
-    *               read with LOW_HIGH order.
-    *               Alternately, for files which shouldn't be
-    *               merged where only the highest priority file should be read, pass HIGH_ONLY.
+    *   file      = The file path and name relative to the mdeDirectory,
+    *   	    without a suffix (e.g. "options").
+    *   readOrder = Read the highest priority or lowest priority files first?
+    *   	    For correct merging, this should be LOW_HIGH when newly-
+    *   	    read items override old ones (as is the case with
+    *   	    DefaultData) and HIGH_LOW when the first-read items
+    *   	    survive. Thus override order needs to be the same for each
+    *   	    section, except the header which is always read with
+    *   	    LOW_HIGH order. Alternately, for files which shouldn't be
+    *   	    merged where only the highest priority file should be read,
+    *   	    pass HIGH_ONLY.
     *   ds        = The dataset, as for mergetag. Note: all actual readers share one dataset.
     *   rdHeader  = Read the headers for each file and merge if rdHeader == true.
     */
-    IReader makeMTReader (char[] file, PRIORITY readOrder, DataSet ds = null, bool rdHeader = false)
-    {
-        FilePath[] files = getFiles (file, readOrder);
-        if (files is null)
-            throw new NoFileException ("Unable to find the file: "~file~"[.mtt|mtb]");
-        
-        return new MTMultiReader (files, ds, rdHeader);
+    IReader makeMTReader (char[] file, PRIORITY readOrder, DataSet ds = null, bool rdHeader = false) {
+        return new MTMultiReader (getFiles (file, readOrder), ds, rdHeader);
+    }
+    
+    /** Creates an MTTagReader for each file (using MTMultiTagReader).
+     *
+     * Params as for makeMTReader.
+     */
+    MTTagReader makeMTTagReader (char[] file, PRIORITY readOrder) {
+        return new MTMultiTagReader (getFiles (file, readOrder));
     }
     
     /** Creates an MT writer for file deciding on the best path to use.
@@ -106,13 +113,24 @@
         return makeWriter (paths[pathsLen-1] ~ file, ds, WriterMethod.Text);
     }
     
-    /** Returns a string listing the file name or names (if readOrder is not HIGH_ONLY and multiple
-      * matches are found), or "no file found". Intended for user output only. */
+    /** Creates an MTTagWriter for file. */
+    MTTagWriter makeMTWriter (char[] file)
+    {
+        // FIXME: use highest priority writable path
+        return makeMTTagWriter (paths[pathsLen-1] ~ file);
+    }
+    
+    /** Returns a string listing the file name or names (if readOrder is not 
+     * HIGH_ONLY and multiple matches are found), or an error message. Intended
+     * for user output only. */
     char[] getFileName (char[] file, PRIORITY readOrder)
     {
-        FilePath[] files = getFiles (file, readOrder);
-        if (files is null)
-            return "no file found";
+        FilePath[] files;
+        try {
+            files = getFiles (file, readOrder);
+        } catch (NoFileException e) {
+            return e.msg;
+        }
         
         char[] ret = files[0].toString;
         foreach (f; files[1..$])
@@ -155,6 +173,8 @@
                 if (readOrder == PRIORITY.HIGH_ONLY) break;
             }
         }
+        if (ret is null)
+            throw new NoFileException ("Unable to find the file: "~filename~"[.mtt|mtb]");
         return ret;
     }
     
@@ -314,7 +334,6 @@
     
     // The maximum number of paths for any one "directory".
     const MAX_PATHS = 4;
-    static assert (MTMultiReader.MAX_READERS == MAX_PATHS, "MAX_PATHS not all equal");
     
     /* Try each path in succession, returning the first to exist and be a folder.
      * If none are valid and create is true, will try creating each in turn.
--- a/mde/gui/widget/layout.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/gui/widget/layout.d	Sun Feb 01 12:36:21 2009 +0000
@@ -913,7 +913,7 @@
         b.spacing = 2;
         foreach (ref wd; b.minWidth)
             wd = 10;
-        b.sizable[1] = b.sizable[3] = true;
+        b.sizable = [false, true, false, true, false];
         b.setWidths;
         assert (b.w == 58);
         
--- a/mde/input/Input.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/input/Input.d	Sun Feb 01 12:36:21 2009 +0000
@@ -646,52 +646,52 @@
         e.type = SDL_KEYDOWN;
         e.key.state = SDL_PRESSED;
         e.key.keysym.sym = 292;	// SDLK_F11
-        ut(e);
+        ut.send(e);
         // Right mouse button up:
         e.type = SDL_MOUSEBUTTONUP;
         e.button.button = 3;	// SDL_BUTTON_RIGHT
         e.button.state = SDL_RELEASED;
         e.button.x = 291;
         e.button.y = 10010;
-        ut(e);
+        ut.send(e);
         // Mouse motion:
         e.type = SDL_MOUSEMOTION;
         e.motion.x = 63;
         e.motion.y = 44;
         e.motion.xrel = 14;
         e.motion.yrel = -1;
-        ut(e);
+        ut.send(e);
         // Joystick 2 button 5 down:
         e.type = SDL_JOYBUTTONDOWN;
         e.jbutton.which = 2;
         e.jbutton.button = 5;
         e.jbutton.state = SDL_PRESSED;
-        ut(e);
+        ut.send(e);
         // Same button released:
         e.jbutton.state = SDL_RELEASED;
-        ut(e);
+        ut.send(e);
         // Joystick 1 axis 8 motion:
         e.type = SDL_JOYAXISMOTION;
         e.jaxis.which = 1;
         e.jaxis.axis = 8;
         e.jaxis.value = 32767;
-        ut(e);
+        ut.send(e);
         // Joystick 22 ball 100 motion:
         e.type = SDL_JOYBALLMOTION;
         e.jball.which = 22;
         e.jball.ball = 100;
         e.jball.xrel = -21;
         e.jball.yrel = 1024;
-        ut(e);
+        ut.send(e);
         // Joystick 214 hat 12 DOWN-RIGHT:
         e.type = SDL_JOYHATMOTION;
         e.jhat.which = 214;
         e.jhat.hat = 12;
         e.jhat.value = SDL_HAT_RIGHTDOWN;
-        ut(e);
+        ut.send(e);
         // Same hat LEFT:
         e.jhat.value = SDL_HAT_LEFT;
-        ut(e);
+        ut.send(e);
         //END Post a lot of events
             
         //BEGIN Check states
--- a/mde/mde.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/mde.d	Sun Feb 01 12:36:21 2009 +0000
@@ -33,6 +33,7 @@
 debug (mdeUnitTest) {                   // These modules contain unittests which wouldn't be run otherwise.
     import mde.file.ssi;
     import mde.file.mergetag.mdeUT;
+    import mde.file.mergetag.MTTagUnittest;
     import mde.lookup.Translation;
     import mde.gui.widget.layout;
 }
--- a/mde/setup/Init.d	Fri Jan 30 15:51:42 2009 +0000
+++ b/mde/setup/Init.d	Sun Feb 01 12:36:21 2009 +0000
@@ -464,7 +464,7 @@
         miscOpts.maxThreads = new IntContent ("maxThreads", 4);	// force up to 4 threads for unittest
         
         logger.level(Logger.Info);              // hide a lot of trace messages
-        logger.info ("You should see some warning messages starting \"InitStage\":");
+        logger.info ("You should see some messages about InitStages not run/failing:");
         // Run the above.
         runStages!(true);
         assert (init1);