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