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

/**
 * Factory to access the most common File_Archive features
 * It uses lazy include, so you dont have to include the files from
 * File/Archive/* directories
 *
 * 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: Archive.php,v 1.90 2008/05/28 19:58:07 cbrunet Exp $
 * @link       http://pear.php.net/package/File_Archive
 */

/**
 * To have access to PEAR::isError and PEAR::raiseError
 * We should probably use lazy include and remove this inclusion...
 */
require_once "PEAR.php";

function File_Archive_cleanCache($file, $group)
{
    $file = split('_', $file);
    if (count($file) != 3) {
        return false; //not a File_Archive file, keep it
    }

    $name = $file[2];
    $name = urldecode($name);

    $group = $file[1];

    //clean the cache only for files in File_Archive groups
    return substr($group, 0, 11) == 'FileArchive' &&
           !file_exists($name); //and only if the related file no longer exists
}

/**
 * Factory to access the most common File_Archive features
 * It uses lazy include, so you dont have to include the files from
 * File/Archive/* directories
 */
class File_Archive
{
    function& _option($name)
    {
        static $container = array(
            'zipCompressionLevel' => 9,
             'gzCompressionLevel' => 9,
            'tmpDirectory' => '.',
            'cache' => null,
            'appendRemoveDuplicates' => false,
            'blockSize' => 65536,
            'cacheCondition' => false
        );
        return $container[$name];
    }
    /**
     * Sets an option that will be used by default by all readers or writers
     * Option names are case sensitive
     * Currently, the following options are used:
     *
     * "cache"
     *      Instance of a Cache_Lite object used to cache some compressed
     *      data to speed up future compressions of files
     *      Default: null (no cache used)
     *
     * "zipCompressionLevel"
     *      Value between 0 and 9 specifying the default compression level used
     *      by Zip writers (0 no compression, 9 highest compression)
     *      Default: 9
     *
     * "gzCompressionLevel"
     *      Value between 0 and 9 specifying the default compression level used
     *      by Gz writers (0 no compression, 9 highest compression)
     *      Default: 9
     *
     * "tmpDirectory"
     *      Directory where the temporary files generated by File_Archive will
     *      be created
     *      Default: '.'
     *
     * "appendRemoveDuplicates"
     *      If set to true, the appender created will by default remove the
     *      file present in the archive when adding a new one. This will slow the
     *      appending of files to archives
     *      Default: false
     *
     * "blockSize"
     *      To transfer data from a reader to a writer, some chunks a read from the
     *      source and written to the writer. This parameter controls the size of the
     *      chunks
     *      Default: 64kB
     *
     * "cacheCondition"
     *      This parameter specifies when a cache should be used. When the cache is
     *      used, the data of the reader is saved in a temporary file for future access.
     *      The cached reader will be read only once, even if you read it several times.
     *      This can be usefull to read compressed files or downloaded files (from http or ftp)
     *      The possible values for this option are
     *       - false: never use cache
     *       - a regexp: A cache will be used if the specified URL matches the regexp
     *         preg_match is used
     *      Default: false
     *      Example: '/^(http|ftp):\/\//' will cache all files downloaded via http or ftp
     *
     */
    function setOption($name, $value)
    {
        $option =& File_Archive::_option($name);
        $option = $value;
        if ($name == 'cache' && $value !== null) {
            //TODO: ask to Cache_Lite to allow that
            $value->_fileNameProtection = false;
        }
    }

    /**
     * Retrieve the value of an option
     */
    function getOption($name)
    {
        return File_Archive::_option($name);
    }

    /**
     * Create a reader to read the URL $URL.
     * If the URL is a directory, it will recursively read that directory.
     * If $uncompressionLevel is not null, the archives (files with extension
     * tar, zip, gz or tgz) will be considered as directories (up to a depth of
     * $uncompressionLevel if $uncompressionLevel > 0). The reader will only
     * read files with a directory depth of $directoryDepth. It reader will
     * replace the given URL ($URL) with $symbolic in the public filenames
     * The default symbolic name is the last filename in the URL (or '' for
     * directories)
     *
     * Examples:
     * Considere the following file system
     * <pre>
     * a.txt
     * b.tar (archive that contains the following files)
     *     c.txt
     *     d.tgz (archive that contains the following files)
     *         e.txt
     *         dir1/
     *             f.txt
     * dir2/
     *     g.txt
     *     dir3/
     *         h.tar (archive that contains the following files)
     *             i.txt
     * </pre>
     *
     * read('.') will return a reader that gives access to following
     * files (recursively read current dir):
     * <pre>
     * a.txt
     * b.tar
     * dir2/g.txt
     * dir2/dir3/h.tar
     * </pre>
     *
     * read('.', 'myBaseDir') will return the following reader:
     * <pre>
     * myBaseDir/a.txt
     * myBaseDir/b.tar
     * myBaseDir/dir2/g.txt
     * myBaseDir/dir2/dir3/h.tar
     * </pre>
     *
     * read('.', '', -1) will return the following reader (uncompress
     * everything)
     * <pre>
     * a.txt
     * b.tar/c.txt
     * b.tar/d.tgz/e.txt
     * b.tar/d.tgz/dir1/f.txt
     * dir2/g.txt
     * dir2/dir3/h.tar/i.txt
     * </pre>
     *
     * read('.', '', 1) will uncompress only one level (so d.tgz will
     * not be uncompressed):
     * <pre>
     * a.txt
     * b.tar/c.txt
     * b.tar/d.tgz
     * dir2/g.txt
     * dir2/dir3/h.tar/i.txt
     * </pre>
     *
     * read('.', '', 0, 0) will not recurse into subdirectories
     * <pre>
     * a.txt
     * b.tar
     * </pre>
     *
     * read('.', '', 0, 1) will recurse only one level in
     * subdirectories
     * <pre>
     * a.txt
     * b.tar
     * dir2/g.txt
     * </pre>
     *
     * read('.', '', -1, 2) will uncompress everything and recurse in
     * only 2 levels in subdirectories or archives
     * <pre>
     * a.txt
     * b.tar/c.txt
     * b.tar/d.tgz/e.txt
     * dir2/g.txt
     * </pre>
     *
     * The recursion level is determined by the real path, not the symbolic one.
     * So read('.', 'myBaseDir', -1, 2) will result to the same files:
     * <pre>
     * myBaseDir/a.txt
     * myBaseDir/b.tar/c.txt
     * myBaseDir/b.tar/d.tgz/e.txt (accepted because the real depth is 2)
     * myBaseDir/dir2/g.txt
     * </pre>
     *
     * Use readSource to do the same thing, reading from a specified reader instead of
     * reading from the system files
     *
     * To read a single file, you can do read('a.txt', 'public_name.txt')
     * If no public name is provided, the default one is the name of the file
     * read('dir2/g.txt') contains the single file named 'g.txt'
     * read('b.tar/c.txt') contains the single file named 'c.txt'
     *
     * Note: This function uncompress files reading their extension
     *       The compressed files must have a tar, zip, gz or tgz extension
     *       Since it is impossible for some URLs to use is_dir or is_file, this
     *       function may not work with
     *       URLs containing folders which name ends with such an extension
     */
    function readSource(&$source, $URL, $symbolic = null,
                  $uncompression = 0, $directoryDepth = -1)
    {
        return File_Archive::_readSource($source, $URL, $reachable, $baseDir,
                  $symbolic, $uncompression, $directoryDepth);
    }

    /**
     * This function performs exactly as readSource, but with two additional parameters
     * ($reachable and $baseDir) that will be set so that $reachable."/".$baseDir == $URL
     * and $reachable can be reached (in case of error)
     *
     * @access private
     */
    function _readSource(&$toConvert, $URL, &$reachable, &$baseDir, $symbolic = null,
                  $uncompression = 0, $directoryDepth = -1)
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }
        if (is_array($URL)) {
            $converted = array();
            foreach($URL as $key => $foo) {
                $converted[] =& File_Archive::_convertToReader($URL[$key]);
            }
            return File_Archive::readMulti($converted);
        }

        //No need to uncompress more than $directoryDepth
        //That's not perfect, and some archives will still be uncompressed just
        //to be filtered out :(
        if ($directoryDepth >= 0) {
            $uncompressionLevel = min($uncompression, $directoryDepth);
        } else {
            $uncompressionLevel = $uncompression;
        }

        require_once 'File/Archive/Reader.php';
        $std = File_Archive_Reader::getStandardURL($URL);

        //Modify the symbolic name if necessary
        $slashPos = strrpos($std, '/');
        if ($symbolic === null) {
            if ($slashPos === false) {
                $realSymbolic = $std;
            } else {
                $realSymbolic = substr($std, $slashPos+1);
            }
        } else {
            $realSymbolic = $symbolic;
        }
        if ($slashPos !== false) {
            $baseFile = substr($std, 0, $slashPos+1);
            $lastFile = substr($std, $slashPos+1);
        } else {
            $baseFile = '';
            $lastFile = $std;
        }

        if (strpos($lastFile, '*')!==false ||
            strpos($lastFile, '?')!==false) {
            //We have to build a regexp here
            $regexp = str_replace(
                array('\*', '\?'),
                array('[^/]*', '[^/]'),
                preg_quote($lastFile)
            );
            $result = File_Archive::_readSource($source, $baseFile,
                                                $reachable, $baseDir, null, 0, -1);
            return File_Archive::filter(
                    File_Archive::predEreg('^'.$regexp.'$'),
                    $result
                   );
        }

        //If the URL can be interpreted as a directory, and we are reading from the file system
        if ((empty($URL) || is_dir($URL)) && $source === null) {
            require_once "File/Archive/Reader/Directory.php";

            if ($uncompressionLevel != 0) {
                require_once "File/Archive/Reader/Uncompress.php";
                $result = new File_Archive_Reader_Uncompress(
                    new File_Archive_Reader_Directory($std, '', $directoryDepth),
                    $uncompressionLevel
                );
            } else {
                $result = new File_Archive_Reader_Directory($std, '', $directoryDepth);
            }

            if ($directoryDepth >= 0) {
                require_once 'File/Archive/Reader/Filter.php';
                require_once 'File/Archive/Predicate/MaxDepth.php';

                $tmp =& File_Archive::filter(
                    new File_Archive_Predicate_MaxDepth($directoryDepth),
                    $result
                );
                unset($result);
                $result =& $tmp;
            }
            if (!empty($realSymbolic)) {
                if ($symbolic === null) {
                    $realSymbolic = '';
                }
                require_once "File/Archive/Reader/ChangeName/AddDirectory.php";
                $tmp = new File_Archive_Reader_ChangeName_AddDirectory(
                    $realSymbolic,
                    $result
                );
                unset($result);
                $result =& $tmp;
            }

        //If the URL can be interpreted as a file, and we are reading from the file system
        } else if (is_file($URL) && substr($URL, -1)!='/' && $source === null) {
            require_once "File/Archive/Reader/File.php";
            $result = new File_Archive_Reader_File($URL, $realSymbolic);

        //Else, we will have to build a complex reader
        } else {
            require_once "File/Archive/Reader/File.php";

            $realPath = $std;

            // Try to find a file with a known extension in the path (
            // (to manage URLs like archive.tar/directory/file)
            $pos = 0;
            do {
                if ($pos+1<strlen($realPath)) {
                    $pos = strpos($realPath, '/', $pos+1);
                } else {
                    $pos = false;
                }
                if ($pos === false) {
                    $pos = strlen($realPath);
                }

                $file = substr($realPath, 0, $pos);
                $baseDir = substr($realPath, $pos+1);
                $dotPos = strrpos($file, '.');
                $extension = '';
                if ($dotPos !== false) {
                    $extension = substr($file, $dotPos+1);
                }
            } while ($pos < strlen($realPath) &&
                (!File_Archive::isKnownExtension($extension) ||
                 (is_dir($file) && $source==null)));

            $reachable = $file;

            //If we are reading from the file system
            if ($source === null) {
                //Create a file reader
                $result = new File_Archive_Reader_File($file);
            } else {
                //Select in the source the file $file

                require_once "File/Archive/Reader/Select.php";
                $result = new File_Archive_Reader_Select($file, $source);
            }

            require_once "File/Archive/Reader/Uncompress.php";
            $tmp = new File_Archive_Reader_Uncompress($result, $uncompressionLevel);
            unset($result);
            $result = $tmp;

            //Select the requested folder in the uncompress reader
            $isDir = $result->setBaseDir($std);
            if (PEAR::isError($isDir)) {
                return $isDir;
            }
            if ($isDir && $symbolic==null) {
                //Default symbolic name for directories is empty
                $realSymbolic = '';
            }

            if ($directoryDepth >= 0) {
                //Limit the maximum depth if necessary
                require_once "File/Archive/Reader/Filter.php";
                require_once "File/Archive/Predicate/MaxDepth.php";

                $tmp = new File_Archive_Reader_Filter(
                    new File_Archive_Predicate(
                        $directoryDepth +
                        substr_count(substr($std, $pos+1), '/')
                    ),
                    $result
                );
                unset($result);
                $result =& $tmp;
            }

            if ($std != $realSymbolic) {
                require_once "File/Archive/Reader/ChangeName/Directory.php";

                //Change the base name to the symbolic one if necessary
                $tmp = new File_Archive_Reader_ChangeName_Directory(
                    $std,
                    $realSymbolic,
                    $result
                );
                unset($result);
                $result =& $tmp;
            }
        }

        $cacheCondition = File_Archive::getOption('cacheCondition');
        if ($cacheCondition !== false &&
            preg_match($cacheCondition, $URL)) {
            $tmp =& File_Archive::cache($result);
            unset($result);
            $result =& $tmp;
        }

        return $result;
    }
    function read($URL, $symbolic = null,
                  $uncompression = 0, $directoryDepth = -1)
    {
        $source = null;
        return File_Archive::readSource($source, $URL, $symbolic, $uncompression, $directoryDepth);
    }

    /**
     * Create a file reader on an uploaded file. The reader will read
     * $_FILES[$name]['tmp_name'] and will have $_FILES[$name]['name']
     * as a symbolic filename.
     *
     * A PEAR error is returned if one of the following happen
     *  - $_FILES[$name] is not set
     *  - $_FILES[$name]['error'] is not 0
     *  - is_uploaded_file returns false
     *
     * @param string $name Index of the file in the $_FILES array
     * @return File_Archive_Reader File reader on the uploaded file
     */
    function readUploadedFile($name)
    {
        if (!isset($_FILES[$name])) {
            return PEAR::raiseError("File $name has not been uploaded");
        }
        switch ($_FILES[$name]['error']) {
        case 0:
            //No error
            break;
        case 1:
            return PEAR::raiseError(
                        "The upload size limit didn't allow to upload file ".
                        $_FILES[$name]['name']
                    );
        case 2:
            return PEAR::raiseError(
                        "The form size limit didn't allow to upload file ".
                        $_FILES[$name]['name']
                   );
        case 3:
            return PEAR::raiseError(
                        "The file was not entirely uploaded"
                   );
        case 4:
            return PEAR::raiseError(
                        "The uploaded file is empty"
                   );
        default:
            return PEAR::raiseError(
                        "Unknown error ".$_FILES[$name]['error']." in file upload. ".
                        "Please, report a bug"
                   );
        }
        if (!is_uploaded_file($_FILES[$name]['tmp_name'])) {
            return PEAR::raiseError("The file is not an uploaded file");
        }

        require_once "File/Archive/Reader/File.php";
        return new File_Archive_Reader_File(
                    $_FILES[$name]['tmp_name'],
                    $_FILES[$name]['name'],
                    $_FILES[$name]['type']
               );
    }

    /**
     * Adds a cache layer above the specified reader
     * The data of the reader is saved in a temporary file for future access.
     * The cached reader will be read only once, even if you read it several times.
     * This can be usefull to read compressed files or downloaded files (from http or ftp)
     *
     * @param mixed $toConvert The reader to cache
     *        It can be a File_Archive_Reader or a string, which will be converted using the
     *        read function
     */
    function cache(&$toConvert)
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }

        require_once 'File/Archive/Reader/Cache.php';
        return new File_Archive_Reader_Cache($source);
    }

    /**
     * Try to interpret the object as a reader
     * Strings are converted to readers using File_Archive::read
     * Arrays are converted to readers using File_Archive::readMulti
     *
     * @access private
     */
    function &_convertToReader(&$source)
    {
        if (is_string($source)) {
            $cacheCondition = File_Archive::getOption('cacheCondition');
            if ($cacheCondition !== false &&
                preg_match($cacheCondition, $source)) {
                $obj = File_Archive::cache(File_Archive::read($source));
                return $obj;
            } else {
                $obj = File_Archive::read($source);
                return $obj;
            }
        } else if (is_array($source)) {
            return File_Archive::readMulti($source);
        } else {
            return $source;
        }
     }

    /**
     * Try to interpret the object as a writer
     * Strings are converted to writers using File_Archive::appender
     * Arrays are converted to writers using a multi writer
     *
     * @access private
     */
    function &_convertToWriter(&$dest)
    {
        if (is_string($dest)) {
            $obj =& File_Archive::appender($dest);
            return $obj;
        } else if (is_array($dest)) {
            require_once 'File/Archive/Writer/Multi.php';
            $writer = new File_Archive_Writer_Multi();
            foreach($dest as $key => $foo) {
                $writer->addWriter($dest[$key]);
            }
            return $writer;
        } else {
            return $dest;
        }
    }

    /**
     * Check if a file with a specific extension can be read as an archive
     * with File_Archive::read*
     * This function is case sensitive.
     *
     * @param string $extension the checked extension
     * @return bool whether this file can be understood reading its extension
     *         Currently, supported extensions are tar, zip, jar, gz, tgz,
     *         tbz, bz2, bzip2, ar, deb
     */
    function isKnownExtension($extension)
    {
        return $extension == 'tar'   ||
               $extension == 'zip'   ||
               $extension == 'jar'   ||
               $extension == 'gz'    ||
               $extension == 'tgz'   ||
               $extension == 'tbz'   ||
               $extension == 'bz2'   ||
               $extension == 'bzip2' ||
               $extension == 'ar'    ||
               $extension == 'deb'   /* ||
               $extension == 'cab'   ||
               $extension == 'rar' */;
    }

    /**
     * Create a reader that will read the single file source $source as
     * a specific archive
     *
     * @param string $extension determines the kind of archive $source contains
     *        $extension is case sensitive
     * @param File_Archive_Reader $source stores the archive
     * @param bool $sourceOpened specifies if the archive is already opened
     *        if false, next will be called on source
     *        Closing the returned archive will close $source iif $sourceOpened
     *        is true
     * @return A File_Archive_Reader that uncompresses the archive contained in
     *         $source interpreting it as a $extension archive
     *         If $extension is not handled return false
     */
    function readArchive($extension, &$toConvert, $sourceOpened = false)
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }

        switch($extension) {
        case 'tgz':
            return File_Archive::readArchive('tar',
                    File_Archive::readArchive('gz', $source, $sourceOpened)
                    );
        case 'tbz':
            return File_Archive::readArchive('tar',
                    File_Archive::readArchive('bz2', $source, $sourceOpened)
                    );
        case 'tar':
            require_once 'File/Archive/Reader/Tar.php';
            return new File_Archive_Reader_Tar($source, $sourceOpened);

        case 'gz':
        case 'gzip':
            require_once 'File/Archive/Reader/Gzip.php';
            return new File_Archive_Reader_Gzip($source, $sourceOpened);

        case 'zip':
        case 'jar':
            require_once 'File/Archive/Reader/Zip.php';
            return new File_Archive_Reader_Zip($source, $sourceOpened);

        case 'bz2':
        case 'bzip2':
            require_once 'File/Archive/Reader/Bzip2.php';
            return new File_Archive_Reader_Bzip2($source, $sourceOpened);

        case 'deb':
        case 'ar':
            require_once 'File/Archive/Reader/Ar.php';
            return new File_Archive_Reader_Ar($source, $sourceOpened);

/*        case 'cab':
            require_once 'File/Archive/Reader/Cab.php';
            return new File_Archive_Reader_Cab($source, $sourceOpened);


        case 'rar':
            require_once "File/Archive/Reader/Rar.php";
            return new File_Archive_Reader_Rar($source, $sourceOpened); */

        default:
            return false;
        }
    }

    /**
     * Contains only one file with data read from a memory buffer
     *
     * @param string $memory content of the file
     * @param string $filename public name of the file
     * @param array $stat statistics of the file. Index 7 (size) will be
     *        overwritten to match the size of $memory
     * @param string $mime mime type of the file. Default will determine the
     *        mime type thanks to the extension of $filename
     * @see File_Archive_Reader_Memory
     */
    function readMemory($memory, $filename, $stat=array(), $mime=null)
    {
        require_once "File/Archive/Reader/Memory.php";
        return new File_Archive_Reader_Memory($memory, $filename, $stat, $mime);
    }

    /**
     * Contains several other sources. Take care the sources don't have several
     * files with the same filename. The sources are given as a parameter, or
     * can be added thanks to the reader addSource method
     *
     * @param array $sources Array of strings or readers that will be added to
     *        the multi reader. If the parameter is a string, a reader will be
     *        built thanks to the read function
     * @see   File_Archive_Reader_Multi, File_Archive::read()
     */
    function readMulti($sources = array())
    {
        require_once "File/Archive/Reader/Multi.php";
        $result = new File_Archive_Reader_Multi();
        foreach ($sources as $index => $foo) {
            $s =& File_Archive::_convertToReader($sources[$index]);
            if (PEAR::isError($s)) {
                return $s;
            } else {
                $result->addSource($s);
            }
        }
        return $result;
    }
    /**
     * Make the files of a source appear as one large file whose content is the
     * concatenation of the content of all the files
     *
     * @param File_Archive_Reader $toConvert The source whose files must be
     *        concatened
     * @param string $filename name of the only file of the created reader
     * @param array $stat statistics of the file. Index 7 (size) will be
     *        overwritten to match the total size of the files
     * @param string $mime mime type of the file. Default will determine the
     *        mime type thanks to the extension of $filename
     * @see   File_Archive_Reader_Concat
     */
    function readConcat(&$toConvert, $filename, $stat=array(), $mime=null)
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }

        require_once "File/Archive/Reader/Concat.php";
        return new File_Archive_Reader_Concat($source, $filename, $stat, $mime);
    }

    /**
     * Changes the name of each file in a reader by applying a custom function
     * The function must return false if the file is to be discarded, or the new
     * name of the file else
     *
     * @param Callable $function Function called to modify the name of the file
     *        $function takes the name of the file as a parameter and returns the
     *        new name, or false if the file must be discarded
     * @param File_Archive_Reader $toConvert The files of this source will be
     *        modified
     * @return File_Archive_Reader a new reader that contains the same files
     *        as $toConvert but with a different name
     */
    function changeName($function, &$toConvert)
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }

        require_once "File/Archive/Reader/ChangeName.php";
        return new File_Archive_Reader_RemoveDirectory($source);
    }

    /**
     * Removes from a source the files that do not follow a given predicat
     *
     * @param File_Archive_Predicate $predicate Only the files for which
     *        $predicate->isTrue() will be kept
     * @param File_Archive_Reader $source Source that will be filtered
     * @see   File_Archive_Reader_Filter
     */
    function filter($predicate, &$toConvert)
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }

        require_once "File/Archive/Reader/Filter.php";
        return new File_Archive_Reader_Filter($predicate, $source);
    }
    /**
     * Predicate that always evaluate to true
     *
     * @see File_Archive_Predicate_True
     */
    function predTrue()
    {
        require_once "File/Archive/Predicate/True.php";
        return new File_Archive_Predicate_True();
    }
    /**
     * Predicate that always evaluate to false
     *
     * @see File_Archive_Predicate_False
     */
    function predFalse()
    {
        require_once "File/Archive/Predicate/False.php";
        return new File_Archive_Predicate_False();
    }
    /**
     * Predicate that evaluates to the logical AND of the parameters
     * You can add other predicates thanks to the
     * File_Archive_Predicate_And::addPredicate() function
     *
     * @param File_Archive_Predicate (any number of them)
     * @see File_Archive_Predicate_And
     */
    function predAnd()
    {
        require_once "File/Archive/Predicate/And.php";
        $pred = new File_Archive_Predicate_And();
        $args = func_get_args();
        foreach ($args as $p) {
            $pred->addPredicate($p);
        }
        return $pred;
    }
    /**
     * Predicate that evaluates to the logical OR of the parameters
     * You can add other predicates thanks to the
     * File_Archive_Predicate_Or::addPredicate() function
     *
     * @param File_Archive_Predicate (any number of them)
     * @see File_Archive_Predicate_Or
     */
    function predOr()
    {
        require_once "File/Archive/Predicate/Or.php";
        $pred = new File_Archive_Predicate_Or();
        $args = func_get_args();
        foreach ($args as $p) {
            $pred->addPredicate($p);
        }
        return $pred;
    }
    /**
     * Negate a predicate
     *
     * @param File_Archive_Predicate $pred Predicate to negate
     * @see File_Archive_Predicate_Not
     */
    function predNot($pred)
    {
        require_once "File/Archive/Predicate/Not.php";
        return new File_Archive_Predicate_Not($pred);
    }
    /**
     * Evaluates to true iif the file is larger than a given size
     *
     * @param int $size the minimal size of the files (in Bytes)
     * @see File_Archive_Predicate_MinSize
     */
    function predMinSize($size)
    {
        require_once "File/Archive/Predicate/MinSize.php";
        return new File_Archive_Predicate_MinSize($size);
    }
    /**
     * Evaluates to true iif the file has been modified after a given time
     *
     * @param int $time Unix timestamp of the minimal modification time of the
     *        files
     * @see File_Archive_Predicate_MinTime
     */
    function predMinTime($time)
    {
        require_once "File/Archive/Predicate/MinTime.php";
        return new File_Archive_Predicate_MinTime($time);
    }
    /**
     * Evaluates to true iif the file has less that a given number of
     * directories in its path
     *
     * @param int $depth Maximal number of directories in path of the files
     * @see File_Archive_Predicate_MaxDepth
     */
    function predMaxDepth($depth)
    {
        require_once "File/Archive/Predicate/MaxDepth.php";
        return new File_Archive_Predicate_MaxDepth($depth);
    }
    /**
     * Evaluates to true iif the extension of the file is in a given list
     *
     * @param array or string $list List or comma separated string of possible
     * extension of the files
     * @see File_Archive_Predicate_Extension
     */
    function predExtension($list)
    {
        require_once "File/Archive/Predicate/Extension.php";
        return new File_Archive_Predicate_Extension($list);
    }
    /**
     * Evaluates to true iif the MIME type of the file is in a given list
     *
     * @param array or string $list List or comma separated string of possible
     *        MIME types of the files. You may enter wildcards like "image/*" to
     *        select all the MIME in class image
     * @see   File_Archive_Predicate_MIME, MIME_Type::isWildcard()
     */
    function predMIME($list)
    {
        require_once "File/Archive/Predicate/MIME.php";
        return new File_Archive_Predicate_MIME($list);
    }
    /**
     * Evaluates to true iif the name of the file follow a given regular
     * expression
     *
     * @param string $ereg regular expression that the filename must follow
     * @see File_Archive_Predicate_Ereg, ereg()
     */
    function predEreg($ereg)
    {
        require_once "File/Archive/Predicate/Ereg.php";
        return new File_Archive_Predicate_Ereg($ereg);
    }
    /**
     * Evaluates to true iif the name of the file follow a given regular
     * expression (case insensitive version)
     *
     * @param string $ereg regular expression that the filename must follow
     * @see File_Archive_Predicate_Eregi, eregi
     */
    function predEregi($ereg)
    {
        require_once "File/Archive/Predicate/Eregi.php";
        return new File_Archive_Predicate_Eregi($ereg);
    }
    /**
     * Evaluates to true only after a given number of evaluations
     * This can be used to select files by index since the evaluation is done
     * once per file
     *
     * @param array The indexes for which the returned predicate will return true
     *        are the keys of the array
     *        The predicate will return true if isset($indexes[$pos])
     */
    function predIndex($indexes)
    {
        require_once "File/Archive/Predicate/Index.php";
        return new File_Archive_Predicate_Index($indexes);
    }
    /**
     * Custom predicate built by supplying a string expression
     *
     * Here are different ways to create a predicate that keeps only files
     * with names shorter than 100 chars
     * <sample>
     *  File_Archive::predCustom("return strlen($name)<100;")
     *  File_Archive::predCustom("strlen($name)<100;")
     *  File_Archive::predCustom("strlen($name)<100")
     *  File_Archive::predCustom("strlen($source->getFilename())<100")
     * </sample>
     *
     * @param string $expression String containing an expression that evaluates
     *        to a boolean. If the expression doesn't contain a return
     *        statement, it will be added at the begining of the expression
     *        A ';' will be added at the end of the expression so that you don't
     *        have to write it. You may use the $name variable to refer to the
     *        current filename (with path...), $time for the modification time
     *        (unix timestamp), $size for the size of the file in bytes, $mime
     *        for the MIME type of the file
     * @see   File_Archive_Predicate_Custom
     */
    function predCustom($expression)
    {
        require_once "File/Archive/Predicate/Custom.php";
        return new File_Archive_Predicate_Custom($expression);
    }

    /**
     * Send the files as a mail attachment
     *
     * @param Mail $mail Object used to send mail (see Mail::factory)
     * @param array or String $to An array or a string with comma separated
     *        recipients
     * @param array $headers The headers that will be passed to the Mail_mime
     *        object
     * @param string $message Text body of the mail
     * @see File_Archive_Writer_Mail
     */
    function toMail($to, $headers, $message, $mail = null)
    {
        require_once "File/Archive/Writer/Mail.php";
        return new File_Archive_Writer_Mail($to, $headers, $message, $mail);
    }
    /**
     * Write the files on the hard drive
     *
     * @param string $baseDir if specified, the files will be created in that
     *        directory. If they don't exist, the directories will automatically
     *        be created
     * @see   File_Archive_Writer_Files
     */
    function toFiles($baseDir = "")
    {
        require_once "File/Archive/Writer/Files.php";
        return new File_Archive_Writer_Files($baseDir);
    }
    /**
     * Send the content of the files to a memory buffer
     *
     * toMemory returns a writer where the data will be written.
     * In this case, the data is accessible using the getData member
     *
     * toVariable returns a writer that will write into the given
     * variable
     *
     * @param out $data if specified, the data will be written to this buffer
     *        Else, you can retrieve the buffer with the
     *        File_Archive_Writer_Memory::getData() function
     * @see   File_Archive_Writer_Memory
     */
    function toMemory()
    {
        $v = '';
        return File_Archive::toVariable($v);
    }
    function toVariable(&$v)
    {
        require_once "File/Archive/Writer/Memory.php";
        return new File_Archive_Writer_Memory($v);
    }
    /**
     * Duplicate the writing operation on two writers
     *
     * @param File_Archive_Writer $a, $b writers where data will be duplicated
     * @see File_Archive_Writer_Multi
     */
    function toMulti(&$aC, &$bC)
    {
        $a =& File_Archive::_convertToWriter($aC);
        $b =& File_Archive::_convertToWriter($bC);

        if (PEAR::isError($a)) {
            return $a;
        }
        if (PEAR::isError($b)) {
            return $b;
        }

        require_once "File/Archive/Writer/Multi.php";
        $writer = new File_Archive_Writer_Multi();
        $writer->addWriter($a);
        $writer->addWriter($b);
        return $writer;
    }
    /**
     * Send the content of the files to the standard output (so to the client
     * for a website)
     *
     * @param bool $sendHeaders If true some headers will be sent to force the
     *        download of the file. Default value is true
     * @see   File_Archive_Writer_Output
     */
    function toOutput($sendHeaders = true)
    {
        require_once "File/Archive/Writer/Output.php";
        return new File_Archive_Writer_Output($sendHeaders);
    }
    /**
     * Compress the data to a tar, gz, tar/gz or zip format
     *
     * @param string $filename name of the archive file
     * @param File_Archive_Writer $innerWriter writer where the archive will be
     *        written
     * @param string $type can be one of tgz, tbz, tar, zip, gz, gzip, bz2,
     *        bzip2 (default is the extension of $filename) or any composition
     *        of them (for example tar.gz or tar.bz2). The case of this
     *        parameter is not important.
     * @param array $stat Statistics of the archive (see stat function)
     * @param bool $autoClose If set to true, $innerWriter will be closed when
     *        the returned archive is close. Default value is true.
     */
    function toArchive($filename, &$toConvert, $type = null,
                       $stat = array(), $autoClose = true)
    {
        $innerWriter =& File_Archive::_convertToWriter($toConvert);
        if (PEAR::isError($innerWriter)) {
            return $innerWriter;
        }
        $shortcuts = array("tgz"   , "tbz"    );
        $reals     = array("tar.gz", "tar.bz2");

        if ($type === null) {
            $extensions = strtolower($filename);
        } else {
            $extensions = strtolower($type);
        }
        $extensions = explode('.', str_replace($shortcuts, $reals, $extensions));
        if ($innerWriter !== null) {
            $writer =& $innerWriter;
        } else {
            $writer = File_Archive::toFiles();
        }
        $nbCompressions = 0;
        $currentFilename = $filename;
        while (($extension = array_pop($extensions)) !== null) {
            unset($next);
            switch($extension) {
            case "tar":
                require_once "File/Archive/Writer/Tar.php";
                $next = new File_Archive_Writer_Tar(
                    $currentFilename, $writer, $stat, $autoClose
                );
                unset($writer); $writer =& $next;
                break;
            case "zip":
                require_once "File/Archive/Writer/Zip.php";
                $next = new File_Archive_Writer_Zip(
                    $currentFilename, $writer, $stat, $autoClose
                );
                unset($writer); $writer =& $next;
                break;
            case "gz":
            case "gzip":
                require_once "File/Archive/Writer/Gzip.php";
                $next = new File_Archive_Writer_Gzip(
                    $currentFilename, $writer, $stat, $autoClose
                );
                unset($writer); $writer =& $next;
                break;
            case "bz2":
            case "bzip2":
                require_once "File/Archive/Writer/Bzip2.php";
                $next = new File_Archive_Writer_Bzip2(
                    $currentFilename, $writer, $stat, $autoClose
                );
                unset($writer); $writer =& $next;
                break;
            case "deb":
            case "ar":
                require_once "File/Archive/Writer/Ar.php";
                $next = new File_Archive_Writer_Ar(
                    $currentFilename, $writer, $stat, $autoClose
                );
                unset($writer); $writer =& $next;
                break;
            default:
                if ($type !== null || $nbCompressions == 0) {
                    return PEAR::raiseError("Archive $extension unknown");
                }
                break;
            }
            $nbCompressions ++;
            $autoClose = true;
            $currentFilename = implode(".", $extensions);
        }
        return $writer;
    }


    /**
     * File_Archive::extract($source, $dest) is equivalent to $source->extract($dest)
     * If $source is a PEAR error, the error will be returned
     * It is thus easier to use this function than $source->extract, since it reduces the number of
     * error checking and doesn't force you to define a variable $source
     *
     * You may use strings as source and dest. In that case the source is automatically
     * converted to a reader using File_Archive::read and the dest is converted to a
     * writer using File_Archive::appender
     * Since PHP doesn't allow to pass literal strings by ref, you will have to use temporary
     * variables.
     * File_Archive::extract($src = 'archive.zip/', $dest = 'dir') will extract the archive to 'dir'
     * It is the same as
     * File_Archive::extract(
     *    File_Archive::read('archive.zip/'),
     *    File_Archive::appender('dir')
     * );
     * You may use any variable in the extract function ($from/$to, $a/$b...).
     *
     * @param File_Archive_Reader $source The source that will be read
     * @param File_Archive_Writer $dest Where to copy $source files
     * @param bool $autoClose if true (default), $dest will be closed after the extraction
     * @param int $bufferSize Size of the buffer to use to move data from the reader to the buffer
     *        If $bufferSize <= 0 (default), the blockSize option is used
     *        You shouldn't need to change that
     * @return null or a PEAR error if an error occured
     */
    function extract(&$sourceToConvert, &$destToConvert, $autoClose = true, $bufferSize = 0)
    {
        $source =& File_Archive::_convertToReader($sourceToConvert);
        if (PEAR::isError($source)) {
            return $source;
        }
        $dest =& File_Archive::_convertToWriter($destToConvert);
        return $source->extract($dest, $autoClose, $bufferSize);
    }

    /**
     * Create a writer that can be used to append files to an archive inside a source
     * If the archive can't be found in the source, it will be created
     * If source is set to null, File_Archive::toFiles will be assumed
     * If type is set to null, the type of the archive will be determined looking at
     * the extension in the URL
     * stat is the array of stat (returned by stat() PHP function of Reader getStat())
     * to use if the archive must be created
     *
     * This function allows to create or append data to nested archives. Only one
     * archive will be created and if your creation requires creating several nested
     * archives, a PEAR error will be returned
     *
     * After this call, $source will be closed and should not be used until the
     * returned writer is closed.
     *
     * @param File_Archive_Reader $source A reader where some files will be appended
     * @param string $URL URL to reach the archive in the source.
     *        if $URL is null, a writer to append files to the $source reader will
     *        be returned
     * @param bool $unique If true, the duplicate files will be deleted on close
     *        Default is false (and setting it to true may have some performance
     *        consequences)
     * @param string $type Extension of the archive (or null to use the one in the URL)
     * @param array $stat Used only if archive is created, array of stat as returned
     *        by PHP stat function or Reader getStat function: stats of the archive)
     *        Time (index 9) will be overwritten to current time
     * @return File_Archive_Writer a writer that you can use to append files to the reader
     */
    function appenderFromSource(&$toConvert, $URL = null, $unique = null,
                                 $type = null, $stat = array())
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }
        if ($unique == null) {
            $unique = File_Archive::getOption("appendRemoveDuplicates");
        }

        //Do not report the fact that the archive does not exist as an error
        PEAR::pushErrorHandling(PEAR_ERROR_RETURN);

        if ($URL === null) {
            $result =& $source;
        } else {
            if ($type === null) {
                $result = File_Archive::_readSource($source, $URL.'/', $reachable, $baseDir);
            } else {
                $result = File_Archive::readArchive(
                            $type,
                            File_Archive::_readSource($source, $URL, $reachable, $baseDir)
                          );
            }
        }

        PEAR::popErrorHandling();

        if (!PEAR::isError($result)) {
            if ($unique) {
                require_once "File/Archive/Writer/UniqueAppender.php";
                return new File_Archive_Writer_UniqueAppender($result);
            } else {
                return $result->makeAppendWriter();
            }
        }

        //The source can't be found and has to be created
        $stat[9] = $stat['mtime'] = time();

        if (empty($baseDir)) {
            if ($source !== null) {
                $writer =& $source->makeWriter();
            } else {
                $writer =& File_Archive::toFiles();
            }
            if (PEAR::isError($writer)) {
                return $writer;
            }

            PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
            $result = File_Archive::toArchive($reachable, $writer, $type);
            PEAR::popErrorHandling();

            if (PEAR::isError($result)) {
                $result = File_Archive::toFiles($reachable);
            }
        } else {
            $reachedSource = File_Archive::readSource($source, $reachable);
            if (PEAR::isError($reachedSource)) {
                return $reachedSource;
            }
            $writer = $reachedSource->makeWriter();
            if (PEAR::isError($writer)) {
                return $writer;
            }

            PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
            $result = File_Archive::toArchive($baseDir, $writer, $type);
            PEAR::popErrorHandling();

            if (PEAR::isError($result)) {
                require_once "File/Archive/Writer/AddBaseName.php";
                $result = new File_Archive_Writer_AddBaseName(
                                       $baseDir, $writer);
                if (PEAR::isError($result)) {
                    return $result;
                }
            }
        }
        return $result;
    }

    /**
     * Create a writer that allows appending new files to an existing archive
     * This function actes as appendToSource with source being the system files
     * $URL can't be null here
     *
     * @param File_Archive_Reader $source A reader where some files will be appended
     * @return File_Archive_Writer a writer that you can use to append files to the reader
     */
    function appender($URL, $unique = null, $type = null, $stat = array())
    {
        $source = null;
        return File_Archive::appenderFromSource($source, $URL, $unique, $type, $stat);
    }

    /**
     * Remove the files that follow a given predicate from the source
     * If URL is null, the files will be removed from the source directly
     * Else, URL must link to a source from which the files will be removed
     *
     * @param File_Archive_Predicate $pred The files that follow the predicate
     *        (for which $pred->isTrue($source) is true) will be erased
     * @param File_Archive_Reader $source A reader that contains the files to remove
     */
    function removeFromSource(&$pred, &$toConvert, $URL = null)
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }
        if ($URL === null) {
            $result = &$source;
        } else {
            if (substr($URL, -1) !== '/') {
                $URL .= '/';
            }
            $result = File_Archive::readSource($source, $URL);
        }

        $writer = $result->makeWriterRemoveFiles($pred);
        if (PEAR::isError($writer)) {
            return $writer;
        }
        $writer->close();
    }

    /**
     * Remove the files that follow a given predicate from the archive specified
     * in $URL
     *
     * @param $URL URL of the archive where some files must be removed
     */
    function remove($pred, $URL)
    {
        $source = null;
        return File_Archive::removeFromSource($pred, $source, $URL);
    }

    /**
     * Remove duplicates from a source, keeping the most recent one (or the one that has highest pos in
     * the archive if the files have same date or no date specified)
     *
     * @param File_Archive_Reader a reader that may contain duplicates
     */
    function removeDuplicatesFromSource(&$toConvert, $URL = null)
    {
        $source =& File_Archive::_convertToReader($toConvert);
        if (PEAR::isError($source)) {
            return $source;
        }
        if ($URL !== null && substr($URL, -1) != '/') {
            $URL .= '/';
        }

        if ($source === null) {
            $source = File_Archive::read($URL);
        }

        require_once "File/Archive/Predicate/Duplicate.php";
        $pred = new File_Archive_Predicate_Duplicate($source);
        $source->close();
        return File_Archive::removeFromSource(
            $pred,
            $source,
            null
        );
    }

    /**
     * Remove duplicates from the archive specified in the URL
     */
    function removeDuplicates($URL)
    {
        $source = null;
        return File_Archive::removeDuplicatesFromSource($source, $URL);
    }
}

?>
