1 /*
2 * @(#)JarDiff.java 1.7 05/11/17
3 *
4 * Copyright (c) 2006 Sun Microsystems, Inc. All Rights Reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * -Redistribution of source code must retain the above copyright notice, this
10 * list of conditions and the following disclaimer.
11 *
12 * -Redistribution in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution.
15 *
16 * Neither the name of Sun Microsystems, Inc. or the names of contributors may
17 * be used to endorse or promote products derived from this software without
18 * specific prior written permission.
19 *
20 * This software is provided "AS IS," without a warranty of any kind. ALL
21 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
22 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
23 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN")
24 * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
25 * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
26 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
27 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
28 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
29 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
30 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
31 *
32 * You acknowledge that this software is not designed, licensed or intended
33 * for use in the design, construction, operation or maintenance of any
34 * nuclear facility.
35 */
36
37 package jnlp.sample.jardiff;
38
39 import java.io.File;
40 import java.io.FileOutputStream;
41 import java.io.IOException;
42 import java.io.InputStream;
43 import java.io.OutputStream;
44 import java.io.StringWriter;
45 import java.io.Writer;
46 import java.util.ArrayList;
47 import java.util.Enumeration;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.Iterator;
51 import java.util.LinkedList;
52 import java.util.List;
53 import java.util.ListIterator;
54 import java.util.Map;
55 import java.util.MissingResourceException;
56 import java.util.ResourceBundle;
57 import java.util.jar.JarEntry;
58 import java.util.jar.JarFile;
59 import java.util.jar.JarOutputStream;
60
61
62 /**
63 * JarDiff is able to create a jar file containing the delta between two
64 * jar files (old and new). The delta jar file can then be applied to the
65 * old jar file to reconstruct the new jar file.
66 * <p/>
67 * Refer to the JNLP spec for details on how this is done.
68 *
69 * @version 1.13, 06/26/03
70 */
71 public class JarDiff
72 implements JarDiffConstants
73 {
74 private static final int DEFAULT_READ_SIZE = 2048;
75
76 private static byte[] newBytes = new byte[DEFAULT_READ_SIZE];
77
78 private static byte[] oldBytes = new byte[DEFAULT_READ_SIZE];
79
80 private static ResourceBundle _resources = null;
81
82 // The JARDiff.java is the stand-along jardiff.jar tool. Thus, we do not
83 // depend on Globals.java and other stuff here. Instead, we use an explicit
84 // _debug flag.
85 private static boolean _debug;
86
87 public static ResourceBundle getResources()
88 {
89 if ( _resources == null )
90 {
91 _resources = ResourceBundle.getBundle( "jnlp/sample/jardiff/resources/strings" );
92 }
93 return _resources;
94 }
95
96 /**
97 * Creates a patch from the two passed in files, writing the result
98 * to <code>os</code>.
99 */
100 public static void createPatch( String oldPath, String newPath, OutputStream os, boolean minimal )
101 throws IOException
102 {
103 JarFile2 oldJar = new JarFile2( oldPath );
104 JarFile2 newJar = new JarFile2( newPath );
105
106 try
107 {
108 Iterator entries;
109 HashMap moved = new HashMap();
110 HashSet visited = new HashSet();
111 HashSet implicit = new HashSet();
112 HashSet moveSrc = new HashSet();
113 HashSet newEntries = new HashSet();
114
115 // FIRST PASS
116 // Go through the entries in new jar and
117 // determine which files are candidates for implicit moves
118 // ( files that has the same filename and same content in old.jar
119 // and new.jar )
120 // and for files that cannot be implicitly moved, we will either
121 // find out whether it is moved or new (modified)
122 entries = newJar.getJarEntries();
123 if ( entries != null )
124 {
125 while ( entries.hasNext() )
126 {
127 JarEntry newEntry = (JarEntry) entries.next();
128 String newname = newEntry.getName();
129
130 // Return best match of contents, will return a name match if possible
131 String oldname = oldJar.getBestMatch( newJar, newEntry );
132 if ( oldname == null )
133 {
134 // New or modified entry
135 if ( _debug )
136 {
137 System.out.println( "NEW: " + newname );
138 }
139 newEntries.add( newname );
140 }
141 else
142 {
143 // Content already exist - need to do a move
144
145 // Should do implicit move? Yes, if names are the same, and
146 // no move command already exist from oldJar
147 if ( oldname.equals( newname ) && !moveSrc.contains( oldname ) )
148 {
149 if ( _debug )
150 {
151 System.out.println( newname + " added to implicit set!" );
152 }
153 implicit.add( newname );
154 }
155 else
156 {
157 // The 1.0.1/1.0 JarDiffPatcher cannot handle
158 // multiple MOVE command with same src.
159 // The work around here is if we are going to generate
160 // a MOVE command with duplicate src, we will
161 // instead add the target as a new file. This way
162 // the jardiff can be applied by 1.0.1/1.0
163 // JarDiffPatcher also.
164 if ( !minimal && ( implicit.contains( oldname ) || moveSrc.contains( oldname ) ) )
165 {
166
167 // generate non-minimal jardiff
168 // for backward compatibility
169
170 if ( _debug )
171 {
172 System.out.println( "NEW: " + newname );
173 }
174 newEntries.add( newname );
175 }
176 else
177 {
178 // Use newname as key, since they are unique
179 if ( _debug )
180 {
181 System.err.println( "moved.put " + newname + " " + oldname );
182 }
183 moved.put( newname, oldname );
184 moveSrc.add( oldname );
185 }
186 // Check if this disables an implicit 'move <oldname> <oldname>'
187 if ( implicit.contains( oldname ) && minimal )
188 {
189
190 if ( _debug )
191 {
192 System.err.println( "implicit.remove " + oldname );
193
194 System.err.println( "moved.put " + oldname + " " + oldname );
195 }
196 implicit.remove( oldname );
197 moved.put( oldname, oldname );
198 moveSrc.add( oldname );
199 }
200
201
202 }
203 }
204 }
205 } //if (entries != null)
206
207 // SECOND PASS: <deleted files> = <oldjarnames> - <implicitmoves> -
208 // <source of move commands> - <new or modified entries>
209 ArrayList deleted = new ArrayList();
210 entries = oldJar.getJarEntries();
211 if ( entries != null )
212 {
213 while ( entries.hasNext() )
214 {
215 JarEntry oldEntry = (JarEntry) entries.next();
216 String oldName = oldEntry.getName();
217 if ( !implicit.contains( oldName ) && !moveSrc.contains( oldName ) &&
218 !newEntries.contains( oldName ) )
219 {
220 if ( _debug )
221 {
222 System.err.println( "deleted.add " + oldName );
223 }
224 deleted.add( oldName );
225 }
226 }
227 }
228
229 //DEBUG
230 if ( _debug )
231 {
232 //DEBUG: print out moved map
233 entries = moved.keySet().iterator();
234 if ( entries != null )
235 {
236 System.out.println( "MOVED MAP!!!" );
237 while ( entries.hasNext() )
238 {
239 String newName = (String) entries.next();
240 String oldName = (String) moved.get( newName );
241 System.out.println( "key is " + newName + " value is " + oldName );
242 }
243 }
244
245 //DEBUG: print out IMOVE map
246 entries = implicit.iterator();
247 if ( entries != null )
248 {
249 System.out.println( "IMOVE MAP!!!" );
250 while ( entries.hasNext() )
251 {
252 String newName = (String) entries.next();
253 System.out.println( "key is " + newName );
254 }
255 }
256 }
257
258 JarOutputStream jos = new JarOutputStream( os );
259
260 // Write out all the MOVEs and REMOVEs
261 createIndex( jos, deleted, moved );
262
263 // Put in New and Modified entries
264 entries = newEntries.iterator();
265 if ( entries != null )
266 {
267
268 while ( entries.hasNext() )
269 {
270 String newName = (String) entries.next();
271 if ( _debug )
272 {
273 System.out.println( "New File: " + newName );
274 }
275 writeEntry( jos, newJar.getEntryByName( newName ), newJar );
276 }
277 }
278
279 jos.finish();
280 jos.close();
281
282 }
283 catch ( IOException ioE )
284 {
285 throw ioE;
286 }
287 finally
288 {
289 try
290 {
291 oldJar.getJarFile().close();
292 }
293 catch ( IOException e1 )
294 {
295 //ignore
296 }
297 try
298 {
299 newJar.getJarFile().close();
300 }
301 catch ( IOException e1 )
302 {
303 //ignore
304 }
305 } // finally
306 }
307
308 /**
309 * Writes the index file out to <code>jos</code>.
310 * <code>oldEntries</code> gives the names of the files that were removed,
311 * <code>movedMap</code> maps from the new name to the old name.
312 */
313 private static void createIndex( JarOutputStream jos, List oldEntries, Map movedMap )
314 throws IOException
315 {
316 StringWriter writer = new StringWriter();
317
318 writer.write( VERSION_HEADER );
319 writer.write( "\r\n" );
320
321 // Write out entries that have been removed
322 for ( Object oldEntry : oldEntries )
323 {
324 String name = (String) oldEntry;
325
326 writer.write( REMOVE_COMMAND );
327 writer.write( " " );
328 writeEscapedString( writer, name );
329 writer.write( "\r\n" );
330 }
331
332 // And those that have moved
333
334 for ( Object o : movedMap.keySet() )
335 {
336 String newName = (String) o;
337 String oldName = (String) movedMap.get( newName );
338
339 writer.write( MOVE_COMMAND );
340 writer.write( " " );
341 writeEscapedString( writer, oldName );
342 writer.write( " " );
343 writeEscapedString( writer, newName );
344 writer.write( "\r\n" );
345 }
346
347 JarEntry je = new JarEntry( INDEX_NAME );
348 byte[] bytes = writer.toString().getBytes( "UTF-8" );
349
350 writer.close();
351 jos.putNextEntry( je );
352 jos.write( bytes, 0, bytes.length );
353 }
354
355 private static void writeEscapedString( Writer writer, String string )
356 throws IOException
357 {
358 int index = 0;
359 int last = 0;
360 char[] chars = null;
361
362 while ( ( index = string.indexOf( ' ', index ) ) != -1 )
363 {
364 if ( last != index )
365 {
366 if ( chars == null )
367 {
368 chars = string.toCharArray();
369 }
370 writer.write( chars, last, index - last );
371 }
372 last = index;
373 index++;
374 writer.write( '\\' );
375 }
376 if ( last != 0 )
377 {
378 writer.write( chars, last, chars.length - last );
379 }
380 else
381 {
382 // no spaces
383 writer.write( string );
384 }
385 }
386
387 private static void writeEntry( JarOutputStream jos, JarEntry entry, JarFile2 file )
388 throws IOException
389 {
390 writeEntry( jos, entry, file.getJarFile().getInputStream( entry ) );
391 }
392
393 private static void writeEntry( JarOutputStream jos, JarEntry entry, InputStream data )
394 throws IOException
395 {
396 jos.putNextEntry( entry );
397
398 try
399 {
400 // Read the entry
401 int size = data.read( newBytes );
402
403 while ( size != -1 )
404 {
405 jos.write( newBytes, 0, size );
406 size = data.read( newBytes );
407 }
408 }
409 catch ( IOException ioE )
410 {
411 throw ioE;
412 }
413 finally
414 {
415 try
416 {
417 data.close();
418 }
419 catch ( IOException e )
420 {
421 //Ignore
422 }
423 }
424 }
425
426
427 /**
428 * JarFile2 wraps a JarFile providing some convenience methods.
429 */
430 private static class JarFile2
431 {
432 private JarFile _jar;
433
434 private List _entries;
435
436 private HashMap _nameToEntryMap;
437
438 private HashMap _crcToEntryMap;
439
440 public JarFile2( String path )
441 throws IOException
442 {
443 _jar = new JarFile( new File( path ) );
444 index();
445 }
446
447 public JarFile getJarFile()
448 {
449 return _jar;
450 }
451
452 public Iterator getJarEntries()
453 {
454 return _entries.iterator();
455 }
456
457 public JarEntry getEntryByName( String name )
458 {
459 return (JarEntry) _nameToEntryMap.get( name );
460 }
461
462 /**
463 * Returns true if the two InputStreams differ.
464 */
465 private static boolean differs( InputStream oldIS, InputStream newIS )
466 throws IOException
467 {
468 int newSize = 0;
469 int oldSize;
470 int total = 0;
471 boolean retVal = false;
472
473 try
474 {
475 while ( newSize != -1 )
476 {
477 newSize = newIS.read( newBytes );
478 oldSize = oldIS.read( oldBytes );
479
480 if ( newSize != oldSize )
481 {
482 if ( _debug )
483 {
484 System.out.println( "\tread sizes differ: " + newSize + " " + oldSize + " total " + total );
485 }
486 retVal = true;
487 break;
488 }
489 if ( newSize > 0 )
490 {
491 while ( --newSize >= 0 )
492 {
493 total++;
494 if ( newBytes[newSize] != oldBytes[newSize] )
495 {
496 if ( _debug )
497 {
498 System.out.println( "\tbytes differ at " + total );
499 }
500 retVal = true;
501 break;
502 }
503 if ( retVal )
504 {
505 //Jump out
506 break;
507 }
508 newSize = 0;
509 }
510 }
511 }
512 }
513 catch ( IOException ioE )
514 {
515 throw ioE;
516 }
517 finally
518 {
519 try
520 {
521 oldIS.close();
522 }
523 catch ( IOException e )
524 {
525 //Ignore
526 }
527 try
528 {
529 newIS.close();
530 }
531 catch ( IOException e )
532 {
533 //Ignore
534 }
535 }
536 return retVal;
537 }
538
539 public String getBestMatch( JarFile2 file, JarEntry entry )
540 throws IOException
541 {
542 // check for same name and same content, return name if found
543 if ( contains( file, entry ) )
544 {
545 return ( entry.getName() );
546 }
547
548 // return name of same content file or null
549 return ( hasSameContent( file, entry ) );
550 }
551
552 public boolean contains( JarFile2 f, JarEntry e )
553 throws IOException
554 {
555
556 JarEntry thisEntry = getEntryByName( e.getName() );
557
558 // Look up name in 'this' Jar2File - if not exist return false
559 if ( thisEntry == null )
560 {
561 return false;
562 }
563
564 // Check CRC - if no match - return false
565 if ( thisEntry.getCrc() != e.getCrc() )
566 {
567 return false;
568 }
569
570 // Check contents - if no match - return false
571 InputStream oldIS = getJarFile().getInputStream( thisEntry );
572 InputStream newIS = f.getJarFile().getInputStream( e );
573 boolean retValue = differs( oldIS, newIS );
574
575 return !retValue;
576 }
577
578 public String hasSameContent( JarFile2 file, JarEntry entry )
579 throws IOException
580 {
581
582 String thisName = null;
583
584 Long crcL = entry.getCrc();
585
586 // check if this jar contains files with the passed in entry's crc
587 if ( _crcToEntryMap.containsKey( crcL ) )
588 {
589 // get the Linked List with files with the crc
590 LinkedList ll = (LinkedList) _crcToEntryMap.get( crcL );
591 // go through the list and check for content match
592 ListIterator li = ll.listIterator( 0 );
593 while ( li.hasNext() )
594 {
595 JarEntry thisEntry = (JarEntry) li.next();
596
597 // check for content match
598 InputStream oldIS = getJarFile().getInputStream( thisEntry );
599 InputStream newIS = file.getJarFile().getInputStream( entry );
600
601 if ( !differs( oldIS, newIS ) )
602 {
603 thisName = thisEntry.getName();
604 return thisName;
605 }
606 }
607 }
608
609 return thisName;
610
611 }
612
613
614 private void index()
615 throws IOException
616 {
617 Enumeration entries = _jar.entries();
618
619 _nameToEntryMap = new HashMap();
620 _crcToEntryMap = new HashMap();
621
622 _entries = new ArrayList();
623 if ( _debug )
624 {
625 System.out.println( "indexing: " + _jar.getName() );
626 }
627 if ( entries != null )
628 {
629 while ( entries.hasMoreElements() )
630 {
631 JarEntry entry = (JarEntry) entries.nextElement();
632
633 long crc = entry.getCrc();
634
635 Long crcL = new Long( crc );
636
637 if ( _debug )
638 {
639 System.out.println( "\t" + entry.getName() + " CRC " + crc );
640 }
641
642 _nameToEntryMap.put( entry.getName(), entry );
643 _entries.add( entry );
644
645 // generate the CRC to entries map
646 if ( _crcToEntryMap.containsKey( crcL ) )
647 {
648 // key exist, add the entry to the correcponding
649 // linked list
650
651 // get the linked list
652 LinkedList ll = (LinkedList) _crcToEntryMap.get( crcL );
653
654 // put in the new entry
655 ll.add( entry );
656
657 // put it back in the hash map
658 _crcToEntryMap.put( crcL, ll );
659 }
660 else
661 {
662 // create a new entry in the hashmap for the new key
663
664 // first create the linked list and put in the new
665 // entry
666 LinkedList ll = new LinkedList();
667 ll.add( entry );
668
669 // create the new entry in the hashmap
670 _crcToEntryMap.put( crcL, ll );
671 }
672
673 }
674 }
675 }
676
677 } // end of class JarFile2
678
679
680 private static void showHelp()
681 {
682 System.out.println(
683 "JarDiff: [-nonminimal (for backward compatibility with 1.0.1/1.0] [-creatediff | -applydiff] [-output file] old.jar new.jar" );
684 }
685
686 // -creatediff -applydiff -debug -output file
687 public static void main( String[] args )
688 throws IOException
689 {
690 boolean diff = true;
691 boolean minimal = true;
692 String outputFile = "out.jardiff";
693
694 for ( int counter = 0; counter < args.length; counter++ )
695 {
696 // for backward compatibilty with 1.0.1/1.0
697 if ( args[counter].equals( "-nonminimal" ) || args[counter].equals( "-n" ) )
698 {
699 minimal = false;
700 }
701 else if ( args[counter].equals( "-creatediff" ) || args[counter].equals( "-c" ) )
702 {
703 diff = true;
704 }
705 else if ( args[counter].equals( "-applydiff" ) || args[counter].equals( "-a" ) )
706 {
707 diff = false;
708 }
709 else if ( args[counter].equals( "-debug" ) || args[counter].equals( "-d" ) )
710 {
711 _debug = true;
712 }
713 else if ( args[counter].equals( "-output" ) || args[counter].equals( "-o" ) )
714 {
715 if ( ++counter < args.length )
716 {
717 outputFile = args[counter];
718 }
719 }
720 else if ( args[counter].equals( "-applydiff" ) || args[counter].equals( "-a" ) )
721 {
722 diff = false;
723 }
724 else
725 {
726 if ( ( counter + 2 ) != args.length )
727 {
728 showHelp();
729 System.exit( 0 );
730 }
731 if ( diff )
732 {
733 try
734 {
735 OutputStream os = new FileOutputStream( outputFile );
736
737 JarDiff.createPatch( args[counter], args[counter + 1], os, minimal );
738 os.close();
739 }
740 catch ( IOException ioe )
741 {
742 try
743 {
744 System.out.println( getResources().getString( "jardiff.error.create" ) + " " + ioe );
745 }
746 catch ( MissingResourceException mre )
747 {
748 }
749 }
750 }
751 else
752 {
753 try
754 {
755 OutputStream os = new FileOutputStream( outputFile );
756
757 new JarDiffPatcher().applyPatch( null, args[counter], args[counter + 1], os );
758 os.close();
759 }
760 catch ( IOException ioe )
761 {
762 try
763 {
764 System.out.println( getResources().getString( "jardiff.error.apply" ) + " " + ioe );
765 }
766 catch ( MissingResourceException mre )
767 {
768 }
769 }
770 }
771 System.exit( 0 );
772 }
773 }
774 showHelp();
775 }
776 }