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 static java.nio.charset.StandardCharsets.US_ASCII;
022
023import java.io.File;
024import java.io.IOException;
025import java.io.OutputStream;
026import java.nio.file.LinkOption;
027import java.nio.file.Path;
028
029import org.apache.commons.compress.archivers.ArchiveEntry;
030import org.apache.commons.compress.archivers.ArchiveOutputStream;
031import org.apache.commons.compress.utils.ArchiveUtils;
032
033/**
034 * Implements the "ar" archive format as an output stream.
035 *
036 * @NotThreadSafe
037 */
038public class ArArchiveOutputStream extends ArchiveOutputStream {
039    /** Fail if a long file name is required in the archive. */
040    public static final int LONGFILE_ERROR = 0;
041
042    /** BSD ar extensions are used to store long file names in the archive. */
043    public static final int LONGFILE_BSD = 1;
044
045    private final OutputStream out;
046    private long entryOffset;
047    private ArArchiveEntry prevEntry;
048    private boolean haveUnclosedEntry;
049    private int longFileMode = LONGFILE_ERROR;
050
051    /** indicates if this archive is finished */
052    private boolean finished;
053
054    public ArArchiveOutputStream(final OutputStream pOut) {
055        this.out = pOut;
056    }
057
058    /**
059     * Calls finish if necessary, and then closes the OutputStream
060     */
061    @Override
062    public void close() throws IOException {
063        try {
064            if (!finished) {
065                finish();
066            }
067        } finally {
068            out.close();
069            prevEntry = null;
070        }
071    }
072
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    @Override
088    public ArchiveEntry createArchiveEntry(final File inputFile, final String entryName)
089        throws IOException {
090        if (finished) {
091            throw new IOException("Stream has already been finished");
092        }
093        return new ArArchiveEntry(inputFile, entryName);
094    }
095
096    /**
097     * {@inheritDoc}
098     *
099     * @since 1.21
100     */
101    @Override
102    public ArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
103        if (finished) {
104            throw new IOException("Stream has already been finished");
105        }
106        return new ArArchiveEntry(inputPath, entryName, options);
107    }
108
109    private long fill(final long pOffset, final long pNewOffset, final char pFill) throws IOException {
110        final long diff = pNewOffset - pOffset;
111
112        if (diff > 0) {
113            for (int i = 0; i < diff; i++) {
114                write(pFill);
115            }
116        }
117
118        return pNewOffset;
119    }
120
121    @Override
122    public void finish() throws IOException {
123        if (haveUnclosedEntry) {
124            throw new IOException("This archive contains unclosed entries.");
125        }
126        if (finished) {
127            throw new IOException("This archive has already been finished");
128        }
129        finished = true;
130    }
131
132    @Override
133    public void putArchiveEntry(final ArchiveEntry pEntry) throws IOException {
134        if (finished) {
135            throw new IOException("Stream has already been finished");
136        }
137
138        final ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry;
139        if (prevEntry == null) {
140            writeArchiveHeader();
141        } else {
142            if (prevEntry.getLength() != entryOffset) {
143                throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
144            }
145
146            if (haveUnclosedEntry) {
147                closeArchiveEntry();
148            }
149        }
150
151        prevEntry = pArEntry;
152
153        writeEntryHeader(pArEntry);
154
155        entryOffset = 0;
156        haveUnclosedEntry = true;
157    }
158
159    /**
160     * Set the long file mode.
161     * This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1).
162     * This specifies the treatment of long file names (names &gt;= 16).
163     * Default is LONGFILE_ERROR.
164     * @param longFileMode the mode to use
165     * @since 1.3
166     */
167    public void setLongFileMode(final int longFileMode) {
168        this.longFileMode = longFileMode;
169    }
170
171    @Override
172    public void write(final byte[] b, final int off, final int len) throws IOException {
173        out.write(b, off, len);
174        count(len);
175        entryOffset += len;
176    }
177
178    private long write(final String data) throws IOException {
179        final byte[] bytes = data.getBytes(US_ASCII);
180        write(bytes);
181        return bytes.length;
182    }
183
184    private void writeArchiveHeader() throws IOException {
185        final byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
186        out.write(header);
187    }
188
189    private void writeEntryHeader(final ArArchiveEntry pEntry) throws IOException {
190
191        long offset = 0;
192        boolean mustAppendName = false;
193
194        final String n = pEntry.getName();
195        final int nLength = n.length();
196        if (LONGFILE_ERROR == longFileMode && nLength > 16) {
197            throw new IOException("File name too long, > 16 chars: "+n);
198        }
199        if (LONGFILE_BSD == longFileMode &&
200            (nLength > 16 || n.contains(" "))) {
201            mustAppendName = true;
202            offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength);
203        } else {
204            offset += write(n);
205        }
206
207        offset = fill(offset, 16, ' ');
208        final String m = "" + pEntry.getLastModified();
209        if (m.length() > 12) {
210            throw new IOException("Last modified too long");
211        }
212        offset += write(m);
213
214        offset = fill(offset, 28, ' ');
215        final String u = "" + pEntry.getUserId();
216        if (u.length() > 6) {
217            throw new IOException("User id too long");
218        }
219        offset += write(u);
220
221        offset = fill(offset, 34, ' ');
222        final String g = "" + pEntry.getGroupId();
223        if (g.length() > 6) {
224            throw new IOException("Group id too long");
225        }
226        offset += write(g);
227
228        offset = fill(offset, 40, ' ');
229        final String fm = "" + Integer.toString(pEntry.getMode(), 8);
230        if (fm.length() > 8) {
231            throw new IOException("Filemode too long");
232        }
233        offset += write(fm);
234
235        offset = fill(offset, 48, ' ');
236        final String s =
237            String.valueOf(pEntry.getLength()
238                           + (mustAppendName ? nLength : 0));
239        if (s.length() > 10) {
240            throw new IOException("Size too long");
241        }
242        offset += write(s);
243
244        offset = fill(offset, 58, ' ');
245
246        offset += write(ArArchiveEntry.TRAILER);
247
248        if (mustAppendName) {
249            offset += write(n);
250        }
251
252    }
253}