| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.commons.compress.archivers.ar; |
| |
| import static java.nio.charset.StandardCharsets.US_ASCII; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.nio.file.LinkOption; |
| import java.nio.file.Path; |
| |
| import org.apache.commons.compress.archivers.ArchiveOutputStream; |
| import org.apache.commons.compress.utils.ArchiveUtils; |
| |
| /** |
| * Implements the "ar" archive format as an output stream. |
| * |
| * @NotThreadSafe |
| */ |
| public class ArArchiveOutputStream extends ArchiveOutputStream<ArArchiveEntry> { |
| |
| private static final char PAD = '\n'; |
| |
| private static final char SPACE = ' '; |
| |
| /** Fail if a long file name is required in the archive. */ |
| public static final int LONGFILE_ERROR = 0; |
| |
| /** BSD ar extensions are used to store long file names in the archive. */ |
| public static final int LONGFILE_BSD = 1; |
| |
| private final OutputStream out; |
| private long entryOffset; |
| private int headerPlus; |
| private ArArchiveEntry prevEntry; |
| private boolean prevEntryOpen; |
| private int longFileMode = LONGFILE_ERROR; |
| |
| /** Indicates if this archive is finished */ |
| private boolean finished; |
| |
| public ArArchiveOutputStream(final OutputStream out) { |
| this.out = out; |
| } |
| |
| /** |
| * @throws IOException |
| */ |
| private void checkFinished() throws IOException { |
| if (finished) { |
| throw new IOException("Stream has already been finished"); |
| } |
| } |
| |
| private String checkLength(final String value, final int max, final String name) throws IOException { |
| if (value.length() > max) { |
| throw new IOException(name + " too long"); |
| } |
| return value; |
| } |
| |
| /** |
| * Calls finish if necessary, and then closes the OutputStream |
| */ |
| @Override |
| public void close() throws IOException { |
| try { |
| if (!finished) { |
| finish(); |
| } |
| } finally { |
| out.close(); |
| prevEntry = null; |
| } |
| } |
| |
| @Override |
| public void closeArchiveEntry() throws IOException { |
| checkFinished(); |
| if (prevEntry == null || !prevEntryOpen) { |
| throw new IOException("No current entry to close"); |
| } |
| if ((headerPlus + entryOffset) % 2 != 0) { |
| out.write(PAD); // Pad byte |
| } |
| prevEntryOpen = false; |
| } |
| |
| @Override |
| public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException { |
| checkFinished(); |
| return new ArArchiveEntry(inputFile, entryName); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * @since 1.21 |
| */ |
| @Override |
| public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException { |
| checkFinished(); |
| return new ArArchiveEntry(inputPath, entryName, options); |
| } |
| |
| @Override |
| public void finish() throws IOException { |
| if (prevEntryOpen) { |
| throw new IOException("This archive contains unclosed entries."); |
| } |
| checkFinished(); |
| finished = true; |
| } |
| |
| private int pad(final int offset, final int newOffset, final char fill) throws IOException { |
| final int diff = newOffset - offset; |
| if (diff > 0) { |
| for (int i = 0; i < diff; i++) { |
| write(fill); |
| } |
| } |
| return newOffset; |
| } |
| |
| @Override |
| public void putArchiveEntry(final ArArchiveEntry entry) throws IOException { |
| checkFinished(); |
| if (prevEntry == null) { |
| writeArchiveHeader(); |
| } else { |
| if (prevEntry.getLength() != entryOffset) { |
| throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset); |
| } |
| if (prevEntryOpen) { |
| closeArchiveEntry(); |
| } |
| } |
| prevEntry = entry; |
| headerPlus = writeEntryHeader(entry); |
| entryOffset = 0; |
| prevEntryOpen = true; |
| } |
| |
| /** |
| * Sets the long file mode. This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1). This specifies the treatment of long file names (names >= 16). Default is |
| * LONGFILE_ERROR. |
| * |
| * @param longFileMode the mode to use |
| * @since 1.3 |
| */ |
| public void setLongFileMode(final int longFileMode) { |
| this.longFileMode = longFileMode; |
| } |
| |
| @Override |
| public void write(final byte[] b, final int off, final int len) throws IOException { |
| out.write(b, off, len); |
| count(len); |
| entryOffset += len; |
| } |
| |
| private int write(final String data) throws IOException { |
| final byte[] bytes = data.getBytes(US_ASCII); |
| write(bytes); |
| return bytes.length; |
| } |
| |
| private void writeArchiveHeader() throws IOException { |
| out.write(ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER)); |
| } |
| |
| private int writeEntryHeader(final ArArchiveEntry entry) throws IOException { |
| int offset = 0; |
| boolean appendName = false; |
| final String eName = entry.getName(); |
| final int nLength = eName.length(); |
| if (LONGFILE_ERROR == longFileMode && nLength > 16) { |
| throw new IOException("File name too long, > 16 chars: " + eName); |
| } |
| if (LONGFILE_BSD == longFileMode && (nLength > 16 || eName.indexOf(SPACE) > -1)) { |
| appendName = true; |
| final String fileNameLen = ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength; |
| if (fileNameLen.length() > 16) { |
| throw new IOException("File length too long, > 16 chars: " + eName); |
| } |
| offset += write(fileNameLen); |
| } else { |
| offset += write(eName); |
| } |
| offset = pad(offset, 16, SPACE); |
| // Last modified |
| offset += write(checkLength(String.valueOf(entry.getLastModified()), 12, "Last modified")); |
| offset = pad(offset, 28, SPACE); |
| // User ID |
| offset += write(checkLength(String.valueOf(entry.getUserId()), 6, "User ID")); |
| offset = pad(offset, 34, SPACE); |
| // Group ID |
| offset += write(checkLength(String.valueOf(entry.getGroupId()), 6, "Group ID")); |
| offset = pad(offset, 40, SPACE); |
| // Mode |
| offset += write(checkLength(String.valueOf(Integer.toString(entry.getMode(), 8)), 8, "File mode")); |
| offset = pad(offset, 48, SPACE); |
| // Size |
| // On overflow, the file size is incremented by the length of the name. |
| offset += write(checkLength(String.valueOf(entry.getLength() + (appendName ? nLength : 0)), 10, "Size")); |
| offset = pad(offset, 58, SPACE); |
| offset += write(ArArchiveEntry.TRAILER); |
| // Name |
| if (appendName) { |
| offset += write(eName); |
| } |
| return offset; |
| } |
| |
| } |