001 /* 002 * Jpkg - Java library and tools for operating system package creation. 003 * 004 * Copyright (c) 2007 Three Rings Design, Inc. 005 * All rights reserved. 006 * 007 * Redistribution and use in source and binary forms, with or without 008 * modification, are permitted provided that the following conditions 009 * are met: 010 * 1. Redistributions of source code must retain the above copyright 011 * notice, this list of conditions and the following disclaimer. 012 * 2. Redistributions in binary form must reproduce the above copyright 013 * notice, this list of conditions and the following disclaimer in the 014 * documentation and/or other materials provided with the distribution. 015 * 3. Neither the name of the copyright owner nor the names of contributors 016 * may be used to endorse or promote products derived from this software 017 * without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 020 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 021 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 022 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 023 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 024 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 025 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 026 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 027 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 028 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 029 * POSSIBILITY OF SUCH DAMAGE. 030 */ 031 package com.threerings.jpkg.ar; 032 033 import java.io.File; 034 import java.io.FileInputStream; 035 import java.io.FileOutputStream; 036 import java.io.IOException; 037 import java.io.InputStream; 038 import java.io.OutputStream; 039 import java.io.UnsupportedEncodingException; 040 import java.util.Arrays; 041 import java.util.Formatter; 042 043 import org.apache.commons.io.IOUtils; 044 045 /** 046 * Creates and works with Unix ar(1) archives. 047 * @see <a href="http://en.wikipedia.org/wiki/Ar_%28Unix%29">The Wikipedia ar (Unix) entry</a> 048 */ 049 public class Archive 050 { 051 /** The character encoding all text added to the archive will be translated into. */ 052 public static final String CHAR_ENCODING = "ASCII"; 053 054 /** The magic header which starts every ar archive. */ 055 public static final byte[] AR_MAGIC; 056 057 /** The byte used to pad data. */ 058 public static final byte[] PADDING; 059 060 /** The length of the header that appears before every file in the archive. */ 061 public static final int FILE_HEADER_LENGTH = 60; 062 063 /** Initialize the AR_MAGIC header and PADDING byte arrays. */ 064 static { 065 try { 066 AR_MAGIC = "!<arch>\012".getBytes(CHAR_ENCODING); 067 PADDING = "\012".getBytes(CHAR_ENCODING); 068 069 } catch (final UnsupportedEncodingException uee) { 070 throw new RuntimeException("Failed to initialize static byte arrays.", uee); 071 } 072 } 073 074 /** 075 * Construct a new archive at the supplied {@link File} path. If the path already exists and is 076 * an ar(1) archive, open the archive for appending. 077 * @throws InvalidMagicException If the file being appended to is an invalid ar(1) archive. 078 * @throws IOException If any exception occurs during file i/o. 079 */ 080 public Archive (File path) 081 throws InvalidMagicException, IOException 082 { 083 _path = path; 084 085 // if the file already exists, and has content, verify it has the correct ar header. 086 if (_path.exists() && _path.length() > 0) { 087 InputStream input = null; 088 try { 089 input = new FileInputStream(_path); 090 final byte[] header = new byte[AR_MAGIC.length]; 091 input.read(header, 0, AR_MAGIC.length); 092 if (!Arrays.equals(header, AR_MAGIC)) { 093 throw new InvalidMagicException("Archive header is invalid: " + Arrays.toString(header)); 094 } 095 096 } finally { 097 IOUtils.closeQuietly(input); 098 } 099 _output = new FileOutputStream(_path, true); 100 101 // otherwise create the file if necessary and write out the header. 102 } else { 103 if (!_path.exists()) { 104 if (!_path.createNewFile()) { 105 throw new IOException("Unable to create archive file at " + _path.getAbsolutePath()); 106 } 107 } 108 109 _output = new FileOutputStream(_path); 110 _output.write(AR_MAGIC); 111 } 112 } 113 114 /** 115 * Append the contents of the supplied {@link ArchiveEntry} to this archive. 116 * @param entry The {@link ArchiveEntry} to add to the archive. 117 * @throws PathnameTooLongException If the path name is too long for an ar(1) archive. 118 * @throws PathnameInvalidException If the path name is invalid, e.g. contains a space. 119 * @throws DataTooLargeException If the data contained in the entry is too large for an ar(1) archive. 120 * @throws IOException If any exception occurs during data i/o. 121 */ 122 public void appendEntry (ArchiveEntry entry) 123 throws PathnameInvalidException, PathnameTooLongException, DataTooLargeException, IOException 124 { 125 // ar(1) only supports storing the file size as an integer. throw an exception if the 126 // data is too large. 127 if (entry.getSize() > Integer.MAX_VALUE) { 128 throw new DataTooLargeException("Data being added to the archive is too large. " + 129 "path=[" + entry.getPath() + "], size=[" + entry.getSize() + "]."); 130 } 131 132 // add the entry header to the archive 133 addEntryHeader(entry); 134 135 // append the entry data to the archive 136 final byte[] buffer = new byte[1024]; 137 int len; 138 final InputStream input = entry.getInputStream(); 139 try { 140 while ((len = input.read(buffer)) > 0) { 141 _output.write(buffer, 0, len); 142 } 143 144 } finally { 145 IOUtils.closeQuietly(input); 146 } 147 148 // pad the data section if necessary 149 if (entry.getSize() % 2 != 0) { 150 _output.write(PADDING); 151 } 152 } 153 154 /** 155 * Add a file header to the archive for the given ArchiveEntry. 156 */ 157 private void addEntryHeader (ArchiveEntry entry) 158 throws IOException, PathnameTooLongException, PathnameInvalidException 159 { 160 if (entry.getPath().getBytes(CHAR_ENCODING).length > 16) { 161 throw new PathnameTooLongException("The supplied path name is too long: " + entry.getPath()); 162 } 163 164 if (entry.getPath().contains(" ")) { 165 throw new PathnameInvalidException("The path name cannot contain spaces: " + entry.getPath()); 166 } 167 168 // set the file mtime to now 169 final int mtime = (int)(System.currentTimeMillis() / 1000L); 170 171 final StringBuffer buffer = new StringBuffer(); 172 final Formatter formatter = new Formatter(buffer); 173 174 /* 175 * Common file header format: 176 * All data is stored in an ASCII representation 177 * Fields 0-15: Name ASCII (null-terminated) 178 * Fields 16-27: Mod date Integer (seconds since epoch) 179 * Fields 28-33: Owner UID Integer (gid) 180 * Fields 34-39: Owner GID Integer (uid) 181 * Fields 40-47: File mode Integer (octal) 182 * Fields 48-57: File size Integer (bytes) 183 * Fields 58-59: File magic Constant (\140\012) 184 */ 185 formatter.format("%-16s%-12s%-6s%-6s%-8o%-10s%s%s", 186 entry.getPath(), mtime, entry.getUserId(), entry.getGroupId(), entry.getMode(), entry.getSize(), '\140', '\012'); 187 188 _output.write(buffer.toString().getBytes(CHAR_ENCODING)); 189 } 190 191 /** 192 * Make sure the {@link OutputStream} gets closed. 193 */ 194 @Override 195 protected void finalize () 196 throws Throwable 197 { 198 try { 199 _output.close(); 200 201 } finally { 202 super.finalize(); 203 } 204 } 205 206 /** The path to the archive being operated upon. */ 207 private final File _path; 208 209 /** The output stream used to append data and files to this archive. */ 210 private final OutputStream _output; 211 }