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.debian;
032    
033    import java.io.ByteArrayInputStream;
034    import java.io.ByteArrayOutputStream;
035    import java.io.IOException;
036    import java.io.InputStream;
037    import java.util.Enumeration;
038    import java.util.Map.Entry;
039    import java.util.zip.GZIPOutputStream;
040    
041    import javax.mail.internet.InternetHeaders;
042    
043    import org.apache.commons.io.IOUtils;
044    import org.apache.tools.tar.TarEntry;
045    import org.apache.tools.tar.TarOutputStream;
046    
047    import com.threerings.jpkg.PackageTarFile;
048    import com.threerings.jpkg.UnixStandardPermissions;
049    import com.threerings.jpkg.ar.ArchiveEntry;
050    
051    /**
052     * Handles the creation of the Debian package control.tar.gz file.
053     */
054    public class ControlFile
055        implements ArchiveEntry
056    {
057        /**
058         * Construct a new ControlFile which creates the contents of control.tar.gz entry in the
059         * Debian package.
060         * @param info The fully populated package meta data.
061         * @param dataTar The fully populated {@link PackageTarFile} represented by this control file.
062         * @throws IOException If any i/o exceptions occur during the control file creation.
063         * @throws ScriptDataTooLargeException If any maintainer script is too large to be added to the tar file.
064         */
065        public ControlFile (PackageInfo info, PackageTarFile dataTar)
066            throws IOException, ScriptDataTooLargeException
067        {
068            _controlData = createTarArray(info, dataTar);
069        }
070    
071        // from ArchiveEntry
072        public InputStream getInputStream ()
073        {
074            return new ByteArrayInputStream(_controlData);
075        }
076    
077        // from ArchiveEntry
078        public long getSize ()
079        {
080            return _controlData.length;
081        }
082    
083        // from ArchiveEntry
084        public String getPath ()
085        {
086            return DEB_AR_CONTROL_FILE;
087        }
088    
089        // from ArchiveEntry
090        public int getUserId ()
091        {
092            return UnixStandardPermissions.ROOT_USER.getId();
093        }
094    
095        // from ArchiveEntry
096        public int getGroupId ()
097        {
098            return UnixStandardPermissions.ROOT_GROUP.getId();
099        }
100    
101        // from ArchiveEntry
102        public int getMode ()
103        {
104            return UnixStandardPermissions.STANDARD_FILE_MODE;
105        }
106    
107        /**
108         * Create the control.tar.gz file as a byte array.
109         */
110        private byte[] createTarArray (PackageInfo info, PackageTarFile dataTar)
111            throws IOException, ScriptDataTooLargeException
112        {
113            // this file will never be big, so do all the work in memory.
114            final ByteArrayOutputStream output = new ByteArrayOutputStream();
115            final TarOutputStream controlTar = new TarOutputStream(new GZIPOutputStream(output));
116            controlTar.setLongFileMode(TarOutputStream.LONGFILE_GNU);
117    
118            try {
119                // construct the tar file.
120                addControlFile(controlTar, info, dataTar);
121                addMd5Sums(controlTar, dataTar);
122                addMaintainerScripts(controlTar, info);
123    
124            } finally {
125                controlTar.close();
126            }
127    
128            return output.toByteArray();
129        }
130    
131        /**
132         * Add the control file to the tar file.
133         */
134        private void addControlFile (TarOutputStream tar, PackageInfo info, PackageTarFile dataTar)
135            throws IOException
136        {
137            // setup the RFC822 formatted header used for package metadata.
138            final InternetHeaders headers = info.getControlHeaders();
139    
140            // add the "Installed-Size" field.
141            headers.addHeader(INSTALLED_SIZE, String.valueOf(dataTar.getTotalDataSize()));
142    
143            final StringBuilder controlFile = new StringBuilder();
144            @SuppressWarnings("unchecked")
145            final
146            Enumeration<String> en = headers.getAllHeaderLines();
147            while (en.hasMoreElements())
148            {
149                controlFile.append(en.nextElement()).append('\n');
150            }
151    
152            final TarEntry entry = standardEntry(DEB_CONTROL_FILE, UnixStandardPermissions.STANDARD_FILE_MODE, controlFile.length());
153            tar.putNextEntry(entry);
154            IOUtils.write(controlFile.toString(), tar);
155            tar.closeEntry();
156        }
157    
158        /**
159         * Add the md5sums file to the tar file.
160         */
161        private void addMd5Sums (TarOutputStream tar, PackageTarFile dataTar)
162            throws IOException
163        {
164            final StringBuilder md5sums = new StringBuilder();
165            for (final Entry<String, String> sum : dataTar.getMd5s().entrySet()) {
166                md5sums.append(sum.getKey()).append(' ').append(sum.getValue()).append('\n');
167            }
168    
169            final TarEntry entry = standardEntry(DEB_MD5_FILE, UnixStandardPermissions.STANDARD_FILE_MODE, md5sums.length());
170            tar.putNextEntry(entry);
171            IOUtils.write(md5sums.toString(), tar);
172            tar.closeEntry();
173        }
174    
175        /**
176         * Add the maintainer scripts to the tar file.
177         */
178        private void addMaintainerScripts (TarOutputStream tar, PackageInfo info)
179            throws IOException, ScriptDataTooLargeException
180        {
181            for (final MaintainerScript script : info.getMaintainerScripts().values()) {
182                if (script.getSize() > Integer.MAX_VALUE) {
183                    throw new ScriptDataTooLargeException(
184                        "The script data is too large for the tar file. script=[" + script.getType().getFilename() + "].");
185                }
186    
187                final TarEntry entry = standardEntry(script.getType().getFilename(), UnixStandardPermissions.EXECUTABLE_FILE_MODE, (int)script.getSize());
188                tar.putNextEntry(entry);
189                IOUtils.copy(script.getStream(), tar);
190                tar.closeEntry();
191            }
192        }
193    
194        /**
195         * Returns a TarEntry object with correct default values.
196         */
197        private TarEntry standardEntry (String name, int mode, int size)
198        {
199            final TarEntry entry = new TarEntry(name);
200            entry.setNames(UnixStandardPermissions.ROOT_USER.getName(), UnixStandardPermissions.ROOT_GROUP.getName());
201            entry.setIds(UnixStandardPermissions.ROOT_USER.getId(), UnixStandardPermissions.ROOT_GROUP.getId());
202            entry.setSize(size);
203            entry.setMode(mode);
204    
205            return entry;
206        }
207    
208        /** The field name in the control file for the installed size of the package. */
209        private static final String INSTALLED_SIZE = "Installed-Size";
210    
211        /** The name of the control file in the Debian package. */
212        private static final String DEB_AR_CONTROL_FILE = "control.tar.gz";
213    
214        /** Constants for entries in the control file. */
215        private static final String DEB_CONTROL_FILE = "control";
216        private static final String DEB_MD5_FILE = "md5sums";
217    
218    
219        /** The control.tar.gz data is held in this byte array after creation. */
220        private final byte[] _controlData;
221    }