ArArchiveOutputStream.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one
  3.  * or more contributor license agreements.  See the NOTICE file
  4.  * distributed with this work for additional information
  5.  * regarding copyright ownership.  The ASF licenses this file
  6.  * to you under the Apache License, Version 2.0 (the
  7.  * "License"); you may not use this file except in compliance
  8.  * with the License.  You may obtain a copy of the License at
  9.  *
  10.  * http://www.apache.org/licenses/LICENSE-2.0
  11.  *
  12.  * Unless required by applicable law or agreed to in writing,
  13.  * software distributed under the License is distributed on an
  14.  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15.  * KIND, either express or implied.  See the License for the
  16.  * specific language governing permissions and limitations
  17.  * under the License.
  18.  */
  19. package org.apache.commons.compress.archivers.ar;

  20. import static java.nio.charset.StandardCharsets.US_ASCII;

  21. import java.io.File;
  22. import java.io.IOException;
  23. import java.io.OutputStream;
  24. import java.nio.file.LinkOption;
  25. import java.nio.file.Path;

  26. import org.apache.commons.compress.archivers.ArchiveOutputStream;
  27. import org.apache.commons.compress.utils.ArchiveUtils;

  28. /**
  29.  * Implements the "ar" archive format as an output stream.
  30.  *
  31.  * @NotThreadSafe
  32.  */
  33. public class ArArchiveOutputStream extends ArchiveOutputStream<ArArchiveEntry> {

  34.     private static final char PAD = '\n';

  35.     private static final char SPACE = ' ';

  36.     /** Fail if a long file name is required in the archive. */
  37.     public static final int LONGFILE_ERROR = 0;

  38.     /** BSD ar extensions are used to store long file names in the archive. */
  39.     public static final int LONGFILE_BSD = 1;

  40.     private long entryOffset;
  41.     private int headerPlus;
  42.     private ArArchiveEntry prevEntry;
  43.     private boolean prevEntryOpen;
  44.     private int longFileMode = LONGFILE_ERROR;

  45.     public ArArchiveOutputStream(final OutputStream out) {
  46.         super(out);
  47.     }

  48.     private String checkLength(final String value, final int max, final String name) throws IOException {
  49.         if (value.length() > max) {
  50.             throw new IOException(name + " too long");
  51.         }
  52.         return value;
  53.     }

  54.     /**
  55.      * Calls finish if necessary, and then closes the OutputStream
  56.      */
  57.     @Override
  58.     public void close() throws IOException {
  59.         try {
  60.             if (!isFinished()) {
  61.                 finish();
  62.             }
  63.         } finally {
  64.             prevEntry = null;
  65.             super.close();
  66.         }
  67.     }

  68.     @Override
  69.     public void closeArchiveEntry() throws IOException {
  70.         checkFinished();
  71.         if (prevEntry == null || !prevEntryOpen) {
  72.             throw new IOException("No current entry to close");
  73.         }
  74.         if ((headerPlus + entryOffset) % 2 != 0) {
  75.             out.write(PAD); // Pad byte
  76.         }
  77.         prevEntryOpen = false;
  78.     }

  79.     @Override
  80.     public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
  81.         checkFinished();
  82.         return new ArArchiveEntry(inputFile, entryName);
  83.     }

  84.     /**
  85.      * {@inheritDoc}
  86.      *
  87.      * @since 1.21
  88.      */
  89.     @Override
  90.     public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
  91.         checkFinished();
  92.         return new ArArchiveEntry(inputPath, entryName, options);
  93.     }

  94.     @Override
  95.     public void finish() throws IOException {
  96.         if (prevEntryOpen) {
  97.             throw new IOException("This archive contains unclosed entries.");
  98.         }
  99.         checkFinished();
  100.         super.finish();
  101.     }

  102.     private int pad(final int offset, final int newOffset, final char fill) throws IOException {
  103.         final int diff = newOffset - offset;
  104.         if (diff > 0) {
  105.             for (int i = 0; i < diff; i++) {
  106.                 write(fill);
  107.             }
  108.         }
  109.         return newOffset;
  110.     }

  111.     @Override
  112.     public void putArchiveEntry(final ArArchiveEntry entry) throws IOException {
  113.         checkFinished();
  114.         if (prevEntry == null) {
  115.             writeArchiveHeader();
  116.         } else {
  117.             if (prevEntry.getLength() != entryOffset) {
  118.                 throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
  119.             }
  120.             if (prevEntryOpen) {
  121.                 closeArchiveEntry();
  122.             }
  123.         }
  124.         prevEntry = entry;
  125.         headerPlus = writeEntryHeader(entry);
  126.         entryOffset = 0;
  127.         prevEntryOpen = true;
  128.     }

  129.     /**
  130.      * Sets the long file mode. This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1). This specifies the treatment of long file names (names &gt;= 16). Default is
  131.      * LONGFILE_ERROR.
  132.      *
  133.      * @param longFileMode the mode to use
  134.      * @since 1.3
  135.      */
  136.     public void setLongFileMode(final int longFileMode) {
  137.         this.longFileMode = longFileMode;
  138.     }

  139.     @Override
  140.     public void write(final byte[] b, final int off, final int len) throws IOException {
  141.         out.write(b, off, len);
  142.         count(len);
  143.         entryOffset += len;
  144.     }

  145.     private int write(final String data) throws IOException {
  146.         final byte[] bytes = data.getBytes(US_ASCII);
  147.         write(bytes);
  148.         return bytes.length;
  149.     }

  150.     private void writeArchiveHeader() throws IOException {
  151.         out.write(ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER));
  152.     }

  153.     private int writeEntryHeader(final ArArchiveEntry entry) throws IOException {
  154.         int offset = 0;
  155.         boolean appendName = false;
  156.         final String eName = entry.getName();
  157.         final int nLength = eName.length();
  158.         if (LONGFILE_ERROR == longFileMode && nLength > 16) {
  159.             throw new IOException("File name too long, > 16 chars: " + eName);
  160.         }
  161.         if (LONGFILE_BSD == longFileMode && (nLength > 16 || eName.indexOf(SPACE) > -1)) {
  162.             appendName = true;
  163.             final String fileNameLen = ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength;
  164.             if (fileNameLen.length() > 16) {
  165.                 throw new IOException("File length too long, > 16 chars: " + eName);
  166.             }
  167.             offset += write(fileNameLen);
  168.         } else {
  169.             offset += write(eName);
  170.         }
  171.         offset = pad(offset, 16, SPACE);
  172.         // Last modified
  173.         offset += write(checkLength(String.valueOf(entry.getLastModified()), 12, "Last modified"));
  174.         offset = pad(offset, 28, SPACE);
  175.         // User ID
  176.         offset += write(checkLength(String.valueOf(entry.getUserId()), 6, "User ID"));
  177.         offset = pad(offset, 34, SPACE);
  178.         // Group ID
  179.         offset += write(checkLength(String.valueOf(entry.getGroupId()), 6, "Group ID"));
  180.         offset = pad(offset, 40, SPACE);
  181.         // Mode
  182.         offset += write(checkLength(String.valueOf(Integer.toString(entry.getMode(), 8)), 8, "File mode"));
  183.         offset = pad(offset, 48, SPACE);
  184.         // Size
  185.         // On overflow, the file size is incremented by the length of the name.
  186.         offset += write(checkLength(String.valueOf(entry.getLength() + (appendName ? nLength : 0)), 10, "Size"));
  187.         offset = pad(offset, 58, SPACE);
  188.         offset += write(ArArchiveEntry.TRAILER);
  189.         // Name
  190.         if (appendName) {
  191.             offset += write(eName);
  192.         }
  193.         return offset;
  194.     }

  195. }