001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one or more
003 *  contributor license agreements.  See the NOTICE file distributed with
004 *  this work for additional information regarding copyright ownership.
005 *  The ASF licenses this file to You under the Apache License, Version 2.0
006 *  (the "License"); you may not use this file except in compliance with
007 *  the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 *  Unless required by applicable law or agreed to in writing, software
012 *  distributed under the License is distributed on an "AS IS" BASIS,
013 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 *  See the License for the specific language governing permissions and
015 *  limitations under the License.
016 *
017 */
018
019/*
020 * This package is based on the work done by Timothy Gerard Endres
021 * (time@ice.com) to whom the Ant project is very grateful for his great code.
022 */
023
024package org.apache.activemq.console.command.store.tar;
025
026import java.io.File;
027import java.util.Date;
028import java.util.Locale;
029
030/**
031 * This class represents an entry in a Tar archive. It consists
032 * of the entry's header, as well as the entry's File. Entries
033 * can be instantiated in one of three ways, depending on how
034 * they are to be used.
035 * <p>
036 * TarEntries that are created from the header bytes read from
037 * an archive are instantiated with the TarEntry( byte[] )
038 * constructor. These entries will be used when extracting from
039 * or listing the contents of an archive. These entries have their
040 * header filled in using the header bytes. They also set the File
041 * to null, since they reference an archive entry not a file.
042 * <p>
043 * TarEntries that are created from Files that are to be written
044 * into an archive are instantiated with the TarEntry( File )
045 * constructor. These entries have their header filled in using
046 * the File's information. They also keep a reference to the File
047 * for convenience when writing entries.
048 * <p>
049 * Finally, TarEntries can be constructed from nothing but a name.
050 * This allows the programmer to construct the entry by hand, for
051 * instance when only an InputStream is available for writing to
052 * the archive, and the header information is constructed from
053 * other information. In this case the header fields are set to
054 * defaults and the File is set to null.
055 *
056 * <p>
057 * The C structure for a Tar Entry's header is:
058 * <pre>
059 * struct header {
060 * char name[NAMSIZ];
061 * char mode[8];
062 * char uid[8];
063 * char gid[8];
064 * char size[12];
065 * char mtime[12];
066 * char chksum[8];
067 * char linkflag;
068 * char linkname[NAMSIZ];
069 * char magic[8];
070 * char uname[TUNMLEN];
071 * char gname[TGNMLEN];
072 * char devmajor[8];
073 * char devminor[8];
074 * } header;
075 * </pre>
076 *
077 */
078
079public class TarEntry implements TarConstants {
080    /** The entry's name. */
081    private StringBuffer name;
082
083    /** The entry's permission mode. */
084    private int mode;
085
086    /** The entry's user id. */
087    private int userId;
088
089    /** The entry's group id. */
090    private int groupId;
091
092    /** The entry's size. */
093    private long size;
094
095    /** The entry's modification time. */
096    private long modTime;
097
098    /** The entry's link flag. */
099    private byte linkFlag;
100
101    /** The entry's link name. */
102    private StringBuffer linkName;
103
104    /** The entry's magic tag. */
105    private StringBuffer magic;
106
107    /** The entry's user name. */
108    private StringBuffer userName;
109
110    /** The entry's group name. */
111    private StringBuffer groupName;
112
113    /** The entry's major device number. */
114    private int devMajor;
115
116    /** The entry's minor device number. */
117    private int devMinor;
118
119    /** The entry's file reference */
120    private File file;
121
122    /** Maximum length of a user's name in the tar file */
123    public static final int MAX_NAMELEN = 31;
124
125    /** Default permissions bits for directories */
126    public static final int DEFAULT_DIR_MODE = 040755;
127
128    /** Default permissions bits for files */
129    public static final int DEFAULT_FILE_MODE = 0100644;
130
131    /** Convert millis to seconds */
132    public static final int MILLIS_PER_SECOND = 1000;
133
134    /**
135     * Construct an empty entry and prepares the header values.
136     */
137    private TarEntry () {
138        this.magic = new StringBuffer(TMAGIC);
139        this.name = new StringBuffer();
140        this.linkName = new StringBuffer();
141
142        String user = System.getProperty("user.name", "");
143
144        if (user.length() > MAX_NAMELEN) {
145            user = user.substring(0, MAX_NAMELEN);
146        }
147
148        this.userId = 0;
149        this.groupId = 0;
150        this.userName = new StringBuffer(user);
151        this.groupName = new StringBuffer("");
152        this.file = null;
153    }
154
155    /**
156     * Construct an entry with only a name. This allows the programmer
157     * to construct the entry's header "by hand". File is set to null.
158     *
159     * @param name the entry name
160     */
161    public TarEntry(String name) {
162        this(name, false);
163    }
164
165    /**
166     * Construct an entry with only a name. This allows the programmer
167     * to construct the entry's header "by hand". File is set to null.
168     *
169     * @param name the entry name
170     * @param preserveLeadingSlashes whether to allow leading slashes
171     * in the name.
172     */
173    public TarEntry(String name, boolean preserveLeadingSlashes) {
174        this();
175
176        name = normalizeFileName(name, preserveLeadingSlashes);
177        boolean isDir = name.endsWith("/");
178
179        this.devMajor = 0;
180        this.devMinor = 0;
181        this.name = new StringBuffer(name);
182        this.mode = isDir ? DEFAULT_DIR_MODE : DEFAULT_FILE_MODE;
183        this.linkFlag = isDir ? LF_DIR : LF_NORMAL;
184        this.userId = 0;
185        this.groupId = 0;
186        this.size = 0;
187        this.modTime = (new Date()).getTime() / MILLIS_PER_SECOND;
188        this.linkName = new StringBuffer("");
189        this.userName = new StringBuffer("");
190        this.groupName = new StringBuffer("");
191        this.devMajor = 0;
192        this.devMinor = 0;
193
194    }
195
196    /**
197     * Construct an entry with a name and a link flag.
198     *
199     * @param name the entry name
200     * @param linkFlag the entry link flag.
201     */
202    public TarEntry(String name, byte linkFlag) {
203        this(name);
204        this.linkFlag = linkFlag;
205        if (linkFlag == LF_GNUTYPE_LONGNAME) {
206            magic = new StringBuffer(GNU_TMAGIC);
207        }
208    }
209
210    /**
211     * Construct an entry for a file. File is set to file, and the
212     * header is constructed from information from the file.
213     *
214     * @param file The file that the entry represents.
215     */
216    public TarEntry(File file) {
217        this();
218
219        this.file = file;
220
221        String fileName = normalizeFileName(file.getPath(), false);
222        this.linkName = new StringBuffer("");
223        this.name = new StringBuffer(fileName);
224
225        if (file.isDirectory()) {
226            this.mode = DEFAULT_DIR_MODE;
227            this.linkFlag = LF_DIR;
228
229            int nameLength = name.length();
230            if (nameLength == 0 || name.charAt(nameLength - 1) != '/') {
231                this.name.append("/");
232            }
233            this.size = 0;
234        } else {
235            this.mode = DEFAULT_FILE_MODE;
236            this.linkFlag = LF_NORMAL;
237            this.size = file.length();
238        }
239
240        this.modTime = file.lastModified() / MILLIS_PER_SECOND;
241        this.devMajor = 0;
242        this.devMinor = 0;
243    }
244
245    /**
246     * Construct an entry from an archive's header bytes. File is set
247     * to null.
248     *
249     * @param headerBuf The header bytes from a tar archive entry.
250     */
251    public TarEntry(byte[] headerBuf) {
252        this();
253        parseTarHeader(headerBuf);
254    }
255
256    /**
257     * Determine if the two entries are equal. Equality is determined
258     * by the header names being equal.
259     *
260     * @param it Entry to be checked for equality.
261     * @return True if the entries are equal.
262     */
263    public boolean equals(TarEntry it) {
264        return getName().equals(it.getName());
265    }
266
267    /**
268     * Determine if the two entries are equal. Equality is determined
269     * by the header names being equal.
270     *
271     * @param it Entry to be checked for equality.
272     * @return True if the entries are equal.
273     */
274    public boolean equals(Object it) {
275        if (it == null || getClass() != it.getClass()) {
276            return false;
277        }
278        return equals((TarEntry) it);
279    }
280
281    /**
282     * Hashcodes are based on entry names.
283     *
284     * @return the entry hashcode
285     */
286    public int hashCode() {
287        return getName().hashCode();
288    }
289
290    /**
291     * Determine if the given entry is a descendant of this entry.
292     * Descendancy is determined by the name of the descendant
293     * starting with this entry's name.
294     *
295     * @param desc Entry to be checked as a descendent of this.
296     * @return True if entry is a descendant of this.
297     */
298    public boolean isDescendent(TarEntry desc) {
299        return desc.getName().startsWith(getName());
300    }
301
302    /**
303     * Get this entry's name.
304     *
305     * @return This entry's name.
306     */
307    public String getName() {
308        return name.toString();
309    }
310
311    /**
312     * Set this entry's name.
313     *
314     * @param name This entry's new name.
315     */
316    public void setName(String name) {
317        this.name = new StringBuffer(normalizeFileName(name, false));
318    }
319
320    /**
321     * Set the mode for this entry
322     *
323     * @param mode the mode for this entry
324     */
325    public void setMode(int mode) {
326        this.mode = mode;
327    }
328
329    /**
330     * Get this entry's link name.
331     *
332     * @return This entry's link name.
333     */
334    public String getLinkName() {
335        return linkName.toString();
336    }
337
338    /**
339     * Get this entry's user id.
340     *
341     * @return This entry's user id.
342     */
343    public int getUserId() {
344        return userId;
345    }
346
347    /**
348     * Set this entry's user id.
349     *
350     * @param userId This entry's new user id.
351     */
352    public void setUserId(int userId) {
353        this.userId = userId;
354    }
355
356    /**
357     * Get this entry's group id.
358     *
359     * @return This entry's group id.
360     */
361    public int getGroupId() {
362        return groupId;
363    }
364
365    /**
366     * Set this entry's group id.
367     *
368     * @param groupId This entry's new group id.
369     */
370    public void setGroupId(int groupId) {
371        this.groupId = groupId;
372    }
373
374    /**
375     * Get this entry's user name.
376     *
377     * @return This entry's user name.
378     */
379    public String getUserName() {
380        return userName.toString();
381    }
382
383    /**
384     * Set this entry's user name.
385     *
386     * @param userName This entry's new user name.
387     */
388    public void setUserName(String userName) {
389        this.userName = new StringBuffer(userName);
390    }
391
392    /**
393     * Get this entry's group name.
394     *
395     * @return This entry's group name.
396     */
397    public String getGroupName() {
398        return groupName.toString();
399    }
400
401    /**
402     * Set this entry's group name.
403     *
404     * @param groupName This entry's new group name.
405     */
406    public void setGroupName(String groupName) {
407        this.groupName = new StringBuffer(groupName);
408    }
409
410    /**
411     * Convenience method to set this entry's group and user ids.
412     *
413     * @param userId This entry's new user id.
414     * @param groupId This entry's new group id.
415     */
416    public void setIds(int userId, int groupId) {
417        setUserId(userId);
418        setGroupId(groupId);
419    }
420
421    /**
422     * Convenience method to set this entry's group and user names.
423     *
424     * @param userName This entry's new user name.
425     * @param groupName This entry's new group name.
426     */
427    public void setNames(String userName, String groupName) {
428        setUserName(userName);
429        setGroupName(groupName);
430    }
431
432    /**
433     * Set this entry's modification time. The parameter passed
434     * to this method is in "Java time".
435     *
436     * @param time This entry's new modification time.
437     */
438    public void setModTime(long time) {
439        modTime = time / MILLIS_PER_SECOND;
440    }
441
442    /**
443     * Set this entry's modification time.
444     *
445     * @param time This entry's new modification time.
446     */
447    public void setModTime(Date time) {
448        modTime = time.getTime() / MILLIS_PER_SECOND;
449    }
450
451    /**
452     * Set this entry's modification time.
453     *
454     * @return time This entry's new modification time.
455     */
456    public Date getModTime() {
457        return new Date(modTime * MILLIS_PER_SECOND);
458    }
459
460    /**
461     * Get this entry's file.
462     *
463     * @return This entry's file.
464     */
465    public File getFile() {
466        return file;
467    }
468
469    /**
470     * Get this entry's mode.
471     *
472     * @return This entry's mode.
473     */
474    public int getMode() {
475        return mode;
476    }
477
478    /**
479     * Get this entry's file size.
480     *
481     * @return This entry's file size.
482     */
483    public long getSize() {
484        return size;
485    }
486
487    /**
488     * Set this entry's file size.
489     *
490     * @param size This entry's new file size.
491     */
492    public void setSize(long size) {
493        this.size = size;
494    }
495
496
497    /**
498     * Indicate if this entry is a GNU long name block
499     *
500     * @return true if this is a long name extension provided by GNU tar
501     */
502    public boolean isGNULongNameEntry() {
503        return linkFlag == LF_GNUTYPE_LONGNAME
504                           && name.toString().equals(GNU_LONGLINK);
505    }
506
507    /**
508     * Return whether or not this entry represents a directory.
509     *
510     * @return True if this entry is a directory.
511     */
512    public boolean isDirectory() {
513        if (file != null) {
514            return file.isDirectory();
515        }
516
517        if (linkFlag == LF_DIR) {
518            return true;
519        }
520
521        if (getName().endsWith("/")) {
522            return true;
523        }
524
525        return false;
526    }
527
528    /**
529     * If this entry represents a file, and the file is a directory, return
530     * an array of TarEntries for this entry's children.
531     *
532     * @return An array of TarEntry's for this entry's children.
533     */
534    public TarEntry[] getDirectoryEntries() {
535        if (file == null || !file.isDirectory()) {
536            return new TarEntry[0];
537        }
538
539        String[]   list = file.list();
540        TarEntry[] result = new TarEntry[list.length];
541
542        for (int i = 0; i < list.length; ++i) {
543            result[i] = new TarEntry(new File(file, list[i]));
544        }
545
546        return result;
547    }
548
549    /**
550     * Write an entry's header information to a header buffer.
551     *
552     * @param outbuf The tar entry header buffer to fill in.
553     */
554    public void writeEntryHeader(byte[] outbuf) {
555        int offset = 0;
556
557        offset = TarUtils.getNameBytes(name, outbuf, offset, NAMELEN);
558        offset = TarUtils.getOctalBytes(mode, outbuf, offset, MODELEN);
559        offset = TarUtils.getOctalBytes(userId, outbuf, offset, UIDLEN);
560        offset = TarUtils.getOctalBytes(groupId, outbuf, offset, GIDLEN);
561        offset = TarUtils.getLongOctalBytes(size, outbuf, offset, SIZELEN);
562        offset = TarUtils.getLongOctalBytes(modTime, outbuf, offset, MODTIMELEN);
563
564        int csOffset = offset;
565
566        for (int c = 0; c < CHKSUMLEN; ++c) {
567            outbuf[offset++] = (byte) ' ';
568        }
569
570        outbuf[offset++] = linkFlag;
571        offset = TarUtils.getNameBytes(linkName, outbuf, offset, NAMELEN);
572        offset = TarUtils.getNameBytes(magic, outbuf, offset, MAGICLEN);
573        offset = TarUtils.getNameBytes(userName, outbuf, offset, UNAMELEN);
574        offset = TarUtils.getNameBytes(groupName, outbuf, offset, GNAMELEN);
575        offset = TarUtils.getOctalBytes(devMajor, outbuf, offset, DEVLEN);
576        offset = TarUtils.getOctalBytes(devMinor, outbuf, offset, DEVLEN);
577
578        while (offset < outbuf.length) {
579            outbuf[offset++] = 0;
580        }
581
582        long chk = TarUtils.computeCheckSum(outbuf);
583
584        TarUtils.getCheckSumOctalBytes(chk, outbuf, csOffset, CHKSUMLEN);
585    }
586
587    /**
588     * Parse an entry's header information from a header buffer.
589     *
590     * @param header The tar entry header buffer to get information from.
591     */
592    public void parseTarHeader(byte[] header) {
593        int offset = 0;
594
595        name = TarUtils.parseName(header, offset, NAMELEN);
596        offset += NAMELEN;
597        mode = (int) TarUtils.parseOctal(header, offset, MODELEN);
598        offset += MODELEN;
599        userId = (int) TarUtils.parseOctal(header, offset, UIDLEN);
600        offset += UIDLEN;
601        groupId = (int) TarUtils.parseOctal(header, offset, GIDLEN);
602        offset += GIDLEN;
603        size = TarUtils.parseOctal(header, offset, SIZELEN);
604        offset += SIZELEN;
605        modTime = TarUtils.parseOctal(header, offset, MODTIMELEN);
606        offset += MODTIMELEN;
607        offset += CHKSUMLEN;
608        linkFlag = header[offset++];
609        linkName = TarUtils.parseName(header, offset, NAMELEN);
610        offset += NAMELEN;
611        magic = TarUtils.parseName(header, offset, MAGICLEN);
612        offset += MAGICLEN;
613        userName = TarUtils.parseName(header, offset, UNAMELEN);
614        offset += UNAMELEN;
615        groupName = TarUtils.parseName(header, offset, GNAMELEN);
616        offset += GNAMELEN;
617        devMajor = (int) TarUtils.parseOctal(header, offset, DEVLEN);
618        offset += DEVLEN;
619        devMinor = (int) TarUtils.parseOctal(header, offset, DEVLEN);
620    }
621
622    /**
623     * Strips Windows' drive letter as well as any leading slashes,
624     * turns path separators into forward slahes.
625     */
626    private static String normalizeFileName(String fileName,
627                                            boolean preserveLeadingSlashes) {
628        String osname = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
629
630        if (osname != null) {
631
632            // Strip off drive letters!
633            // REVIEW Would a better check be "(File.separator == '\')"?
634
635            if (osname.startsWith("windows")) {
636                if (fileName.length() > 2) {
637                    char ch1 = fileName.charAt(0);
638                    char ch2 = fileName.charAt(1);
639
640                    if (ch2 == ':'
641                        && ((ch1 >= 'a' && ch1 <= 'z')
642                            || (ch1 >= 'A' && ch1 <= 'Z'))) {
643                        fileName = fileName.substring(2);
644                    }
645                }
646            } else if (osname.indexOf("netware") > -1) {
647                int colon = fileName.indexOf(':');
648                if (colon != -1) {
649                    fileName = fileName.substring(colon + 1);
650                }
651            }
652        }
653
654        fileName = fileName.replace(File.separatorChar, '/');
655
656        // No absolute pathnames
657        // Windows (and Posix?) paths can start with "\\NetworkDrive\",
658        // so we loop on starting /'s.
659        while (!preserveLeadingSlashes && fileName.startsWith("/")) {
660            fileName = fileName.substring(1);
661        }
662        return fileName;
663    }
664}