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    
024    package org.apache.activemq.console.command.store.tar;
025    
026    import java.io.File;
027    import java.util.Date;
028    import 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    
079    public 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    }