view mde/file/mergetag/MTTagReader.d @ 151:e785e98d3b78

Updated for compatibility with tango 0.99.8.
author Diggory Hardy <diggory.hardy@gmail.com>
date Sat, 04 Apr 2009 17:32:18 +0200
parents 9f035cd139c6
children
line wrap: on
line source

/* 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 getMTTagReader (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);
        
        pos = 6;
        section = CurrentVersionString;
    }
    
    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-1];
                    sectionTag = true;
                    return true;
                }
            }
            else if (fbuf[pos] == '!') {		// possibly a comment; check next char
                comment = true;				// starting a comment
                ++pos;
            } 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 = getMTTagReader (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;
}