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
036 private final OutputStream out;
037 private long archiveOffset = 0;
038 private long entryOffset = 0;
039 private ArArchiveEntry prevEntry;
040 private boolean haveUnclosedEntry = false;
041
042 /** indicates if this archive is finished */
043 private boolean finished = false;
044
045 public ArArchiveOutputStream( final OutputStream pOut ) {
046 this.out = pOut;
047 }
048
049 private long writeArchiveHeader() throws IOException {
050 byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
051 out.write(header);
052 return header.length;
053 }
054
055 /** {@inheritDoc} */
056 public void closeArchiveEntry() throws IOException {
057 if(finished) {
058 throw new IOException("Stream has already been finished");
059 }
060 if (prevEntry == null || !haveUnclosedEntry){
061 throw new IOException("No current entry to close");
062 }
063 if ((entryOffset % 2) != 0) {
064 out.write('\n'); // Pad byte
065 archiveOffset++;
066 }
067 haveUnclosedEntry = false;
068 }
069
070 /** {@inheritDoc} */
071 public void putArchiveEntry( final ArchiveEntry pEntry ) throws IOException {
072 if(finished) {
073 throw new IOException("Stream has already been finished");
074 }
075
076 ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry;
077 if (prevEntry == null) {
078 archiveOffset += writeArchiveHeader();
079 } else {
080 if (prevEntry.getLength() != entryOffset) {
081 throw new IOException("length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
082 }
083
084 if (haveUnclosedEntry) {
085 closeArchiveEntry();
086 }
087 }
088
089 prevEntry = pArEntry;
090
091 archiveOffset += writeEntryHeader(pArEntry);
092
093 entryOffset = 0;
094 haveUnclosedEntry = true;
095 }
096
097 private long fill( final long pOffset, final long pNewOffset, final char pFill ) throws IOException {
098 final long diff = pNewOffset - pOffset;
099
100 if (diff > 0) {
101 for (int i = 0; i < diff; i++) {
102 write(pFill);
103 }
104 }
105
106 return pNewOffset;
107 }
108
109 private long write( final String data ) throws IOException {
110 final byte[] bytes = data.getBytes("ascii");
111 write(bytes);
112 return bytes.length;
113 }
114
115 private long writeEntryHeader( final ArArchiveEntry pEntry ) throws IOException {
116
117 long offset = 0;
118
119 final String n = pEntry.getName();
120 if (n.length() > 16) {
121 throw new IOException("filename too long, > 16 chars: "+n);
122 }
123 offset += write(n);
124
125 offset = fill(offset, 16, ' ');
126 final String m = "" + (pEntry.getLastModified());
127 if (m.length() > 12) {
128 throw new IOException("modified too long");
129 }
130 offset += write(m);
131
132 offset = fill(offset, 28, ' ');
133 final String u = "" + pEntry.getUserId();
134 if (u.length() > 6) {
135 throw new IOException("userid too long");
136 }
137 offset += write(u);
138
139 offset = fill(offset, 34, ' ');
140 final String g = "" + pEntry.getGroupId();
141 if (g.length() > 6) {
142 throw new IOException("groupid too long");
143 }
144 offset += write(g);
145
146 offset = fill(offset, 40, ' ');
147 final String fm = "" + Integer.toString(pEntry.getMode(), 8);
148 if (fm.length() > 8) {
149 throw new IOException("filemode too long");
150 }
151 offset += write(fm);
152
153 offset = fill(offset, 48, ' ');
154 final String s = "" + pEntry.getLength();
155 if (s.length() > 10) {
156 throw new IOException("size too long");
157 }
158 offset += write(s);
159
160 offset = fill(offset, 58, ' ');
161
162 offset += write(ArArchiveEntry.TRAILER);
163
164 return offset;
165 }
166
167 public void write(byte[] b, int off, int len) throws IOException {
168 out.write(b, off, len);
169 count(len);
170 entryOffset += len;
171 }
172
173 /**
174 * Calls finish if necessary, and then closes the OutputStream
175 */
176 public void close() throws IOException {
177 if(!finished) {
178 finish();
179 }
180 out.close();
181 prevEntry = null;
182 }
183
184 /** {@inheritDoc} */
185 public ArchiveEntry createArchiveEntry(File inputFile, String entryName)
186 throws IOException {
187 if(finished) {
188 throw new IOException("Stream has already been finished");
189 }
190 return new ArArchiveEntry(inputFile, entryName);
191 }
192
193 /** {@inheritDoc} */
194 public void finish() throws IOException {
195 if(haveUnclosedEntry) {
196 throw new IOException("This archive contains unclosed entries.");
197 } else if(finished) {
198 throw new IOException("This archive has already been finished");
199 }
200 finished = true;
201 }
202 }