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