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 java.io.File; 022import java.io.IOException; 023import java.io.OutputStream; 024import java.nio.charset.StandardCharsets; 025import java.nio.file.LinkOption; 026import java.nio.file.Path; 027 028import org.apache.commons.compress.archivers.ArchiveEntry; 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 { 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 pOut) { 054 this.out = pOut; 055 } 056 057 /** 058 * Set the long file mode. 059 * This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1). 060 * This specifies the treatment of long file names (names >= 16). 061 * Default is LONGFILE_ERROR. 062 * @param longFileMode the mode to use 063 * @since 1.3 064 */ 065 public void setLongFileMode(final int longFileMode) { 066 this.longFileMode = longFileMode; 067 } 068 069 private void writeArchiveHeader() throws IOException { 070 final byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER); 071 out.write(header); 072 } 073 074 @Override 075 public void closeArchiveEntry() throws IOException { 076 if(finished) { 077 throw new IOException("Stream has already been finished"); 078 } 079 if (prevEntry == null || !haveUnclosedEntry){ 080 throw new IOException("No current entry to close"); 081 } 082 if (entryOffset % 2 != 0) { 083 out.write('\n'); // Pad byte 084 } 085 haveUnclosedEntry = false; 086 } 087 088 @Override 089 public void putArchiveEntry(final ArchiveEntry pEntry) throws IOException { 090 if(finished) { 091 throw new IOException("Stream has already been finished"); 092 } 093 094 final ArArchiveEntry pArEntry = (ArArchiveEntry)pEntry; 095 if (prevEntry == null) { 096 writeArchiveHeader(); 097 } else { 098 if (prevEntry.getLength() != entryOffset) { 099 throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset); 100 } 101 102 if (haveUnclosedEntry) { 103 closeArchiveEntry(); 104 } 105 } 106 107 prevEntry = pArEntry; 108 109 writeEntryHeader(pArEntry); 110 111 entryOffset = 0; 112 haveUnclosedEntry = true; 113 } 114 115 private long fill(final long pOffset, final long pNewOffset, final char pFill) throws IOException { 116 final long diff = pNewOffset - pOffset; 117 118 if (diff > 0) { 119 for (int i = 0; i < diff; i++) { 120 write(pFill); 121 } 122 } 123 124 return pNewOffset; 125 } 126 127 private long write(final String data) throws IOException { 128 final byte[] bytes = data.getBytes(StandardCharsets.US_ASCII); 129 write(bytes); 130 return bytes.length; 131 } 132 133 private void writeEntryHeader(final ArArchiveEntry pEntry) throws IOException { 134 135 long offset = 0; 136 boolean mustAppendName = false; 137 138 final String n = pEntry.getName(); 139 final int nLength = n.length(); 140 if (LONGFILE_ERROR == longFileMode && nLength > 16) { 141 throw new IOException("File name too long, > 16 chars: "+n); 142 } 143 if (LONGFILE_BSD == longFileMode && 144 (nLength > 16 || n.contains(" "))) { 145 mustAppendName = true; 146 offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX 147 + String.valueOf(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}