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