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
039    private static final char PAD = '\n';
040
041    private static final char SPACE = ' ';
042
043    /** Fail if a long file name is required in the archive. */
044    public static final int LONGFILE_ERROR = 0;
045
046    /** BSD ar extensions are used to store long file names in the archive. */
047    public static final int LONGFILE_BSD = 1;
048
049    private final OutputStream out;
050    private long entryOffset;
051    private int headerPlus;
052    private ArArchiveEntry prevEntry;
053    private boolean prevEntryOpen;
054    private int longFileMode = LONGFILE_ERROR;
055
056    /** Indicates if this archive is finished */
057    private boolean finished;
058
059    public ArArchiveOutputStream(final OutputStream out) {
060        this.out = out;
061    }
062
063    /**
064     * @throws IOException
065     */
066    private void checkFinished() throws IOException {
067        if (finished) {
068            throw new IOException("Stream has already been finished");
069        }
070    }
071
072    private String checkLength(final String value, final int max, final String name) throws IOException {
073        if (value.length() > max) {
074            throw new IOException(name + " too long");
075        }
076        return value;
077    }
078
079    /**
080     * Calls finish if necessary, and then closes the OutputStream
081     */
082    @Override
083    public void close() throws IOException {
084        try {
085            if (!finished) {
086                finish();
087            }
088        } finally {
089            out.close();
090            prevEntry = null;
091        }
092    }
093
094    @Override
095    public void closeArchiveEntry() throws IOException {
096        checkFinished();
097        if (prevEntry == null || !prevEntryOpen) {
098            throw new IOException("No current entry to close");
099        }
100        if ((headerPlus + entryOffset) % 2 != 0) {
101            out.write(PAD); // Pad byte
102        }
103        prevEntryOpen = false;
104    }
105
106    @Override
107    public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException {
108        checkFinished();
109        return new ArArchiveEntry(inputFile, entryName);
110    }
111
112    /**
113     * {@inheritDoc}
114     *
115     * @since 1.21
116     */
117    @Override
118    public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
119        checkFinished();
120        return new ArArchiveEntry(inputPath, entryName, options);
121    }
122
123    @Override
124    public void finish() throws IOException {
125        if (prevEntryOpen) {
126            throw new IOException("This archive contains unclosed entries.");
127        }
128        checkFinished();
129        finished = true;
130    }
131
132    private int pad(final int offset, final int newOffset, final char fill) throws IOException {
133        final int diff = newOffset - offset;
134        if (diff > 0) {
135            for (int i = 0; i < diff; i++) {
136                write(fill);
137            }
138        }
139        return newOffset;
140    }
141
142    @Override
143    public void putArchiveEntry(final ArArchiveEntry entry) throws IOException {
144        checkFinished();
145        if (prevEntry == null) {
146            writeArchiveHeader();
147        } else {
148            if (prevEntry.getLength() != entryOffset) {
149                throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
150            }
151            if (prevEntryOpen) {
152                closeArchiveEntry();
153            }
154        }
155        prevEntry = entry;
156        headerPlus = writeEntryHeader(entry);
157        entryOffset = 0;
158        prevEntryOpen = true;
159    }
160
161    /**
162     * 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
163     * LONGFILE_ERROR.
164     *
165     * @param longFileMode the mode to use
166     * @since 1.3
167     */
168    public void setLongFileMode(final int longFileMode) {
169        this.longFileMode = longFileMode;
170    }
171
172    @Override
173    public void write(final byte[] b, final int off, final int len) throws IOException {
174        out.write(b, off, len);
175        count(len);
176        entryOffset += len;
177    }
178
179    private int write(final String data) throws IOException {
180        final byte[] bytes = data.getBytes(US_ASCII);
181        write(bytes);
182        return bytes.length;
183    }
184
185    private void writeArchiveHeader() throws IOException {
186        out.write(ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER));
187    }
188
189    private int writeEntryHeader(final ArArchiveEntry entry) throws IOException {
190        int offset = 0;
191        boolean appendName = false;
192        final String eName = entry.getName();
193        final int nLength = eName.length();
194        if (LONGFILE_ERROR == longFileMode && nLength > 16) {
195            throw new IOException("File name too long, > 16 chars: " + eName);
196        }
197        if (LONGFILE_BSD == longFileMode && (nLength > 16 || eName.indexOf(SPACE) > -1)) {
198            appendName = true;
199            final String fileNameLen = ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength;
200            if (fileNameLen.length() > 16) {
201                throw new IOException("File length too long, > 16 chars: " + eName);
202            }
203            offset += write(fileNameLen);
204        } else {
205            offset += write(eName);
206        }
207        offset = pad(offset, 16, SPACE);
208        // Last modified
209        offset += write(checkLength(String.valueOf(entry.getLastModified()), 12, "Last modified"));
210        offset = pad(offset, 28, SPACE);
211        // User ID
212        offset += write(checkLength(String.valueOf(entry.getUserId()), 6, "User ID"));
213        offset = pad(offset, 34, SPACE);
214        // Group ID
215        offset += write(checkLength(String.valueOf(entry.getGroupId()), 6, "Group ID"));
216        offset = pad(offset, 40, SPACE);
217        // Mode
218        offset += write(checkLength(String.valueOf(Integer.toString(entry.getMode(), 8)), 8, "File mode"));
219        offset = pad(offset, 48, SPACE);
220        // Size
221        // On overflow, the file size is incremented by the length of the name.
222        offset += write(checkLength(String.valueOf(entry.getLength() + (appendName ? nLength : 0)), 10, "Size"));
223        offset = pad(offset, 58, SPACE);
224        offset += write(ArArchiveEntry.TRAILER);
225        // Name
226        if (appendName) {
227            offset += write(eName);
228        }
229        return offset;
230    }
231
232}