<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Abstract base class for all the readers
 *
 * A reader is a compilation of serveral files that can be read
 *
 * PHP versions 4 and 5
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330,Boston,MA 02111-1307 USA
 *
 * @category   File Formats
 * @package    File_Archive
 * @author     Vincent Lascaux <vincentlascaux@php.net>
 * @copyright  1997-2005 The PHP Group
 * @license    http://www.gnu.org/copyleft/lesser.html  LGPL
 * @version    CVS: $Id$
 * @link       http://pear.php.net/package/File_Archive
 */

require_once "PEAR.php";

/**
 * Abstract base class for all the readers
 *
 * A reader is a compilation of serveral files that can be read
 */
class File_Archive_Reader
{
    /**
     * Move to the next file or folder in the reader
     *
     * @return bool false iif no more files are available
     */
    function next()
    {
        return false;
    }

    /**
     * Move to the next file whose name is in directory $filename
     * or is exactly $filename
     *
     * @param string $filename Name of the file to find in the archive
     * @param bool $close If true, close the reader and search from the first file
     * @return bool whether the file was found in the archive or not
     */
    function select($filename, $close = true)
    {
        $std = $this->getStandardURL($filename);
        if (substr($std, -1)=='/') {
            $std = substr($std, 0, -1);
        }

        if ($close) {
            $error = $this->close();
            if (PEAR::isError($error)) {
                return $error;
            }
        }
        while (($error = $this->next()) === true) {
            $sourceName = $this->getFilename();
            if (
                  empty($std) ||

                //$std is a file
                  $std == $sourceName ||

                //$std is a directory
                  (strncmp($std.'/', $sourceName, strlen($std)+1) == 0 &&
                   strlen($sourceName) > strlen($std)+1)
               ) {
                return true;
            }
        }
        return $error;
    }

    /**
     * Returns the standard path
     * Changes \ to /
     * Removes the .. and . from the URL
     * @param string $path a valid URL that may contain . or .. and \
     * @static
     */
    function getStandardURL($path)
    {
        if ($path == '.') {
            return '';
        }
        $std = str_replace("\\", "/", $path);
        while ($std != ($std = preg_replace("/[^\/:?]+\/\.\.\//", "", $std))) ;
        $std = str_replace("/./", "", $std);
        if (strncmp($std, "./", 2) == 0) {
            /**
             * If return value is going to be / (root on POSIX) 
             */
            if (substr($std, 2) === '/') {
                return $std;
            }
            return substr($std, 2);
        } else {
            return $std;
        }
    }

    /**
     * Returns the name of the file currently read by the reader
     *
     * Warning: undefined behaviour if no call to next have been
     * done or if last call to next has returned false
     *
     * @return string Name of the current file
     */
    function getFilename()
    {
        return PEAR::raiseError("Reader abstract function call (getFilename)");
    }

    /**
     * Returns the list of filenames from the current pos to the end of the source
     * The source will be closed after having called this function
     * This function goes through the whole archive (which may be slow).
     * If you intend to work on the reader, doing it in one pass would be faster
     *
     * @return array filenames from the current pos to the end of the source
     */
    function getFileList()
    {
        $result = array();
        while ( ($error = $this->next()) === true) {
            $result[] = $this->getFilename();
        }
        $this->close();
        if (PEAR::isError($error)) {
            return $error;
        } else {
            return $result;
        }
    }

    /**
     * Returns an array of statistics about the file
     * (see the PHP stat function for more information)
     *
     * The returned array may be empty, even if readers should try
     * their best to return as many data as possible
     */
    function getStat() { return array(); }

    /**
     * Returns the MIME associated with the current file
     * The default function does that by looking at the extension of the file
     */
    function getMime()
    {
        require_once "File/Archive/Reader/MimeList.php";
        return File_Archive_Reader_GetMime($this->getFilename());
    }

    /**
     * If the current file of the archive is a physical file,
     *
     * @return the name of the physical file containing the data
     *         or null if no such file exists
     *
     * The data filename may not be the same as the filename.
     */
    function getDataFilename() { return null; }

    /**
     * Reads some data from the current file
     * If the end of the file is reached, returns null
     * If $length is not specified, reads up to the end of the file
     * If $length is specified reads up to $length
     */
    function getData($length = -1)
    {
        return PEAR::raiseError("Reader abstract function call (getData)");
    }

    /**
     * Skip some data and returns how many bytes have been skipped
     * This is strictly equivalent to
     *  return strlen(getData($length))
     * But could be far more efficient
     */
    function skip($length = -1)
    {
        $data = $this->getData($length);
        if (PEAR::isError($data)) {
            return $data;
        } else {
            return strlen($data);
        }
    }

    /**
     * Move the current position back of a given amount of bytes.
     * Not all readers may implement this function (a PEAR error will
     * be returned if the reader can't rewind)
     *
     * @param int $length number of bytes to seek before the current pos
     *        or -1 to move back to the begining of the current file
     * @return the number of bytes really rewinded (which may be less than
     *        $length if the current pos is less than $length
     */
    function rewind($length = -1)
    {
        return PEAR::raiseError('Rewind function is not implemented on this reader');
    }

    /**
     * Returns the current offset in the current file
     */
    function tell()
    {
        $offset = $this->rewind();
        $this->skip($offset);
        return $offset;
    }

    /**
     * Put back the reader in the state it was before the first call
     * to next()
     */
    function close()
    {
    }

    /**
     * Sends the current file to the Writer $writer
     * The data will be sent by chunks of at most $bufferSize bytes
     * If $bufferSize <= 0 (default), the blockSize option is used
     */
    function sendData(&$writer, $bufferSize = 0)
    {
        if (PEAR::isError($writer)) {
            return $writer;
        }
        if ($bufferSize <= 0) {
            $bufferSize = File_Archive::getOption('blockSize');
        }

        $filename = $this->getDataFilename();
        if ($filename !== null) {
            $error = $writer->writeFile($filename);
            if (PEAR::isError($error)) {
                return $error;
            }
        } else {
            while (($data = $this->getData($bufferSize)) !== null) {
                if (PEAR::isError($data)) {
                    return $data;
                }
                $error = $writer->writeData($data);
                if (PEAR::isError($error)) {
                    return $error;
                }
            }
        }
    }

    /**
     * Sends the whole reader to $writer and close the reader
     *
     * @param File_Archive_Writer $writer Where to write the files of the reader
     * @param bool $autoClose If true, close $writer at the end of the function.
     *        Default value is true
     * @param int $bufferSize Size of the chunks that will be sent to the writer
     *        If $bufferSize <= 0 (default value), the blockSize option is used
     */
    function extract(&$writer, $autoClose = true, $bufferSize = 0)
    {
        if (PEAR::isError($writer)) {
            $this->close();
            return $writer;
        }

        while (($error = $this->next()) === true) {
            if ($writer->newFileNeedsMIME()) {
                $mime = $this->getMime();
            } else {
                $mime = null;
            }
           
            $error = $writer->newFile(
                $this->getFilename(),
                $this->getStat(),
                $mime
            );
            if (PEAR::isError($error)) {
                break;
            }
            $error = $this->sendData($writer, $bufferSize);
            if (PEAR::isError($error)) {
                break;
            }
        }
        $this->close();
        if ($autoClose) {
            $writer->close();
        }
        if (PEAR::isError($error)) {
            return $error;
        }
    }

    /**
     * Extract only one file (given by the URL)
     *
     * @param string $filename URL of the file to extract from this
     * @param File_Archive_Writer $writer Where to write the file
     * @param bool $autoClose If true, close $writer at the end of the function
     *        Default value is true
     * @param int $bufferSize Size of the chunks that will be sent to the writer
     *        If $bufferSize <= 0 (default value), the blockSize option is used
     */
    function extractFile($filename, &$writer,
                         $autoClose = true, $bufferSize = 0)
    {
        if (PEAR::isError($writer)) {
            return $writer;
        }

        if (($error = $this->select($filename)) === true) {
            $result = $this->sendData($writer, $bufferSize);
            if (!PEAR::isError($result)) {
                $result = true;
            }
        } else if ($error === false) {
            $result = PEAR::raiseError("File $filename not found");
        } else {
            $result = $error;
        }
        if ($autoClose) {
            $error = $writer->close();
            if (PEAR::isError($error)) {
                return $error;
            }
        }
        return $result;
    }

    /**
     * Return a writer that allows appending files to the archive
     * After having called makeAppendWriter, $this is closed and should not be
     * used until the returned writer is closed.
     *
     * @return a writer that will allow to append files to an existing archive
     * @see makeWriter
     */
    function makeAppendWriter()
    {
        require_once "File/Archive/Predicate/False.php";
        return $this->makeWriterRemoveFiles(new File_Archive_Predicate_False());
    }

    /**
     * Return a writer that has the same properties as the one returned by
     * makeAppendWriter, but after having removed all the files that follow a
     * given predicate.
     * After a call to makeWriterRemoveFiles, $this is closed and should not
     * be used until the returned writer is closed
     *
     * @param File_Archive_Predicate $pred the predicate verified by removed files
     * @return File_Archive_Writer that allows to append files to the archive
     */
    function makeWriterRemoveFiles($pred)
    {
        return PEAR::raiseError("Reader abstract function call (makeWriterRemoveFiles)");
    }

    /**
     * Returns a writer that removes the current file
     * This is a syntaxic sugar for makeWriterRemoveFiles(new File_Archive_Predicate_Current());
     */
    function makeWriterRemove()
    {
        require_once "File/Archive/Predicate/Current.php";
        return $this->makeWriterRemoveFiles(new File_Archive_Predicate_Current());
    }

    /**
     * Removes the current file from the reader
     */
    function remove()
    {
        $writer = $this->makeWriterRemove();
        if (PEAR::isError($writer)) {
            return $writer;
        }
        $writer->close();
    }

    /**
     * Return a writer that has the same properties as the one returned by makeWriter, but after
     * having removed a block of data from the current file. The writer will append data to the current file
     * no data (other than the block) will be removed
     *
     * @param array Lengths of the blocks. The first one will be discarded, the second one kept, the third
     *        one discarded... If the sum of the blocks is less than the size of the file, the comportment is the
     *        same as if a last block was set in the array to reach the size of the file
     *        if $length is -1, the file is truncated from the specified pos
     *        It is possible to specify blocks of size 0
     * @param int $seek relative pos of the block
     */
    function makeWriterRemoveBlocks($blocks, $seek = 0)
    {
        return PEAR::raiseError("Reader abstract function call (makeWriterRemoveBlocks)");
    }
}

?>