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