001    /*
002     * This file is part of muCommander, http://www.mucommander.com
003     * Copyright (C) 2002-2008 Maxence Bernard
004     *
005     * muCommander is free software; you can redistribute it and/or modify
006     * it under the terms of the GNU General Public License as published by
007     * the Free Software Foundation; either version 3 of the License, or
008     * (at your option) any later version.
009     *
010     * muCommander is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    
019    package com.mucommander.file.impl.ar;
020    
021    import com.mucommander.file.AbstractFile;
022    import com.mucommander.file.AbstractROArchiveFile;
023    import com.mucommander.file.ArchiveEntry;
024    import com.mucommander.io.ByteLimitInputStream;
025    import com.mucommander.io.StreamUtils;
026    
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.util.Vector;
030    
031    /**
032     * ArArchiveFile provides read-only access to archives in the unix AR format. Both the BSD and GNU variants (which adds
033     * support for extended filenames) are supported.
034     *
035     * @see com.mucommander.file.impl.ar.ArFormatProvider
036     * @author Maxence Bernard
037     */
038    public class ArArchiveFile extends AbstractROArchiveFile {
039    
040        /** GNU variant: extended filenames contained in the special // entry's data */
041        private byte gnuExtendedNames[];
042    
043    
044        /**
045         * Creates a ArArchiveFile around the given file.
046         */
047        public ArArchiveFile(AbstractFile file) {
048            super(file);
049        }
050    
051    
052        /**
053         * Skips the global header: "!<arch>" string followed by LF char (8 characters in total).
054         */
055        private static void skipGlobalHeader(InputStream in) throws IOException {
056            StreamUtils.skipFully(in, 8);
057        }
058    
059    
060        /**
061         * Reads the next file header and returns an ArchiveEntry representing the entry.
062         */
063        private ArchiveEntry getNextEntry(InputStream in) throws IOException {
064            byte fileHeader[] = new byte[60];
065    
066            try {
067                // Fully read the 60 file header bytes. If it cannot be read, it most likely means we've reached
068                // the end of the archive.
069                StreamUtils.readFully(in, fileHeader);
070            }
071            catch(IOException e) {
072                return null;
073            }
074    
075            try {
076                // Read the 16 filename characters and trim string to remove any trailing white space
077                String name = new String(fileHeader, 0, 16).trim();
078    
079                // Read the 12 file date characters, trim string to remove any trailing white space
080                // and parse date as a long.
081                // If the entry is the special // GNU one (see below), date is null and thus should not be parsed
082                // (would throw a NumberFormatException)
083                long date = name.equals("//")?0:Long.parseLong(new String(fileHeader, 16, 12).trim()) * 1000;
084    
085                // No use for file's Owner ID, Group ID and mode at the moment, skip them
086    
087                // Read the 10 file size characters, trim string to remove any trailing white space
088                // and parse size as a long
089                long size = Long.parseLong(new String(fileHeader, 48, 10).trim());
090    
091                // BSD variant : BSD ar store extended filenames by placing the string "#1/" followed by the file name length
092                // in the file name field, and appending the real filename to the file header.
093                if(name.startsWith("#1/")) {
094                    // Read extended name
095                    int extendedNameLength = Integer.parseInt(name.substring(3, name.length()));
096                    name = new String(StreamUtils.readFully(in, new byte[extendedNameLength])).trim();
097                    // Decrease remaining file size
098                    size -= extendedNameLength;
099                }
100                // GNU variant: GNU ar stores multiple extended filenames in the data section of a file with the name "//",
101                // this record is referred to by future headers. A header references an extended filename by storing a "/"
102                // followed by a decimal offset to the start of the filename in the extended filename data section.
103                // This entry appears first in the archive, i.e. before any other entries.
104                else if(name.equals("//")) {
105                    this.gnuExtendedNames = StreamUtils.readFully(in, new byte[(int)size]);
106    
107                    // Skip one padding byte if size is odd
108                    if(size%2!=0)
109                        StreamUtils.skipFully(in, 1);
110    
111                    // Don't return this entry which should not be visible, but recurse to return next entry instead
112                    return getNextEntry(in);
113                }
114                // GNU variant: entry with an extended name, look up extended name in // entry
115                else if(this.gnuExtendedNames!=null && name.startsWith("/")) {
116                    int off = Integer.parseInt(name.substring(1, name.length()));
117                    name = "";
118                    byte b;
119                    while((b=this.gnuExtendedNames[off++])!='/')
120                        name += (char)b;
121                }
122    
123                return new ArchiveEntry(name, false, date, size);
124            }
125            // Re-throw IOException
126            catch(IOException e) {
127                if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("Caught IOException: "+e);
128    
129                throw e;
130            }
131            // Catch any other exceptions (NumberFormatException for instance) and throw an IOException instead
132            catch(Exception e2) {
133                if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("Caught Exception: "+e2);
134    
135                throw new IOException();
136            }
137        }
138    
139    
140        /**
141         * Skips the current entry's file data, using the given entry's size.
142         */
143        private static void skipEntryData(InputStream in, ArchiveEntry entry) throws IOException {
144            long size = entry.getSize();
145    
146            // Skip file's data, plus 1 padding byte if size is odd
147            StreamUtils.skipFully(in, size + (size%2));
148        }
149    
150        
151        ////////////////////////////////////////
152        // AbstractArchiveFile implementation //
153        ////////////////////////////////////////
154    
155        public Vector getEntries() throws IOException {
156            Vector entries = new Vector();
157            InputStream in = getInputStream();
158            skipGlobalHeader(in);
159    
160            try {
161                ArchiveEntry entry;
162                while((entry = getNextEntry(in))!=null) {
163                    if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("Adding entry "+entry.getName());
164    
165                    skipEntryData(in, entry);
166                    entries.add(entry);
167                }
168            }
169            finally {
170                // Close input stream
171                in.close();
172            }
173    
174            return entries;
175        }
176    
177    
178        public InputStream getEntryInputStream(ArchiveEntry entry) throws IOException {
179            InputStream in = getInputStream();
180            skipGlobalHeader(in);
181    
182            ArchiveEntry currentEntry;
183            while((currentEntry = getNextEntry(in))!=null) {
184                if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("currentEntry="+currentEntry.getName()+" entry="+entry.getName());
185    
186                if(currentEntry.getName().equals(entry.getName())) {
187                    if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("found entry "+entry.getName());
188                   return new ByteLimitInputStream(in, entry.getSize());
189                }
190    
191                skipEntryData(in, currentEntry);
192            }
193    
194            if(com.mucommander.Debug.ON) com.mucommander.Debug.trace("throwing IOException");
195    
196            // Entry not found, should not normally happen
197            throw new IOException();
198        }
199    }