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