View Javadoc
1   /*
2    * Copyright (C) 2005-2015 Schlichtherle IT Services.
3    * All rights reserved. Use is subject to license terms.
4    */
5   package net.java.truevfs.comp.tardriver;
6   
7   import edu.umd.cs.findbugs.annotations.CreatesObligation;
8   import java.io.DataInputStream;
9   import java.io.EOFException;
10  import java.io.IOException;
11  import java.io.InputStream;
12  import java.io.OutputStream;
13  import java.io.PushbackInputStream;
14  import java.nio.channels.SeekableByteChannel;
15  import java.nio.file.NoSuchFileException;
16  import java.util.Arrays;
17  import java.util.Collections;
18  import java.util.Iterator;
19  import java.util.LinkedHashMap;
20  import java.util.Map;
21  import java.util.Objects;
22  import javax.annotation.CheckForNull;
23  import javax.annotation.WillNotClose;
24  import javax.annotation.concurrent.NotThreadSafe;
25  import net.java.truecommons.cio.AbstractInputSocket;
26  import net.java.truecommons.cio.Entry;
27  import net.java.truecommons.cio.Entry.Type;
28  import static net.java.truecommons.cio.Entry.Type.DIRECTORY;
29  import static net.java.truecommons.cio.Entry.Type.FILE;
30  import net.java.truecommons.cio.InputService;
31  import net.java.truecommons.cio.InputSocket;
32  import net.java.truecommons.cio.IoBuffer;
33  import net.java.truecommons.cio.IoBufferPool;
34  import net.java.truecommons.cio.OutputSocket;
35  import net.java.truecommons.io.Source;
36  import net.java.truecommons.io.Streams;
37  import net.java.truecommons.shed.ExceptionBuilder;
38  import static net.java.truecommons.shed.HashMaps.OVERHEAD_SIZE;
39  import static net.java.truecommons.shed.HashMaps.initialCapacity;
40  import net.java.truecommons.shed.SuppressedExceptionBuilder;
41  import net.java.truevfs.kernel.spec.FsArchiveDriver;
42  import net.java.truevfs.kernel.spec.FsModel;
43  import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
44  import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
45  import static org.apache.commons.compress.archivers.tar.TarConstants.DEFAULT_BLKSIZE;
46  import static org.apache.commons.compress.archivers.tar.TarConstants.DEFAULT_RCDSIZE;
47  import static org.apache.commons.compress.archivers.tar.TarConstants.GIDLEN;
48  import static org.apache.commons.compress.archivers.tar.TarConstants.MODELEN;
49  import static org.apache.commons.compress.archivers.tar.TarConstants.MODTIMELEN;
50  import static org.apache.commons.compress.archivers.tar.TarConstants.NAMELEN;
51  import static org.apache.commons.compress.archivers.tar.TarConstants.SIZELEN;
52  import static org.apache.commons.compress.archivers.tar.TarConstants.UIDLEN;
53  import org.apache.commons.compress.archivers.tar.TarUtils;
54  
55  /**
56   * An input service for reading TAR files.
57   * <p>
58   * Note that the constructor of this class extracts each entry in the archive
59   * to a temporary file!
60   * This may be very time and space consuming for large archives, but is
61   * the fastest implementation for subsequent random access, since there
62   * is no way the archive driver could predict the client application's
63   * behavior.
64   *
65   * @see    TarOutputService
66   * @author Christian Schlichtherle
67   */
68  @NotThreadSafe
69  public final class TarInputService
70  implements InputService<TarDriverEntry> {
71  
72      private static final byte[] NULL_RECORD = new byte[DEFAULT_RCDSIZE];
73  
74      private static final int CHECKSUM_OFFSET
75              = NAMELEN + MODELEN + UIDLEN + GIDLEN + SIZELEN + MODTIMELEN;
76  
77      /** Maps entry names to I/O pool entries. */
78      private final Map<String, TarDriverEntry>
79              entries = new LinkedHashMap<>(initialCapacity(OVERHEAD_SIZE));
80  
81      private final TarDriver driver;
82  
83      @CreatesObligation
84      public TarInputService(
85              final FsModel model,
86              final Source source,
87              final TarDriver driver)
88      throws EOFException, IOException {
89          Objects.requireNonNull(model);
90          this.driver = Objects.requireNonNull(driver);
91          try (final InputStream in = source.stream()) {
92              try {
93                  unpack(newValidatedTarArchiveInputStream(in));
94              } catch (final Throwable ex) {
95                  try {
96                      close0();
97                  } catch (final Throwable ex2) {
98                      ex.addSuppressed(ex2);
99                  }
100                 throw ex;
101             }
102         }
103     }
104 
105     private void unpack(final @WillNotClose TarArchiveInputStream tain)
106     throws IOException {
107         final TarDriver driver = this.driver;
108         final IoBufferPool pool = driver.getPool();
109         for (   TarArchiveEntry tinEntry;
110                 null != (tinEntry = tain.getNextTarEntry()); ) {
111             final String name = name(tinEntry);
112             TarDriverEntry entry = entries.get(name);
113             if (null != entry)
114                 entry.release();
115             entry = driver.newEntry(name, tinEntry);
116             if (!tinEntry.isDirectory()) {
117                 final IoBuffer buffer = pool.allocate();
118                 entry.setBuffer(buffer);
119                 try {
120                     try (OutputStream out = buffer.output().stream(null)) {
121                         Streams.cat(tain, out);
122                     }
123                 } catch (final Throwable ex) {
124                     try {
125                         buffer.release();
126                     } catch (final Throwable ex2) {
127                         ex.addSuppressed(ex2);
128                     }
129                     throw ex;
130                 }
131             }
132             entries.put(name, entry);
133         }
134     }
135 
136     private static String name(final TarArchiveEntry entry) {
137         final String name = entry.getName();
138         final Type type = entry.isDirectory() ? DIRECTORY : FILE;
139         return FsArchiveDriver.normalize(name, type);
140     }
141 
142     /**
143      * Returns a newly created and validated {@link TarArchiveInputStream}.
144      * This method performs a simple validation by computing the checksum
145      * for the first record only.
146      * This method is required because the {@code TarArchiveInputStream}
147      * unfortunately does not do any validation!
148      *
149      * @param  in the stream to read from.
150      * @return A stream which holds all the data {@code in} did.
151      * @throws EOFException on unexpected end-of-file.
152      * @throws IOException on any I/O error.
153      */
154     private TarArchiveInputStream newValidatedTarArchiveInputStream(
155             final @WillNotClose InputStream in)
156     throws EOFException, IOException {
157         final byte[] buf = new byte[DEFAULT_RCDSIZE];
158         final InputStream vin = readAhead(in, buf);
159         // If the record is the null record, the TAR file is empty and we're
160         // done with validating.
161         if (!Arrays.equals(buf, NULL_RECORD)) {
162             final long expected;
163             try {
164                 expected = TarUtils.parseOctal(buf, CHECKSUM_OFFSET, 8);
165             } catch (final IllegalArgumentException ex) {
166                 throw new TarException("Invalid initial record in TAR file!", ex);
167             }
168             for (int i = 0; i < 8; i++)
169                 buf[CHECKSUM_OFFSET + i] = ' ';
170             final long actual = TarUtils.computeCheckSum(buf);
171             if (expected != actual)
172                 throw new TarException(
173                         "Invalid initial record in TAR file: Expected / actual checksum : "
174                         + expected + " / " + actual + "!");
175         }
176         return new TarArchiveInputStream(   vin,
177                                             DEFAULT_BLKSIZE,
178                                             DEFAULT_RCDSIZE,
179                                             driver.getEncoding());
180     }
181 
182     /**
183      * Fills {@code buf} with data from the given input stream and
184      * returns an input stream from which you can still read all data,
185      * including the data in buf.
186      *
187      * @param  in The stream to read from.
188      * @param  buf The buffer to fill entirely with data.
189      * @return A stream which holds all the data {@code in} did.
190      * @throws EOFException on unexpected end-of-file.
191      * @throws IOException on any I/O error.
192      */
193     private static InputStream readAhead(
194             final @WillNotClose InputStream in,
195             final byte[] buf)
196     throws EOFException, IOException {
197         if (in.markSupported()) {
198             in.mark(buf.length);
199             new DataInputStream(in).readFully(buf);
200             in.reset();
201             return in;
202         } else {
203             final PushbackInputStream
204                     pin = new PushbackInputStream(in, buf.length);
205             new DataInputStream(pin).readFully(buf);
206             pin.unread(buf);
207             return pin;
208         }
209     }
210 
211     @Override
212     public int size() {
213         return entries.size();
214     }
215 
216     @Override
217     public Iterator<TarDriverEntry> iterator() {
218         return Collections.unmodifiableCollection(entries.values()).iterator();
219     }
220 
221     @Override
222     public @CheckForNull TarDriverEntry entry(String name) {
223         return entries.get(name);
224     }
225 
226     @Override
227     public InputSocket<TarDriverEntry> input(final String name) {
228         Objects.requireNonNull(name);
229 
230         final class Input extends AbstractInputSocket<TarDriverEntry> {
231             @Override
232             public TarDriverEntry target() throws IOException {
233                 final TarDriverEntry entry = entry(name);
234                 if (null == entry)
235                     throw new NoSuchFileException(name, null, "Entry not found!");
236                 if (entry.isDirectory())
237                     throw new NoSuchFileException(name, null, "Cannot read directory entries!");
238                 return entry;
239             }
240 
241             @Override
242             public InputStream stream(OutputSocket<? extends Entry> peer)
243             throws IOException {
244                 return socket().stream(peer);
245             }
246 
247             @Override
248             public SeekableByteChannel channel(OutputSocket<? extends Entry> peer)
249             throws IOException {
250                 return socket().channel(peer);
251             }
252 
253             InputSocket<? extends IoBuffer> socket() throws IOException {
254                 return target().getBuffer().input();
255             }
256         } // Input
257 
258         return new Input();
259     }
260 
261     @Override
262     public void close() throws IOException {
263         close0();
264     }
265 
266     private void close0() throws IOException {
267         final ExceptionBuilder<IOException, IOException>
268                     builder = new SuppressedExceptionBuilder<>();
269         for (final Iterator<TarDriverEntry> i = entries.values().iterator();
270                 i.hasNext();
271                 i.remove()) {
272             try {
273                 i.next().release();
274             } catch (final IOException ex) {
275                 builder.warn(ex);
276             }
277         }
278         builder.check();
279     }
280 }