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 }