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;
024import java.nio.charset.StandardCharsets;
025import java.nio.file.LinkOption;
026import java.nio.file.Path;
027
028import org.apache.commons.compress.archivers.ArchiveEntry;
029import org.apache.commons.compress.archivers.ArchiveOutputStream;
030import org.apache.commons.compress.utils.ArchiveUtils;
031
032/**
033 * Implements the "ar" archive format as an output stream.
034 *
035 * @NotThreadSafe
036 */
037public class ArArchiveOutputStream extends ArchiveOutputStream {
038    /** Fail if a long file name is required in the archive. */
039    public static final int LONGFILE_ERROR = 0;
040
041    /** BSD ar extensions are used to store long file names in the archive. */
042    public static final int LONGFILE_BSD = 1;
043
044    private final OutputStream out;
045    private long entryOffset;
046    private ArArchiveEntry prevEntry;
047    private boolean haveUnclosedEntry;
048    private int longFileMode = LONGFILE_ERROR;
049
050    /** indicates if this archive is finished */
051    private boolean finished;
052
053    public ArArchiveOutputStream(final OutputStream pOut) {
054        this.out = pOut;
055    }
056
057    /**
058     * Set the long file mode.
059     * This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1).
060     * This specifies the treatment of long file names (names >= 16).
061     * Default is LONGFILE_ERROR.
062     * @param longFileMode the mode to use
063     * @since 1.3
064     */
065    public void setLongFileMode(final int longFileMode) {
066        this.longFileMode = longFileMode;
067    }
068
069    private void writeArchiveHeader() throws IOException {
070        final byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
071        out.write(header);
072    }
073
074    @Override
075    public void closeArchiveEntry() throws IOException {
076        if(finished) {
077            throw new IOException("Stream has already been finished");
078        }
079        if (prevEntry == null || !haveUnclosedEntry){
080            throw new IOException("No current entry to close");
081        }
082        if (entryOffset % 2 != 0) {
083            out.write('\n'); // Pad byte
084        }
085        haveUnclosedEntry = false;
086    }
087
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        final 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(StandardCharsets.US_ASCII);
129        write(bytes);
130        return bytes.length;
131    }
132
133    private void writeEntryHeader(final ArArchiveEntry pEntry) throws IOException {
134
135        long offset = 0;
136        boolean mustAppendName = false;
137
138        final String n = pEntry.getName();
139        final int nLength = n.length();
140        if (LONGFILE_ERROR == longFileMode && nLength > 16) {
141            throw new IOException("File name too long, > 16 chars: "+n);
142        }
143        if (LONGFILE_BSD == longFileMode &&
144            (nLength > 16 || n.contains(" "))) {
145            mustAppendName = true;
146            offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX
147                            + String.valueOf(nLength));
148        } else {
149            offset += write(n);
150        }
151
152        offset = fill(offset, 16, ' ');
153        final String m = "" + pEntry.getLastModified();
154        if (m.length() > 12) {
155            throw new IOException("Last modified too long");
156        }
157        offset += write(m);
158
159        offset = fill(offset, 28, ' ');
160        final String u = "" + pEntry.getUserId();
161        if (u.length() > 6) {
162            throw new IOException("User id too long");
163        }
164        offset += write(u);
165
166        offset = fill(offset, 34, ' ');
167        final String g = "" + pEntry.getGroupId();
168        if (g.length() > 6) {
169            throw new IOException("Group id too long");
170        }
171        offset += write(g);
172
173        offset = fill(offset, 40, ' ');
174        final String fm = "" + Integer.toString(pEntry.getMode(), 8);
175        if (fm.length() > 8) {
176            throw new IOException("Filemode too long");
177        }
178        offset += write(fm);
179
180        offset = fill(offset, 48, ' ');
181        final String s =
182            String.valueOf(pEntry.getLength()
183                           + (mustAppendName ? nLength : 0));
184        if (s.length() > 10) {
185            throw new IOException("Size too long");
186        }
187        offset += write(s);
188
189        offset = fill(offset, 58, ' ');
190
191        offset += write(ArArchiveEntry.TRAILER);
192
193        if (mustAppendName) {
194            offset += write(n);
195        }
196
197    }
198
199    @Override
200    public void write(final byte[] b, final int off, final 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        try {
212            if (!finished) {
213                finish();
214            }
215        } finally {
216            out.close();
217            prevEntry = null;
218        }
219    }
220
221    @Override
222    public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName)
223        throws IOException {
224        if (finished) {
225            throw new IOException("Stream has already been finished");
226        }
227        return new ArArchiveEntry(inputFile, entryName);
228    }
229
230    /**
231     * {@inheritDoc}
232     *
233     * @since 1.21
234     */
235    @Override
236    public ArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
237        if (finished) {
238            throw new IOException("Stream has already been finished");
239        }
240        return new ArArchiveEntry(inputPath, entryName, options);
241    }
242
243    @Override
244    public void finish() throws IOException {
245        if(haveUnclosedEntry) {
246            throw new IOException("This archive contains unclosed entries.");
247        }
248        if(finished) {
249            throw new IOException("This archive has already been finished");
250        }
251        finished = true;
252    }
253}