1<?php
2//
3
4/**
5 * package::i.tools
6 *
7 * php-downloader    v1.0    -    www.ipunkt.biz
8 *
9 * (c)    2002 - www.ipunkt.biz (rok)
10 */
11/**
12 * =======================================================================
13 * Name:
14 * tar Class
15 *
16 * Author:
17 * Josh Barger <joshb@npt.com>
18 *
19 * Description:
20 * This class reads and writes Tape-Archive (TAR) Files and Gzip
21 * compressed TAR files, which are mainly used on UNIX systems.
22 * This class works on both windows AND unix systems, and does
23 * NOT rely on external applications!! Woohoo!
24 *
25 * Usage:
26 * Copyright (C) 2002  Josh Barger
27 *
28 * This library is free software; you can redistribute it and/or
29 * modify it under the terms of the GNU Lesser General Public
30 * License as published by the Free Software Foundation; either
31 * version 2.1 of the License, or (at your option) any later version.
32 *
33 * This library is distributed in the hope that it will be useful,
34 * but WITHOUT ANY WARRANTY; without even the implied warranty of
35 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
36 * Lesser General Public License for more details at:
37 * http://www.gnu.org/copyleft/lesser.html
38 *
39 * If you use this script in your application/website, please
40 * send me an e-mail letting me know about it :)
41 *
42 * Bugs:
43 * Please report any bugs you might find to my e-mail address
44 * at joshb@npt.com.  If you have already created a fix/patch
45 * for the bug, please do send it to me so I can incorporate it into my release.
46 *
47 * Version History:
48 * 1.0    04/10/2002    - InitialRelease
49 *
50 * 2.0    04/11/2002    - Merged both tarReader and tarWriter
51 * classes into one
52 * - Added support for gzipped tar files
53 * Remember to name for .tar.gz or .tgz
54 * if you use gzip compression!
55 * :: THIS REQUIRES ZLIB EXTENSION ::
56 * - Added additional comments to
57 * functions to help users
58 * - Added ability to remove files and
59 * directories from archive
60 * 2.1    04/12/2002    - Fixed serious bug in generating tar
61 * - Created another example file
62 * - Added check to make sure ZLIB is
63 * installed before running GZIP
64 * compression on TAR
65 * 2.2    05/07/2002    - Added automatic detection of Gzipped
66 * tar files (Thanks go to Jidgen Falch
67 * for the idea)
68 * - Changed "private" functions to have
69 * special function names beginning with
70 * two underscores
71 * =======================================================================
72 * XOOPS changes onokazu <webmaster@xoops.org>
73 *
74 * 12/25/2002 - Added flag to addFile() function for binary files
75 *
76 * =======================================================================
77 */
78
79/**
80 * tar Class
81 *
82 * This class reads and writes Tape-Archive (TAR) Files and Gzip
83 * compressed TAR files, which are mainly used on UNIX systems.
84 * This class works on both windows AND unix systems, and does
85 * NOT rely on external applications!! Woohoo!
86 *
87 * @author     Josh Barger <joshb@npt.com>
88 * @copyright  Copyright (C) 2002  Josh Barger
89 * @package    kernel
90 * @subpackage core
91 */
92class Tar
93{
94    /**
95     * *#@+
96     * Unprocessed Archive Information
97     */
98    public $filename;
99    public $isGzipped;
100    public $tar_file;
101    /**
102     * *#@-
103     */
104
105    /**
106     * *#@+
107     * Processed Archive Information
108     */
109    public $files;
110    public $directories;
111    public $numFiles;
112    public $numDirectories;
113    /**
114     * *#@-
115     */
116
117    /**
118     * Class Constructor -- Does nothing...
119     */
120    public function __construct()
121    {
122    }
123
124    /**
125     * Computes the unsigned Checksum of a file's header
126     * to try to ensure valid file
127     *
128     * @param string $bytestring
129     *
130     * @return int|string
131     * @access private
132     */
133    public function __computeUnsignedChecksum($bytestring)
134    {
135        $unsigned_chksum = '';
136        for ($i = 0; $i < 512; ++$i) {
137            $unsigned_chksum += ord($bytestring[$i]);
138        }
139        for ($i = 0; $i < 8; ++$i) {
140            $unsigned_chksum -= ord($bytestring[148 + $i]);
141            $unsigned_chksum += ord(' ') * 8;
142        }
143
144        return $unsigned_chksum;
145    }
146
147    /**
148     * Converts a NULL padded string to a non-NULL padded string
149     *
150     * @param  string $string
151     * @return string
152     * @access private
153     */
154    public function __parseNullPaddedString($string)
155    {
156        $position = strpos($string, chr(0));
157
158        return substr($string, 0, $position);
159    }
160
161    /**
162     * This function parses the current TAR file
163     *
164     * @return bool always TRUE
165     * @access private
166     */
167    public function __parseTar()
168    {
169        // Read Files from archive
170        $tar_length     = strlen($this->tar_file);
171        $main_offset    = 0;
172        $this->numFiles = 0;
173        while ($main_offset < $tar_length) {
174            // If we read a block of 512 nulls, we are at the end of the archive
175            if (substr($this->tar_file, $main_offset, 512) == str_repeat(chr(0), 512)) {
176                break;
177            }
178            // Parse file name
179            $file_name = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset, 100));
180            // Parse the file mode
181            $file_mode = substr($this->tar_file, $main_offset + 100, 8);
182            // Parse the file user ID
183            $file_uid = octdec(substr($this->tar_file, $main_offset + 108, 8));
184            // Parse the file group ID
185            $file_gid = octdec(substr($this->tar_file, $main_offset + 116, 8));
186            // Parse the file size
187            $file_size = octdec(substr($this->tar_file, $main_offset + 124, 12));
188            // Parse the file update time - unix timestamp format
189            $file_time = octdec(substr($this->tar_file, $main_offset + 136, 12));
190            // Parse Checksum
191            $file_chksum = octdec(substr($this->tar_file, $main_offset + 148, 6));
192            // Parse user name
193            $file_uname = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset + 265, 32));
194            // Parse Group name
195            $file_gname = $this->__parseNullPaddedString(substr($this->tar_file, $main_offset + 297, 32));
196            // Make sure our file is valid
197            if ($this->__computeUnsignedChecksum(substr($this->tar_file, $main_offset, 512)) != $file_chksum) {
198                return false;
199            }
200            // Parse File Contents
201            $file_contents = substr($this->tar_file, $main_offset + 512, $file_size);
202
203            /**
204             * ### Unused Header Information ###
205             * $activeFile["typeflag"]        = substr($this->tar_file,$main_offset + 156,1);
206             * $activeFile["linkname"]        = substr($this->tar_file,$main_offset + 157,100);
207             * $activeFile["magic"]        = substr($this->tar_file,$main_offset + 257,6);
208             * $activeFile["version"]        = substr($this->tar_file,$main_offset + 263,2);
209             * $activeFile["devmajor"]        = substr($this->tar_file,$main_offset + 329,8);
210             * $activeFile["devminor"]        = substr($this->tar_file,$main_offset + 337,8);
211             * $activeFile["prefix"]        = substr($this->tar_file,$main_offset + 345,155);
212             * $activeFile["endheader"]    = substr($this->tar_file,$main_offset + 500,12);
213             */
214
215            if ($file_size > 0) {
216                // Increment number of files
217                $this->numFiles++;
218                // Create us a new file in our array
219                $activeFile =& $this->files[];
220                // Asign Values
221                $activeFile['name']       = $file_name;
222                $activeFile['mode']       = $file_mode;
223                $activeFile['size']       = $file_size;
224                $activeFile['time']       = $file_time;
225                $activeFile['user_id']    = $file_uid;
226                $activeFile['group_id']   = $file_gid;
227                $activeFile['user_name']  = $file_uname;
228                $activeFile['group_name'] = $file_gname;
229                $activeFile['checksum']   = $file_chksum;
230                $activeFile['file']       = $file_contents;
231            } else {
232                // Increment number of directories
233                $this->numDirectories++;
234                // Create a new directory in our array
235                $activeDir =& $this->directories[];
236                // Assign values
237                $activeDir['name']       = $file_name;
238                $activeDir['mode']       = $file_mode;
239                $activeDir['time']       = $file_time;
240                $activeDir['user_id']    = $file_uid;
241                $activeDir['group_id']   = $file_gid;
242                $activeDir['user_name']  = $file_uname;
243                $activeDir['group_name'] = $file_gname;
244                $activeDir['checksum']   = $file_chksum;
245            }
246            // Move our offset the number of blocks we have processed
247            $main_offset += 512 + (ceil($file_size / 512) * 512);
248        }
249
250        return true;
251    }
252
253    /**
254     * Read a non gzipped tar file in for processing.
255     *
256     * @param  string $filename full filename
257     * @return bool   always TRUE
258     * @access private
259     */
260    public function __readTar($filename = '')
261    {
262        // Set the filename to load
263        if (!$filename) {
264            $filename = $this->filename;
265        }
266        // Read in the TAR file
267        $fp             = fopen($filename, 'rb');
268        $this->tar_file = fread($fp, filesize($filename));
269        fclose($fp);
270
271        if ($this->tar_file[0] == chr(31) && $this->tar_file[1] == chr(139) && $this->tar_file[2] == chr(8)) {
272            if (!function_exists('gzinflate')) {
273                return false;
274            }
275            $this->isGzipped = true;
276            $this->tar_file  = gzinflate(substr($this->tar_file, 10, -4));
277        }
278        // Parse the TAR file
279        $this->__parseTar();
280
281        return true;
282    }
283
284    /**
285     * Generates a TAR file from the processed data
286     *
287     * @return bool always TRUE
288     * @access private
289     */
290    public function __generateTar()
291    {
292        // Clear any data currently in $this->tar_file
293        unset($this->tar_file);
294        // Generate Records for each directory, if we have directories
295        if ($this->numDirectories > 0) {
296            foreach ($this->directories as $key => $information) {
297                unset($header);
298                // Generate tar header for this directory
299                // Filename, Permissions, UID, GID, size, Time, checksum, typeflag, linkname, magic, version, user name, group name, devmajor, devminor, prefix, end
300                $header .= str_pad($information['name'], 100, chr(0));
301                $header .= str_pad(decoct($information['mode']), 7, '0', STR_PAD_LEFT) . chr(0);
302                $header .= str_pad(decoct($information['user_id']), 7, '0', STR_PAD_LEFT) . chr(0);
303                $header .= str_pad(decoct($information['group_id']), 7, '0', STR_PAD_LEFT) . chr(0);
304                $header .= str_pad(decoct(0), 11, '0', STR_PAD_LEFT) . chr(0);
305                $header .= str_pad(decoct($information['time']), 11, '0', STR_PAD_LEFT) . chr(0);
306                $header .= str_repeat(' ', 8);
307                $header .= '5';
308                $header .= str_repeat(chr(0), 100);
309                $header .= str_pad('ustar', 6, chr(32));
310                $header .= chr(32) . chr(0);
311                $header .= str_pad('', 32, chr(0));
312                $header .= str_pad('', 32, chr(0));
313                $header .= str_repeat(chr(0), 8);
314                $header .= str_repeat(chr(0), 8);
315                $header .= str_repeat(chr(0), 155);
316                $header .= str_repeat(chr(0), 12);
317                // Compute header checksum
318                $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)), 6, '0', STR_PAD_LEFT);
319                for ($i = 0; $i < 6; ++$i) {
320                    $header[148 + $i] = substr($checksum, $i, 1);
321                }
322                $header[154] = chr(0);
323                $header[155] = chr(32);
324                // Add new tar formatted data to tar file contents
325                $this->tar_file .= $header;
326            }
327        }
328        // Generate Records for each file, if we have files (We should...)
329        if ($this->numFiles > 0) {
330            $this->tar_file = '';
331            foreach ($this->files as $key => $information) {
332                unset($header);
333                // Generate the TAR header for this file
334                // Filename, Permissions, UID, GID, size, Time, checksum, typeflag, linkname, magic, version, user name, group name, devmajor, devminor, prefix, end
335                $header = str_pad($information['name'], 100, chr(0));
336                $header .= str_pad(decoct($information['mode']), 7, '0', STR_PAD_LEFT) . chr(0);
337                $header .= str_pad(decoct($information['user_id']), 7, '0', STR_PAD_LEFT) . chr(0);
338                $header .= str_pad(decoct($information['group_id']), 7, '0', STR_PAD_LEFT) . chr(0);
339                $header .= str_pad(decoct($information['size']), 11, '0', STR_PAD_LEFT) . chr(0);
340                $header .= str_pad(decoct($information['time']), 11, '0', STR_PAD_LEFT) . chr(0);
341                $header .= str_repeat(' ', 8);
342                $header .= '0';
343                $header .= str_repeat(chr(0), 100);
344                $header .= str_pad('ustar', 6, chr(32));
345                $header .= chr(32) . chr(0);
346                $header .= str_pad($information['user_name'], 32, chr(0)); // How do I get a file's user name from PHP?
347                $header .= str_pad($information['group_name'], 32, chr(0)); // How do I get a file's group name from PHP?
348                $header .= str_repeat(chr(0), 8);
349                $header .= str_repeat(chr(0), 8);
350                $header .= str_repeat(chr(0), 155);
351                $header .= str_repeat(chr(0), 12);
352                // Compute header checksum
353                $checksum = str_pad(decoct($this->__computeUnsignedChecksum($header)), 6, '0', STR_PAD_LEFT);
354                for ($i = 0; $i < 6; ++$i) {
355                    $header[148 + $i] = substr($checksum, $i, 1);
356                }
357                $header[154] = chr(0);
358                $header[155] = chr(32);
359                // Pad file contents to byte count divisible by 512
360                $file_contents = str_pad($information['file'], ceil($information['size'] / 512) * 512, chr(0));
361                // Add new tar formatted data to tar file contents
362                $this->tar_file .= $header . $file_contents;
363            }
364        }
365        // Add 512 bytes of NULLs to designate EOF
366        $this->tar_file .= str_repeat(chr(0), 512);
367
368        return true;
369    }
370
371    /**
372     * Open a TAR file
373     *
374     * @param  string $filename
375     * @return bool
376     */
377    public function openTAR($filename)
378    {
379        // Clear any values from previous tar archives
380        unset($this->filename, $this->isGzipped, $this->tar_file, $this->files, $this->directories, $this->numFiles, $this->numDirectories);
381
382        // If the tar file doesn't exist...
383        if (!file_exists($filename)) {
384            return false;
385        }
386
387        $this->filename = $filename;
388        // Parse this file
389        $this->__readTar();
390
391        return true;
392    }
393
394    /**
395     * Appends a tar file to the end of the currently opened tar file.
396     *
397     * @param  string $filename
398     * @return bool
399     */
400    public function appendTar($filename)
401    {
402        // If the tar file doesn't exist...
403        if (!file_exists($filename)) {
404            return false;
405        }
406        $this->__readTar($filename);
407
408        return true;
409    }
410
411    /**
412     * Retrieves information about a file in the current tar archive
413     *
414     * @param  string $filename
415     * @return string|false FALSE on fail
416     */
417    public function getFile($filename)
418    {
419        if ($this->numFiles > 0) {
420            foreach ($this->files as $key => $information) {
421                if ($information['name'] == $filename) {
422                    return $information;
423                }
424            }
425        }
426
427        return false;
428    }
429
430    /**
431     * Retrieves information about a directory in the current tar archive
432     *
433     * @param  string $dirname
434     * @return string|false FALSE on fail
435     */
436    public function getDirectory($dirname)
437    {
438        if ($this->numDirectories > 0) {
439            foreach ($this->directories as $key => $information) {
440                if ($information['name'] == $dirname) {
441                    return $information;
442                }
443            }
444        }
445
446        return false;
447    }
448
449    /**
450     * Check if this tar archive contains a specific file
451     *
452     * @param  string $filename
453     * @return bool
454     */
455    public function containsFile($filename)
456    {
457        if ($this->numFiles > 0) {
458            foreach ($this->files as $key => $information) {
459                if ($information['name'] == $filename) {
460                    return true;
461                }
462            }
463        }
464
465        return false;
466    }
467
468    /**
469     * Check if this tar archive contains a specific directory
470     *
471     * @param  string $dirname
472     * @return bool
473     */
474    public function containsDirectory($dirname)
475    {
476        if ($this->numDirectories > 0) {
477            foreach ($this->directories as $key => $information) {
478                if ($information['name'] == $dirname) {
479                    return true;
480                }
481            }
482        }
483
484        return false;
485    }
486
487    /**
488     * Add a directory to this tar archive
489     *
490     * @param  string $dirname
491     * @return bool
492     */
493    public function addDirectory($dirname)
494    {
495        if (!file_exists($dirname)) {
496            return false;
497        }
498        // Get directory information
499        $file_information = stat($dirname);
500        // Add directory to processed data
501        $this->numDirectories++;
502        $activeDir             =& $this->directories[];
503        $activeDir['name']     = $dirname;
504        $activeDir['mode']     = $file_information['mode'];
505        $activeDir['time']     = $file_information['time'];
506        $activeDir['user_id']  = $file_information['uid'];
507        $activeDir['group_id'] = $file_information['gid'];
508        $activeDir['checksum'] = $checksum;
509
510        return true;
511    }
512
513    /**
514     * Add a file to the tar archive
515     *
516     * @param  string  $filename
517     * @param  boolean $binary Binary file?
518     * @return bool
519     */
520    public function addFile($filename, $binary = false)
521    {
522        // Make sure the file we are adding exists!
523        if (!file_exists($filename)) {
524            return false;
525        }
526        // Make sure there are no other files in the archive that have this same filename
527        if ($this->containsFile($filename)) {
528            return false;
529        }
530        // Get file information
531        $file_information = stat($filename);
532        // Read in the file's contents
533        if (!$binary) {
534            $fp = fopen($filename, 'r');
535        } else {
536            $fp = fopen($filename, 'rb');
537        }
538        $file_contents = fread($fp, filesize($filename));
539        fclose($fp);
540        // Add file to processed data
541        $this->numFiles++;
542        $activeFile               =& $this->files[];
543        $activeFile['name']       = $filename;
544        $activeFile['mode']       = $file_information['mode'];
545        $activeFile['user_id']    = $file_information['uid'];
546        $activeFile['group_id']   = $file_information['gid'];
547        $activeFile['size']       = $file_information['size'];
548        $activeFile['time']       = $file_information['mtime'];
549        $activeFile['checksum']   = isset($checksum) ? $checksum : '';
550        $activeFile['user_name']  = '';
551        $activeFile['group_name'] = '';
552        $activeFile['file']       = trim($file_contents);
553
554        return true;
555    }
556
557    /**
558     * Remove a file from the tar archive
559     *
560     * @param  string $filename
561     * @return bool
562     */
563    public function removeFile($filename)
564    {
565        if ($this->numFiles > 0) {
566            foreach ($this->files as $key => $information) {
567                if ($information['name'] == $filename) {
568                    $this->numFiles--;
569                    unset($this->files[$key]);
570
571                    return true;
572                }
573            }
574        }
575
576        return false;
577    }
578
579    /**
580     * Remove a directory from the tar archive
581     *
582     * @param  string $dirname
583     * @return bool
584     */
585    public function removeDirectory($dirname)
586    {
587        if ($this->numDirectories > 0) {
588            foreach ($this->directories as $key => $information) {
589                if ($information['name'] == $dirname) {
590                    $this->numDirectories--;
591                    unset($this->directories[$key]);
592
593                    return true;
594                }
595            }
596        }
597
598        return false;
599    }
600
601    /**
602     * Write the currently loaded tar archive to disk
603     *
604     * @return bool
605     */
606    public function saveTar()
607    {
608        if (!$this->filename) {
609            return false;
610        }
611        // Write tar to current file using specified gzip compression
612        $this->toTar($this->filename, $this->isGzipped);
613
614        return true;
615    }
616
617    /**
618     * Saves tar archive to a different file than the current file
619     *
620     * @param  string $filename
621     * @param  bool   $useGzip Use GZ compression?
622     * @return bool
623     */
624    public function toTar($filename, $useGzip)
625    {
626        if (!$filename) {
627            return false;
628        }
629        // Encode processed files into TAR file format
630        $this->__generateTar();
631        // GZ Compress the data if we need to
632        if ($useGzip) {
633            // Make sure we have gzip support
634            if (!function_exists('gzencode')) {
635                return false;
636            }
637            $file = gzencode($this->tar_file);
638        } else {
639            $file = $this->tar_file;
640        }
641        // Write the TAR file
642        $fp = fopen($filename, 'wb');
643        fwrite($fp, $file);
644        fclose($fp);
645
646        return true;
647    }
648
649    /**
650     * Sends tar archive to stdout
651     *
652     * @param  string $filename
653     * @param  bool   $useGzip Use GZ compression?
654     * @return string
655     */
656    public function toTarOutput($filename, $useGzip)
657    {
658        if (!$filename) {
659            return false;
660        }
661        // Encode processed files into TAR file format
662        $this->__generateTar();
663        // GZ Compress the data if we need to
664        if ($useGzip) {
665            // Make sure we have gzip support
666            if (!function_exists('gzencode')) {
667                return false;
668            }
669            $file = gzencode($this->tar_file);
670        } else {
671            $file = $this->tar_file;
672        }
673
674        return $file;
675    }
676}
677