blob: 7eb4071b326c78f8bb201f732e58767c63bd51c4 [file] [log] [blame]
/*
* 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 java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import org.apache.commons.compress.archivers.ArchiveEntry;
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 {
/** 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 ArArchiveEntry prevEntry;
private boolean haveUnclosedEntry;
private int longFileMode = LONGFILE_ERROR;
/** indicates if this archive is finished */
private boolean finished;
public ArArchiveOutputStream(final OutputStream pOut) {
this.out = pOut;
}
/**
* Set 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;
}
private void writeArchiveHeader() throws IOException {
final byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER);
out.write(header);
}
@Override
public void closeArchiveEntry() throws IOException {
if(finished) {
throw new IOException("Stream has already been finished");
}
if (prevEntry == null || !haveUnclosedEntry){
throw new IOException("No current entry to close");
}
if (entryOffset % 2 != 0) {
out.write('\n'); // Pad byte
}
haveUnclosedEntry = false;
}
@Override
public void putArchiveEntry(final ArchiveEntry pEntry) throws IOException {
if(finished) {
throw new IOException("Stream has already been finished");
}
final ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry;
if (prevEntry == null) {
writeArchiveHeader();
} else {
if (prevEntry.getLength() != entryOffset) {
throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset);
}
if (haveUnclosedEntry) {
closeArchiveEntry();
}
}
prevEntry = pArEntry;
writeEntryHeader(pArEntry);
entryOffset = 0;
haveUnclosedEntry = true;
}
private long fill(final long pOffset, final long pNewOffset, final char pFill) throws IOException {
final long diff = pNewOffset - pOffset;
if (diff > 0) {
for (int i = 0; i < diff; i++) {
write(pFill);
}
}
return pNewOffset;
}
private long write(final String data) throws IOException {
final byte[] bytes = data.getBytes(StandardCharsets.US_ASCII);
write(bytes);
return bytes.length;
}
private void writeEntryHeader(final ArArchiveEntry pEntry) throws IOException {
long offset = 0;
boolean mustAppendName = false;
final String n = pEntry.getName();
final int nLength = n.length();
if (LONGFILE_ERROR == longFileMode && nLength > 16) {
throw new IOException("File name too long, > 16 chars: "+n);
}
if (LONGFILE_BSD == longFileMode &&
(nLength > 16 || n.contains(" "))) {
mustAppendName = true;
offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX
+ String.valueOf(nLength));
} else {
offset += write(n);
}
offset = fill(offset, 16, ' ');
final String m = "" + pEntry.getLastModified();
if (m.length() > 12) {
throw new IOException("Last modified too long");
}
offset += write(m);
offset = fill(offset, 28, ' ');
final String u = "" + pEntry.getUserId();
if (u.length() > 6) {
throw new IOException("User id too long");
}
offset += write(u);
offset = fill(offset, 34, ' ');
final String g = "" + pEntry.getGroupId();
if (g.length() > 6) {
throw new IOException("Group id too long");
}
offset += write(g);
offset = fill(offset, 40, ' ');
final String fm = "" + Integer.toString(pEntry.getMode(), 8);
if (fm.length() > 8) {
throw new IOException("Filemode too long");
}
offset += write(fm);
offset = fill(offset, 48, ' ');
final String s =
String.valueOf(pEntry.getLength()
+ (mustAppendName ? nLength : 0));
if (s.length() > 10) {
throw new IOException("Size too long");
}
offset += write(s);
offset = fill(offset, 58, ' ');
offset += write(ArArchiveEntry.TRAILER);
if (mustAppendName) {
offset += write(n);
}
}
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
out.write(b, off, len);
count(len);
entryOffset += len;
}
/**
* 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 ArchiveEntry createArchiveEntry(final File inputFile, final String entryName)
throws IOException {
if (finished) {
throw new IOException("Stream has already been finished");
}
return new ArArchiveEntry(inputFile, entryName);
}
/**
* {@inheritDoc}
*
* @since 1.21
*/
@Override
public ArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException {
if (finished) {
throw new IOException("Stream has already been finished");
}
return new ArArchiveEntry(inputPath, entryName, options);
}
@Override
public void finish() throws IOException {
if(haveUnclosedEntry) {
throw new IOException("This archive contains unclosed entries.");
}
if(finished) {
throw new IOException("This archive has already been finished");
}
finished = true;
}
}