View Javadoc
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  
21  import static java.nio.charset.StandardCharsets.US_ASCII;
22  
23  import java.io.File;
24  import java.io.IOException;
25  import java.io.OutputStream;
26  import java.nio.file.LinkOption;
27  import java.nio.file.Path;
28  
29  import org.apache.commons.compress.archivers.ArchiveOutputStream;
30  import org.apache.commons.compress.utils.ArchiveUtils;
31  
32  /**
33   * Implements the "ar" archive format as an output stream.
34   *
35   * @NotThreadSafe
36   */
37  public class ArArchiveOutputStream extends ArchiveOutputStream<ArArchiveEntry> {
38  
39      private static final char PAD = '\n';
40  
41      private static final char SPACE = ' ';
42  
43      /** Fail if a long file name is required in the archive. */
44      public static final int LONGFILE_ERROR = 0;
45  
46      /** BSD ar extensions are used to store long file names in the archive. */
47      public static final int LONGFILE_BSD = 1;
48  
49      private long entryOffset;
50      private int headerPlus;
51      private ArArchiveEntry prevEntry;
52      private boolean prevEntryOpen;
53      private int longFileMode = LONGFILE_ERROR;
54  
55      public ArArchiveOutputStream(final OutputStream out) {
56          super(out);
57      }
58  
59      private String checkLength(final String value, final int max, final String name) throws IOException {
60          if (value.length() > max) {
61              throw new IOException(name + " too long");
62          }
63          return value;
64      }
65  
66      /**
67       * Calls finish if necessary, and then closes the OutputStream
68       */
69      @Override
70      public void close() throws IOException {
71          try {
72              if (!isFinished()) {
73                  finish();
74              }
75          } finally {
76              prevEntry = null;
77              super.close();
78          }
79      }
80  
81      @Override
82      public void closeArchiveEntry() throws IOException {
83          checkFinished();
84          if (prevEntry == null || !prevEntryOpen) {
85              throw new IOException("No current entry to close");
86          }
87          if ((headerPlus + entryOffset) % 2 != 0) {
88              out.write(PAD); // Pad byte
89          }
90          prevEntryOpen = false;
91      }
92  
93      @Override
94      public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
95          checkFinished();
96          return new ArArchiveEntry(inputFile, entryName);
97      }
98  
99      /**
100      * {@inheritDoc}
101      *
102      * @since 1.21
103      */
104     @Override
105     public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
106         checkFinished();
107         return new ArArchiveEntry(inputPath, entryName, options);
108     }
109 
110     @Override
111     public void finish() throws IOException {
112         if (prevEntryOpen) {
113             throw new IOException("This archive contains unclosed entries.");
114         }
115         checkFinished();
116         super.finish();
117     }
118 
119     private int pad(final int offset, final int newOffset, final char fill) throws IOException {
120         final int diff = newOffset - offset;
121         if (diff > 0) {
122             for (int i = 0; i < diff; i++) {
123                 write(fill);
124             }
125         }
126         return newOffset;
127     }
128 
129     @Override
130     public void putArchiveEntry(final ArArchiveEntry entry) throws IOException {
131         checkFinished();
132         if (prevEntry == null) {
133             writeArchiveHeader();
134         } else {
135             if (prevEntry.getLength() != entryOffset) {
136                 throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
137             }
138             if (prevEntryOpen) {
139                 closeArchiveEntry();
140             }
141         }
142         prevEntry = entry;
143         headerPlus = writeEntryHeader(entry);
144         entryOffset = 0;
145         prevEntryOpen = true;
146     }
147 
148     /**
149      * 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
150      * LONGFILE_ERROR.
151      *
152      * @param longFileMode the mode to use
153      * @since 1.3
154      */
155     public void setLongFileMode(final int longFileMode) {
156         this.longFileMode = longFileMode;
157     }
158 
159     @Override
160     public void write(final byte[] b, final int off, final int len) throws IOException {
161         out.write(b, off, len);
162         count(len);
163         entryOffset += len;
164     }
165 
166     private int write(final String data) throws IOException {
167         final byte[] bytes = data.getBytes(US_ASCII);
168         write(bytes);
169         return bytes.length;
170     }
171 
172     private void writeArchiveHeader() throws IOException {
173         out.write(ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER));
174     }
175 
176     private int writeEntryHeader(final ArArchiveEntry entry) throws IOException {
177         int offset = 0;
178         boolean appendName = false;
179         final String eName = entry.getName();
180         final int nLength = eName.length();
181         if (LONGFILE_ERROR == longFileMode && nLength > 16) {
182             throw new IOException("File name too long, > 16 chars: " + eName);
183         }
184         if (LONGFILE_BSD == longFileMode && (nLength > 16 || eName.indexOf(SPACE) > -1)) {
185             appendName = true;
186             final String fileNameLen = ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength;
187             if (fileNameLen.length() > 16) {
188                 throw new IOException("File length too long, > 16 chars: " + eName);
189             }
190             offset += write(fileNameLen);
191         } else {
192             offset += write(eName);
193         }
194         offset = pad(offset, 16, SPACE);
195         // Last modified
196         offset += write(checkLength(String.valueOf(entry.getLastModified()), 12, "Last modified"));
197         offset = pad(offset, 28, SPACE);
198         // User ID
199         offset += write(checkLength(String.valueOf(entry.getUserId()), 6, "User ID"));
200         offset = pad(offset, 34, SPACE);
201         // Group ID
202         offset += write(checkLength(String.valueOf(entry.getGroupId()), 6, "Group ID"));
203         offset = pad(offset, 40, SPACE);
204         // Mode
205         offset += write(checkLength(String.valueOf(Integer.toString(entry.getMode(), 8)), 8, "File mode"));
206         offset = pad(offset, 48, SPACE);
207         // Size
208         // On overflow, the file size is incremented by the length of the name.
209         offset += write(checkLength(String.valueOf(entry.getLength() + (appendName ? nLength : 0)), 10, "Size"));
210         offset = pad(offset, 58, SPACE);
211         offset += write(ArArchiveEntry.TRAILER);
212         // Name
213         if (appendName) {
214             offset += write(eName);
215         }
216         return offset;
217     }
218 
219 }