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