|
1 # |
|
2 # Copyright (c) 2003-2009 Nokia Corporation and/or its subsidiary(-ies). |
|
3 # All rights reserved. |
|
4 # This component and the accompanying materials are made available |
|
5 # under the terms of the License "Eclipse Public License v1.0" |
|
6 # which accompanies this distribution, and is available |
|
7 # at the URL "http://www.eclipse.org/legal/epl-v10.html". |
|
8 # |
|
9 # Initial Contributors: |
|
10 # Nokia Corporation - initial contribution. |
|
11 # |
|
12 # Contributors: |
|
13 # |
|
14 # Description: |
|
15 # |
|
16 |
|
17 package EvalidCompare; |
|
18 |
|
19 use strict; |
|
20 our $VERSION = '1.00'; |
|
21 use IO::Handle; |
|
22 use IO::File; |
|
23 use Cwd; |
|
24 |
|
25 use File::Temp qw/ tempfile tempdir /; |
|
26 use File::Find; |
|
27 use File::Path; |
|
28 use File::Basename; |
|
29 use File::Copy; |
|
30 |
|
31 # |
|
32 # Constants. |
|
33 # |
|
34 |
|
35 my %typeLookup = ( |
|
36 'ARM PE-COFF executable' => 'ignore', |
|
37 'E32 EXE' => 'e32', |
|
38 'E32 DLL' => 'e32', |
|
39 'Uncompressed E32 EXE' => 'e32', |
|
40 'Uncompressed E32 DLL' => 'e32', |
|
41 'Compressed E32 EXE' => 'e32', |
|
42 'Compressed E32 DLL' => 'e32', |
|
43 'Intel DLL' => 'intel_pe', |
|
44 'Intel EXE' => 'intel_pe', |
|
45 'MSDOS EXE' => 'intel_pe', |
|
46 'Intel object' => 'intel', |
|
47 'Intel library' => 'intel', |
|
48 'ELF library' => 'elf', |
|
49 'ARM object' => 'arm', |
|
50 'ARM library' => 'arm', |
|
51 'unknown format' => 'identical', |
|
52 'Java class' => 'identical', |
|
53 'ZIP file' => 'zip', |
|
54 'Permanent File Store' => 'permanent_file_store', |
|
55 'SIS file' => 'identical', |
|
56 'MSVC database' => 'ignore', |
|
57 'MAP file' => 'map', |
|
58 'SGML file' => 'sgml', |
|
59 'Preprocessed text' => 'preprocessed_text', |
|
60 'ELF file' => 'elf', |
|
61 'Unknown COFF object' => 'identical', |
|
62 'Unknown library' => 'identical', |
|
63 'chm file' => 'chm_file', |
|
64 'Header file' => 'header', |
|
65 'Distribution Policy' => 'distpol' |
|
66 ); |
|
67 |
|
68 |
|
69 # %TEMPDIR% and %FILE% are magic words for the expandor |
|
70 # they will be replaced with suitable values when used |
|
71 # they also enabled an order of expandor arguments where the filename is not last |
|
72 my %typeHandler = ( |
|
73 e32 => {reader => 'elf2e32 --dump --e32input=', filter => \&Elf2E32Filter}, |
|
74 arm => {reader => 'nm --no-sort', filter => \&NmFilter, retry => 1, relative_paths => 1}, |
|
75 elf => {reader => 'elfdump -i', filter => \&ElfDumpFilter, rawretry => 1}, |
|
76 intel => {reader => '%EPOCROOT%epoc32\gcc_mingw\bin\nm --no-sort', filter => \&NmFilter, rawretry => 1, relative_paths => 1, skipstderr => 1}, |
|
77 intel_pe => {reader => 'pe_dump', filter => \&FilterNone, rawretry => 1}, |
|
78 zip => {reader => '"'.$FindBin::Bin.'/unzip" -l -v', filter => \&UnzipFilter, rawretry => 1}, |
|
79 map => {filter => \&MapFilter, skipblanks => 1}, |
|
80 sgml => {filter => \&SgmlFilter}, |
|
81 preprocessed_text => {filter => \&PreprocessedTextFilter}, |
|
82 permanent_file_store => {reader => 'pfsdump -c -v', filter => \&PermanentFileStoreFilter, rawretry => 1, relative_paths => 1}, |
|
83 ignore => {filter => \&FilterAll}, |
|
84 chm_file => {expandor => 'hh -decompile %TEMPDIR% %FILE%', rawretry => 1}, |
|
85 header => {filter => \&FilterCVSTags}, |
|
86 distpol => {filter => \&DistributionPolicyFilter} |
|
87 ); |
|
88 |
|
89 |
|
90 # |
|
91 # Globals. |
|
92 # |
|
93 |
|
94 my $log; |
|
95 my $verbose; |
|
96 my $toRoot; |
|
97 my $dumpDir; |
|
98 |
|
99 undef $dumpDir; |
|
100 |
|
101 |
|
102 # |
|
103 # Public. |
|
104 # |
|
105 |
|
106 sub CompareFiles { |
|
107 my $file1 = shift; |
|
108 my $file2 = shift; |
|
109 $verbose = defined($_[0]) ? shift : 0; |
|
110 $log = defined($_[0]) ? shift : *STDOUT; |
|
111 # Try binary compare first (to keep semantics the same as evalid)... |
|
112 if (DoCompareFiles($file1, $file2, 'unknown format')) { |
|
113 return 1,'identical'; |
|
114 } |
|
115 my $type = IdentifyFileType($file1); |
|
116 if ($typeLookup{$type} eq 'identical') { |
|
117 return 0,$type; # We already know a binary compare is going to return false. |
|
118 } |
|
119 return DoCompareFiles($file1, $file2, $type),$type; |
|
120 } |
|
121 |
|
122 sub GenerateSignature { |
|
123 my $file = shift; |
|
124 $dumpDir = shift; |
|
125 $verbose = defined($_[0]) ? shift : 0; |
|
126 $log = defined($_[0]) ? shift : *STDOUT; |
|
127 my $md5; |
|
128 |
|
129 if (eval "require Digest::MD5") { # Prefer Digest::MD5, if available. |
|
130 $md5 = Digest::MD5->new(); |
|
131 } elsif (eval "require MD5") { # Try old version of MD5, if available. |
|
132 $md5 = new MD5; |
|
133 } elsif (eval "require Digest::Perl::MD5") { # Try Perl (Slow) version of MD5, if available. |
|
134 $md5 = Digest::Perl::MD5->new(); |
|
135 } else { |
|
136 die "Error: Cannot load any MD5 Modules"; |
|
137 } |
|
138 |
|
139 my $type = IdentifyFileType($file); |
|
140 WriteFilteredData($file, $type, $md5); |
|
141 return $md5->hexdigest(), $type; |
|
142 } |
|
143 |
|
144 |
|
145 # |
|
146 # Private. |
|
147 # |
|
148 |
|
149 sub IdentifyFileType { |
|
150 my $file = shift; |
|
151 open (FILE, $file) or die "Error: Couldn't open \"$file\" for reading: $!\n"; |
|
152 binmode (FILE); |
|
153 my $typeBuf; |
|
154 read (FILE, $typeBuf, 512); |
|
155 close (FILE); |
|
156 my ($uid1, $uid2, $uid3, $checksum) = unpack "V4", $typeBuf; |
|
157 |
|
158 # NB. Need to use the s modifier so that '.' will match \x0A |
|
159 |
|
160 if ($typeBuf =~ /^.\x00\x00\x10.{12}EPOC.{8}(....).{12}(.)..(.)/s) { |
|
161 # E32 Image file with a 0x100000?? UID1 |
|
162 # $2 is the flag field indicating an EXE or a DLL |
|
163 # $3 is the flag byte indicating compressable executables |
|
164 # $1 is the format field indicating compression type |
|
165 # See e32tools\inc\e32image.h |
|
166 # |
|
167 my $typename = "E32 EXE"; |
|
168 if ((ord $2) & 0x1) { |
|
169 $typename = "E32 DLL"; |
|
170 } |
|
171 if ((ord $3) >= 0x1) { |
|
172 if ((ord $1) != 0) { |
|
173 $typename = "Compressed $typename"; |
|
174 } |
|
175 else { |
|
176 $typename = "Uncompressed $typename"; |
|
177 } |
|
178 } |
|
179 return $typename; |
|
180 } |
|
181 |
|
182 if ($typeBuf =~ /^\x4D\x5A.{38}\x00{20}(....)/s) { |
|
183 # A standard 64-byte MS-DOS header with e_magic == IMAGE_DOS_SIGNATURE |
|
184 # $1 is e_lfanew, which we expect to point to a COFF header |
|
185 |
|
186 my $offset = unpack "V",$1; |
|
187 if ($offset + 24 <= length $typeBuf) { |
|
188 $typeBuf = substr $typeBuf, $offset; |
|
189 } |
|
190 else { |
|
191 open FILE, $file or die "Error: Couldn't open \"$file\" for reading: $!\n"; |
|
192 binmode FILE; |
|
193 seek FILE, $offset, 0; |
|
194 read FILE, $typeBuf, 512; |
|
195 close FILE; |
|
196 } |
|
197 |
|
198 if ($typeBuf =~ /^PE\0\0\x4c\x01.{16}(..)/s) { |
|
199 # A PE signature "PE\0\0" followed by a COFF header with |
|
200 # machine type IMAGE_FILE_MACHINE_I386 |
|
201 # $1 is the characteristics field |
|
202 # |
|
203 if ((unpack "v",$1) & 0x2000) { |
|
204 return "Intel DLL"; |
|
205 } |
|
206 else { |
|
207 return "Intel EXE"; |
|
208 } |
|
209 } |
|
210 elsif($typeBuf =~ /^PE\0\0\0\x0a/) { |
|
211 # A PE signature "PE\0\0" followed by ARM COFF file magic value 0xA00 |
|
212 return "ARM PE-COFF executable"; |
|
213 } |
|
214 else { |
|
215 return "MSDOS EXE"; |
|
216 } |
|
217 } |
|
218 |
|
219 if ($typeBuf =~ /^(\x4c\x01|\x00\x0A).(\x00|\x01).{4}...\x00/s) { |
|
220 # COFF header with less than 512 sections and a symbol table |
|
221 # at an offset no greater than 0x00ffffff |
|
222 |
|
223 if ($1 eq "\x4c\x01") { |
|
224 return "Intel object"; |
|
225 } |
|
226 elsif ($1 eq "\x00\x0A") { |
|
227 return "ARM object"; |
|
228 } |
|
229 else { |
|
230 return "Unknown COFF object"; |
|
231 } |
|
232 } |
|
233 |
|
234 if ($typeBuf =~ /^!<arch>\x0A(.{48}([0-9 ]{10})\x60\x0A(......))/s) { |
|
235 # library - could be MARM or WINS |
|
236 |
|
237 $typeBuf = $1; |
|
238 my $member_start = 8; |
|
239 |
|
240 open (FILE, $file) or die "Error: Couldn't open \"$file\" for reading: $!\n"; |
|
241 binmode (FILE); |
|
242 |
|
243 while ($typeBuf =~ /^.{48}([0-9 ]{10})\x60\x0A(......)/s) { |
|
244 # $1 is the size of the archive member, $2 is first 6 bytes of the file |
|
245 # There may be several different sorts of file in the archive, and we |
|
246 # need to scan through until we find a type we recognize: |
|
247 # $2 == 0x0A00 would be ARM COFF, 0x014C would be Intel COFF |
|
248 if ($2 =~ /^\x00\x0A/) { |
|
249 close FILE; |
|
250 return "ARM library"; |
|
251 } |
|
252 if ($2 =~ /^\x4C\x01/) { |
|
253 close FILE; |
|
254 return "Intel library"; |
|
255 } |
|
256 my $elfBuf = $2; |
|
257 if ($2 =~ /^\x7F\x45\x4C\x46/) { |
|
258 close FILE; |
|
259 my $dataEncodingLib = substr($elfBuf, 5, 6); |
|
260 if ( $dataEncodingLib =~ /^\x02/) { |
|
261 # e_ident[EI_DATA] == 2 (Data Encoding ELFDATA2MSB - big endian) |
|
262 # this is not supported by Elfdump hence it is treated as 'unknown format' |
|
263 return 'unknown library'; |
|
264 } |
|
265 else { |
|
266 return "ELF library"; |
|
267 } |
|
268 } |
|
269 |
|
270 $member_start += 60 + $1; |
|
271 if ($member_start & 0x1) { |
|
272 $member_start += 1; # align to multiple of 2 bytes |
|
273 } |
|
274 seek FILE, $member_start, 0; |
|
275 read FILE, $typeBuf, 512; |
|
276 } |
|
277 close FILE; |
|
278 return "Unknown library"; |
|
279 } |
|
280 |
|
281 if ($typeBuf =~ /^\xCA\xFE\xBA\xBE/) { |
|
282 # Java class file - should have match as a straight binary comparison |
|
283 return "Java class"; |
|
284 } |
|
285 |
|
286 if ($typeBuf =~ /^PK\x03\x04/) { |
|
287 # ZIP file |
|
288 return "ZIP file"; |
|
289 } |
|
290 |
|
291 if ($uid1 && $uid1==0x10000050) { |
|
292 # Permanent File Store |
|
293 return "Permanent File Store"; |
|
294 } |
|
295 |
|
296 if ($uid1 && $uid2 && $uid3 && $checksum && $uid3==0x10000419) { |
|
297 if (($uid1==0x100002c3 && $uid2==0x1000006d && $checksum==0x128ca96f) # narrow |
|
298 || ($uid1==0x10003b0b && $uid2==0x1000006d && $checksum==0x75e21a1d) # unicode |
|
299 || ($uid1==0x10009205 && $uid2==0x10003a12 && $checksum==0x986a0c25)) # new format |
|
300 { |
|
301 # SIS file |
|
302 return "SIS file"; |
|
303 } |
|
304 } |
|
305 |
|
306 if ($typeBuf =~ /^Microsoft [^\x0A]+ [Dd]atabase/s) { |
|
307 return "MSVC database"; |
|
308 } |
|
309 |
|
310 if ($typeBuf =~ /^\S.+ needed due to / || $typeBuf =~ /^Archive member included.*because of file/) { |
|
311 # GCC MAP file |
|
312 return "MAP file"; |
|
313 } |
|
314 |
|
315 if ($typeBuf =~ /Preferred load address is/) { |
|
316 # Developer Studio MAP file |
|
317 return "MAP file"; |
|
318 } |
|
319 |
|
320 if ($typeBuf =~ /^Address\s+Size\s+Name\s+Subname\s+Module/) { |
|
321 # CodeWarrior MAP file |
|
322 return "MAP file"; |
|
323 } |
|
324 |
|
325 if ($typeBuf =~ /^ARM Linker,/) { |
|
326 # RVCT MAP file |
|
327 return "MAP file"; |
|
328 } |
|
329 |
|
330 if ($typeBuf =~ /<!DOCTYPE/i) { |
|
331 # XML or HTML file - need to ignore javadoc generation dates |
|
332 return "SGML file"; |
|
333 } |
|
334 |
|
335 if ($typeBuf =~ /^# 1 ".*"(\x0D|\x0A)/s) { |
|
336 # Output of CPP |
|
337 return "Preprocessed text"; |
|
338 } |
|
339 |
|
340 if ($typeBuf =~ /^\x7F\x45\x4C\x46/) { |
|
341 my $dataEncoding = substr($typeBuf, 5, 6); |
|
342 if ( $dataEncoding =~ /^\x02/) { |
|
343 # e_ident[EI_DATA] == 2 (Data Encoding ELFDATA2MSB - big endian) |
|
344 # this is not supported by Elfdump hence it is treated as 'unknown format' |
|
345 return 'unknown format'; |
|
346 } |
|
347 else { |
|
348 return "ELF file";; |
|
349 } |
|
350 } |
|
351 |
|
352 if ($typeBuf =~/^ITSF/) { |
|
353 # chm file |
|
354 return "chm file"; |
|
355 } |
|
356 |
|
357 if ($file =~ m/\.(iby|h|hby|hrh|oby|rsg|cpp)$/i) { |
|
358 return "Header file"; |
|
359 } |
|
360 |
|
361 if ($file =~ /distribution\.policy$/i) { |
|
362 return "Distribution Policy" |
|
363 } |
|
364 |
|
365 return 'unknown format'; |
|
366 } |
|
367 |
|
368 sub WriteFilteredData { |
|
369 my $file = shift; |
|
370 my $type = shift; |
|
371 my $md5 = shift; |
|
372 my $dumpDirExpandedFile = shift; |
|
373 |
|
374 my (@dumpDirBuffer); |
|
375 |
|
376 unless (exists $typeLookup{$type}) { |
|
377 die "Invalid file type \"$type\""; |
|
378 } |
|
379 $type = $typeLookup{$type}; |
|
380 |
|
381 # Check to see if this file type requires expanding first |
|
382 if (exists $typeHandler{$type}->{expandor}) |
|
383 { |
|
384 my $expandor = $typeHandler{$type}->{expandor}; |
|
385 # Create two temporary directories |
|
386 my $tempdir = tempdir ( "EvalidExpand_XXXXXX", DIR => File::Spec->tmpdir, CLEANUP => 1); |
|
387 |
|
388 # Build the Expandor commandline |
|
389 $expandor =~ s/%TEMPDIR%/$tempdir/g; |
|
390 $expandor =~ s/%FILE%/$file/g; |
|
391 |
|
392 # Expand files |
|
393 my $output = `$expandor 2>&1`; |
|
394 print($log "Expanding using $expandor output was:-\n$output") if ($verbose); |
|
395 if ($? > 0) |
|
396 { |
|
397 print ($log "$expandor exited with $?") if ($verbose); |
|
398 # set type to be identical for retry if raw |
|
399 if ($typeHandler{$type}->{rawretry} == 1) |
|
400 { |
|
401 $type = 'identical'; |
|
402 } else { |
|
403 print "ERROR: failed to start $expandor (" .($?). ") - reporting failure\n"; |
|
404 } |
|
405 } else { |
|
406 # Process all files in $tempdir |
|
407 my @FileList; |
|
408 find(sub { push @FileList, $File::Find::name if (! -d);}, $tempdir); |
|
409 foreach my $expandfile (@FileList) |
|
410 { |
|
411 my $dumpDirExpandedFilename = ""; |
|
412 |
|
413 if ($dumpDir) |
|
414 { |
|
415 $dumpDirExpandedFilename = $expandfile; |
|
416 $dumpDirExpandedFilename =~ s/^.*EvalidExpand_\w+//; |
|
417 $dumpDirExpandedFilename = $file.$dumpDirExpandedFilename; |
|
418 } |
|
419 |
|
420 my $type = IdentifyFileType($expandfile); |
|
421 |
|
422 &WriteFilteredData($expandfile, $type, $md5, $dumpDirExpandedFilename); |
|
423 } |
|
424 } |
|
425 } elsif ($type ne 'identical') { |
|
426 unless (exists $typeHandler{$type}) { |
|
427 die "Invalid comparison type \"$type\""; |
|
428 } |
|
429 my $reader = $typeHandler{$type}->{reader}; |
|
430 my $filter = $typeHandler{$type}->{filter}; |
|
431 my $retry = $typeHandler{$type}->{retry} || 0; |
|
432 my $rawretry = $typeHandler{$type}->{rawretry} || 0; |
|
433 my $skipblanks = $typeHandler{$type}->{skipblanks} || 0; |
|
434 my $relativePaths = $typeHandler{$type}->{relative_paths} || 0; |
|
435 my $dosPaths = $typeHandler{$type}->{dos_paths} || 0; |
|
436 |
|
437 my $skipstderr = $typeHandler{$type}->{skipstderr} || 0; |
|
438 my $redirectstd = "2>&1"; |
|
439 |
|
440 if ($skipstderr) { |
|
441 $redirectstd = "2>NUL"; |
|
442 } |
|
443 |
|
444 if ($relativePaths) { |
|
445 $file = RelativePath($file); |
|
446 } |
|
447 if ($dosPaths) { |
|
448 $file =~ s/\//\\/g; # convert to DOS-style backslash separators |
|
449 } |
|
450 |
|
451 my $raw; |
|
452 if ($reader) { |
|
453 $raw = IO::File->new("$reader \"$file\" $redirectstd |") or die "Error: Couldn't run \"$reader $file\": $!\n"; |
|
454 } |
|
455 else { |
|
456 $raw = IO::File->new("$file") or die "Error: Couldn't open \"$file\": $!\n"; |
|
457 } |
|
458 while (my $line = <$raw>) { |
|
459 &$filter(\$line); |
|
460 next if $skipblanks and $line =~ /^\s*$/; |
|
461 $md5->add($line); |
|
462 push @dumpDirBuffer, $line if ($dumpDir); |
|
463 } |
|
464 Drain($raw); |
|
465 $raw->close(); |
|
466 |
|
467 # Retry once if reader failed and reader has retry specified |
|
468 if ((($?>>8) != 0) && ($retry == 1)) |
|
469 { |
|
470 print "Warning: $reader failed (" .($?>>8). ") on $file - retrying\n"; |
|
471 # Reset MD5 |
|
472 $md5->reset; |
|
473 undef @dumpDirBuffer if ($dumpDir); |
|
474 $raw = IO::File->new("$reader \"$file\" $redirectstd |") or die "Error: Couldn't run \"$reader $file\": $!\n"; |
|
475 while (my $line = <$raw>) |
|
476 { |
|
477 &$filter(\$line); |
|
478 next if $skipblanks and $line =~ /^\s*$/; |
|
479 $md5->add($line); |
|
480 push @dumpDirBuffer, $line if ($dumpDir); |
|
481 } |
|
482 Drain($raw); |
|
483 $raw->close(); |
|
484 if (($?>>8) != 0) |
|
485 { |
|
486 print "Error: $reader failed again (" .($?>>8) .") on $file - reporting failure\n"; |
|
487 } |
|
488 } |
|
489 |
|
490 # Retry as raw if specified |
|
491 if (($?>>8) != 0) { |
|
492 if ($rawretry) |
|
493 { |
|
494 if ($reader =~ /^pfsdump/) { |
|
495 print "Warning: $reader failed (". ($?>>8) .") on file $file - retrying as raw binary\n"; |
|
496 } |
|
497 else { |
|
498 print "Info: something wrong to execute $reader (". ($?>>8) .") on file $file - retrying as raw binary\n"; |
|
499 } |
|
500 # Set type to be identical so it will try it as a raw binary stream |
|
501 $type = 'identical'; |
|
502 } else { |
|
503 print "Error: $reader failed (". ($?>>8) .") on file $file - not retrying as raw binary\n"; |
|
504 } |
|
505 } |
|
506 } |
|
507 if ($type eq 'identical') { |
|
508 # Reset md5 as it might have been used in reader section |
|
509 $md5->reset; |
|
510 undef @dumpDirBuffer if ($dumpDir); |
|
511 # Treat 'identical' as a special case - no filtering, just write raw binary stream. |
|
512 my $raw = IO::File->new($file) or die "Error: Couldn't open \"$file\" for reading: $!\n"; |
|
513 binmode($raw); |
|
514 my $buf; |
|
515 while ($raw->read($buf, 4096)) { |
|
516 $md5->add($buf); |
|
517 } |
|
518 $raw->close(); |
|
519 } |
|
520 |
|
521 my $dumpDirFilename = $file; |
|
522 $dumpDirFilename = $dumpDirExpandedFile if ($dumpDirExpandedFile); |
|
523 dumpDescriptiveOutput ($file, $dumpDirFilename, @dumpDirBuffer) if ($dumpDir); |
|
524 |
|
525 # Make sure the $? is reset for the next file otherwise it will report errors |
|
526 $? = 0; |
|
527 } |
|
528 |
|
529 sub DoCompareFiles { |
|
530 my $file1 = shift; |
|
531 my $file2 = shift; |
|
532 my $type = shift; |
|
533 my $same = 0; |
|
534 unless (exists $typeLookup{$type}) { |
|
535 die "Invalid file type \"$type\""; |
|
536 } |
|
537 |
|
538 $type = $typeLookup{$type}; |
|
539 |
|
540 # Check to see if this file type requires expanding first |
|
541 if (exists $typeHandler{$type}->{expandor}) |
|
542 { |
|
543 $same = &ExpandAndCompareFiles($file1, $file2, $typeHandler{$type}->{expandor}); |
|
544 # Check for Expanding error |
|
545 if ($same == -1) |
|
546 { |
|
547 if ($typeHandler{$type}->{rawretry} == 1) |
|
548 { |
|
549 # Set type to be identical if rawrety is set |
|
550 $type = 'identical'; |
|
551 print($log "Warning: Expandor $typeHandler{$type}->{expandor} failed for $file1 or $file2 : retrying as raw\n") if ($verbose); |
|
552 } else { |
|
553 die "Error: Expandor $typeHandler{$type}->{expandor} failed for $file1 or $file2\n"; |
|
554 } |
|
555 } else { |
|
556 return $same; |
|
557 } |
|
558 } |
|
559 |
|
560 if ($type ne 'identical') |
|
561 { |
|
562 unless (exists $typeHandler{$type}) { |
|
563 die "Invalid comparison type \"$type\""; |
|
564 } |
|
565 |
|
566 my $reader = $typeHandler{$type}->{reader}; |
|
567 my $filter = $typeHandler{$type}->{filter}; |
|
568 my $retry = $typeHandler{$type}->{retry} || 0; |
|
569 my $skipblanks= $typeHandler{$type}->{skipblanks} || 0; |
|
570 my $rawretry = $typeHandler{$type}->{rawretry} || 0; |
|
571 my $relativePaths = $typeHandler{$type}->{relative_paths} || 0; |
|
572 my $skipstderr = $typeHandler{$type}->{skipstderr} || 0; |
|
573 my $redirectstd = "2>&1"; |
|
574 |
|
575 if ($skipstderr) { |
|
576 $redirectstd = "2>NUL"; |
|
577 } |
|
578 |
|
579 if ($relativePaths) { |
|
580 $file1 = RelativePath($file1); |
|
581 $file2 = RelativePath($file2); |
|
582 } |
|
583 my $fileHandle1; |
|
584 my $fileHandle2; |
|
585 if ($reader) { |
|
586 $fileHandle1 = IO::File->new("$reader \"$file1\" $redirectstd |") or die "Error: Couldn't run \"$reader $file1\": $!\n"; |
|
587 $fileHandle2 = IO::File->new("$reader \"$file2\" $redirectstd |") or die "Error: Couldn't run \"$reader $file2\": $!\n"; |
|
588 } |
|
589 else { |
|
590 $fileHandle1 = IO::File->new("$file1") or die "Error: Couldn't open \"$file1\": $!\n"; |
|
591 $fileHandle2 = IO::File->new("$file2") or die "Error: Couldn't open \"$file2\": $!\n"; |
|
592 } |
|
593 $same = CompareTexts($fileHandle1, $fileHandle2, $filter, $file1, $skipblanks); |
|
594 Drain($fileHandle1, $fileHandle2); |
|
595 |
|
596 $fileHandle1->close(); |
|
597 my $status1 = $?>>8; |
|
598 $fileHandle2->close(); |
|
599 my $status2 = $?>>8; |
|
600 if (($retry) && ($status1 != 0 or $status2 != 0)) |
|
601 { |
|
602 print ($log "Warning: $reader failed ($status1, $status2) - retrying\n"); |
|
603 |
|
604 # Repeat previous code by hand, rather than calling DoCompareFiles |
|
605 # again: if it's a systematic failure that would be a never ending loop... |
|
606 |
|
607 $fileHandle1 = IO::File->new("$reader \"$file1\" $redirectstd |") or die "Error: Couldn't run \"$reader $file1\": $!\n"; |
|
608 $fileHandle2 = IO::File->new("$reader \"$file2\" $redirectstd |") or die "Error: Couldn't run \"$reader $file2\": $!\n"; |
|
609 $same = CompareTexts($fileHandle1, $fileHandle2, $filter, $file1, $skipblanks); |
|
610 Drain($fileHandle1, $fileHandle2); |
|
611 $fileHandle1->close(); |
|
612 $status1 = $?>>8; |
|
613 $fileHandle2->close(); |
|
614 $status2 = $?>>8; |
|
615 if ($status1 != 0 or $status2 != 0) |
|
616 { |
|
617 print ($log "Warning: $reader failed again ($status1, $status2) - reporting failure\n"); |
|
618 $same = 0; |
|
619 } |
|
620 } |
|
621 |
|
622 # Retry as raw if specified |
|
623 if (($rawretry)&& ($status1 != 0 or $status2 != 0)) |
|
624 { |
|
625 if ($rawretry) |
|
626 { |
|
627 print ($log "Warning: $reader failed (" .($?>>8). ") on a file retrying as raw binary\n"); |
|
628 # Set type to be identical so it will try it as a raw binary stream |
|
629 $type = 'identical'; |
|
630 } else { |
|
631 print ($log "Error: $reader failed (" .($?>>8). ") on a file not retrying as raw binary\n"); |
|
632 } |
|
633 } |
|
634 |
|
635 } |
|
636 |
|
637 if ($type eq 'identical') { |
|
638 # Treat 'identical' as a special case - no filtering, just do raw binary stream comparison. |
|
639 my $fileHandle1 = IO::File->new($file1) or die "Error: Couldn't open \"$file1\" for reading: $!\n"; |
|
640 my $fileHandle2 = IO::File->new($file2) or die "Error: Couldn't open \"$file2\" for reading: $!\n"; |
|
641 binmode($fileHandle1); |
|
642 binmode($fileHandle2); |
|
643 $same = CompareStreams($fileHandle1, $fileHandle2, $file1); |
|
644 } |
|
645 |
|
646 # Make sure the $? is reset for the next file otherwise it will report errors |
|
647 $? = 0; |
|
648 |
|
649 return $same; |
|
650 } |
|
651 |
|
652 sub CompareStreams { |
|
653 my $fileHandle1 = shift; |
|
654 my $fileHandle2 = shift; |
|
655 my $filename = shift; |
|
656 my $same = 1; |
|
657 my $offset = -4096; |
|
658 my $buf1; |
|
659 my $buf2; |
|
660 while ($same) { |
|
661 my $len1 = $fileHandle1->read($buf1, 4096); |
|
662 my $len2 = $fileHandle2->read($buf2, 4096); |
|
663 if ($len1 == 0 and $len2 == 0) { |
|
664 return 1; |
|
665 } |
|
666 $same = $buf1 eq $buf2; |
|
667 $offset += 4096; |
|
668 } |
|
669 if ($verbose) { |
|
670 my @bytes1 = unpack "C*", $buf1; |
|
671 my @bytes2 = unpack "C*", $buf2; |
|
672 foreach my $thisByte (@bytes1) { |
|
673 if ($thisByte != $bytes2[0]) { |
|
674 printf $log "Binary comparison: %s failed at byte %d: %02x != %02x\n", $filename, $offset, $thisByte, $bytes2[0]; |
|
675 last; |
|
676 } |
|
677 shift @bytes2; |
|
678 $offset+=1; |
|
679 } |
|
680 } |
|
681 return 0; |
|
682 } |
|
683 |
|
684 sub NextSignificantLine { |
|
685 my $filehandle = shift; |
|
686 my $linenumber = shift; |
|
687 my $cleanersub = shift; |
|
688 my $skipblanks = shift; |
|
689 |
|
690 while (!eof($filehandle)) { |
|
691 my $line = <$filehandle>; |
|
692 $$linenumber++; |
|
693 $cleanersub->(\$line); |
|
694 return $line if !$skipblanks or $line !~ /^\s*$/; |
|
695 } |
|
696 return undef; # on eof |
|
697 } |
|
698 |
|
699 sub CompareTexts { |
|
700 my $filehandle1 = shift; |
|
701 my $filehandle2 = shift; |
|
702 my $cleaner = shift; |
|
703 my $filename = shift; |
|
704 my $skipblanks = shift; |
|
705 my $lineNum1 = 0; |
|
706 my $lineNum2 = 0; |
|
707 |
|
708 while (1) { |
|
709 my $line1 = NextSignificantLine($filehandle1, \$lineNum1, $cleaner, $skipblanks); |
|
710 my $line2 = NextSignificantLine($filehandle2, \$lineNum2, $cleaner, $skipblanks); |
|
711 |
|
712 return 0 if defined($line1) != defined($line2); # eof vs. significant content |
|
713 return 1 if !defined($line1) and !defined($line2); # eof on both files |
|
714 |
|
715 if ($line1 ne $line2) { |
|
716 printf($log "Text comparison: %s failed at lines %d/%d\n< %s> %s\n", |
|
717 $filename, $lineNum1, $lineNum2, $line1, $line2) if $verbose; |
|
718 return 0; |
|
719 } |
|
720 } |
|
721 } |
|
722 |
|
723 sub Drain { |
|
724 foreach my $handle (@_) { |
|
725 while (my $line = <$handle>) { |
|
726 } |
|
727 } |
|
728 } |
|
729 |
|
730 sub RelativePath { |
|
731 my $name = shift; |
|
732 if (($name =~ /^\\[^\\]/) || ($name =~ /^\//)) { # abs path (unix or windows), not UNC |
|
733 unless ($toRoot) { |
|
734 $toRoot = getcwd(); |
|
735 $toRoot =~ s/\//\\/g; |
|
736 $toRoot =~ s/^[a-zA-Z]:\\(.*)$/$1/; |
|
737 $toRoot =~ s/[^\\]+/../g; |
|
738 if ($toRoot =~ /^$/) { |
|
739 $toRoot = '.'; # because we are starting in the root |
|
740 } |
|
741 } |
|
742 return $toRoot.$name; |
|
743 } |
|
744 return $name; |
|
745 } |
|
746 |
|
747 # Function to expand compressed formats and recompare expanded files |
|
748 # This is the file against file implementation |
|
749 # It returns one identical / non indentical result based on all files in the |
|
750 # expanded content. i.e one non identical expanded file will cause the non |
|
751 # expanded file to be reported as non identical. |
|
752 sub ExpandAndCompareFiles |
|
753 { |
|
754 my $file1 = shift; |
|
755 my $file2 = shift; |
|
756 my $expandor = shift; |
|
757 |
|
758 # Create two temporary directories |
|
759 my $tempdir1 = tempdir ( "EvalidExpand_XXXXXX", DIR => File::Spec->tmpdir, CLEANUP => 1); |
|
760 my $tempdir2 = tempdir ( "EvalidExpand_XXXXXX", DIR => File::Spec->tmpdir, CLEANUP => 1); |
|
761 |
|
762 # Build the Expandor commandline |
|
763 my $cmd1 = $expandor; |
|
764 $cmd1 =~ s/%TEMPDIR%/$tempdir1/g; |
|
765 $cmd1 =~ s/%FILE%/$file1/g; |
|
766 |
|
767 my $cmd2 = $expandor; |
|
768 $cmd2 =~ s/%TEMPDIR%/$tempdir2/g; |
|
769 $cmd2 =~ s/%FILE%/$file2/g; |
|
770 |
|
771 # Expand files |
|
772 my $output = `$cmd1 2>&1`; |
|
773 print($log "Expanding using $cmd1 output was:-\n$output") if ($verbose); |
|
774 if ($? > 0) |
|
775 { |
|
776 print ($log "$cmd1 exited with $?") if ($verbose); |
|
777 return -1; |
|
778 } |
|
779 |
|
780 $output = `$cmd2 2>&1`; |
|
781 print($log "Expanding using $cmd2 output was:-\n$output") if ($verbose); |
|
782 if ($? > 0) |
|
783 { |
|
784 print ($log "$cmd2 exited with $?") if ($verbose); |
|
785 return -1; |
|
786 } |
|
787 |
|
788 # Produce full filelist of expanded files without directory names |
|
789 my %iFileList1; |
|
790 $tempdir1 =~ s#\\#/#g; # Make sure the dir seperators are / for consistent and easier matching. |
|
791 find sub { |
|
792 if (!-d) |
|
793 { |
|
794 my ($fixedpath) = $File::Find::name; |
|
795 $fixedpath =~ s#\\#/#g; |
|
796 my ($relpath) = $File::Find::name =~ /$tempdir1(.*)/i; |
|
797 $iFileList1{$relpath} = "left"; |
|
798 } |
|
799 }, $tempdir1; |
|
800 |
|
801 my %iFileList2; |
|
802 $tempdir2 =~ s#\\#/#g; # Make sure the dir seperators are / for consistent and easier matching. |
|
803 find sub { |
|
804 if (!-d) |
|
805 { |
|
806 my ($fixedpath) = $File::Find::name; |
|
807 $fixedpath =~ s#\\#/#g; |
|
808 my ($relpath) = $File::Find::name =~ /$tempdir2(.*)/i; |
|
809 $iFileList2{$relpath} = "right"; |
|
810 } |
|
811 }, $tempdir2; |
|
812 |
|
813 #Work out the if the two file lists are different |
|
814 foreach my $file (sort keys %iFileList1) |
|
815 { |
|
816 if (! defined $iFileList2{$file}) |
|
817 { |
|
818 # If the filename does not exist in the second filelist the compressed files cannot be the same. |
|
819 print ($log "Did not find $file in $file2\n") if ($verbose); |
|
820 return 0; |
|
821 } else { |
|
822 delete $iFileList2{$file} |
|
823 } |
|
824 } |
|
825 |
|
826 # There are extra files in the second compressed file therefore the compressed files cannot be the same. |
|
827 if (scalar(keys %iFileList2) > 0) |
|
828 { |
|
829 print ($log "$file2 contained more files than $file1\n") if ($verbose); |
|
830 return 0; |
|
831 } |
|
832 |
|
833 print($log "Comparing content\n") if ($verbose); |
|
834 #filelist1 and filelist2 contain all the same filenames, now compare the contents of each file |
|
835 my $same = -1; # Variable to store collated result of comparison, assume an error |
|
836 foreach my $file (keys %iFileList1) |
|
837 { |
|
838 my $type; |
|
839 ($same, $type) = CompareFiles($tempdir1.$file,$tempdir2.$file, $verbose, $log); |
|
840 print ($log "Comparing $tempdir1.$file against $tempdir2.$file\n") if ($verbose); |
|
841 last if ($same == 0); # do not bother comparing more files if one of the expanded files is different. |
|
842 } |
|
843 |
|
844 #Cleanup the temporary directories |
|
845 rmtree([$tempdir1,$tempdir2]); |
|
846 |
|
847 return $same; |
|
848 } |
|
849 |
|
850 # Create descriptive versions of input files in response to the -d option to MD5 generation |
|
851 sub dumpDescriptiveOutput ($$@) |
|
852 { |
|
853 my ($originalFile, $dumpDirFile, @content) = @_; |
|
854 |
|
855 my $currentDir = cwd; |
|
856 my $drive = ""; |
|
857 $dumpDirFile =~ s/^.://; # Remove drive letter |
|
858 |
|
859 $drive = $1 if ($currentDir =~ /^(\w{1}:)\//); |
|
860 |
|
861 my $DUMPFILE = $dumpDir; |
|
862 $DUMPFILE = cwd."\\$dumpDir" if ($dumpDir !~ /^(\\|\/|\w{1}:\\)/); |
|
863 $DUMPFILE = $drive.$dumpDir if ($dumpDir =~ /^\\/); |
|
864 $DUMPFILE .= "\\" if ($DUMPFILE !~ /(\\|\/)$/); |
|
865 $DUMPFILE .= $dumpDirFile; |
|
866 $DUMPFILE =~ s/\//\\/g; |
|
867 |
|
868 # This is most likely to come about due to maintaining path structures in expanded archives e.g. .chm files |
|
869 if (length ($DUMPFILE) > 255) |
|
870 { |
|
871 print ("Warning: Not attempting to create \"$DUMPFILE\" as it exceeds Windows MAX_PATH limit.\n"); |
|
872 return; |
|
873 } |
|
874 |
|
875 mkpath (dirname ($DUMPFILE)); |
|
876 |
|
877 my $success = 0; |
|
878 |
|
879 if (@content) |
|
880 { |
|
881 if (open DUMPFILE, "> $DUMPFILE") |
|
882 { |
|
883 print DUMPFILE $_ foreach (@content); |
|
884 close DUMPFILE; |
|
885 $success = 1; |
|
886 } |
|
887 } |
|
888 else |
|
889 { |
|
890 $success = 1 if (copy ($originalFile, $DUMPFILE)); |
|
891 } |
|
892 |
|
893 print ("Warning: Cannot create \"$DUMPFILE\".\n") if (!$success); |
|
894 } |
|
895 |
|
896 |
|
897 # |
|
898 # Filters. |
|
899 # |
|
900 |
|
901 sub Elf2E32Filter { |
|
902 my $line = shift; |
|
903 if ($$line =~ /Time Stamp:|E32ImageFile|Header CRC:/) { # Ignore time stamps, file name and Header CRC which uses the timestamp. |
|
904 $$line = ''; |
|
905 } |
|
906 if ($$line =~ /imports from /) { |
|
907 $$line = lc $$line; # DLL names are not case-sensitive in the Symbian platform loader |
|
908 } |
|
909 } |
|
910 |
|
911 sub ElfDumpFilter { |
|
912 my $line = shift; |
|
913 $$line =~ s/^\tProgram header offset.*$/Program header offset/; |
|
914 $$line =~ s/^\tSection header offset.*$/Section header offset/; |
|
915 $$line =~ s/#<DLL>(\S+\.\S+)#<\\DLL>/#<DLL>\L$1\E#<\\DLL>/; # DLL names are not case-sensitive in the Symbian platform loader |
|
916 if ($$line =~ /^\.(rel\.)?debug_/) { |
|
917 $$line = ''; # additional debug-related information - not considered significant |
|
918 } |
|
919 } |
|
920 |
|
921 sub NmFilter { |
|
922 my $line = shift; |
|
923 $$line =~ s/^.*:$//; # ignore the filenames |
|
924 $$line =~ s/\.\.\\[^(]*\\//g; |
|
925 $$line =~ s/\.\.\/[^(]*\///g; # ignore pathnames of object files |
|
926 $$line =~ s/^BFD: (.*)$//; # ignore the Binary File Descriptor(BFD) warning messages |
|
927 if ($$line =~ /^(.+ (_head|_))\w+_(EPOC32_\w+(_LIB|_iname))$/i) { |
|
928 # dlltool uses the "-o" argument string as the basis for a "unique symbol", but |
|
929 # doesn't turn the name into a canonical form first. |
|
930 # dh.o: |
|
931 # U ________EPOC32_RELEASE_ARM4_UREL_EIKCOCTL_LIB_iname |
|
932 # 00000000 ? _head_______EPOC32_RELEASE_ARM4_UREL_EIKCOCTL_LIB |
|
933 $$line = uc "$1_..._$3\n"; |
|
934 } |
|
935 } |
|
936 |
|
937 |
|
938 sub MapFilter { |
|
939 my $line = shift; |
|
940 $$line =~ s/([d-z])\d*s_?\d+\.o/$1s999.o/; # ignore the names of intermediate files in .LIB |
|
941 $$line =~ s/([d-z])\d*([ht])\.o/$1$2.o/; # ignore the names of intermediate files in .LIB |
|
942 $$line =~ s-/-\\-go; # convert / into \ |
|
943 $$line =~ s/(\.\.\\|.:\\)[^(]*\\//g; # ignore pathnames of object files |
|
944 $$line =~ s/\.stab.*$//; # ignore .stab and .stabstr lines |
|
945 $$line =~ s/0x.*size before relaxing//; # ignore additional comments about .stab and .stabstr |
|
946 $$line =~ s/(_head|_)\w+_(EPOC32_\w+(_LIB|_iname))/$1_,,,_$3/; # dlltool-generated unique symbols |
|
947 $$line =~ s/Timestamp is .*$//; # ignore timestamps in DevStudio map files |
|
948 if ($$line =~ /^ARM Linker,/) { |
|
949 $$line = ''; |
|
950 } # ignore the message that armlink's license will expire. (in RVCT MAP file) |
|
951 if ($$line =~ /^Your license/) { |
|
952 $$line = ''; |
|
953 } |
|
954 $$line =~ s/\s__T\d{8}\s/ __Tnnnnnnnn /; # ignore RVCT generated internal symbols |
|
955 if ($$line =~ /0x00000000 Number 0 /) { # ignore filenames in RVCT link maps |
|
956 $$line = ''; |
|
957 } |
|
958 |
|
959 # Ignore various case differences: |
|
960 |
|
961 ## RVCT |
|
962 |
|
963 # source filenames turning up in mangled symbols e.g.: |
|
964 # __sti___13_BALServer_cpp 0x000087c9 Thumb Code 52 BALServer.o(.text) |
|
965 $$line =~ s/^(\s+__sti___\d+_)(\w+)(.*\(\.text\))$/$1\L$2\E$3/; |
|
966 |
|
967 # object filenames e.g.: |
|
968 # .text 0x0000a01c Section 164 AssertE.o(.text) |
|
969 $$line =~ s/^(\s+\.text\s+0x[0-9A-Fa-f]{8}\s+Section\s+\d+\s+)(.+)(\(\.text\))$/$1\L$2\E$3/; |
|
970 |
|
971 ## WINSCW |
|
972 |
|
973 # import/static libraries processed listed in the last section e.g.: |
|
974 #1 EDLL.LIB |
|
975 #99 EDLL.LIB (not used) |
|
976 $$line =~ s/^(\d{1,2} {5,6})(\w+\.lib)( \(not used\)|)$/$1\L$2\E$3/i; |
|
977 } |
|
978 |
|
979 sub UnzipFilter { |
|
980 my $line = shift; |
|
981 $$line =~ s/^Archive:.*$/Archive/; # ignore the archive names |
|
982 # Line format of unzip -l -v |
|
983 # Length Method Size Ratio Date Time CRC-32 Name, Date can be dd-mm-yy or mm/dd/yy |
|
984 $$line =~ s/ (\d+).*? ..-..-..\s+..:.. / ($1) 99-99-99 99:99 /; # ignore (Method Size Ratio Date Time) on contained files |
|
985 $$line =~ s^ (\d+).*? ..\/..\/..\s+..:.. ^ ($1) 99-99-99 99:99 ^; # ignore (Method Size Ratio Date Time) on contained files |
|
986 } |
|
987 |
|
988 sub SgmlFilter { |
|
989 my $line = shift; |
|
990 $$line =~ s/<!--.*-->//; # ignore comments such as "generated by javadoc" |
|
991 } |
|
992 |
|
993 sub PreprocessedTextFilter { |
|
994 my $line = shift; |
|
995 $$line =~ s/^# \d+ ".*"( \d)?$//; # ignore #include history |
|
996 } |
|
997 |
|
998 sub FilterCVSTags { |
|
999 my $line = shift; |
|
1000 $$line =~ s#//\s+\$(?:Id|Name|Header|Date|DateTime|Change|File|Revision|Author):.*\$$##m; |
|
1001 # Remove tags like: |
|
1002 # // $Id: //my/perforce/here $ |
|
1003 # which may be inserted into source code by some licensees |
|
1004 } |
|
1005 |
|
1006 sub PermanentFileStoreFilter { |
|
1007 my $line = shift; |
|
1008 $$line =~ s/^Dumping .*$/Dumping (file)/; # ignore the source file names |
|
1009 } |
|
1010 |
|
1011 sub DistributionPolicyFilter { |
|
1012 my $line = shift; |
|
1013 $$line =~ s/# DistPolGen.*//; |
|
1014 } |
|
1015 |
|
1016 sub FilterAll { |
|
1017 my $line = shift; |
|
1018 $$line = ''; |
|
1019 } |
|
1020 |
|
1021 sub FilterNone { |
|
1022 } |
|
1023 |
|
1024 1; |
|
1025 |
|
1026 __END__ |
|
1027 |
|
1028 =head1 NAME |
|
1029 |
|
1030 EvalidCompare.pm - Utilities for comparing the contents of files. |
|
1031 |
|
1032 =head1 DESCRIPTION |
|
1033 |
|
1034 This package has been largely factored out of the C<e32toolp> tool C<evalid>. The main pieces of borrowed functionality are the ability to identify file types by examining their content, and the ability to filter irrelevant data out of files to allow comparisons to be performed. This refactoring was done in order to allow both direct and indirect comparisons of files to be supported. Direct comparisions are done by reading a pair of files (in the same way the C<evalid> does). Indirect comparisons are done by generating MD5 signatures of the files to be compared. The later method allows comparisons to be performed much more efficiently, because only one file need be present provided the signature of the other is known. |
|
1035 |
|
1036 =head1 INTERFACE |
|
1037 |
|
1038 =head2 CompareFiles |
|
1039 |
|
1040 Expects to be passed a pair of file names. May optionally also be passed a verbosity level (defaults to 0) and a file handle for logging purposes (defaults to *STDIN). Returns 1 if the files match, 0 if not. Firstly does a raw binary compare of the two files. If they match, no further processing is done and 1 is returned. If not, the type of the first file is found and the files are re-compared, this time ignoring data known to be irrelevant for the file type. The result of this compare is then returned. |
|
1041 |
|
1042 =head2 GenerateSignature |
|
1043 |
|
1044 Expects to be passed a file name. May optionally also be passed a verbosity level (defaults to 0) and a file handle for logging purposes (defaults to *STDIN). Returns an MD5 signature of the specified file contents, having ignored irrelevant data associted with its type. This signature may subsequently be used to verify that the contents of the file has not been altered in a significant way. |
|
1045 |
|
1046 =head1 KNOWN BUGS |
|
1047 |
|
1048 None. |
|
1049 |
|
1050 =head1 COPYRIGHT |
|
1051 |
|
1052 Copyright (c) 2005-2009 Nokia Corporation and/or its subsidiary(-ies). |
|
1053 All rights reserved. |
|
1054 This component and the accompanying materials are made available |
|
1055 under the terms of the License "Eclipse Public License v1.0" |
|
1056 which accompanies this distribution, and is available |
|
1057 at the URL "http://www.eclipse.org/legal/epl-v10.html". |
|
1058 |
|
1059 Initial Contributors: |
|
1060 Nokia Corporation - initial contribution. |
|
1061 |
|
1062 Contributors: |
|
1063 |
|
1064 Description: |
|
1065 |
|
1066 =cut |
|
1067 |
|
1068 __END__ |