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      /** Fail if a long file name is required in the archive. */
39      public static final int LONGFILE_ERROR = 0;
40  
41      /** BSD ar extensions are used to store long file names in the archive. */
42      public static final int LONGFILE_BSD = 1;
43  
44      private final OutputStream out;
45      private long entryOffset;
46      private ArArchiveEntry prevEntry;
47      private boolean haveUnclosedEntry;
48      private int longFileMode = LONGFILE_ERROR;
49  
50      /** Indicates if this archive is finished */
51      private boolean finished;
52  
53      public ArArchiveOutputStream(final OutputStream out) {
54          this.out = out;
55      }
56  
57      /**
58       * Calls finish if necessary, and then closes the OutputStream
59       */
60      @Override
61      public void close() throws IOException {
62          try {
63              if (!finished) {
64                  finish();
65              }
66          } finally {
67              out.close();
68              prevEntry = null;
69          }
70      }
71  
72      @Override
73      public void closeArchiveEntry() throws IOException {
74          if (finished) {
75              throw new IOException("Stream has already been finished");
76          }
77          if (prevEntry == null || !haveUnclosedEntry) {
78              throw new IOException("No current entry to close");
79          }
80          if (entryOffset % 2 != 0) {
81              out.write('\n'); // Pad byte
82          }
83          haveUnclosedEntry = false;
84      }
85  
86      @Override
87      public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
88          if (finished) {
89              throw new IOException("Stream has already been finished");
90          }
91          return new ArArchiveEntry(inputFile, entryName);
92      }
93  
94      /**
95       * {@inheritDoc}
96       *
97       * @since 1.21
98       */
99      @Override
100     public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
101         if (finished) {
102             throw new IOException("Stream has already been finished");
103         }
104         return new ArArchiveEntry(inputPath, entryName, options);
105     }
106 
107     private long fill(final long pOffset, final long pNewOffset, final char pFill) throws IOException {
108         final long diff = pNewOffset - pOffset;
109 
110         if (diff > 0) {
111             for (int i = 0; i < diff; i++) {
112                 write(pFill);
113             }
114         }
115 
116         return pNewOffset;
117     }
118 
119     @Override
120     public void finish() throws IOException {
121         if (haveUnclosedEntry) {
122             throw new IOException("This archive contains unclosed entries.");
123         }
124         if (finished) {
125             throw new IOException("This archive has already been finished");
126         }
127         finished = true;
128     }
129 
130     @Override
131     public void putArchiveEntry(final ArArchiveEntry entry) throws IOException {
132         if (finished) {
133             throw new IOException("Stream has already been finished");
134         }
135 
136         if (prevEntry == null) {
137             writeArchiveHeader();
138         } else {
139             if (prevEntry.getLength() != entryOffset) {
140                 throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
141             }
142 
143             if (haveUnclosedEntry) {
144                 closeArchiveEntry();
145             }
146         }
147 
148         prevEntry = entry;
149 
150         writeEntryHeader(entry);
151 
152         entryOffset = 0;
153         haveUnclosedEntry = true;
154     }
155 
156     /**
157      * 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
158      * LONGFILE_ERROR.
159      *
160      * @param longFileMode the mode to use
161      * @since 1.3
162      */
163     public void setLongFileMode(final int longFileMode) {
164         this.longFileMode = longFileMode;
165     }
166 
167     @Override
168     public void write(final byte[] b, final int off, final int len) throws IOException {
169         out.write(b, off, len);
170         count(len);
171         entryOffset += len;
172     }
173 
174     private long write(final String data) throws IOException {
175         final byte[] bytes = data.getBytes(US_ASCII);
176         write(bytes);
177         return bytes.length;
178     }
179 
180     private void writeArchiveHeader() throws IOException {
181         final byte[] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
182         out.write(header);
183     }
184 
185     private void writeEntryHeader(final ArArchiveEntry entry) throws IOException {
186 
187         long offset = 0;
188         boolean mustAppendName = false;
189 
190         final String n = entry.getName();
191         final int nLength = n.length();
192         if (LONGFILE_ERROR == longFileMode && nLength > 16) {
193             throw new IOException("File name too long, > 16 chars: " + n);
194         }
195         if (LONGFILE_BSD == longFileMode && (nLength > 16 || n.contains(" "))) {
196             mustAppendName = true;
197             offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength);
198         } else {
199             offset += write(n);
200         }
201 
202         offset = fill(offset, 16, ' ');
203         final String m = "" + entry.getLastModified();
204         if (m.length() > 12) {
205             throw new IOException("Last modified too long");
206         }
207         offset += write(m);
208 
209         offset = fill(offset, 28, ' ');
210         final String u = "" + entry.getUserId();
211         if (u.length() > 6) {
212             throw new IOException("User id too long");
213         }
214         offset += write(u);
215 
216         offset = fill(offset, 34, ' ');
217         final String g = "" + entry.getGroupId();
218         if (g.length() > 6) {
219             throw new IOException("Group id too long");
220         }
221         offset += write(g);
222 
223         offset = fill(offset, 40, ' ');
224         final String fm = "" + Integer.toString(entry.getMode(), 8);
225         if (fm.length() > 8) {
226             throw new IOException("Filemode too long");
227         }
228         offset += write(fm);
229 
230         offset = fill(offset, 48, ' ');
231         final String s = String.valueOf(entry.getLength() + (mustAppendName ? nLength : 0));
232         if (s.length() > 10) {
233             throw new IOException("Size too long");
234         }
235         offset += write(s);
236 
237         offset = fill(offset, 58, ' ');
238 
239         offset += write(ArArchiveEntry.TRAILER);
240 
241         if (mustAppendName) {
242             offset += write(n);
243         }
244 
245     }
246 }