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 039 private static final char PAD = '\n'; 040 041 private static final char SPACE = ' '; 042 043 /** Fail if a long file name is required in the archive. */ 044 public static final int LONGFILE_ERROR = 0; 045 046 /** BSD ar extensions are used to store long file names in the archive. */ 047 public static final int LONGFILE_BSD = 1; 048 049 private final OutputStream out; 050 private long entryOffset; 051 private int headerPlus; 052 private ArArchiveEntry prevEntry; 053 private boolean prevEntryOpen; 054 private int longFileMode = LONGFILE_ERROR; 055 056 /** Indicates if this archive is finished */ 057 private boolean finished; 058 059 public ArArchiveOutputStream(final OutputStream out) { 060 this.out = out; 061 } 062 063 /** 064 * @throws IOException 065 */ 066 private void checkFinished() throws IOException { 067 if (finished) { 068 throw new IOException("Stream has already been finished"); 069 } 070 } 071 072 private String checkLength(final String value, final int max, final String name) throws IOException { 073 if (value.length() > max) { 074 throw new IOException(name + " too long"); 075 } 076 return value; 077 } 078 079 /** 080 * Calls finish if necessary, and then closes the OutputStream 081 */ 082 @Override 083 public void close() throws IOException { 084 try { 085 if (!finished) { 086 finish(); 087 } 088 } finally { 089 out.close(); 090 prevEntry = null; 091 } 092 } 093 094 @Override 095 public void closeArchiveEntry() throws IOException { 096 checkFinished(); 097 if (prevEntry == null || !prevEntryOpen) { 098 throw new IOException("No current entry to close"); 099 } 100 if ((headerPlus + entryOffset) % 2 != 0) { 101 out.write(PAD); // Pad byte 102 } 103 prevEntryOpen = false; 104 } 105 106 @Override 107 public ArArchiveEntry createArchiveEntry(final File inputFile, final String entryName) throws IOException { 108 checkFinished(); 109 return new ArArchiveEntry(inputFile, entryName); 110 } 111 112 /** 113 * {@inheritDoc} 114 * 115 * @since 1.21 116 */ 117 @Override 118 public ArArchiveEntry createArchiveEntry(final Path inputPath, final String entryName, final LinkOption... options) throws IOException { 119 checkFinished(); 120 return new ArArchiveEntry(inputPath, entryName, options); 121 } 122 123 @Override 124 public void finish() throws IOException { 125 if (prevEntryOpen) { 126 throw new IOException("This archive contains unclosed entries."); 127 } 128 checkFinished(); 129 finished = true; 130 } 131 132 private int pad(final int offset, final int newOffset, final char fill) throws IOException { 133 final int diff = newOffset - offset; 134 if (diff > 0) { 135 for (int i = 0; i < diff; i++) { 136 write(fill); 137 } 138 } 139 return newOffset; 140 } 141 142 @Override 143 public void putArchiveEntry(final ArArchiveEntry entry) throws IOException { 144 checkFinished(); 145 if (prevEntry == null) { 146 writeArchiveHeader(); 147 } else { 148 if (prevEntry.getLength() != entryOffset) { 149 throw new IOException("Length does not match entry (" + prevEntry.getLength() + " != " + entryOffset); 150 } 151 if (prevEntryOpen) { 152 closeArchiveEntry(); 153 } 154 } 155 prevEntry = entry; 156 headerPlus = writeEntryHeader(entry); 157 entryOffset = 0; 158 prevEntryOpen = true; 159 } 160 161 /** 162 * Sets the long file mode. This can be LONGFILE_ERROR(0) or LONGFILE_BSD(1). This specifies the treatment of long file names (names >= 16). Default is 163 * LONGFILE_ERROR. 164 * 165 * @param longFileMode the mode to use 166 * @since 1.3 167 */ 168 public void setLongFileMode(final int longFileMode) { 169 this.longFileMode = longFileMode; 170 } 171 172 @Override 173 public void write(final byte[] b, final int off, final int len) throws IOException { 174 out.write(b, off, len); 175 count(len); 176 entryOffset += len; 177 } 178 179 private int write(final String data) throws IOException { 180 final byte[] bytes = data.getBytes(US_ASCII); 181 write(bytes); 182 return bytes.length; 183 } 184 185 private void writeArchiveHeader() throws IOException { 186 out.write(ArchiveUtils.toAsciiBytes(ArArchiveEntry.HEADER)); 187 } 188 189 private int writeEntryHeader(final ArArchiveEntry entry) throws IOException { 190 int offset = 0; 191 boolean appendName = false; 192 final String eName = entry.getName(); 193 final int nLength = eName.length(); 194 if (LONGFILE_ERROR == longFileMode && nLength > 16) { 195 throw new IOException("File name too long, > 16 chars: " + eName); 196 } 197 if (LONGFILE_BSD == longFileMode && (nLength > 16 || eName.indexOf(SPACE) > -1)) { 198 appendName = true; 199 final String fileNameLen = ArArchiveInputStream.BSD_LONGNAME_PREFIX + nLength; 200 if (fileNameLen.length() > 16) { 201 throw new IOException("File length too long, > 16 chars: " + eName); 202 } 203 offset += write(fileNameLen); 204 } else { 205 offset += write(eName); 206 } 207 offset = pad(offset, 16, SPACE); 208 // Last modified 209 offset += write(checkLength(String.valueOf(entry.getLastModified()), 12, "Last modified")); 210 offset = pad(offset, 28, SPACE); 211 // User ID 212 offset += write(checkLength(String.valueOf(entry.getUserId()), 6, "User ID")); 213 offset = pad(offset, 34, SPACE); 214 // Group ID 215 offset += write(checkLength(String.valueOf(entry.getGroupId()), 6, "Group ID")); 216 offset = pad(offset, 40, SPACE); 217 // Mode 218 offset += write(checkLength(String.valueOf(Integer.toString(entry.getMode(), 8)), 8, "File mode")); 219 offset = pad(offset, 48, SPACE); 220 // Size 221 // On overflow, the file size is incremented by the length of the name. 222 offset += write(checkLength(String.valueOf(entry.getLength() + (appendName ? nLength : 0)), 10, "Size")); 223 offset = pad(offset, 58, SPACE); 224 offset += write(ArArchiveEntry.TRAILER); 225 // Name 226 if (appendName) { 227 offset += write(eName); 228 } 229 return offset; 230 } 231 232}