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