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 /** Fail if a long file name is required in the archive. */ 039 public static final int LONGFILE_ERROR = 0; 040 041 /** BSD ar extensions are used to store long file names in the archive. */ 042 public static final int LONGFILE_BSD = 1; 043 044 private final OutputStream out; 045 private long entryOffset; 046 private ArArchiveEntry prevEntry; 047 private boolean haveUnclosedEntry; 048 private int longFileMode = LONGFILE_ERROR; 049 050 /** Indicates if this archive is finished */ 051 private boolean finished; 052 053 public ArArchiveOutputStream(final OutputStream out) { 054 this.out = out; 055 } 056 057 /** 058 * Calls finish if necessary, and then closes the OutputStream 059 */ 060 @Override 061 public void close() throws IOException { 062 try { 063 if (!finished) { 064 finish(); 065 } 066 } finally { 067 out.close(); 068 prevEntry = null; 069 } 070 } 071 072 @Override 073 public void closeArchiveEntry() throws IOException { 074 if (finished) { 075 throw new IOException("Stream has already been finished"); 076 } 077 if (prevEntry == null || !haveUnclosedEntry) { 078 throw new IOException("No current entry to close"); 079 } 080 if (entryOffset % 2 != 0) { 081 out.write('\n'); // Pad byte 082 } 083 haveUnclosedEntry = false; 084 } 085 086 @Override 087 public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException { 088 if (finished) { 089 throw new IOException("Stream has already been finished"); 090 } 091 return new ArArchiveEntry(inputFile, entryName); 092 } 093 094 /** 095 * {@inheritDoc} 096 * 097 * @since 1.21 098 */ 099 @Override 100 public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException { 101 if (finished) { 102 throw new IOException("Stream has already been finished"); 103 } 104 return new ArArchiveEntry(inputPath, entryName, options); 105 } 106 107 private long fill(final long pOffset, final long pNewOffset, final char pFill) throws IOException { 108 final long diff = pNewOffset - pOffset; 109 110 if (diff > 0) { 111 for (int i = 0; i < diff; i++) { 112 write(pFill); 113 } 114 } 115 116 return pNewOffset; 117 } 118 119 @Override 120 public void finish() throws IOException { 121 if (haveUnclosedEntry) { 122 throw new IOException("This archive contains unclosed entries."); 123 } 124 if (finished) { 125 throw new IOException("This archive has already been finished"); 126 } 127 finished = true; 128 } 129 130 @Override 131 public void putArchiveEntry(final ArArchiveEntry entry) throws IOException { 132 if (finished) { 133 throw new IOException("Stream has already been finished"); 134 } 135 136 if (prevEntry == null) { 137 writeArchiveHeader(); 138 } else { 139 if (prevEntry.getLength() != entryOffset) { 140 throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset); 141 } 142 143 if (haveUnclosedEntry) { 144 closeArchiveEntry(); 145 } 146 } 147 148 prevEntry = entry; 149 150 writeEntryHeader(entry); 151 152 entryOffset = 0; 153 haveUnclosedEntry = true; 154 } 155 156 /** 157 * 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 158 * LONGFILE_ERROR. 159 * 160 * @param longFileMode the mode to use 161 * @since 1.3 162 */ 163 public void setLongFileMode(final int longFileMode) { 164 this.longFileMode = longFileMode; 165 } 166 167 @Override 168 public void write(final byte[] b, final int off, final int len) throws IOException { 169 out.write(b, off, len); 170 count(len); 171 entryOffset += len; 172 } 173 174 private long write(final String data) throws IOException { 175 final byte[] bytes = data.getBytes(US_ASCII); 176 write(bytes); 177 return bytes.length; 178 } 179 180 private void writeArchiveHeader() throws IOException { 181 final byte[] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER); 182 out.write(header); 183 } 184 185 private void writeEntryHeader(final ArArchiveEntry entry) throws IOException { 186 187 long offset = 0; 188 boolean mustAppendName = false; 189 190 final String n = entry.getName(); 191 final int nLength = n.length(); 192 if (LONGFILE_ERROR == longFileMode && nLength > 16) { 193 throw new IOException("File name too long, > 16 chars: " + n); 194 } 195 if (LONGFILE_BSD == longFileMode && (nLength > 16 || n.contains(" "))) { 196 mustAppendName = true; 197 offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength); 198 } else { 199 offset += write(n); 200 } 201 202 offset = fill(offset, 16, ' '); 203 final String m = "" + entry.getLastModified(); 204 if (m.length() > 12) { 205 throw new IOException("Last modified too long"); 206 } 207 offset += write(m); 208 209 offset = fill(offset, 28, ' '); 210 final String u = "" + entry.getUserId(); 211 if (u.length() > 6) { 212 throw new IOException("User id too long"); 213 } 214 offset += write(u); 215 216 offset = fill(offset, 34, ' '); 217 final String g = "" + entry.getGroupId(); 218 if (g.length() > 6) { 219 throw new IOException("Group id too long"); 220 } 221 offset += write(g); 222 223 offset = fill(offset, 40, ' '); 224 final String fm = "" + Integer.toString(entry.getMode(), 8); 225 if (fm.length() > 8) { 226 throw new IOException("Filemode too long"); 227 } 228 offset += write(fm); 229 230 offset = fill(offset, 48, ' '); 231 final String s = String.valueOf(entry.getLength() + (mustAppendName ? nLength : 0)); 232 if (s.length() > 10) { 233 throw new IOException("Size too long"); 234 } 235 offset += write(s); 236 237 offset = fill(offset, 58, ' '); 238 239 offset += write(ArArchiveEntry.TRAILER); 240 241 if (mustAppendName) { 242 offset += write(n); 243 } 244 245 } 246}