/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file.archive.ar; import com.mucommander.commons.file.archive.ArchiveEntry; import com.mucommander.commons.file.archive.ArchiveEntryIterator; import com.mucommander.commons.io.StreamUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; /** * An <code>ArchiveEntryIterator</code> that iterates through an AR archive. * * @author Maxence Bernard */ class ArArchiveEntryIterator implements ArchiveEntryIterator { private static final Logger LOGGER = LoggerFactory.getLogger(ArArchiveEntryIterator.class); /** InputStream to the the archive file */ private InputStream in; /** The current entry, where the stream is currently positionned */ private ArchiveEntry currentEntry; /** GNU variant: extended filenames contained in the special // entry's data */ private byte gnuExtendedNames[]; /** * Creates a new <code>ArArchiveEntryIterator</code> that parses the given AR <code>InputStream</code>. * The <code>InputStream</code> will be closed by {@link #close()}. * * @param in an AR archive <code>InputStream</code> * @throws IOException if an I/O error occurred while initializing this iterator */ ArArchiveEntryIterator(InputStream in) throws IOException { this.in = in; // Skip the global header: "!<arch>" string followed by LF char (8 characters in total). StreamUtils.skipFully(in, 8); } /** * Reads the next file header and returns an {@link ArchiveEntry} representing the entry. * * @return an ArchiveEntry representing the entry * @throws IOException if an error occurred */ ArchiveEntry getNextEntry() throws IOException { byte fileHeader[] = new byte[60]; try { // Fully read the 60 file header bytes. If it cannot be read, it most likely means we've reached // the end of the archive. StreamUtils.readFully(in, fileHeader); } catch(IOException e) { return null; } try { // Read the 16 filename characters and trim string to remove any trailing white space String name = new String(fileHeader, 0, 16).trim(); // Read the 12 file date characters, trim string to remove any trailing white space // and parse date as a long. // If the entry is the special // GNU one (see below), date is null and thus should not be parsed // (would throw a NumberFormatException) long date = name.equals("//")?0:Long.parseLong(new String(fileHeader, 16, 12).trim()) * 1000; // No use for file's Owner ID, Group ID and mode at the moment, skip them // Read the 10 file size characters, trim string to remove any trailing white space // and parse size as a long long size = Long.parseLong(new String(fileHeader, 48, 10).trim()); // BSD variant : BSD ar store extended filenames by placing the string "#1/" followed by the file name length // in the file name field, and appending the real filename to the file header. if(name.startsWith("#1/")) { // Read extended name int extendedNameLength = Integer.parseInt(name.substring(3, name.length())); name = new String(StreamUtils.readFully(in, new byte[extendedNameLength])).trim(); // Decrease remaining file size size -= extendedNameLength; } // GNU variant: GNU ar stores multiple extended filenames in the data section of a file with the name "//", // this record is referred to by future headers. A header references an extended filename by storing a "/" // followed by a decimal offset to the start of the filename in the extended filename data section. // This entry appears first in the archive, i.e. before any other entries. else if(name.equals("//")) { this.gnuExtendedNames = StreamUtils.readFully(in, new byte[(int)size]); // Skip one padding byte if size is odd if(size%2!=0) StreamUtils.skipFully(in, 1); // Don't return this entry which should not be visible, but recurse to return next entry instead return getNextEntry(); } // GNU variant: entry with an extended name, look up extended name in // entry else if(this.gnuExtendedNames!=null && name.startsWith("/")) { int off = Integer.parseInt(name.substring(1, name.length())); name = ""; byte b; while((b=this.gnuExtendedNames[off++])!='/') name += (char)b; } return new ArchiveEntry(name, false, date, size, true); } // Re-throw IOException catch(IOException e) { LOGGER.info("Caught IOException", e); throw e; } // Catch any other exceptions (NumberFormatException for instance) and throw an IOException instead catch(Exception e2) { LOGGER.info("Caught Exception", e2); throw new IOException(); } } ///////////////////////////////////////// // ArchiveEntryIterator implementation // ///////////////////////////////////////// public ArchiveEntry nextEntry() throws IOException { if(currentEntry!=null) { // Skip the current entry's data, plus 1 padding byte if size is odd long size = currentEntry.getSize(); StreamUtils.skipFully(in, size + (size%2)); } // Get the next entry, if any currentEntry = getNextEntry(); return currentEntry; } public void close() throws IOException { in.close(); } }