View Javadoc

1   /*
2    * Copyright 2005 The Apache Software Foundation.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.vafer.jdeb;
17  
18  import java.io.ByteArrayInputStream;
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.FileNotFoundException;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.math.BigInteger;
27  import java.security.DigestOutputStream;
28  import java.security.MessageDigest;
29  import java.security.NoSuchAlgorithmException;
30  import java.text.ParseException;
31  import java.text.SimpleDateFormat;
32  import java.util.Arrays;
33  import java.util.Date;
34  import java.util.Locale;
35  import java.util.zip.GZIPOutputStream;
36  
37  import org.apache.tools.bzip2.CBZip2OutputStream;
38  import org.apache.tools.tar.TarEntry;
39  import org.apache.tools.tar.TarOutputStream;
40  import org.vafer.jdeb.ar.ArEntry;
41  import org.vafer.jdeb.ar.ArOutputStream;
42  import org.vafer.jdeb.changes.ChangeSet;
43  import org.vafer.jdeb.changes.ChangesProvider;
44  import org.vafer.jdeb.descriptors.ChangesDescriptor;
45  import org.vafer.jdeb.descriptors.InvalidDescriptorException;
46  import org.vafer.jdeb.descriptors.PackageDescriptor;
47  import org.vafer.jdeb.signing.SigningUtils;
48  import org.vafer.jdeb.utils.InformationOutputStream;
49  import org.vafer.jdeb.utils.Utils;
50  import org.vafer.jdeb.utils.VariableResolver;
51  
52  /**
53   * The processor does the actual work of building the deb related files.
54   * It is been used by the ant task and (later) the maven plugin.
55   * 
56   * @author Torsten Curdt <tcurdt@vafer.org>
57   */
58  public class Processor {
59  
60  	private final Console console;
61  	private final VariableResolver resolver;
62  
63  	private static final class Total {
64  		private BigInteger count = BigInteger.valueOf(0);
65  
66  		public void add(long size) {
67  			count = count.add(BigInteger.valueOf(size));
68  		}
69  
70  		public String toString() {
71  			return "" + count;
72  		}
73  
74  		public BigInteger toBigInteger() {
75  			return count;
76  		}
77  	}
78  
79  	public Processor( final Console pConsole, final VariableResolver pResolver ) {
80  		console = pConsole;
81  		resolver = pResolver;
82  	}
83  
84  	private void addTo( final ArOutputStream pOutput, final String pName, final String pContent ) throws IOException {
85  		final byte[] content = pContent.getBytes(); 
86  		pOutput.putNextEntry(new ArEntry(pName, content.length));
87  		pOutput.write(content);
88  	}
89  
90  	private void addTo( final ArOutputStream pOutput, final String pName, final File pContent ) throws IOException {
91  		pOutput.putNextEntry(new ArEntry(pName, pContent.length()));
92  		
93  		final InputStream input = new FileInputStream(pContent);
94  		try {
95  			Utils.copy(input, pOutput);
96  		} finally {
97  			input.close();
98  		}
99  	}
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 }