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 */
019package org.apache.commons.compress.archivers.ar;
020
021import java.io.File;
022import java.io.IOException;
023import java.io.OutputStream;
024
025import org.apache.commons.compress.archivers.ArchiveEntry;
026import org.apache.commons.compress.archivers.ArchiveOutputStream;
027import org.apache.commons.compress.utils.ArchiveUtils;
028
029/**
030 * Implements the "ar" archive format as an output stream.
031 *
032 * @NotThreadSafe
033 */
034public 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 1.3
061     */
062    public void setLongFileMode(final int longFileMode) {
063        this.longFileMode = longFileMode;
064    }
065
066    private long writeArchiveHeader() throws IOException {
067        final byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
068        out.write(header);
069        return header.length;
070    }
071
072    @Override
073    public void closeArchiveEntry() throws IOException {
074        if(finished) {
075            throw new IOException("Stream has already been finished");
076        }
077        if (prevEntry == null || !haveUnclosedEntry){
078            throw new IOException("No current entry to close");
079        }
080        if (entryOffset % 2 != 0) {
081            out.write('\n'); // Pad byte
082        }
083        haveUnclosedEntry = false;
084    }
085
086    @Override
087    public void putArchiveEntry( final ArchiveEntry pEntry ) throws IOException {
088        if(finished) {
089            throw new IOException("Stream has already been finished");
090        }
091
092        final ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry;
093        if (prevEntry == null) {
094            writeArchiveHeader();
095        } else {
096            if (prevEntry.getLength() != entryOffset) {
097                throw new IOException("length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
098            }
099
100            if (haveUnclosedEntry) {
101                closeArchiveEntry();
102            }
103        }
104
105        prevEntry = pArEntry;
106
107        writeEntryHeader(pArEntry);
108
109        entryOffset = 0;
110        haveUnclosedEntry = true;
111    }
112
113    private long fill( final long pOffset, final long pNewOffset, final char pFill ) throws IOException {
114        final long diff = pNewOffset - pOffset;
115
116        if (diff > 0) {
117            for (int i = 0; i < diff; i++) {
118                write(pFill);
119            }
120        }
121
122        return pNewOffset;
123    }
124
125    private long write( final String data ) throws IOException {
126        final byte[] bytes = data.getBytes("ascii");
127        write(bytes);
128        return bytes.length;
129    }
130
131    private long writeEntryHeader( final ArArchiveEntry pEntry ) throws IOException {
132
133        long offset = 0;
134        boolean mustAppendName = false;
135
136        final String n = pEntry.getName();
137        if (LONGFILE_ERROR == longFileMode && n.length() > 16) {
138            throw new IOException("filename too long, > 16 chars: "+n);
139        }
140        if (LONGFILE_BSD == longFileMode &&
141            (n.length() > 16 || n.contains(" "))) {
142            mustAppendName = true;
143            offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX
144                            + String.valueOf(n.length()));
145        } else {
146            offset += write(n);
147        }
148
149        offset = fill(offset, 16, ' ');
150        final String m = "" + pEntry.getLastModified();
151        if (m.length() > 12) {
152            throw new IOException("modified too long");
153        }
154        offset += write(m);
155
156        offset = fill(offset, 28, ' ');
157        final String u = "" + pEntry.getUserId();
158        if (u.length() > 6) {
159            throw new IOException("userid too long");
160        }
161        offset += write(u);
162
163        offset = fill(offset, 34, ' ');
164        final String g = "" + pEntry.getGroupId();
165        if (g.length() > 6) {
166            throw new IOException("groupid too long");
167        }
168        offset += write(g);
169
170        offset = fill(offset, 40, ' ');
171        final String fm = "" + Integer.toString(pEntry.getMode(), 8);
172        if (fm.length() > 8) {
173            throw new IOException("filemode too long");
174        }
175        offset += write(fm);
176
177        offset = fill(offset, 48, ' ');
178        final String s =
179            String.valueOf(pEntry.getLength()
180                           + (mustAppendName ? n.length() : 0));
181        if (s.length() > 10) {
182            throw new IOException("size too long");
183        }
184        offset += write(s);
185
186        offset = fill(offset, 58, ' ');
187
188        offset += write(ArArchiveEntry.TRAILER);
189
190        if (mustAppendName) {
191            offset += write(n);
192        }
193
194        return offset;
195    }
196
197    @Override
198    public void write(final byte[] b, final int off, final int len) throws IOException {
199        out.write(b, off, len);
200        count(len);
201        entryOffset += len;
202    }
203
204    /**
205     * Calls finish if necessary, and then closes the OutputStream
206     */
207    @Override
208    public void close() throws IOException {
209        try {
210            if (!finished) {
211                finish();
212            }
213        } finally {
214            out.close();
215            prevEntry = null;
216        }
217    }
218
219    @Override
220    public ArchiveEntry createArchiveEntry(final File inputFile, final 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    @Override
229    public void finish() throws IOException {
230        if(haveUnclosedEntry) {
231            throw new IOException("This archive contains unclosed entries.");
232        } else if(finished) {
233            throw new IOException("This archive has already been finished");
234        }
235        finished = true;
236    }
237}