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 }