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 */ 019 package org.apache.commons.compress.archivers.ar; 020 021 import java.io.File; 022 import java.io.IOException; 023 import java.io.OutputStream; 024 025 import org.apache.commons.compress.archivers.ArchiveEntry; 026 import org.apache.commons.compress.archivers.ArchiveOutputStream; 027 import org.apache.commons.compress.utils.ArchiveUtils; 028 029 /** 030 * Implements the "ar" archive format as an output stream. 031 * 032 * @NotThreadSafe 033 */ 034 public 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(int longFileMode) { 063 this.longFileMode = longFileMode; 064 } 065 066 private long writeArchiveHeader() throws IOException { 067 byte [] header = ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER); 068 out.write(header); 069 return header.length; 070 } 071 072 /** {@inheritDoc} */ 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 /** {@inheritDoc} */ 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 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("ascii"); 129 write(bytes); 130 return bytes.length; 131 } 132 133 private long writeEntryHeader( final ArArchiveEntry pEntry ) throws IOException { 134 135 long offset = 0; 136 boolean mustAppendName = false; 137 138 final String n = pEntry.getName(); 139 if (LONGFILE_ERROR == longFileMode && n.length() > 16) { 140 throw new IOException("filename too long, > 16 chars: "+n); 141 } 142 if (LONGFILE_BSD == longFileMode && 143 (n.length() > 16 || n.indexOf(" ") > -1)) { 144 mustAppendName = true; 145 offset += write(ArArchiveInputStream.BSD_LONGNAME_PREFIX 146 + String.valueOf(n.length())); 147 } else { 148 offset += write(n); 149 } 150 151 offset = fill(offset, 16, ' '); 152 final String m = "" + (pEntry.getLastModified()); 153 if (m.length() > 12) { 154 throw new IOException("modified too long"); 155 } 156 offset += write(m); 157 158 offset = fill(offset, 28, ' '); 159 final String u = "" + pEntry.getUserId(); 160 if (u.length() > 6) { 161 throw new IOException("userid too long"); 162 } 163 offset += write(u); 164 165 offset = fill(offset, 34, ' '); 166 final String g = "" + pEntry.getGroupId(); 167 if (g.length() > 6) { 168 throw new IOException("groupid too long"); 169 } 170 offset += write(g); 171 172 offset = fill(offset, 40, ' '); 173 final String fm = "" + Integer.toString(pEntry.getMode(), 8); 174 if (fm.length() > 8) { 175 throw new IOException("filemode too long"); 176 } 177 offset += write(fm); 178 179 offset = fill(offset, 48, ' '); 180 final String s = 181 String.valueOf(pEntry.getLength() 182 + (mustAppendName ? n.length() : 0)); 183 if (s.length() > 10) { 184 throw new IOException("size too long"); 185 } 186 offset += write(s); 187 188 offset = fill(offset, 58, ' '); 189 190 offset += write(ArArchiveEntry.TRAILER); 191 192 if (mustAppendName) { 193 offset += write(n); 194 } 195 196 return offset; 197 } 198 199 @Override 200 public void write(byte[] b, int off, 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 if(!finished) { 212 finish(); 213 } 214 out.close(); 215 prevEntry = null; 216 } 217 218 /** {@inheritDoc} */ 219 @Override 220 public ArchiveEntry createArchiveEntry(File inputFile, 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 /** {@inheritDoc} */ 229 @Override 230 public void finish() throws IOException { 231 if(haveUnclosedEntry) { 232 throw new IOException("This archive contains unclosed entries."); 233 } else if(finished) { 234 throw new IOException("This archive has already been finished"); 235 } 236 finished = true; 237 } 238 }