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 >= 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}