001    /*
002     * Copyright 2005 The Apache Software Foundation.
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     * 
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.vafer.jdeb;
017    
018    import java.io.ByteArrayInputStream;
019    import java.io.File;
020    import java.io.FileInputStream;
021    import java.io.FileNotFoundException;
022    import java.io.FileOutputStream;
023    import java.io.IOException;
024    import java.io.InputStream;
025    import java.io.OutputStream;
026    import java.math.BigInteger;
027    import java.security.DigestOutputStream;
028    import java.security.MessageDigest;
029    import java.security.NoSuchAlgorithmException;
030    import java.text.ParseException;
031    import java.text.SimpleDateFormat;
032    import java.util.Arrays;
033    import java.util.Date;
034    import java.util.Locale;
035    import java.util.zip.GZIPOutputStream;
036    
037    import org.apache.tools.bzip2.CBZip2OutputStream;
038    import org.apache.tools.tar.TarEntry;
039    import org.apache.tools.tar.TarOutputStream;
040    import org.vafer.jdeb.ar.ArEntry;
041    import org.vafer.jdeb.ar.ArOutputStream;
042    import org.vafer.jdeb.changes.ChangeSet;
043    import org.vafer.jdeb.changes.ChangesProvider;
044    import org.vafer.jdeb.descriptors.ChangesDescriptor;
045    import org.vafer.jdeb.descriptors.InvalidDescriptorException;
046    import org.vafer.jdeb.descriptors.PackageDescriptor;
047    import org.vafer.jdeb.signing.SigningUtils;
048    import org.vafer.jdeb.utils.InformationOutputStream;
049    import org.vafer.jdeb.utils.Utils;
050    import org.vafer.jdeb.utils.VariableResolver;
051    
052    /**
053     * The processor does the actual work of building the deb related files.
054     * It is been used by the ant task and (later) the maven plugin.
055     * 
056     * @author Torsten Curdt <tcurdt@vafer.org>
057     */
058    public class Processor {
059    
060            private final Console console;
061            private final VariableResolver resolver;
062    
063            private static final class Total {
064                    private BigInteger count = BigInteger.valueOf(0);
065    
066                    public void add(long size) {
067                            count = count.add(BigInteger.valueOf(size));
068                    }
069    
070                    public String toString() {
071                            return "" + count;
072                    }
073    
074                    public BigInteger toBigInteger() {
075                            return count;
076                    }
077            }
078    
079            public Processor( final Console pConsole, final VariableResolver pResolver ) {
080                    console = pConsole;
081                    resolver = pResolver;
082            }
083    
084            private void addTo( final ArOutputStream pOutput, final String pName, final String pContent ) throws IOException {
085                    final byte[] content = pContent.getBytes(); 
086                    pOutput.putNextEntry(new ArEntry(pName, content.length));
087                    pOutput.write(content);
088            }
089    
090            private void addTo( final ArOutputStream pOutput, final String pName, final File pContent ) throws IOException {
091                    pOutput.putNextEntry(new ArEntry(pName, pContent.length()));
092                    
093                    final InputStream input = new FileInputStream(pContent);
094                    try {
095                            Utils.copy(input, pOutput);
096                    } finally {
097                            input.close();
098                    }
099            }
100            
101            /**
102             * Create the debian archive with from the provided control files and data producers.
103             * 
104             * @param pControlFiles
105             * @param pData
106             * @param pOutput
107             * @param compression the compression method used for the data file (gzip, bzip2 or anything else for no compression)
108             * @return PackageDescriptor
109             * @throws PackagingException
110             */
111            public PackageDescriptor createDeb( final File[] pControlFiles, final DataProducer[] pData, final File pOutput, String compression ) throws PackagingException, InvalidDescriptorException {
112    
113                    File tempData = null;
114                    File tempControl = null;
115    
116                    try {
117                            tempData = File.createTempFile("deb", "data");                  
118                            tempControl = File.createTempFile("deb", "control");                    
119    
120                            console.println("Building data");
121                            final StringBuffer md5s = new StringBuffer();
122                            final BigInteger size = buildData(pData, tempData, md5s, compression);
123    
124                            console.println("Building control");
125                            final PackageDescriptor packageDescriptor = buildControl(pControlFiles, size, md5s, tempControl);
126    
127                            if (!packageDescriptor.isValid()) {
128                                    throw new InvalidDescriptorException(packageDescriptor);
129                            }
130    
131                            final InformationOutputStream output = new InformationOutputStream(new FileOutputStream(pOutput), MessageDigest.getInstance("MD5"));
132    
133                            final ArOutputStream ar = new ArOutputStream(output);
134    
135                            addTo(ar, "debian-binary", "2.0\n");
136                            addTo(ar, "control.tar.gz", tempControl);
137                            addTo(ar, "data.tar" + getExtension(compression), tempData);
138                            
139                            ar.close();
140    
141                            // intermediate values
142                            packageDescriptor.set("MD5", output.getMd5());
143                            packageDescriptor.set("Size", "" + output.getSize());
144                            packageDescriptor.set("File", pOutput.getName());
145    
146                            return packageDescriptor;
147    
148                    } catch(InvalidDescriptorException e) {
149                            throw e;
150                    } catch(Exception e) {
151                            throw new PackagingException("Could not create deb package", e);
152                    } finally {
153                            if (tempData != null) {
154                                    if (!tempData.delete()) {
155                                            throw new PackagingException("Could not delete " + tempData);                                   
156                                    }
157                            }
158                            if (tempControl != null) {
159                                    if (!tempControl.delete()) {
160                                            throw new PackagingException("Could not delete " + tempControl);                                        
161                                    }
162                            }
163                    }
164            }
165    
166            /**
167             * Return the extension of a file compressed with the specified method.
168             *
169             * @param pCompression the compression method used
170             * @return
171             */
172            private String getExtension( final String pCompression ) {
173                    if ("gzip".equals(pCompression)) {
174                            return ".gz";
175                    } else if ("bzip2".equals(pCompression)) {
176                            return ".bz2";
177                    } else {
178                            return "";
179                    }
180            }
181    
182            /**
183             * Create changes file based on the provided PackageDescriptor.
184             * If pRing, pKey and pPassphrase are provided the changes file will also be signed.
185             * It returns a ChangesDescriptor reflecting the changes  
186             * @param pPackageDescriptor
187             * @param pChangesProvider
188             * @param pRing
189             * @param pKey
190             * @param pPassphrase
191             * @param pOutput
192             * @return ChangesDescriptor
193             * @throws IOException
194             */
195            public ChangesDescriptor createChanges( final PackageDescriptor pPackageDescriptor, final ChangesProvider pChangesProvider, final InputStream pRing, final String pKey, final String pPassphrase, final OutputStream pOutput ) throws IOException, InvalidDescriptorException {
196    
197                    final ChangeSet[] changeSets = pChangesProvider.getChangesSets();
198                    final ChangesDescriptor changesDescriptor = new ChangesDescriptor(pPackageDescriptor, changeSets);
199    
200                    changesDescriptor.set("Format", "1.7");
201    
202                    if (changesDescriptor.get("Binary") == null) {
203                            changesDescriptor.set("Binary", changesDescriptor.get("Package"));
204                    }
205    
206                    if (changesDescriptor.get("Source") == null) {
207                            changesDescriptor.set("Source", changesDescriptor.get("Package"));
208                    }
209    
210                    if (changesDescriptor.get("Description") == null) {
211                            changesDescriptor.set("Description", "update to " + changesDescriptor.get("Version"));
212                    }
213    
214                    final StringBuffer files = new StringBuffer("\n");
215                    files.append(' ').append(changesDescriptor.get("MD5"));
216                    files.append(' ').append(changesDescriptor.get("Size"));
217                    files.append(' ').append(changesDescriptor.get("Section"));
218                    files.append(' ').append(changesDescriptor.get("Priority"));
219                    files.append(' ').append(changesDescriptor.get("File"));                        
220                    changesDescriptor.set("Files", files.toString());
221    
222                    if (!changesDescriptor.isValid()) {
223                            throw new InvalidDescriptorException(changesDescriptor);
224                    }
225                    
226                    final String changes = changesDescriptor.toString();
227                    //console.println(changes);
228    
229                    final byte[] changesBytes = changes.getBytes("UTF-8");
230    
231                    if (pRing == null || pKey == null || pPassphrase == null) {                     
232                            pOutput.write(changesBytes);
233                            pOutput.close();                        
234                            return changesDescriptor;
235                    }
236    
237                    console.println("Signing changes with key " + pKey);
238    
239                    final InputStream input = new ByteArrayInputStream(changesBytes);
240    
241                    try {
242                            SigningUtils.clearSign(input, pRing, pKey, pPassphrase, pOutput);               
243                    } catch (Exception e) {
244                            e.printStackTrace();
245                    }
246    
247                    pOutput.close();
248    
249                    return changesDescriptor;
250            }
251    
252            /**
253             * Build control archive of the deb
254             * @param pControlFiles
255             * @param pDataSize
256             * @param pChecksums
257             * @param pOutput
258             * @return
259             * @throws FileNotFoundException
260             * @throws IOException
261             * @throws ParseException
262             */
263            private PackageDescriptor buildControl( final File[] pControlFiles, final BigInteger pDataSize, final StringBuffer pChecksums, final File pOutput ) throws IOException, ParseException {
264    
265                    PackageDescriptor packageDescriptor = null;
266    
267                    final TarOutputStream outputStream = new TarOutputStream(new GZIPOutputStream(new FileOutputStream(pOutput)));
268                    outputStream.setLongFileMode(TarOutputStream.LONGFILE_GNU);
269    
270                    for (int i = 0; i < pControlFiles.length; i++) {
271                            final File file = pControlFiles[i];
272    
273                            if (file.isDirectory()) {
274                                    continue;
275                            }
276    
277                            final TarEntry entry = new TarEntry(file);
278    
279                            final String name = file.getName();
280    
281                            entry.setName(name);
282    
283                            if ("control".equals(name)) {
284                                    packageDescriptor = new PackageDescriptor(new FileInputStream(file), resolver);
285    
286                                    if (packageDescriptor.get("Date") == null) {
287                                            SimpleDateFormat fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH); // Mon, 26 Mar 2007 11:44:04 +0200 (RFC 2822)
288                                            // FIXME Is this field allowed in package descriptors ?
289                                            packageDescriptor.set("Date", fmt.format(new Date()));
290                                    }
291    
292                                    if (packageDescriptor.get("Distribution") == null) {
293                                            packageDescriptor.set("Distribution", "unknown");
294                                    }
295    
296                                    if (packageDescriptor.get("Urgency") == null) {
297                                            packageDescriptor.set("Urgency", "low");
298                                    }
299    
300                                    final String debFullName = System.getenv("DEBFULLNAME");
301                                    final String debEmail = System.getenv("DEBEMAIL");
302    
303                                    if (debFullName != null && debEmail != null) {
304                                            packageDescriptor.set("Maintainer", debFullName + " <" + debEmail + ">");
305                                            console.println("Using maintainer from the environment variables.");
306                                    }
307    
308                                    continue;
309                            }                       
310    
311                            final InputStream inputStream = new FileInputStream(file);
312    
313                            outputStream.putNextEntry(entry);
314    
315                            Utils.copy(inputStream, outputStream);                                                          
316    
317                            outputStream.closeEntry();
318    
319                            inputStream.close();
320    
321                    }
322    
323                    if (packageDescriptor == null) {
324                            throw new FileNotFoundException("No control file in " + Arrays.toString(pControlFiles));
325                    }
326    
327                    packageDescriptor.set("Installed-Size", pDataSize.divide(BigInteger.valueOf(1024)).toString());
328    
329                    addEntry("control", packageDescriptor.toString(), outputStream);
330    
331                    addEntry("md5sums", pChecksums.toString(), outputStream);
332    
333                    outputStream.close();
334    
335                    return packageDescriptor;
336            }
337    
338            /**
339             * Build the data archive of the deb from the provided DataProducers
340             * @param pData
341             * @param pOutput
342             * @param pChecksums
343             * @param pCompression the compression method used for the data file (gzip, bzip2 or anything else for no compression)
344             * @return
345             * @throws NoSuchAlgorithmException
346             * @throws IOException
347             */
348            private BigInteger buildData( final DataProducer[] pData, final File pOutput, final StringBuffer pChecksums, String pCompression ) throws NoSuchAlgorithmException, IOException {
349    
350                    OutputStream out = new FileOutputStream(pOutput);
351                    if ("gzip".equals(pCompression)) {
352                            out = new GZIPOutputStream(out);
353                    } else if ("bzip2".equals(pCompression)) {
354                            out.write("BZ".getBytes());
355                            out = new CBZip2OutputStream(out);
356                    }
357                    
358                    final TarOutputStream outputStream = new TarOutputStream(out);
359                    outputStream.setLongFileMode(TarOutputStream.LONGFILE_GNU);
360    
361                    final MessageDigest digest = MessageDigest.getInstance("MD5");
362    
363                    final Total dataSize = new Total();
364    
365                    final DataConsumer receiver = new DataConsumer() {
366                            public void onEachDir( String dirname, String linkname, String user, int uid, String group, int gid, int mode, long size ) throws IOException {
367    
368                                    if (!dirname.endsWith("/")) {
369                                            dirname = dirname + "/";
370                                    }
371    
372                                    if (!dirname.startsWith("/")) {
373                                            dirname = "/" + dirname;
374                                    }
375    
376                                    TarEntry entry = new TarEntry(dirname);
377    
378                                    // FIXME: link is in the constructor
379                                    entry.setUserName(user);
380                                    entry.setUserId(uid);
381                                    entry.setGroupName(group);
382                                    entry.setGroupId(gid);
383                                    entry.setMode(mode);
384                                    entry.setSize(0);
385    
386                                    outputStream.putNextEntry(entry);
387    
388                                    console.println("dir: " + dirname);
389    
390                                    outputStream.closeEntry();
391                            }
392    
393                            public void onEachFile( InputStream inputStream, String filename, String linkname, String user, int uid, String group, int gid, int mode, long size ) throws IOException {
394    
395                                    if (!filename.startsWith("/")) {
396                                            filename = "/" + filename;
397                                    }
398    
399                                    TarEntry entry = new TarEntry(filename);
400    
401                                    // FIXME: link is in the constructor
402                                    entry.setUserName(user);
403                                    entry.setUserId(uid);
404                                    entry.setGroupName(group);
405                                    entry.setGroupId(gid);
406                                    entry.setMode(mode);
407                                    entry.setSize(size);
408    
409                                    outputStream.putNextEntry(entry);
410    
411                                    dataSize.add(size);
412    
413                                    digest.reset();
414    
415                                    Utils.copy(inputStream, new DigestOutputStream(outputStream, digest));
416                                    
417                                    final String md5 = Utils.toHex(digest.digest());
418    
419                                    outputStream.closeEntry();
420    
421                                    console.println(
422                                                    "file:" + entry.getName() +
423                                                    " size:" + entry.getSize() +
424                                                    " mode:" + entry.getMode() +
425                                                    " linkname:" + entry.getLinkName() +
426                                                    " username:" + entry.getUserName() +
427                                                    " userid:" + entry.getUserId() +
428                                                    " groupname:" + entry.getGroupName() +
429                                                    " groupid:" + entry.getGroupId() +
430                                                    " modtime:" + entry.getModTime() +
431                                                    " md5: " + md5
432                                    );
433    
434                                    pChecksums.append(md5).append(" ").append(entry.getName()).append('\n');
435    
436                            }                                       
437                    };
438    
439                    for (int i = 0; i < pData.length; i++) {
440                            final DataProducer data = pData[i];
441                            data.produce(receiver);
442                    }
443    
444                    outputStream.close();
445    
446                    console.println("Total size: " + dataSize);
447    
448                    return dataSize.count;
449            }
450    
451            private static void addEntry( final String pName, final String pContent, final TarOutputStream pOutput ) throws IOException {
452                    final byte[] data = pContent.getBytes("UTF-8");
453    
454                    final TarEntry entry = new TarEntry(pName);
455                    entry.setSize(data.length);
456    
457                    pOutput.putNextEntry(entry);
458                    pOutput.write(data);
459                    pOutput.closeEntry();           
460            }
461    
462    
463    }