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