Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
20.00% covered (danger)
20.00%
8 / 40
CRAP
28.27% covered (danger)
28.27%
266 / 941
tdb
0.00% covered (danger)
0.00%
0 / 1
20.00% covered (danger)
20.00%
8 / 40
36239.19
28.12% covered (danger)
28.12%
264 / 939
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 tdb
0.00% covered (danger)
0.00%
0 / 1
12.00
66.67% covered (warning)
66.67%
16 / 24
 createDatabase
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
19 / 19
 removeDatabase
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 removeTable
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
18 / 18
 getNumberOfRecords
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getTableList
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getFieldList
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 9
 readHeader
100.00% covered (success)
100.00%
1 / 1
17
100.00% covered (success)
100.00%
58 / 58
 setFp
0.00% covered (danger)
0.00%
0 / 1
3.04
83.33% covered (warning)
83.33%
10 / 12
 cleanUp
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 7
 isTable
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
6 / 6
 createTable
0.00% covered (danger)
0.00%
0 / 1
12.61
83.78% covered (warning)
83.78%
31 / 37
 addField
0.00% covered (danger)
0.00%
0 / 1
306
0.00% covered (danger)
0.00%
0 / 58
 editField
0.00% covered (danger)
0.00%
0 / 1
506
0.00% covered (danger)
0.00%
0 / 81
 removeField
0.00% covered (danger)
0.00%
0 / 1
182
0.00% covered (danger)
0.00%
0 / 54
 edit
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 36
 delete
0.00% covered (danger)
0.00%
0 / 1
5.10
83.87% covered (warning)
83.87%
26 / 31
 sortAndBuild
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 sort
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 48
 reBuild
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 add
0.00% covered (danger)
0.00%
0 / 1
16.62
76.27% covered (warning)
76.27%
45 / 59
 fileIdById
0.00% covered (danger)
0.00%
0 / 1
3.14
75.00% covered (warning)
75.00%
9 / 12
 get_ref_data
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 get
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 19
 listRec
0.00% covered (danger)
0.00%
0 / 1
110
0.00% covered (danger)
0.00%
0 / 38
 parseQueryString
0.00% covered (danger)
0.00%
0 / 1
992
0.00% covered (danger)
0.00%
0 / 56
 query
0.00% covered (danger)
0.00%
0 / 1
1260
0.00% covered (danger)
0.00%
0 / 104
 basicQuery
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 parseRecord
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 14
 bytesToSeek
0.00% covered (danger)
0.00%
0 / 1
3.33
66.67% covered (warning)
66.67%
4 / 6
 rewriteMemo
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 readMemo
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 15
 deleteMemo
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 20
 writeMemo
0.00% covered (danger)
0.00%
0 / 1
306
0.00% covered (danger)
0.00%
0 / 48
 check
0.00% covered (danger)
0.00%
0 / 1
17.38
62.50% covered (warning)
62.50%
10 / 16
 sendError
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 9
 define_error_handler
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 version
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 deXSS
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
<?php
namespace TextDb;
    /*
     variable types:
     string - anything goes, length restriction (>1 bytes)
     number - numbers only (some symbols allowed), length restriction (>1 bytes)
     memo - anything goes, NO length restriction (7 bytes)
     id - auto-increment id field used to retrieve specific records (7 bytes)
     */
//TDB will print errors instead of using trigger_error() or a user defined error handler
/**
 *
 */
use TextDb\Exception\DatabaseExistsException;
use TextDb\Exception\InvalidDirectoryException;
use TextDb\Exception\InvalidFilePointerException;
use TextDb\Exception\NotWritableException;
use TextDb\Exception\InvalidTableException;
use TextDb\Exception\UndefinedDatabaseException;
use TextDb\Exception\UndefinedWorkingDirectoryException;
DEFINE('TDB_PRINT_ERRORS', FALSE);
//TDB will include the file and line number of your script that led to the error
/**
 *
 */
DEFINE('TDB_ERROR_INCLUDE_ORIGIN', TRUE);
/**
 * Class tdb
 * @package TextDb
 */
class tdb {
    /**
     * @var array
     */
    protected $fp = array();     // allowing for multiply file pointers
    /**
     * @var string
     */
    protected $workingDir;       // working directory
    /**
     * @var string
     */
    protected $Db;               // database.tdb
    /**
     * @var array
     */
    protected $Tables;           // list of tables in the database
    /**
     * @var bool
     */
    protected $error_handler = false; // user defined error handler
    /**
     * @var array
     */
    protected $_header = array(); // cache publics
    /**
     * @var array
     */
    protected $_query = array();
    /**
     * @var array
     */
    protected $_fileId = array();
    /**
     * @var array
     */
    protected $_ref = array();
    /**
     * @var array
     */
    protected $_firstBlankMemoBlockRef = array();
    //Does not store FPs but physical file addy, does not clear for cleanUp() etc.
    //prompts statsclearcache()
    /**
     * @var array
     */
    protected $editedTable = array();
    /**
     * tdb constructor.
     */
    public function __construct()
    {
    }
    /**
     * Defines the directory and database.  Must be ran before most functions can function.
     *
     * @param string $dir
     * @param string $db
     * @return bool
     */
    public function tdb($dir="", $db="") {
        if($dir == "" && $db == "") return false;
        $dir = str_replace("../", "", $dir);
        if(substr($dir, -1) != "/") $dir .= "/";
        if(substr($db, -4) != '.tdb') $db .= '.tdb';
        if(is_dir($dir)) {
            if(!is_writable($dir)) {
                $this->sendError(E_ERROR, "Fatal: Working directory ($dir) is not writable, try chmod 777 if 666 does not work.", __LINE__);
            } else {
                if(is_writable($dir.$db)) {
                    if(filesize($dir.$db) != 0) {
                        $f = fopen($dir.$db, "rb");
                        @$this->Tables = trim(@fread($f, filesize($dir.$db)));
                        fclose($f);
                        $this->Tables = explode("\n", $this->Tables);
                    } else $this->Tables = array();
                    $this->Db = $db;
                    $this->workingDir = $dir;
                    return true;
                } else {
                    $this->sendError(E_ERROR, "Fatal: Database ($db) is not writable, try chmod 777 if 666 does not work.", __LINE__);
                    return false;
                }
            }
        } else {
            $this->sendError(E_USER_ERROR, "Failed setting working directory ($dir), not a valid path.", __LINE__);
            return false;
        }
        return true;
    }
    /**
     * Creates a database.  Run tdb() before handling the database
     *
     * @param string $dir
     * @param string $filename
     * @throws DatabaseExistsException if the database already exists
     * @throws NotWritableException if the directory isn't writable
     * @throws InvalidDirectoryException if the directory doesn't exist
     */
    public function createDatabase($dir, $filename) {
        $dir = str_replace("../", "", $dir);
        if(substr($dir, -1) != "/") {
            $dir .= "/";
        }
        if(substr($filename, -4) != ".tdb") {
            $filename .= ".tdb";
        }
        if(file_exists($dir.$filename)) {
            throw new DatabaseExistsException();
        }
        if(is_dir($dir)) {
            if(!is_writable($dir)) {
                throw new NotWritableException();
            } else {
                $f = fopen($dir.$filename, "wb");
                fwrite($f, "");
                fclose($f);
                $this->tdb($dir, $filename);
            }
        } else {
            throw new InvalidDirectoryException();
        }
    }
    /**
     * Deletes the database as well as its tables
     *
     * @return bool
     */
    public function removeDatabase() {
        $this->check();
        foreach($this->Tables as $ta) {
            if(trim($ta) != "") $this->removeTable($ta);
        }
        unlink($this->workingDir.$this->Db);
        return true;
    }
    /**
     * Deletes a table managed be the defined database.
     *
     * @param string $tableName
     * @return bool
     */
    public function removeTable($tableName) {
        $this->check();
        if(substr($tableName, 0, (strlen($this->Db)-4)) != substr($this->Db, 0, -4)) $tableName = substr($this->Db, 0, -4).'_'.$tableName;
        if(!$this->isTable($tableName)) {
            throw new InvalidTableException();
        }
        foreach($this->Tables as $key => $ta) {
            if($ta == $tableName) {
                unset($this->Tables[$key]);
                $f = fopen($this->workingDir.$this->Db, 'wb');
                flock($f, 2);
                fwrite($f, implode("\n", $this->Tables));
                flock($f, 3);
                fclose($f);
            }
        }
        unlink($this->workingDir.$tableName.'.ta');
        unlink($this->workingDir.$tableName.'.memo');
        unlink($this->workingDir.$tableName.'.ref');
        return true;
    }
    /**
     * Returns the number of records.
     *
     * @param string $fp
     * @return int count on success, bool false on fail
     */
    public function getNumberOfRecords($fp) {
        $this->check($fp);
        return substr_count($this->get_ref_data($fp), chr(31));
    }
    /**
     * Returns the names of tables.
     *
     * @return array tables on success, bool false on fail
     */
    function getTableList() {
        $this->check();
        return $this->Tables;
    }
    /**
     * Retrieves the list of fields of a table and their parameters
     *
     * @param string $fp
     * @return array fields on success, bool false on fail
     */
    function getFieldList($fp) {
        $this->check($fp);
        $header = array();
        $this->readHeader($fp, $header);
        $returnArr = array();
        $cHeader = count($header) - 8;
        for($i=1;$i<=$cHeader;$i++) {
            $returnArr[] = $header[$i];
        }
        return $returnArr;
    }
    /**
     * Retrieves the headers.
     *
     * @param string $fp
     * @param array $header
     * @return bool
     */
    function readHeader($fp, &$header) {
        $this->check($fp);
        if(isset($this->_header[$fp])) {
            $header = $this->_header[$fp];
            return true;
        }
        $header = array();
        $f = fopen($this->fp[$fp].'.ta', 'rb');
        if(!$f) return false;
        $header["headLen"] = "";
        while(!feof($f)) {
            $next = fgetc($f);
            if(ord($next) == 28) break;
            else $header["headLen"] .= $next;
        }
        if(is_numeric($header['headLen'])) $header['headLen'] = (int)$header['headLen'];
        $header["raw"] = fread($f, $header["headLen"]);
        $header["fieldsraw"] = explode(chr(29), $header["raw"]);
        for($i=0;$i<count($header["fieldsraw"]);$i++) {
            $tmp = explode(chr(31), $header["fieldsraw"][$i]);
            $header[$tmp[0]]["fName"] = $tmp[3];
            $header[$tmp[0]]["fType"] = $tmp[1];
            $header[$tmp[0]]["fLength"] = (int)$tmp[2];
            unset($tmp);
        }
        unset($header["raw"],$header["fieldsraw"]);
        $header["recLen"] = "";
        while(!feof($f)) {
            $next = fgetc($f);
            if(ord($next) == 28) break;
            else $header["recLen"] .= $next;
        }
        if(is_numeric($header['recLen'])) $header['recLen'] = (int)$header['recLen'];
        $header["curId"] = "";
        $header["idPos"] = ftell($f);
        while(!feof($f)) {
            $next = fgetc($f);
            if(ord($next) == 28) break;
            else $header["curId"] .= $next;
        }
        $header["curId"] = (int)trim($header["curId"]);
        $header["lastBlank"] = "";
        $header["blankPos"] = ftell($f);
        while(!feof($f)) {
            $next = fgetc($f);
            if(ord($next) == 28) break;
            else $header["lastBlank"] .= $next;
        }
        $header["lastBlank"] = (int)trim($header["lastBlank"]);
        $header["blockLength"] = "";
        while(!feof($f)) {
            $next = fgetc($f);
            if(ord($next) == 28) break;
            else $header["blockLength"] .= $next;
        }
        if(is_numeric($header['blockLength'])) $header['blockLength'] = (int)$header['blockLength'];
        $header["recPos"] = ftell($f);
        fclose($f);
        $this->_header[$fp] = $header;
        return true;
    }
    /**
     * Sets a filepointer to a table.  Necessary for most functions.
     *
     * @param string $fp
     * @param string $table
     * @return bool
     */
    function setFp($fp, $table) {
        if(substr($table, 0, (strlen($this->Db)-4)) != substr($this->Db, 0, -4)) $table = substr($this->Db, 0, -4).'_'.$table;
        if($this->isTable($table)) {
            $this->fp[$fp] = $this->workingDir.$table;
            unset($this->_header[$fp]);
            unset($this->_query[$fp]);
            unset($this->_fileId[$fp]);
            unset($this->_ref[$fp]);
            unset($this->_firstBlankMemoBlockRef[$fp]);
        } else {
            $this->sendError(E_WARNING, "$table is not a valid table in $this->Db", __LINE__);
            return false;
        }
        return true;
    }
    /**
     * Clears cached information on tables aswell as filepoints ($fp)
     *
     */
    function cleanUp() {
        $this->fp = array();
        $this->_header = array();
        $this->_query = array();
        $this->_fileId = array();
        $this->_ref = array();
        $this->_firstBlankMemoBlockRef = array();
    }
    /**
     * Determines if the table exists in the defined database
     *
     * @param string $table
     * @return bool
     */
    function isTable($table) {
        $this->check();
        if(substr($table, 0, (strlen($this->Db) -4)) != substr($this->Db, 0, -4)) $table = substr($this->Db, 0, -4).'_'.$table;
        foreach($this->Tables as $ta) {
            if($ta == $table) return true;
        }
        return false;
    }
    /**
     * @param $table
     * @param $fields
     * @param string $block_length
     * @return bool
     */
    function createTable($table, $fields, $block_length="100") {
        $this->check();
        $block_length = $block_length + 8; //To store the next fp of every block + end of text chr
        if(substr($table, 0, (strlen($this->Db) -4) != substr($this->Db, 0, -4))) $table = substr($this->Db, 0, -4).'_'.$table;
        if(!is_array($fields)) {
            $this->sendError(E_USER_ERROR, "\$fields must be an array", __LINE__);
            return false;
        } else {
            // check if table already exists
            if(file_exists($this->workingDir.$table) || in_array($table, $this->Tables)) {
                $this->sendError(E_WARNING, "Table ($this->workingDir$table) already exists", __LINE__);
                return false;
            }
            // start building the header
            $h_fields = array();
            $h_recLen = 0;
            for($i=0;$i<count($fields);$i++) {
                if($fields[$i][1] != "string" && $fields[$i][1] != "number" && $fields[$i][1] != "memo" && $fields[$i][1] != "id") {
                    $this->sendError(E_USER_ERROR, "Field type must be either string, number, memo, or id.", __LINE__);
                    return false;
                }
                if($fields[$i][1] == "id" || $fields[$i][1] == 'memo') $fields[$i][2] = "7";
                //if($fields[$i][1] == "memo") $fields[$i][2] = "10"; //memo functions only use 7 chars
                $h_fields[] = ($i + 1).chr(31).$fields[$i][1].chr(31).$fields[$i][2].chr(31).$fields[$i][0];
                $h_recLen += $fields[$i][2];
            }
            $h_fields = implode(chr(29), $h_fields);
            $h_cid =       "0      ";
            $h_lastBlank = "-1     ";
            $header = strlen($h_fields).chr(28).$h_fields.$h_recLen.chr(28).$h_cid.chr(28).$h_lastBlank.chr(28).$block_length.chr(28);
            // write the table header
            $f = fopen($this->workingDir.$table.'.ta', 'wb');
            fwrite($f, $header);
            fclose($f);
            $f = fopen($this->workingDir.$table.'.memo', 'wb');
            fwrite($f, '-1'.str_repeat(' ', 5).str_repeat(' ', ($block_length - 7))); //This is the memo header aswell as block #0.
            fclose($f);
            $f = fopen($this->workingDir.$table.'.ref', 'wb');
            fwrite($f, "");
            fclose($f);
            // write the table in the database
            $this->Tables[] = $table;
            $f = fopen($this->workingDir.$this->Db, 'wb');
            fwrite($f, implode("\n", $this->Tables));
            fclose($f);
            return true;
        }
    }
    /**
     * Adds another field to a table
     *
     * @param string $fp
     * @param array $field
     * @return bool
     */
    function addField($fp, $field) {
        $this->check($fp);
        $header = array();
        $this->readHeader($fp, $header);
        $cHeader = count($header) - 8;
        for($i=1;$i<=$cHeader;$i++) {
            if($header[$i]["fName"] == $field[0]) {
                $this->sendError(E_USER_WARNING, "Field already exists, aborting...", __LINE__);
                return false;
            }
        }
        $cHeader += 1; // for the new field
        //name, type, size
        if($field[1] != "string" && $field[1] != "number" && $field[1] != "memo" && $field[1] != "id") {
            $this->sendError(E_USER_ERROR, "Field type must be either string, number, memo, or id.", __LINE__);
            return false;
        }
        if($field[1] == "id" || $field[1] == "memo") $field[2] = "7";
        //if($field[1] == "memo") $field[2] = "10";
        $header[$cHeader]["fName"] = $field[0];
        $header[$cHeader]["fType"] = $field[1];
        $header[$cHeader]["fLength"] = $field[2];
        // start building the header
        $h_fields = array();
        $h_recLen = 0;
        for($i=1;$i<=$cHeader;$i++) {
            $h_fields[] = $i.chr(31).$header[$i]["fType"].chr(31).$header[$i]["fLength"].chr(31).$header[$i]["fName"];
            $h_recLen += $header[$i]["fLength"];
        }
        $h_fields = implode(chr(29), $h_fields);
        $h_cid = $header["curId"].str_repeat(" ", 7 - strlen($header["curId"]));
        $h_lastBlank = $header["lastBlank"].str_repeat(" ", 7 - strlen($header["lastBlank"]));
        $h_header = strlen($h_fields).chr(28).$h_fields.$h_recLen.chr(28).$h_cid.chr(28).$h_lastBlank.chr(28).$header["blockLength"].chr(28);
        // rebuild the table with the new field
        $eRecSize = filesize($this->fp[$fp].'.ta') - $header["recPos"];
        $eRecCount = $eRecSize / $header["recLen"];
        $f = fopen($this->fp[$fp].'.ta', 'rb');
        // open up temp file for writing the buffers
        $newFnam = $this->workingDir."tmpF_".md5(uniqid(rand()));
        $newF = fopen($newFnam, "wb");
        // write the header to the new table file
        fwrite($newF, $h_header);
        $value = "";
        if($header[$cHeader]["fType"] == "id") {
            $ref_data = $this->get_ref_data($fp);
            $ref_arr = explode(chr(31), $ref_data);
            $rfArr = array();
            foreach($ref_arr as $ref_tmp) {
                $ref_tmp = explode(':', $ref_tmp);
                $rfArr[$ref_tmp[1]-1] = $ref_tmp[0];
            }
        }
        $cid = 1;
        while(!feof($f)) {
            if($cid > $eRecCount) break;
            $value = "";
            fseek($f, $this->bytesToSeek($fp, $header, $cid));
            if($header[$cHeader]["fType"] == "id" && isset($rfArr[$cid])) $value = $rfArr[$cid];
            // write the record with the new field
            fwrite($newF, fread($f, $header["recLen"]).$value.str_repeat(" ", $header[$cHeader]["fLength"] - strlen($value)));
            $cid++;
        }
        fclose($newF);
        fclose($f);
        unlink($this->fp[$fp].'.ta');
        rename($newFnam, $this->fp[$fp].'.ta');
        if(isset($this->_header[$fp])) unset($this->_header[$fp]);
        return true;
    }
    /**
     * Edits a field's parameters
     *
     * @param string $fp
     * @param string $oldfield
     * @param array $field
     * @return bool
     */
    function editField($fp, $oldfield, $field=array()) {
        $this->check($fp);
        $header = array();
        $this->readHeader($fp, $header);
        $cHeader = count($header) - 8;
        $foundField = false;
        for($i=1;$i<=$cHeader;$i++) {
            if($header[$i]["fName"] == $oldfield) {
                $foundField = true;
                $fieldId = $i;
                break;
            }
        }
        if(!$foundField) {
            $this->sendError(E_USER_WARNING, "Cannot remove '$oldfield' field, it does not exist.", __LINE__);
            return false;
        }
        //name, type, size
        if($field[1] != "string" && $field[1] != "number" && $field[1] != "memo" && $field[1] != "id") {
            $this->sendError(E_USER_ERROR, "New field type must be either string, number, memo, or id.", __LINE__);
            return false;
        }
        if($field[1] == "id" || $field[1] == 'memo') $field[2] = "7";
        //if($field[1] == "memo") $field[2] = "10"; //memo functions only use 7 chars
        $oldlength = $header[$fieldId]["fLength"];
        $oldType = $header[$fieldId]["fType"];
        $header[$fieldId]["fName"] = $field[0];
        $header[$fieldId]["fType"] = $field[1];
        $header[$fieldId]["fLength"] = $field[2];
        // start building the header
        $h_fields = array();
        $h_recLen = 0;
        for($i=1;$i<=$cHeader;$i++) {
            $h_fields[] = $i.chr(31).$header[$i]["fType"].chr(31).$header[$i]["fLength"].chr(31).$header[$i]["fName"];
            $h_recLen += $header[$i]["fLength"];
        }
        $h_fields = implode(chr(29), $h_fields);
        $h_cid = $header["curId"].str_repeat(" ", 7 - strlen($header["curId"]));
        $h_lastBlank = $header["lastBlank"].str_repeat(" ", 7 - strlen($header["lastBlank"]));
        $h_header = strlen($h_fields).chr(28).$h_fields.$h_recLen.chr(28).$h_cid.chr(28).$h_lastBlank.chr(28).$header["blockLength"].chr(28);
        // rebuild the table with the new field
        $eRecSize = filesize($this->fp[$fp].'.ta') - $header["recPos"];
        $eRecCount = $eRecSize / $header["recLen"];
        $f = fopen($this->fp[$fp].'.ta', 'rb');
        // open up temp file for writing the buffers
        $newFnam = $this->workingDir."tmpF_".md5(uniqid(rand()));
        $newF = fopen($newFnam, "wb");
        // write the header to the new table file
        fwrite($newF, $h_header);
        $cid = 1;
        while(!feof($f)) {
            if($cid > $eRecCount) break;
            fseek($f, $this->bytesToSeek($fp, $header, $cid));
            if(fread($f, 1) == chr(24)) {
                fwrite($newF, chr(24).fread($f, 7).str_repeat(' ', $h_recLen - 8));
                $cid++;
                continue;
            } else fseek($f, -1, SEEK_CUR);
            for($i=1;$i<=$cHeader;$i++) {
                if($i != $fieldId) {
                    fwrite($newF, fread($f, $header[$i]["fLength"]));
                } else {
                    $value = rtrim(fread($f, $oldlength));
                    if($oldType == "memo" && $header[$i]["fType"] != "memo") {
                        $memo = $value;
                        $value = $this->readMemo($fp, $memo, $header);
                        $this->deleteMemo($fp, $memo, $header);
                        unset($memo);
                    }
                    $field = $value;
                    if($header[$i]["fType"] == "memo") {
                        $this->writeMemo($fp, $field, $header);
                    } elseif($header[$i]["fType"] == "string") {
                        $field = substr($field, 0, $header[$i]["fLength"]);
                    } elseif($header[$i]["fType"] == "number") {
                        $field = preg_replace("/[^0-9.-]/i", "", $field);
                        $field = substr($field, 0, $header[$i]["fLength"]);
                    } elseif($header[$i]["fType"] == "id") {
                        $field = $header["curId"];
                    }
                    $field = $field.str_repeat(" ", $header[$i]["fLength"] - strlen($field));
                    fwrite($newF, $field);
                }
            }
            $cid++;
        }
        fclose($newF);
        fclose($f);
        unlink($this->fp[$fp].'.ta');
        rename($newFnam, $this->fp[$fp].'.ta');
        $this->_header[$fp] = $header;
        return true;
    }
    /**
     * Removes a column from the table
     *
     * @param string $fp
     * @param string $field
     * @return bool
     */
    function removeField($fp, $field) {
        $this->check($fp);
        $header = array();
        $this->readHeader($fp, $header);
        $cHeader = count($header) - 8;
        $foundField = false;
        for($i=1;$i<=$cHeader;$i++) {
            if($header[$i]["fName"] == $field) {
                $foundField = true;
                break;
            }
        }
        if(!$foundField) {
            $this->sendError(E_USER_WARNING, "Cannot remove '$field' field, it does not exist.", __LINE__);
            return false;
        }
        // start building the header
        $h_fields = array();
        $h_recLen = 0;
        for($i=1,$j=1;$i<=$cHeader;$i++) {
            if($header[$i]["fName"] != $field) {
                $h_fields[] = $j++.chr(31).$header[$i]["fType"].chr(31).$header[$i]["fLength"].chr(31).$header[$i]["fName"];
                $h_recLen += $header[$i]["fLength"];
            }
        }
        $h_fields = implode(chr(29), $h_fields);
        $h_cid = $header["curId"].str_repeat(" ", 7 - strlen($header["curId"]));
        $h_lastBlank = $header["lastBlank"].str_repeat(" ", 7 - strlen($header["lastBlank"]));
        $h_header = strlen($h_fields).chr(28).$h_fields.$h_recLen.chr(28).$h_cid.chr(28).$h_lastBlank.chr(28).$header["blockLength"].chr(28);
        // rebuild the table with the field removed
        $eRecSize = filesize($this->fp[$fp].'.ta') - $header["recPos"];
        $eRecCount = $eRecSize / $header["recLen"];
        $f = fopen($this->fp[$fp].'.ta', 'rb');
        // open up temp file for writing the buffers
        $newFnam = $this->workingDir."tmpF_".md5(uniqid(rand()));
        $newF = fopen($newFnam, "wb");
        // write the header to the new table file
        fwrite($newF, $h_header);
        $cid = 1;
        while(!feof($f)) {
            if($cid > $eRecCount) break;
            fseek($f, $this->bytesToSeek($fp, $header, $cid));
            if(fread($f, 1) == chr(24)) {
                fwrite($newF, chr(24).fread($f, 7).str_repeat(' ', $h_recLen - 8));
                $cid++;
                continue;
            } else fseek($f, -1, SEEK_CUR);
            for($i=1;$i<=$cHeader;$i++) {
                if($header[$i]["fName"] != $field) {
                    fwrite($newF, fread($f, $header[$i]["fLength"]));
                } elseif($header[$i]["fType"] != "memo") fseek($f, $header[$i]["fLength"], SEEK_CUR);
                else $this->deleteMemo($fp, trim(fread($f, $header[$i]["fLength"])), $header);
            }
            $cid++;
        }
        fclose($newF);
        fclose($f);
        unlink($this->fp[$fp].'.ta');
        rename($newFnam, $this->fp[$fp].'.ta');
        if(isset($this->_header[$fp])) unset($this->_header[$fp]);
        return true;
    }
    /**
     * Edits a record.
     *
     * @param string $fp
     * @param int $id
     * @param array $recArr
     * @return bool
     */
    function edit($fp, $id, $recArr) {
        $this->check($fp);
        $header = array();
        if(FALSE === ($fileId = $this->fileIdById($fp, $id))) {
            $this->sendError(E_WARNING, "Unable to execute edit().  Unable to retrieve fileID", __LINE__);
            return false;
        }
        $this->readHeader($fp, $header);
        $f = fopen($this->fp[$fp].'.ta', 'r+b');
        //fseek($f, $this->bytesToSeek($fp, $header, $fileId));
        $offset = $this->bytesToSeek($fp, $header, $fileId);
        //edit the record
        $cHeader = count($header) - 8;
        for($i=1;$i<=$cHeader;$i++) {
            fseek($f, $offset);
            $field = "";
            if(isset($recArr[$header[$i]["fName"]])) {
                $field = $recArr[$header[$i]["fName"]];
                if($header[$i]["fType"] == "memo") {
                    $this->deleteMemo($fp, trim(fread($f, $header[$i]["fLength"])), $header);
                    fseek($f, $offset);
                    $field = $this->writeMemo($fp, $field, $header);
                } elseif($header[$i]["fType"] == "string") {
                    //$field = preg_replace("/[^a-z0-9 ,.:?/#]/i", "", $field);
                    $field = substr($field, 0, $header[$i]["fLength"]);
                } elseif($header[$i]["fType"] == "number") {
                    $field = preg_replace("/[^0-9.-]/i", "", $field);
                    $field = substr($field, 0, $header[$i]["fLength"]);
                } elseif($header[$i]["fType"] == "id") {
                    $theId = trim(fread($f, $header[$i]["fLength"]));
                    fseek($f, $offset);
                    $field = $theId;
                }
                $field = $field.str_repeat(" ", $header[$i]["fLength"] - strlen($field));
                fwrite($f, $field);
            }
            $offset += $header[$i]["fLength"];
        }
        fclose($f);
        if(isset($this->_query[$fp])) unset($this->_query[$fp]);
        return true;
    }
    /**
     * Deletes a record.
     *
     * @param string $fp
     * @param int $id
     * @return bool
     */
    function delete($fp, $id) {
        $this->check($fp);
        if(FALSE === ($fileId = $this->fileIdById($fp, $id))) {
            $this->sendError(E_WARNING, "Unable to execute delete().  Unable to retrieve fileID", __LINE__);
            return false;
        }
        $header = array();
        $this->readHeader($fp, $header);
        $f = fopen($this->fp[$fp].'.ta', 'r+b');
        fseek($f, $this->bytesToSeek($fp, $header, $fileId));
        //Gather memo fps
        $offset = 0;
        for($i=1, $hmax=(count($header)-8);$i<=$hmax;$i++) {
            if($header[$i]["fType"] == "memo") {
                fseek($f, $offset, SEEK_CUR);
                $this->deleteMemo($fp, trim(fread($f, $header[$i]["fLength"])), $header);
                $offset = 0;
            } else $offset += $header[$i]["fLength"];
        }
        //erase the record
        fseek($f, $this->bytesToSeek($fp, $header, $fileId));
        fwrite($f, chr(24).$header['lastBlank'].str_repeat(" ", ($header["recLen"] - (strlen($header['lastBlank']) + 1))));
        //fwrite($f, chr(24).$header['lastBlank']);
        $this->_header[$fp]['lastBlank'] = $fileId;
        fseek($f, $header['blankPos']);
        $fileId = substr($fileId, 0, 7);
        fwrite($f, $fileId.str_repeat(' ', 7 - strlen($fileId)));
        fclose($f);
        $ref_data = chr(31).$this->get_ref_data($fp);
        $ref_data = substr(str_replace(chr(31).$id.':'.$fileId.chr(31), chr(31), $ref_data), 1);
        $f = fopen($this->fp[$fp].'.ref', 'wb');
        $this->_ref[$fp] = $ref_data;
        fwrite($f, $ref_data);
        fclose($f);
        if(isset($this->_query[$fp])) unset($this->_query[$fp]);
        return true;
    }
    /**
     * Obsolete function, passes arguments to tdb::sort()
     *
     * @param string $fp
     * @param string $fieldName
     * @param string $direction
     * @return bool
     */
    function sortAndBuild($fp, $fieldName, $direction="ASC") {
        $this->sendError(E_USER_NOTICE, 'tdb::sortAndBuild() function obsolete.  Update scripts accordingly', __LINE__);
        return $this->sort($fp, $fieldName, $direction);
    }
    //new sys not avail.
    /**
     * Sorts records in a specific order based on a field
     *
     * @param string $fp
     * @param string $fieldName
     * @param string $direction[Optional]
     * @return bool
     */
    function sort($fp, $fieldName, $direction="ASC") {
        $this->check($fp);
        $header = array();
        $this->readHeader($fp, $header);
        $sortFieldIndex = 0;
        $sortType = ""; //numeric or string
        //first make sure this fieldname exists
        $exists = false;
        for($i=1;$i<=count($header)-8;$i++) {
            if($header[$i]["fName"] == $fieldName) {
                $sortFieldIndex = $i;
                $exists = true;
                break;
            }
        }
        unset($i);
        if($exists) {
            if($header[$sortFieldIndex]["fType"] == "number" ||  $header[$sortFieldIndex]["fType"] == "id") {
                $sortType = SORT_NUMERIC;
            } elseif($header[$sortFieldIndex]["fType"] == "string") {
                $sortType = SORT_STRING;
            } else {
                $this->sendError(E_USER_WARNING, "You can only sort the following field types: number, id, and string", __LINE__);
                return false;
            }
        } else {
            $this->sendError(E_USER_WARNING, "The field '$fieldName' does not exist", __LINE__);
            return false;
        }
        //build the sorting array
        $arrById = array();
        $row = $this->listRec($fp, 1);
        for($i=0;$i<count($row);$i++) {
            $arrById[$row[$i][$fieldName]] = $row[$i]['id'];
        }
        //sort
        if($direction == "ASC") ksort($arrById, $sortType);
        elseif($direction == "DESC") krsort($arrById, $sortType);
        reset($arrById);
        // grab all the references
        $ref_data = $this->get_ref_data($fp);
        $refArrOld = explode(chr(31), substr($ref_data, 0, -1));
        $refArr = array();
        foreach($refArrOld as $refArrTmp) {
            $refTmp = explode(':', $refArrTmp);
            $refArr[$refTmp[0]] = $refArrTmp;
        }
        //Rebuild the references
        $new_ref = '';
        /*for($i=0;$i<count($row);$i++) {
         $arrInfo = each($arrById);
         $id = $arrInfo["key"] + 1;
         $new_ref .= $refArr[$id].":".($i + 1).chr(31);
         }*/
        foreach($arrById as $arrInfo) {
            $new_ref .= $refArr[$arrInfo].chr(31);
        }
        $this->_ref[$fp] = $new_ref;
        $f = fopen($this->fp[$fp].'.ref', 'wb');
        fwrite($f, $new_ref);
        fclose($f);
        return true;
    }
    /**
     * Obsolete function
     *
     */
    function reBuild() {
        $this->sendError(E_USER_NOTICE, 'tdb::reBuild() function obsolete.  Update scripts accordingly', __LINE__);
    }
    /**
     * Add a record to table $fp, using values from $recArr
     *
     * @param string $fp
     * @param array $recArr
     * @return bool false on fail, int ID on success
     */
    public function add($fp, $recArr) {
        $this->check($fp);
        $header = array();
        $this->readHeader($fp, $header);
        $header["curId"]++;
        //update the cache curId as well
        $this->_header[$fp]["curId"]++;
        if($header["curId"] > 5000000) {
            $this->sendError(E_WARNING, "Maximum records reached (5,000,000) aborting...", __LINE__);
            return false;
        }
        $record = "";
        for($i=1, $cHeader = (count($header) - 8);$i<=$cHeader;$i++) {
            if(isset($recArr[$header[$i]["fName"]])) $field = $recArr[$header[$i]["fName"]];
            else $field = "";
            if($header[$i]["fType"] == "memo") {
                $field = $this->writeMemo($fp, $field, $header);
            } elseif($header[$i]["fType"] == "string") {
                //$field = preg_replace("/[^a-z0-9 ,.:?/#]/i", "", $field);
                //$field = substr($field, 0, $header[$i]["fLength"]);
            } elseif($header[$i]["fType"] == "number") {
                $field = preg_replace("/[^0-9.-]/i", "", $field);
                //$field = substr($field, 0, $header[$i]["fLength"]);
            } elseif($header[$i]["fType"] == "id") {
                $field = $header["curId"];
            }
            $field = substr($field, 0, $header[$i]["fLength"]);
            $field = $field.str_repeat(" ", $header[$i]["fLength"] - strlen($field));
            $record[] = $field;
        }
        $record = implode("", $record);
        if(strlen($record) != $header["recLen"]) {
            $this->sendError(E_ERROR, "There was an error adding the record.", __LINE__);
            return false;
        }
        if($header['lastBlank'] != -1) {
            $fileId = $header['lastBlank'];
        } elseif(isset($this->_fileId[$fp])) {
            $this->_fileId[$fp]++;
            $fileId = $this->_fileId[$fp];
        } else {
            $fileId = (filesize($this->fp[$fp].'.ta') - $header["recPos"]) / $header["recLen"] + 1;
            $this->_fileId[$fp] = $fileId;
        }
        $f = fopen($this->fp[$fp].'.ref', 'ab');
        flock($f, 2);
        fwrite($f, $header["curId"].":".$fileId.chr(31));
        flock($f, 3);
        if(isset($this->_ref[$fp])) $this->_ref[$fp] .= $header["curId"].":".$fileId.chr(31);
        fclose($f);
        $f = fopen($this->fp[$fp].'.ta', 'r+b');
        flock($f, 2);
        fseek($f, $header["idPos"]);
        fwrite($f, $header["curId"]);
        if($header['lastBlank'] == -1) fseek($f, 0, SEEK_END);
        else {
            fseek($f, $this->bytesToSeek($fp, $header, $fileId));
            $nextBlank = ltrim(fread($f, 8), chr(24));
            fseek($f, $header['blankPos']);
            $nextBlank = substr($nextBlank, 0, 7);
            fwrite($f, $nextBlank);
            $this->_header[$fp]['lastBlank'] = (int)trim($nextBlank);
            fseek($f, $this->bytesToSeek($fp, $header, $fileId));
        }
        fwrite($f, $record);
        flock($f, 3);
        fclose($f);
        if(!in_array($this->fp[$fp], $this->editedTable)) $this->editedTable[] = $this->fp[$fp];
        return $header["curId"];
    }
    /**
     * Finds the physical address of a record based on its ID
     *
     * @param string $fp
     * @param int $id
     * @return int fileID on success, bool false on fail
     */
    function fileIdById($fp, $id) {
        $this->check($fp);
        $ref_data = $this->get_ref_data($fp);
        $ref_data = chr(31).$ref_data;
        if(FALSE !== ($pos1 = strpos($ref_data, chr(31).$id.':'))) {
            $pos1 = $pos1 + strlen(chr(31).$id.':');
            $length = strpos($ref_data, chr(31), $pos1) - $pos1;
            $fileId = substr($ref_data, $pos1, $length);
            if((int)$fileId <= 0) {
                $this->sendError(E_ERROR, "fileIdById() found a nonpositive integer fileId(\"$fileId\") based on ID(\"$id\") in ".$this->fp[$fp].".  Dumping reference file:".$ref_data.". \$pos1:$pos1. \$length:$length.", __LINE__);
                return false;
            }
            return  (int)$fileId;
            //return substr($ref_data, $pos1, $length);
        }
        return false;
    }
    /**
     * Retrieves the ref information of a table based on the $fp
     *
     * @param string $fp
     * @return string
     */
    function get_ref_data($fp) {
        $this->check($fp);
        if(!isset($this->_ref[$fp])) $this->_ref[$fp] = file_get_contents($this->fp[$fp].'.ref');
        return $this->_ref[$fp];
    }
    /**
     * @param $fp
     * @param $id
     * @param array $fields
     * @return array|bool
     */
    function get($fp, $id, $fields=array('*')) {
        if(FALSE === ($fileId = $this->fileIdById($fp, $id))) {
            //$this->sendError(E_WARNING, "Unable to execute get().  Unable to retrieve fileID", __LINE__);
            return false;
        }
        $reqFields = array();
        if($fields[0] == "*") {
            $reqFields = array("*");
        } else {
            foreach($fields as $incField) {
                $reqFields[$incField] = true;
            }
        }
        $header = array();
        $this->readHeader($fp, $header);
        $f = fopen($this->fp[$fp].'.ta', 'rb');
        fseek($f, $this->bytesToSeek($fp, $header, $fileId));
        $buffer = fread($f, $header["recLen"]);
        fclose($f);
        if(ord($buffer{0}) != 28) {
            //return $this->parseRecord($fp, $buffer, $header); //new method
            return array($this->parseRecord($fp, $buffer, $header, $reqFields)); //old method
        }
        $this->sendError(E_USER_NOTICE, 'Unable to parse record with recordId of '.$id.' at fileId:'.$fileId.' in get() at '.$this->fp[$fp], __LINE__);
        return false;
    }
    /**
     * Retrieves records in sequential order
     *
     * @param string $fp
     * @param int $start
     * @param int $howmany
     * @param array $fields
     * @return array records on success, bool false on fail
     */
    function listRec($fp, $start, $howmany=-1, $fields=array("*")) {
        $this->check($fp);
        $return = array();
        $pos2 = 0;
        $pos1 = 0;
        $ref_data = $this->get_ref_data($fp);
        for($i=1;$i<$start;$i++) { //skipping 1st rec, b/c default: $pos1 = 0;
            $pos1 = strpos($ref_data, chr(31), $pos1) + 1;
        }
        if($pos1 > strlen($ref_data)) {
            $this->sendError(E_ERROR, 'Searching for records past the end of file in listRec('.$fp.')', __LINE__);
            return false;
        }
        $reqFields = array();
        if($fields[0] == "*") {
            $reqFields = array("*");
        } else {
            foreach($fields as $incField) {
                $reqFields[$incField] = true;
            }
        }
        $header = array();
        $this->readHeader($fp, $header);
        $f = fopen($this->fp[$fp].'.ta', 'rb');
        while(FALSE !== ($pos2 = strpos($ref_data, chr(31), $pos1))) {
            if($howmany == 0) break;
            $fileId = substr(strstr(substr($ref_data, $pos1, ($pos2 - $pos1)), ':'), 1);
            $pos1 = $pos2 + 1;
            if($fileId === FALSE) continue;
            fseek($f, $this->bytesToSeek($fp, $header, $fileId));
            $buffer = fread($f, $header["recLen"]);
            if(ord($buffer{0}) != 28) {
                $return[] = $this->parseRecord($fp, $buffer, $header, $reqFields);
                $howmany--;
            }
            else $this->sendError(E_PARSE, 'Unable to parse record at fileId:'.$fileId.' in listRec() at '.$this->fp[$fp], __LINE__);
        }
        unset($buffer);
        fclose($f);
        if(empty($return)) return false;
        return $return;
    }
    /**
     * Internal function used to parse a querystring for the query() function
     *
     * @param string $query
     * @return bool false on fail, array search terms on success
     */
    function parseQueryString($query) {
        if(trim($query) == "") return FALSE;
        //first thing is first, we need to find out if we are using =,?,<,>,!
        $pos1 = strpos($query, "=");
        $pos2 = strpos($query, "?");
        $pos3 = strpos($query, ">");
        $pos4 = strpos($query, "<");
        $pos5 = strpos($query, "!");
        $pointer = 0;
        $result = array();
        $lenquery = strlen($query);
        $i = 0;
        while($pos1 !== FALSE || $pos2 !== FALSE || $pos3 !== FALSE || $pos4 !== FALSE || $pos5 !== FALSE) {
            //find out which one came first
            $pos = FALSE;
            if($pos1 !== FALSE) {
                $pos = $pos1;
                $type = "=";
            }
            if($pos2 !== FALSE && $pos2 < $pos || $pos2 !== FALSE && $pos === FALSE) {
                $pos = $pos2;
                $type = "?";
            }
            if($pos3 !== FALSE && $pos3 < $pos || $pos3 !== FALSE && $pos === FALSE) {
                $pos = $pos3;
                $type = ">";
            }
            if($pos4 !== FALSE && $pos4 < $pos || $pos4 !== FALSE && $pos === FALSE) {
                $pos = $pos4;
                $type = "<";
            }
            if($pos5 !== FALSE && $pos5 < $pos || $pos5 !== FALSE && $pos === FALSE) {
                $pos = $pos5;
                $type = "!";
            }
            $field = substr($query, $pointer, ($pos - $pointer));
            $pointer = $pos + 1;
            //get the search text
            if(substr($query, $pointer, 1) != "'") $this->sendError(E_USER_ERROR, "Missing quote (') in query syntax", __LINE__);
            $pointer += 1;
            $text_pos = strpos($query, "'", $pointer);
            if($text_pos === FALSE) $this->sendError(E_USER_ERROR, "Invalid query syntax, missing ending quote (') in search term", __LINE__);
            $text = substr($query, $pointer, ($text_pos - $pointer));
            $pointer = $text_pos + 1;
            if($pointer >= $lenquery) $b = "&&";
            else {
                $b = substr($query, $pointer, 2);
                $pointer += 2;
            }
            if($b != "&&" && $b != "||") $this->sendError(E_USER_ERROR, "Invalid query syntax, missing '&&' or '||' between search terms", __LINE__);
            $result[$i][] = array("field" => $field, "type" => $type, "value" => $text);
            if($b == "||") {
                //create new list
                $i += 1;
            }
            //get our next term...
            $pos1 = strpos($query, "=", $pointer);
            $pos2 = strpos($query, "?", $pointer);
            $pos3 = strpos($query, ">", $pointer);
            $pos4 = strpos($query, "<", $pointer);
            $pos5 = strpos($query, "!", $pointer);
        }
        if($result !== FALSE) return $result;
        else return FALSE;
    }
    /**
     * Queries a table based on the query string's parameters.
     *
     * @param string $fp
     * @param string $query
     * @param int $start[optional]
     * @param int $howmany[optional]
     * @return bool false on fail, array records on success
     */
    function query($fp, $query, $start=1, $howmany=-1, $fields=array("*")) {
        $this->check($fp);
        $tmpfields = implode(",", $fields);
        if(!empty($this->_query[$fp])) {
            foreach($this->_query[$fp] as $cached_query) {
                if(
                    $cached_query["query_string"] == $query &&
                    $cached_query["start"] == $start &&
                    $cached_query["howmany"] == $howmany &&
                    $cached_query["fields"] == $tmpfields)
                    return $cached_query["result"];
            }
        }
        $original_start = $start;
        $original_howmany = $howmany;
        $original_query = $query;
        $original_fields = $tmpfields;
        $string = $query;
        unset($query);
        $query_array = $this->parseQueryString($string);
        if(empty($query_array)) $this->sendError(E_USER_ERROR, "Invalid query syntax or empty query string", __LINE__);
        $header = array();
        $this->readHeader($fp, $header);
        $reqFields = array();
        if($fields[0] == "*") {
            //$getAllFields = true;
            $reqFields = array("*");
        } else {
            //$getAllFields = false;
            foreach($fields as $incField) {
                $reqFields[$incField] = true;
            }
        }
        $fieldOffsets[$header[1]["fName"]]["offset"] = 0;
        $fieldOffsets[$header[1]["fName"]]["length"] = $header[1]["fLength"];
        $fieldOffsets[$header[1]["fName"]]["type"] = $header[1]["fType"];
        $total = 0;
        //if($getAllFields) $reqFields[$header[1]["fName"]] = true;
        for($i=2;$i<count($header)-7;$i++) {
            $fieldOffsets[$header[$i]["fName"]]["offset"] = $total + $header[$i-1]["fLength"];
            $fieldOffsets[$header[$i]["fName"]]["length"] = $header[$i]["fLength"];
            $fieldOffsets[$header[$i]["fName"]]["type"] = $header[$i]["fType"];
            $total += $header[$i-1]["fLength"];
            //if($getAllFields) $reqFields[$header[1]["fName"]] = true;
        }
        $return = array();
        $start_c = 1;
        $ref_pos1 = 0;
        $ref_data = $this->get_ref_data($fp);
        $ref_pos2 = 0;
        $f = fopen($this->fp[$fp].'.ta', 'rb');
        while(FALSE !== ($ref_pos2 = strpos($ref_data, chr(31), $ref_pos1))) {
            if(FALSE === ($fileId = strstr(substr($ref_data, $ref_pos1, ($ref_pos2 - $ref_pos1)), ':'))) {
                $ref_pos1 = $ref_pos2 + 1;
                continue;
            }
            $fileId = (int) substr($fileId, 1);
            $ref_pos1 = $ref_pos2 + 1;
            if($howmany == 0) break;
            //add OR functionality BEGIN
            $foundMatch = false;
            foreach($query_array as $query) {
                $pass = true;
                for($i=0;$i<count($query);$i++){
                    if(!$pass) break;
                    if($foundMatch) break;
                    $field = $query[$i]["field"];
                    $value = $query[$i]["value"];
                    if(!isset($fieldOffsets[$field])) $this->sendError(E_USER_ERROR, "Cannot run query, the field '$field' does not exist in this table", __LINE__);
                    fseek($f, $this->bytesToSeek($fp, $header, $fileId) + $fieldOffsets[$field]["offset"]);
                    $fieldValue = rtrim(fread($f, $fieldOffsets[$field]["length"]));
                    $fieldType = $fieldOffsets[$field]["type"];
                    if($fieldType == "memo") {
                        $fieldValue = $this->readMemo($fp, $fieldValue, $header);
                    }
                    if($query[$i]["type"] == "=") {
                        if(trim(strtolower($fieldValue)) != strtolower($value)) $pass = false;
                    } elseif($query[$i]["type"] == "?") {
                        if(strpos(strtolower($fieldValue), strtolower($value)) <= -1) $pass = false;
                    } elseif($query[$i]["type"] == "<") {
                        if((double) $fieldValue >= (double) $value) $pass = false;
                    } elseif($query[$i]["type"] == ">") {
                        if((double) $fieldValue <= (double) $value) $pass = false;
                    } elseif($query[$i]["type"] == "!") {
                        if(trim(strtolower($fieldValue)) == strtolower($value)) $pass = false;
                    } else {
                        $this->sendError(E_USER_ERROR, "Invalid query syntax, missing operator (=,?,>,<,!)", __LINE__);
                        $pass = false;
                    }
                }
                if($pass && !$foundMatch) {
                    if($start_c < $start) {
                        //echo $start_c.'<='.$start;
                        $start_c++;
                        continue;
                    }
                    //we have a match, return it
                    fseek($f, $this->bytesToSeek($fp, $header, $fileId));
                    $buffer = fread($f, $header["recLen"]);
                    if(ord($buffer{0}) != 28) {
                        $return[] = $this->parseRecord($fp, $buffer, $header, $reqFields);
                        $howmany--;
                        $foundMatch = true;
                    }
                    else $this->sendError(E_PARSE, 'Unable to parse record at fileId:'.$fileId.' in listRec() at '.$this->fp[$fp], __LINE__);
                    //if($howmany_c >= $howmany && $howmany != -1) break;
                }
                //END add OR functionalilty
            }
        }
        unset($buffer);
        fclose($f);
        //cache the result
        $this->_query[$fp][] = array("result" => $return,
            "query_string" => $original_query,
            "start" => $original_start,
            "howmany" => $original_howmany,
            "fields" => $original_fields);
        if(empty($return)) return false;
        return $return;
    }
    /**
     * Queries a table without a query string using only one field.
     *
     * @param string $fp
     * @param string $field
     * @param string $value
     * @param int $start[optional]
     * @param int $howmany[optional]
     * @return bool false on fail, array records on success
     */
    function basicQuery($fp, $field, $value, $start = 1, $howmany = -1, $fields=array('*')) {
        return $this->query($fp, "$field='$value'", $start, $howmany,$fields);
    }
    /**
     * Internal function to parse raw records into arrays
     *
     * @param string $fp
     * @param string $rawRecord
     * @param array $header
     * @param array $reqFields
     * @return array
     */
    function parseRecord($fp, $rawRecord, $header, $reqFields=array("*")) {
        if(ord($rawRecord{0}) == 28) {
            $this->sendError(E_PARSE, 'Unable to parse record in parseRecord() at '.$this->fp[$fp], __LINE__);
            return array();
        }
        $cHeader = count($header) - 8;
        $pos = 0;
        for($i=1;$i<=$cHeader;$i++) {
            if(isset($reqFields[$header[$i]["fName"]]) || $reqFields[0] == "*") {
                $value = rtrim(substr($rawRecord, $pos, $header[$i]["fLength"]));
                if($header[$i]["fType"] == "memo") $value = $this->readMemo($fp, $value, $header);
                $fRec[$header[$i]["fName"]] = $value;
            }
            $pos = $pos + $header[$i]["fLength"];
        }
        return $fRec;
    }
    /**
     * Internal function to seek to a paricular record based on the fileID
     *
     * @param string $fp
     * @param array $header
     * @param int $recordId
     * @return bool false on fail, int seek on success
     */
    function bytesToSeek($fp, $header, $recordId) {
        $recordId--;
        $seek = $header["recPos"] + ($recordId * $header["recLen"]);
        if($seek < $header['recPos']) {$this->sendError(E_ERROR, "The record($recordId) you are trying to access before the RECORD_START_POSTION of the file(seeking ".$seek." at ".$this->fp[$fp].").", __LINE__); return false; }
        if($seek < filesize($this->fp[$fp].'.ta')) return $seek;
        else $this->sendError(E_ERROR, "The record($recordId) you are trying to access is past the end of the file(seeking ".$seek." at ".$this->fp[$fp].").", __LINE__);
        return false;
    }
    /**
     * Obsolete function
     *
     */
    function rewriteMemo() {
        $this->sendError(E_USER_NOTICE, 'rewriteMemo() function obsolete.  Update scripts accordingly', __LINE__);
    }
    /**
     * Retrieves information from the memo file based on the given index.
     *
     * @param string $fp
     * @param int $index
     * @param array $header
     * @return string
     */
    function readMemo($fp, $index, $header) {
        $readIndexes = array();
        $return = '';
        $next = $index;
        $f = fopen($this->fp[$fp].'.memo', 'rb');
        while(!empty($next) && $next > 0) {
            if(!ctype_digit($next)) die('<b>Fatal Error</b>(line '.__LINE__.'):The Script encountered a non-numeric value for readMemo(): readMemo("'.$this->fp[$fp].'", "'.$index.'", $header) literally at "'.$next.'"position ('.$next.' of '.(filesize($this->fp[$fp].'.memo') / $header["blockLength"]).' block) in the memo file.<br />');
            // Store read index to make sure we don't loop forever if indexes are messed up
            $readIndexes[$next] = true;
            fseek($f, ($next * $header["blockLength"]));
            $next = trim(fread($f, 7));
            // Make sure the next index hasn't already been read
            if(isset($readIndexes[$next])) die('<b>Fatal Error</b>(line '.__LINE__.'): There is an error in '.$this->fp[$fp].'.memo, this needs to be corrected. The error starts on index <b>'.$index.'</b>');
            if((ftell($f) - 7) == $next * $header["blockLength"]) die('<b>Fatal Error</b>(line '.__LINE__.'): Script entered an endless loop in readMemo("'.$this->fp[$fp].'", "'.$index.'", $header) at "'.$next.'" position ('.$next.' of '.(filesize($this->fp[$fp].'.memo') / $header["blockLength"]).' block) in the memo file.<br />');
            $return .= substr(rtrim(fread($f, $header["blockLength"] - 7)), 0, -1);
        }
        fclose($f);
        //str_replace added to remove <x> from all queries. Should not be used for non-UPB context.
        return str_replace('&lt;x&gt;','',$return);
    }
    /**
     * Deletes information associated with  the index from the memo file.
     *
     * @param string $fp
     * @param int $index
     * @param array $header
     * @return void
     */
    function deleteMemo($fp, $index, $header) {
        if($index == '0') die('<b>Fatal Error</b>(line '.__LINE__.'): Tried to delete 0th memo record in '.$this->fp[$fp].'. Literally: '.$index);
        if(!(ctype_digit($index) && !empty($index))) return true;
        $readIndexes = array();
        $next = $index;
        $f = fopen($this->fp[$fp].'.memo', 'r+b');
        if(empty($this->_firstBlankMemoBlockRef[$fp])) $first_blank_memo_block_ref = trim(fread($f, 7));
        else $first_blank_memo_block_ref = $this->_firstBlankMemoBlockRef[$fp];
        while(ctype_digit($next) && !empty($next)) {
            // Store read index to make sure we don't loop forever if indexes are messed up
            $readIndexes[$next] = true;
            fseek($f, $next * $header["blockLength"]);
            $next = trim(fread($f, 7));
            // Make sure the next index hasn't already been read
            if(isset($readIndexes[$next])) die('<b>Fatal Error</b>(line '.__LINE__.'): There is an error in '.$this->fp[$fp].'.memo, this needs to be corrected. The error starts on index <b>'.$index.'</b>');
            if((ftell($f) - 7) == $next * $header["blockLength"]) die('<b>Fatal Error</b>(line '.__LINE__.'): Script entered an endless loop in deleteMemo("'.$this->fp[$fp].'", "'.$index.'", $header) at ('.$next.' of '.(filesize($this->fp[$fp].'.memo') / $header["blockLength"]).' block) in the memo file.<br />');
        }
        fseek($f, -7, SEEK_CUR);
        fwrite($f, $first_blank_memo_block_ref.str_repeat(' ', 7 - strlen($first_blank_memo_block_ref)));
        fseek($f, 0);
        fwrite($f, $index.str_repeat(' ', 7 - strlen($index)));
        $this->_firstBlankMemoBlockRef[$fp] = $index;
        return true;
    }
    /**
     * Writes the data into the memo file
     *
     * @param string $fp
     * @param string $oriData
     * @param array $header
     * @return int
     */
    function writeMemo($fp, $oriData, $header) {
        $data = trim($oriData,"\t\n\r\0\x0B"); //strip all but whitespace from both ends of data
        if(strlen($data) == 0) return;
        $f = fopen($this->fp[$fp].'.memo', 'r+b');
        if(empty($this->_firstBlankMemoBlockRef[$fp])) {
            $next = trim(fread($f, 7));
            $this->_firstBlankMemoBlockRef[$fp] = $next;
        } else $next = $this->_firstBlankMemoBlockRef[$fp];
        $readIndexes = array();
        while(ctype_digit($next) && !empty($next) && !(strlen($data) == 0)) {
            if(!isset($return)) $return = $next;
            // Store read index to make sure we don't loop forever if indexes are messed up
            $readIndexes[$next] = true;
            fseek($f, $next * $header["blockLength"]);
            $next = trim(fread($f, 7));
            // Make sure the next index hasn't already been read
            if(isset($readIndexes[$next])) die('<b>Fatal Error</b>(line '.__LINE__.'): There is an error in '.$this->fp[$fp].'.memo, this needs to be corrected. The error starts on index <b>'.$index.'</b>');
            if(ftell($f) - 7 == $next * $header["blockLength"]) die('<b>Fatal Error</b>(line '.__LINE__.'): Script entered an endless loop in writeMemo("'.$this->fp[$fp].'", "'.$oriData.'", $header) at "'.$next.'" position in the memo file.<br />');
            if(strlen($data) > ($header["blockLength"] - 8)) { //if it won't fit
                fwrite($f, substr($data, 0, $header["blockLength"] - 8).chr(3));
                $data = substr($data, $header["blockLength"] - 8);
            } else {
                fseek($f, -7, SEEK_CUR);
                fwrite($f, '-1'.str_repeat(' ', 5).$data.chr(3).str_repeat(' ', ($header["blockLength"] - (strlen($data) + 8))));
                $data = '';
            }
        }
        if(!(strlen($data) == 0) && ftell($f) >= $header["blockLength"]) {
            fseek($f, -($header["blockLength"]), SEEK_CUR);
            $last_write_offset = ftell($f);
            fseek($f, 0, SEEK_END);
            $EOF_index = ftell($f) / $header["blockLength"];
            fseek($f, $last_write_offset);
            fwrite($f, $EOF_index.str_repeat(' ', 7 - strlen($EOF_index)));
        }
        fseek($f, 0);
        fwrite($f, $next.str_repeat(' ', 7 - strlen($next)));
        $this->_firstBlankMemoBlockRef[$fp] = $next;
        fseek($f, 0, SEEK_END);
        while(!(strlen($data) == 0)) {
            $next = (ftell($f) / $header["blockLength"]) + 1;
            if(!isset($return)) $return = $next - 1;
            if(!is_integer($next) || $next < 0) die('<b>Fatal Error</b>(line '.__LINE__.'):The Script encountered a non-numeric value for writeMemo(): writeMemo("'.$this->fp[$fp].'", "'.$oriData.'", $header) literally at "'.$next.'" position ('.($next / $header["blockLength"]).' of '.(filesize($this->fp[$fp].'.memo') / $header["blockLength"]).' block) in the memo file.<br />');
            if(strlen($data) > ($header["blockLength"] - 8)) { //if it won't fit
                fwrite($f, $next.str_repeat(' ', 7 - strlen($next)).substr($data, 0, $header["blockLength"] - 8).chr(3));
                $data = substr($data, $header["blockLength"] - 8);
            } else {
                fwrite($f, '-1'.str_repeat(' ', 5).$data.chr(3).str_repeat(' ', ($header["blockLength"] - (strlen($data) + 8))));
                $data = '';
            }
        }
        fclose($f);
        return $return;
    }
    /**
     * Checks to validate a working database, directory, and [optional]table
     *
     * @param int $line
     * @param string $fp
     * @return bool
     */
    function check($fp = null) {
        clearstatcache();
        if(!isset($this->Db) || $this->Db == '') {
            throw new UndefinedDatabaseException();
        }
        if(!isset($this->workingDir) || $this->workingDir == '') {
            throw new UndefinedWorkingDirectoryException();
        }
        if(!file_exists($this->workingDir.$this->Db)) {
            throw new DatabaseExistsException();
        }
        if($fp != null) {
            if($fp == '') {
                throw new InvalidFilePointerException();
            }
            if(!isset($this->fp[$fp]) || $this->fp[$fp] == '') {
                throw new InvalidFilePointerException();
            }
            if(!file_exists($this->fp[$fp].'.ta')) {
                throw new InvalidTableException();
            }
        }
    }
    /**
     * @param $errno
     * @param $errMsg
     * @param string $line
     */
    function sendError($errno, $errMsg, $line = '') {
        if(TDB_ERROR_INCLUDE_ORIGIN == TRUE) {
            $error_trace = debug_backtrace();
            $error_origin = '  Executed on '.$error_trace[count($error_trace) -1]['file'].' at line '.$error_trace[count($error_trace) -1]['line'].'.';
            $errMsg .= $error_origin;
        }
        if(TDB_PRINT_ERRORS === TRUE) print('<b>Text Database Error</b>: '.$errMsg. ((($line != null || $line != '') && (TDB_ERROR_INCLUDE_ORIGIN === false)) ? ' near line '.$line : '').'<br />');
        elseif($this->error_handler !== FALSE) call_user_func_array($this->error_handler, array($errno, $errMsg, 'tdb.php', $line));
        else trigger_error('<b>Text Database Error</b>: '.$errMsg. (($line != null || $line != '') ? ' near line '.$line : ''));
    }
    /**
     * Defines an error handler
     *
     * @param object $object
     * @param string $function
     */
    function define_error_handler(&$object, $function) {
        $this->error_handler = array(&$object, $function);
    }
    /**
     * Returns the version of tdb class in use
     *
     * @return string
     */
    function version() {
        return "4.4.4";
    }
    /**
     * @param $text
     * @return mixed
     */
    function deXSS($text) {
        //echo "$text::".substr_count('&lt;x&gt;',$text)."<br>";
        return str_replace('&lt;x&gt;','',$text);
    }
}