View Javadoc
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 }