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