001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements. See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership. The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied. See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019 package org.apache.commons.compress.archivers.ar;
020
021 import java.io.File;
022 import java.io.IOException;
023 import java.io.OutputStream;
024
025 import org.apache.commons.compress.archivers.ArchiveEntry;
026 import org.apache.commons.compress.archivers.ArchiveOutputStream;
027 import org.apache.commons.compress.utils.ArchiveUtils;
028
029 /**
030 * Implements the "ar" archive format as an output stream.
031 *
032 * @NotThreadSafe
033 */
034 public class ArArchiveOutputStream extends ArchiveOutputStream {
035 /** Fail if a long file name is required in the archive. */
036 public static final int LONGFILE_ERROR = 0;
037
038 /** BSD ar extensions are used to store long file names in the archive. */
039 public static final int LONGFILE_BSD = 1;
040
041 private final OutputStream out;
042 private long entryOffset = 0;
043 private ArArchiveEntry prevEntry;
044 private boolean haveUnclosedEntry = false;
045 private int longFileMode = LONGFILE_ERROR;
046
047 /** indicates if this archive is finished */
048 private boolean finished = false;
049
050 public ArArchiveOutputStream( final OutputStream pOut ) {
051 this.out = pOut;
052 }
053
054 /**
055 * Set the long file mode.
056 * This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1).
057 * This specifies the treatment of long file names (names >= 16).
058 * Default is LONGFILE_ERROR.
059 * @param longFileMode the mode to use
060 * @since Apache Commons Compress 1.3
061 */
062 public void setLongFileMode(int longFileMode) {
063 this.longFileMode = longFileMode;
064 }
065
066 private long writeArchiveHeader() throws IOException {
067 byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
068 out.write(header);
069 return header.length;
070 }
071
072 /** {@inheritDoc} */
073 @Override
074 public void closeArchiveEntry() throws IOException {
075 if(finished) {
076 throw new IOException("Stream has already been finished");
077 }
078 if (prevEntry == null || !haveUnclosedEntry){
079 throw new IOException("No current entry to close");
080 }
081 if ((entryOffset % 2) != 0) {
082 out.write('\n'); // Pad byte
083 }
084 haveUnclosedEntry = false;
085 }
086
087 /** {@inheritDoc} */
088 @Override
089 public void putArchiveEntry( final ArchiveEntry pEntry ) throws IOException {
090 if(finished) {
091 throw new IOException("Stream has already been finished");
092 }
093
094 ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry;
095 if (prevEntry == null) {
096 writeArchiveHeader();
097 } else {
098 if (prevEntry.getLength() != entryOffset) {
099 throw new IOException("length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
100 }
101
102 if (haveUnclosedEntry) {
103 closeArchiveEntry();
104 }
105 }
106
107 prevEntry = pArEntry;
108
109 writeEntryHeader(pArEntry);
110
111 entryOffset = 0;
112 haveUnclosedEntry = true;
113 }
114
115 private long fill( final long pOffset, final long pNewOffset, final char pFill ) throws IOException {
116 final long diff = pNewOffset - pOffset;
117
118 if (diff > 0) {
119 for (int i = 0; i < diff; i++) {
120 write(pFill);
121 }
122 }
123
124 return pNewOffset;
125 }
126
127 private long write( final String data ) throws IOException {
128 final byte[] bytes = data.getBytes("ascii");
129 write(bytes);
130 return bytes.length;
131 }
132
133 private long writeEntryHeader( final ArArchiveEntry pEntry ) throws IOException {
134
135 long offset = 0;
136 boolean mustAppendName = false;
137
138 final String n = pEntry.getName();
139 if (LONGFILE_ERROR == longFileMode && n.length() > 16) {
140 throw new IOException("filename too long, > 16 chars: "+n);
141 }
142 if (LONGFILE_BSD == longFileMode &&
143 (n.length() > 16 || n.indexOf(" ") > -1)) {
144 mustAppendName = true;
145 offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX
146 + String.valueOf(n.length()));
147 } else {
148 offset += write(n);
149 }
150
151 offset = fill(offset, 16, ' ');
152 final String m = "" + (pEntry.getLastModified());
153 if (m.length() > 12) {
154 throw new IOException("modified too long");
155 }
156 offset += write(m);
157
158 offset = fill(offset, 28, ' ');
159 final String u = "" + pEntry.getUserId();
160 if (u.length() > 6) {
161 throw new IOException("userid too long");
162 }
163 offset += write(u);
164
165 offset = fill(offset, 34, ' ');
166 final String g = "" + pEntry.getGroupId();
167 if (g.length() > 6) {
168 throw new IOException("groupid too long");
169 }
170 offset += write(g);
171
172 offset = fill(offset, 40, ' ');
173 final String fm = "" + Integer.toString(pEntry.getMode(), 8);
174 if (fm.length() > 8) {
175 throw new IOException("filemode too long");
176 }
177 offset += write(fm);
178
179 offset = fill(offset, 48, ' ');
180 final String s =
181 String.valueOf(pEntry.getLength()
182 + (mustAppendName ? n.length() : 0));
183 if (s.length() > 10) {
184 throw new IOException("size too long");
185 }
186 offset += write(s);
187
188 offset = fill(offset, 58, ' ');
189
190 offset += write(ArArchiveEntry.TRAILER);
191
192 if (mustAppendName) {
193 offset += write(n);
194 }
195
196 return offset;
197 }
198
199 @Override
200 public void write(byte[] b, int off, int len) throws IOException {
201 out.write(b, off, len);
202 count(len);
203 entryOffset += len;
204 }
205
206 /**
207 * Calls finish if necessary, and then closes the OutputStream
208 */
209 @Override
210 public void close() throws IOException {
211 if(!finished) {
212 finish();
213 }
214 out.close();
215 prevEntry = null;
216 }
217
218 /** {@inheritDoc} */
219 @Override
220 public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
221 throws IOException {
222 if(finished) {
223 throw new IOException("Stream has already been finished");
224 }
225 return new ArArchiveEntry(inputFile, entryName);
226 }
227
228 /** {@inheritDoc} */
229 @Override
230 public void finish() throws IOException {
231 if(haveUnclosedEntry) {
232 throw new IOException("This archive contains unclosed entries.");
233 } else if(finished) {
234 throw new IOException("This archive has already been finished");
235 }
236 finished = true;
237 }
238 }