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