Imported Upstream version 1.1.0.99.1
[platform/upstream/syncevolution.git] / test / synccompare.pl
1 #! /usr/bin/env perl
2 #
3 # Copyright (C) 2008 Funambol, Inc.
4 # Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
5 # Copyright (C) 2009 Intel Corporation
6 #
7 # Usage: <file>
8 #        <left file> <right file>
9 # Either normalizes a file or compares two of them in a side-by-side
10 # diff.
11 #
12 # Checks environment variables:
13 #
14 # CLIENT_TEST_SERVER=funambol|scheduleworld|egroupware|synthesis
15 #       Enables code which simplifies the text files just like
16 #       certain well-known servers do. This is useful for testing
17 #       to ignore the data loss introduced by these servers or (for
18 #       users) to simulate the effect of these servers on their data.
19 #
20 # CLIENT_TEST_CLIENT=evolution|addressbook (Mac OS X/iPhone)
21 #       Same as for servers this replicates the effect of storing
22 #       data in the clients.
23 #
24 # CLIENT_TEST_LEFT_NAME="before sync"
25 # CLIENT_TEST_RIGHT_NAME="after sync"
26 # CLIENT_TEST_REMOVED="removed during sync"
27 # CLIENT_TEST_ADDED="added during sync"
28 #       Setting these variables changes the default legend
29 #       print above the left and right file during a
30 #       comparison.
31 #
32 # CLIENT_TEST_COMPARISON_FAILED=1
33 #       Overrides the default error code when changes are found.
34 #
35 # This program is free software; you can redistribute it and/or
36 # modify it under the terms of the GNU Lesser General Public
37 # License as published by the Free Software Foundation; either
38 # version 2.1 of the License, or (at your option) version 3.
39 #
40 # This program is distributed in the hope that it will be useful,
41 # but WITHOUT ANY WARRANTY; without even the implied warranty of
42 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
43 # Lesser General Public License for more details.
44 #
45 # You should have received a copy of the GNU Lesser General Public
46 # License along with this library; if not, write to the Free Software
47 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
48 # 02110-1301  USA
49
50
51 use strict;
52 use encoding 'utf8';
53 use Algorithm::Diff;
54
55 # ignore differences caused by specific servers or local backends?
56 my $server = $ENV{CLIENT_TEST_SERVER};
57 my $client = $ENV{CLIENT_TEST_CLIENT} || "evolution";
58 my $scheduleworld = $server =~ /scheduleworld/;
59 my $synthesis = $server =~ /synthesis/;
60 my $zyb = $server =~ /zyb/;
61 my $mobical = $server =~ /mobical/;
62 my $memotoo = $server =~ /memotoo/;
63 my $nokia_7210c = $server =~ /nokia_7210c/;
64 my $ovi = $server =~ /Ovi/;
65
66 # TODO: this hack ensures that any synchronization is limited to
67 # properties supported by Synthesis. Remove this again.
68 # $synthesis = 1;
69
70 my $egroupware = $server =~ /egroupware/;
71 my $funambol = $server =~ /funambol/;
72 my $google = $server =~ /google/;
73 my $evolution = $client =~ /evolution/;
74 my $addressbook = $client =~ /addressbook/;
75
76 sub Usage {
77   print "$0 <vcards.vcf\n";
78   print "   normalizes one file (stdin or single argument), prints to stdout\n";
79   print "$0 vcards1.vcf vcards2.vcf\n";
80   print "   compares the two files\n";
81   print "Also works for iCalendar files.\n";
82 }
83
84 sub uppercase {
85   my $text = shift;
86   $text =~ tr/a-z/A-Z/;
87   return $text;
88 }
89
90 sub sortlist {
91   my $list = shift;
92   return join(",", sort(split(/,/, $list)));
93 }
94
95 sub splitvalue {
96   my $prop = shift;
97   my $values = shift;
98   my $eol = shift;
99
100   my @res = ();
101   foreach my $val (split (/;/, $values)) {
102       push(@res, $prop, ":", $val, $eol);
103   }
104   return join("", @res);
105 }
106
107 # parameters: text, width to use for reformatted lines
108 # returns list of lines without line breaks
109 sub Normalize {
110   $_ = shift;
111   my $width = shift;
112
113   s/\r//g;
114
115   my @items = ();
116
117   foreach $_ ( split( /(?:(?<=\nEND:VCARD)|(?<=\nEND:VCALENDAR))\n*/ ) ) {
118     # undo line continuation
119     s/\n\s//gs;
120     # ignore charset specifications, assume UTF-8
121     s/;CHARSET="?UTF-8"?//g;
122
123     # UID may differ, but only in vCards and journal entries:
124     # in calendar events the UID needs to be preserved to handle
125     # meeting invitations/replies correctly
126     s/((VCARD|VJOURNAL).*)^UID:[^\n]*\n/$1/msg;
127
128     # exact order of categories is irrelevant
129     s/^CATEGORIES:(\S+)/"CATEGORIES:" . sortlist($1)/mge;
130
131     # expand <foo> shortcuts to TYPE=<foo>
132     while (s/^(ADR|EMAIL|TEL)([^:\n]*);(HOME|OTHER|WORK|PARCEL|INTERNET|CAR|VOICE|CELL|PAGER)/$1;TYPE=$3/mg) {}
133
134     # the distinction between an empty and a missing property
135     # is vague and handled differently, so ignore empty properties
136     s/^[^:\n]*:;*\n//mg;
137
138     # use separate TYPE= fields
139     while( s/^(\w*[^:\n]*);TYPE=(\w*),(\w*)/$1;TYPE=$2;TYPE=$3/mg ) {}
140
141     # make TYPE uppercase (in vCard 3.0 at least those parameters are case-insensitive)
142     while( s/^(\w*[^:\n]*);TYPE=(\w*?[a-z]\w*?)([;:])/ $1 . ";TYPE=" . uppercase($2) . $3 /mge ) {}
143
144     # replace parameters with a sorted parameter list
145     s!^([^;:\n]*);(.*?):!$1 . ";" . join(';',sort(split(/;/, $2))) . ":"!meg;
146
147     # EXDATE;VALUE=DATE is the default, no need to show it
148     s/^EXDATE;VALUE=DATE:/EXDATE:/mg;
149
150     # multiple EXDATEs may be joined into one, use separate properties as normal form
151     s/^(EXDATE[^:]*):(.*)(\r?\n)/splitvalue($1, $2, $3)/mge;
152
153     # sort value lists of specific properties
154     s!^(RRULE.*):(.*)!$1 . ":" . join(';',sort(split(/;/, $2)))!meg;
155
156     # INTERVAL=1 is the default and thus can be removed
157     s/^RRULE(.*?);INTERVAL=1(;|$)/RRULE$1$2/mg;
158
159     # Ignore remaining "other" email, address and telephone type - this is
160     # an Evolution specific extension which might not be preserved.
161     s/^(ADR|EMAIL|TEL)([^:\n]*);TYPE=OTHER/$1$2/mg;
162     # TYPE=PREF on the other hand is not used by Evolution, but
163     # might be sent back.
164     s/^(ADR|EMAIL)([^:\n]*);TYPE=PREF/$1$2/mg;
165     # Evolution does not need TYPE=INTERNET for email
166     s/^(EMAIL)([^:\n]*);TYPE=INTERNET/$1$2/mg;
167     # ignore TYPE=PREF in address, does not matter in Evolution
168     s/^((ADR|LABEL)[^:\n]*);TYPE=PREF/$1/mg;
169     # ignore extra separators in multi-value fields
170     s/^((ORG|N|(ADR[^:\n]*?)):.*?);*$/$1/mg;
171     # the type of certain fields is ignore by Evolution
172     s/^X-(AIM|GROUPWISE|ICQ|YAHOO);TYPE=HOME/X-$1/gm;
173     # Evolution ignores an additional pager type
174     s/^TEL;TYPE=PAGER;TYPE=WORK/TEL;TYPE=PAGER/gm;
175     # PAGER property is sent by Evolution, but otherwise ignored
176     s/^LABEL[;:].*\n//mg;
177     # TYPE=VOICE is the default in Evolution and may or may not appear in the vcard;
178     # this simplification is a bit too agressive and hides the problematic
179     # TYPE=PREF,VOICE combination which Evolution does not handle :-/
180     s/^TEL([^:\n]*);TYPE=VOICE,([^:\n]*):/TEL$1;TYPE=$2:/mg;
181     s/^TEL([^:\n]*);TYPE=([^;:\n]*),VOICE([^:\n]*):/TEL$1;TYPE=$2$3:/mg;
182     s/^TEL([^:\n]*);TYPE=VOICE([^:\n]*):/TEL$1$2:/mg;
183     # don't care about the TYPE property of PHOTOs
184     s/^PHOTO;(.*)TYPE=[A-Z]*/PHOTO;$1/mg;
185     # encoding is not case sensitive, skip white space in the middle of binary data
186     if (s/^PHOTO;.*?ENCODING=(b|B|BASE64).*?:\s*/PHOTO;ENCODING=B: /mgi) {
187       while (s/^PHOTO(.*?): (\S+)[\t ]+(\S+)/PHOTO$1: $2$3/mg) {}
188     }
189     # ignore extra day factor in front of weekday
190     s/^RRULE:(.*)BYDAY=\+?1(\D)/RRULE:$1BYDAY=$2/mg;
191     # remove default VALUE=DATE-TIME
192     s/^(DTSTART|DTEND)([^:\n]*);VALUE=DATE-TIME/$1$2/mg;
193
194     # normalize values which look like a date to YYYYMMDD because the hyphen is optional
195     s/:(\d{4})-(\d{2})-(\d{2})/:$1$2$3/g;
196
197     # mailto is case insensitive
198     s/^((ATTENDEE|ORGANIZER).*):[Mm][Aa][Ii][Ll][Tt][Oo]:/$1:mailto:/mg;
199
200     # remove fields which may differ
201     s/^(PRODID|CREATED|DTSTAMP|LAST-MODIFIED|REV):.*\r?\n?//gm;
202     # remove optional fields
203     s/^(METHOD|X-WSS-[A-Z]*):.*\r?\n?//gm;
204
205     # trailing line break(s) in a DESCRIPTION may or may not be
206     # removed or added by servers
207     s/^DESCRIPTION:(.*?)(\\n)+$/DESCRIPTION:$1/gm;
208
209     # use the shorter property name when there are alternatives,
210     # but avoid duplicates
211     foreach my $i ("SPOUSE", "MANAGER", "ASSISTANT", "ANNIVERSARY") {
212         if (/^X-\Q$i\E:(.*?)$/m) {
213             s/^X-EVOLUTION-\Q$i\E:\Q$1\E\n//m;
214         }
215     }
216     s/^X-EVOLUTION-(SPOUSE|MANAGER|ASSISTANT|ANNIVERSARY)/X-$1/gm;
217
218     # if there is no DESCRIPTION in a VJOURNAL, then use the
219     # summary: that's what is done when exchanging such a
220     # VJOURNAL as plain text
221     if (/^BEGIN:VJOURNAL$/m && !/^DESCRIPTION/m) {
222         s/^SUMMARY:(.*)$/SUMMARY:$1\nDESCRIPTION:$1/m;
223     }
224
225     # Strip trailing digits from TZID. They are appended by
226     # Evolution and SyncEvolution to distinguish VTIMEZONE
227     # definitions which have the same TZID, but different rules.
228     s/(^TZID:|;TZID=)([^;:]*?) \d+/$1$2/gm;
229
230     # Strip trailing -(Standard) from TZID. Evolution 2.24.5 adds
231     # that (not sure exactly where that comes from).
232     s/(^TZID:|;TZID=)([^;:]*?)-\(Standard\)/$1$2/gm;
233
234     # VTIMEZONE and TZID do not have to be preserved verbatim as long
235     # as the replacement is still representing the same timezone.
236     # Reduce TZIDs which specify a proper location
237     # to their location part and strip the VTIMEZONE - makes the
238     # diff shorter, too.
239     my $location = "[^\n]*((?:Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Brazil|Canada|Chile|Egypt|Eire|Europe|Hongkong|Iceland|India|Iran|Israel|Jamaica|Japan|Kwajalein|Libya|Mexico|Mideast|Navajo|Pacific|Poland|Portugal|Singapore|Turkey|Zulu)[-a-zA-Z0-9_/]*)";
240     s;^BEGIN:VTIMEZONE.*?^TZID:$location.*^END:VTIMEZONE;BEGIN:VTIMEZONE\n  TZID:$1 [...]\nEND:VTIMEZONE;gms;
241     s;TZID=$location;TZID=$1;gm;
242
243     # normalize iCalendar 2.0
244     if (/^BEGIN:(VEVENT|VTODO|VJOURNAL)$/m) {
245         # CLASS=PUBLIC is the default, no need to show it
246         s/^CLASS:PUBLIC\r?\n//m;
247     }
248
249     if ($scheduleworld || $egroupware || $synthesis || $addressbook || $funambol ||$google || $mobical || $memotoo) {
250       # does not preserve X-EVOLUTION-UI-SLOT=
251       s/^(\w+)([^:\n]*);X-EVOLUTION-UI-SLOT=\d+/$1$2/mg;
252     }
253
254     if ($scheduleworld) {
255       # cannot distinguish EMAIL types
256       s/^EMAIL;TYPE=\w*/EMAIL/mg;
257       # replaces certain TZIDs with more up-to-date ones
258       s;TZID(=|:)/(scheduleworld.com|softwarestudio.org)/Olson_\d+_\d+/;TZID$1/foo.com/Olson_20000101_1/;mg;
259     }
260
261     if ($synthesis || $mobical) {
262       # only preserves ORG "Company", but loses "Department" and "Office"
263       s/^ORG:([^;:\n]+)(;[^\n]*)/ORG:$1/mg;
264     }
265
266     if ($funambol) {
267       # only preserves ORG "Company";"Department", but loses "Office"
268       s/^ORG:([^;:\n]+)(;[^;:\n]*)(;[^\n]*)/ORG:$1$2/mg;
269       # drops the second address line
270       s/^ADR(.*?):([^;]*?);[^;]*?;/ADR$1:$2;;/mg;
271       # has no concept of "preferred" phone number
272       s/^(TEL.*);TYPE=PREF/$1/mg;
273     }
274
275    if($google) {
276       # ignore the PHOTO encoding data 
277       s/^PHOTO(.*?): .*\n/^PHOTO$1: [...]\n/mg; 
278       # FN propertiey is not correct 
279       s/^FN:.*\n/FN$1: [...]\n/mg;
280       # ';' in NOTE is lost by the server: ; in middle is replace by white space
281       # while ; in the end is omitted.
282       while (s!^NOTE:(.*)\\\;(.+)\n!NOTE:$1 $2\n!mg) {}
283       s!^NOTE:(.*)\\\;\n!NOTE:$1\n!mg;
284       # ';' in ORG is lost 
285       while (s!^ORG:(.*)\;(.*)\n!ORG:$1 $2\n!mg) {}
286       # Not support car type in telephone
287       s!^TEL\;TYPE=CAR(.*)\n!TEL$1\n!mg;
288       # some properties are lost
289       s/^(X-EVOLUTION-FILE-AS|NICKNAME|BDAY|CATEGORIES|CALURI|FBURL|ROLE|URL|X-AIM|X-EVOLUTION-UI-SLOT|X-ANNIVERSARY|X-ASSISTANT|X-EVOLUTION-BLOG-URL|X-EVOLUTION-VIDEO-URL|X-GROUPWISE|X-ICQ|X-MANAGER|X-SPOUSE|X-MOZILLA-HTML|X-YAHOO)(;[^:;\n]*)*:.*\r?\n?//gm;
290     }
291
292     if ($addressbook) {
293       # some properties cannot be stored
294       s/^(X-MOZILLA-HTML|X-EVOLUTION-FILE-AS|X-EVOLUTION-ANNIVERSARY|X-EVOLUTION-BLOG-URL|X-EVOLUTION-VIDEO-URL|X-GROUPWISE|ROLE|CATEGORIES|FBURL|CALURI|FN)(;[^:;\n]*)*:.*\r?\n?//gm;
295       # only some parts of ADR are preserved
296       my $type;
297       s/^ADR(.*?)\:(.*)/$type=($1 || ""); @_ = split(\/(?<!\\);\/, $2); "ADR:;;" . ($_[2] || "") . ";" . ($_[3] || "") . ";" . ($_[4] || "") . ";" . ($_[5] || "") . ";" . ($_[6] || "")/gme;
298       # TYPE=CAR not supported
299       s/;TYPE=CAR//g;
300     }
301
302     if ($synthesis) {
303       # does not preserve certain properties
304       s/^(FN|BDAY|X-MOZILLA-HTML|X-EVOLUTION-FILE-AS|X-AIM|NICKNAME|UID|PHOTO|CALURI|SEQUENCE|TRANSP|ORGANIZER)(;[^:;\n]*)*:.*\r?\n?//gm;
305       # default ADR is HOME
306       s/^ADR;TYPE=HOME/ADR/gm;
307       # only some parts of N are preserved
308       s/^N((?:;[^;:]*)*)\:(.*)/@_ = split(\/(?<!\\);\/, $2); "N$1:$_[0];" . ($_[1] || "") . ";;" . ($_[3] || "")/gme;
309       # this vcard contains too many ADR and PHONE entries - ignore it
310       if (/This is a test case which uses almost all Evolution fields/) {
311         next;
312       }
313       # breaks lines at semicolons, which adds white space
314       while( s/^ADR:(.*); +/ADR:$1;/gm ) {}
315       # no attributes stored for ATTENDEEs
316       s/^ATTENDEE;.*?:/ATTENDEE:/msg;
317     }
318
319     if ($synthesis) {
320       # VALARM not supported
321       s/^BEGIN:VALARM.*?END:VALARM\r?\n?//msg;
322     }
323
324     if ($egroupware) {
325       # CLASS:PUBLIC is added if none exists (as in our test cases),
326       # several properties not preserved
327       s/^(BDAY|CATEGORIES|FBURL|PHOTO|FN|X-[A-Z-]*|CALURI|CLASS|NICKNAME|UID|TRANSP|PRIORITY|SEQUENCE)(;[^:;\n]*)*:.*\r?\n?//gm;
328       # org gets truncated
329       s/^ORG:([^;:\n]*);.*/ORG:$1/gm;
330     }
331
332     if ($funambol) {
333       # several properties are not preserved
334       s/^(CALURI|FBURL|X-MOZILLA-HTML|X-EVOLUTION-FILE-AS|X-AIM|X-EVOLUTION-BLOG-URL|X-EVOLUTION-VIDEO-URL|X-GROUPWISE|X-ICQ|X-YAHOO|X-ASSISTANT)(;[^:;\n]*)*:.*\r?\n?//gm;
335
336       # quoted-printable line breaks are =0D=0A, not just single =0A
337       s/(?<!=0D)=0A/=0D=0A/g;
338       # only three email addresses, fourth one from test case gets lost
339       s/^EMAIL:john.doe\@yet.another.world\n\r?//mg;
340       # this particular type is not preserved
341       s/ADR;TYPE=PARCEL:Test Box #3/ADR;TYPE=HOME:Test Box #3/;
342     }
343     if ($funambol) {
344       #several properties are not preserved by funambol server in icalendar2.0 format
345       s/^(UID|SEQUENCE|TRANSP|LAST-MODIFIED|X-EVOLUTION-ALARM-UID)(;[^:;\n]*)*:.*\r?\n?//gm;
346       if (/^BEGIN:VEVENT/m ) {
347         #several properties are not preserved by funambol server in itodo2.0 format and
348         s/^(RECURRENCE-ID|ATTENDEE)(;[^:;\n]*)*:.*\r?\n?//gm;
349         #REPEAT:0 is added by funambol server so ignore it
350         s/^(REPEAT:0).*\r?\n?//gm;
351         #CN parameter is lost by funambol server
352         s/^ORGANIZER([^:\n]*);CN=([^:\n]*)(;[^:\n])*:(.*\r?\n?)/ORGANIZER$1$3:$4/mg;
353       }
354
355       if (/^BEGIN:VTODO/m ) {
356         #several properties are not preserved by funambol server in itodo2.0 format and
357         s/^(STATUS|URL)(;[^:;\n]*)*:.*\r?\n?//gm;
358
359         #some new properties are added by funambol server
360         s/^(CLASS:PUBLIC|PERCENT-COMPLETE:0).*\r?\n?//gm;
361       }
362     }
363
364     if($nokia_7210c) {
365         if (/BEGIN:VCARD/m) {
366             #ignore PREF, as it will added by default
367             s/^TEL([^:\n]*);TYPE=PREF/TEL$1/mg;
368             #remove non-digit prefix in TEL
369             s/^TEL([^:\n]*):(\D*)/TEL$1:/mg;
370             #properties N mismatch, sometimes lost part of components
371             s/^(N|X-EVOLUTION-FILE-AS):.*\r?\n?/$1:[...]\n/gm;
372             #strip spaces in 'NOTE'
373             while (s/^(NOTE|DESCRIPTION):(\S+)[\t ]+(\S+)/$1:$2$3/mg) {}
374             #preserve 80 chars in NOTE
375             s/^NOTE:(.{70}).*\r?\n?/NOTE:$1\n/mg;
376             #preserve one ADDR
377
378             # ignore the PHOTO encoding data, sometimes it add a default photo
379             s/^PHOTO(.*?): .*\n//mg; 
380             #s/^(ADR)([^:;\n]*)(;TYPE=[^:\n]*)?:.*\r?\n?/$1:$4\n/mg;
381
382             #lost properties
383             s/^(NICKNAME|CATEGORIES|CALURI|FBURL|ROLE|X-AIM|X-ANNIVERSARY|X-ASSISTANT|X-EVOLUTION-BLOG-URL|X-EVOLUTION-VIDEO-URL|X-GROUPWISE|X-ICQ|X-MANAGER|X-SPOUSE|X-MOZILLA-HTML|X-YAHOO)(;[^:;\n]*)*:.*\r?\n?//gm;
384         }
385
386         if (/^BEGIN:VEVENT/m ) {
387             #The properties phones add by default
388             s/^(PRIORITY|CATEGORIES)(;[^:;\n]*)*:.*\r?\n?//gm;
389             #strip spaces in 'DESCRIPTION'
390             while (s/^DESCRIPTION:(\S+)[\t ]+(\S+)/DESCRIPTION:$1$2/mg) {}
391
392         }
393
394         if (/^BEGIN:VTODO/m) {
395             #mismatch properties
396             s/^(PRIORITY)(;[^:;\n]*)*:.*\r?\n?/$1:[...]\n/gm;
397             #lost properties
398             s/^(STATUS|DTSTART|CATEGORIES)(;[^:;\n]*)*:.*\r?\n?//gm;
399         }
400
401         #Testing with phones using vcalendar, do not support UID
402         s/^(UID|CLASS|SEQUENCE|TRANSP)(;[^:;\n]*)*:.*\r?\n?//gm;
403     }
404
405     if ($ovi) {
406         if (/^BEGIN:VCARD/m) {
407             #lost properties
408             s/^(X-AIM|CALURI|URL|FBURL|PHOTO|EMAIL)(;[^:;\n]*)*:.*\r?\n?//gm;
409             #FN value mismatch (reordring and adding , by the server)
410             s/^FN:.*\r?\n?/FN:[...]\n/gm;
411             #X-EVOLUTION-FILE-AS adding '\' by the server
412             while (s/^X-EVOLUTION-FILE-AS:(.*)\\(.*)/X-EVOLUTION-FILE-AS:$1$2/gm) {}
413
414             # does not preserve X-EVOLUTION-UI-SLOT=
415             s/^(\w+)([^:\n]*);X-EVOLUTION-UI-SLOT=\d+/$1$2/mg;
416
417             # does not preserve third ADR
418             s/^ADR:Test Box #3.*\n\r?//mg;
419         }
420
421         if (/^BEGIN:VEVENT/m) {
422             #Testing with vcalendar, do not support UID
423             s/^(UID|SEQUENCE|TRANSP)(;[^:;\n]*)*:.*\r?\n?//gm;
424             #Add PRORITY by default
425             s/^(PRIORITY)(;[^:;\n]*)*:.*\r?\n?//gm;
426             # VALARM not supported
427             s/^BEGIN:VALARM.*?END:VALARM\r?\n?//msg;
428         }
429
430         if (/^BEGIN:VTODO/m) {
431             #Testing with vcalendar, do not support UID
432             s/^(UID|SEQUENCE|PERCENT-COMPLETE)(;[^:;\n]*)*:.*\r?\n?//gm;
433             #Mismatch DTSTART, COMPLETED
434             s/^(DTSTART|COMPLETED)(;[^:;\n]*)*:.*\r?\n?/$1:[...]\n/gm;
435         }
436     }
437
438     if ($funambol || $egroupware || $nokia_7210c) {
439       # NOTE may be truncated due to length resistrictions
440       s/^(NOTE(;[^:;\n]*)*:.{0,160}).*(\r?\n?)/$1$3/gm;
441     }
442     if ($memotoo) {
443       if (/^BEGIN:VCARD/m ) {
444         s/^(FN|FBURL|CALURI|CATEGORIES|ROLE|X-MOZILLA-HTML|PHOTO|X-EVOLUTION-FILE-AS|X-EVOLUTION-BLOG-URL|X-EVOLUTION-VIDEO-URL|X-GROUPWISE|X-ICQ|X-YAHOO)(;[^:;\n]*)*:.*\r?\n?//gm;
445         # only preserves ORG "Company", but loses "Department" and "Office"
446         s/^ORG:([^;:\n]+)(;[^;:\n]+)(;[^\n]*)/ORG:$1$2/mg;
447         # only preserves first 6 fields of 'ADR'
448         s/^ADR((;[^;:\n]*)*:)([^;:\n]*)((;[^;:\n]*){5})(;[^;:\n]*)/ADR$1$3$4/mg;
449         # strip 'TYPE=HOME' 
450         s/^URL([^\n:]*);TYPE=HOME/URL$1/mg;
451         s/^EMAIL([^\n:]*);TYPE=HOME/EMAIL$1/mg;
452       }
453       if (/^BEGIN:VEVENT/m ) {
454         s/^(UID|SEQUENCE|TRANSP|RECURRENCE-ID|X-EVOLUTION-ALARM-UID|ORGANIZER)(;[^:;\n]*)*:.*\r?\n?//gm;
455         # RELATED=START is the default behavior though server will lost it
456         s/^TRIGGER([^\n:]*);RELATED=START/TRIGGER$1/mg;
457         # some parameters of 'ATTENDEE' will be lost by server
458         s/^ATTENDEE([^\n:]*);CUTYPE=([^\n;:]*)/ATTENDEE$1/mg;
459         s/^ATTENDEE([^\n:]*);LANGUAGE=([^\n;:]*)/ATTENDEE$1/mg;
460         s/^ATTENDEE([^\n:]*);ROLE=([^\n;:]*)/ATTENDEE$1/mg;
461         s/^ATTENDEE([^\n:]*);RSVP=([^\n;:]*)/ATTENDEE$1/mg;
462         s/^ATTENDEE([^\n:]*);CN=([^\n;:]*)/ATTENDEE$1/mg;
463         s/^ATTENDEE([^\n:]*);PARTSTAT=([^\n;:]*)/ATTENDEE$1/mg;
464         if (/^BEGIN:VALARM/m ) {
465             s/^(DESCRIPTION)(;[^:;\n]*)*:.*\r?\n?//mg;
466         }
467       }
468       if (/^BEGIN:VTODO/m ) {
469         s/^(UID|SEQUENCE|URL|CLASS|PRIORITY)(;[^:;\n]*)*:.*\r?\n?//gm;
470         s/^PERCENT-COMPLETE:0\r?\n?//gm;
471       }
472     }
473     if ($mobical) {
474       s/^(CALURI|CATEGORIES|FBURL|NICKNAME|X-MOZILLA-HTML|X-EVOLUTION-FILE-AS|X-ANNIVERSARY|X-ASSISTANT|X-EVOLUTION-BLOG-URL|X-EVOLUTION-VIDEO-URL|X-GROUPWISE|X-ICQ|X-MANAGER|X-SPOUSE|X-YAHOO|X-AIM)(;[^:;\n]*)*:.*\r?\n?//gm;
475
476       # some workrounds here for mobical's bug 
477       s/^(FN|BDAY)(;[^:;\n]*)*:.*\r?\n?//gm;
478
479       if (/^BEGIN:VEVENT/m ) {
480         s/^(UID|SEQUENCE|CLASS|TRANSP|RECURRENCE-ID|ATTENDEE|ORGANIZER|AALARM|DALARM)(;[^:;\n]*)*:.*\r?\n?//gm;
481       }
482
483       if (/^BEGIN:VTODO/m ) {
484         s/^(UID|SEQUENCE|DTSTART|URL|PERCENT-COMPLETE|CLASS)(;[^:;\n]*)*:.*\r?\n?//gm;
485         s/^PRIORITY:0\r?\n?//gm;
486       }
487     }
488
489     if ($zyb) {
490         s/^(CALURI|CATEGORIES|FBURL|NICKNAME|X-MOZILLA-HTML|PHOTO|X-EVOLUTION-FILE-AS|X-ANNIVERSARY|X-ASSISTANT|X-EVOLUTION-BLOG-URL|X-EVOLUTION-VIDEO-URL|X-GROUPWISE|X-ICQ|X-MANAGER|X-SPOUSE|X-YAHOO|X-AIM)(;[^:;\n]*)*:.*\r?\n?//gm;
491     }
492
493     # treat X-MOZILLA-HTML=FALSE as if the property didn't exist
494     s/^X-MOZILLA-HTML:FALSE\r?\n?//gm;
495
496     my @formatted = ();
497
498     # Modify lines to cover not more than
499     # $width characters by folding lines (as done for the N or SUMMARY above),
500     # but also indent each inner BEGIN/END block by 2 spaces
501     # and finally sort the lines.
502     # We need to keep a stack of open blocks in @formatted:
503     # - BEGIN creates another open block
504     # - END closes it, sorts it, and adds as single string to the parent block
505     push @formatted, [];
506     foreach $_ (split /\n/, $_) {
507       if (/^BEGIN:/) {
508         # start a new block
509         push @formatted, [];
510       }
511
512       my $spaces = "  " x ($#formatted - 1);
513       my $thiswidth = $width -1 - length($spaces);
514       $thiswidth = 1 if $thiswidth <= 0;
515       s/(.{$thiswidth})(?!$)/$1\n /g;
516       s/^(.*)$/$spaces$1/mg;
517       push @{$formatted[$#formatted]}, $_;
518
519       if (/^\s*END:/) {
520         my $block = pop @formatted;
521         my $begin = shift @{$block};
522         my $end = pop @{$block};
523
524         # Keep begin/end as first/last line,
525         # inbetween sort, but so that N or SUMMARY are
526         # at the top. This ensures that the order of items
527         # is the same, even if individual properties differ.
528         # Also put indented blocks at the end, not the top.
529         sub numspaces {
530           my $str = shift;
531           $str =~ /^(\s*)/;
532           return length($1);
533         }
534         $_ = join("\n",
535                   $begin,
536                   sort( { $a =~ /^\s*(N|SUMMARY):/ ? -1 :
537                           $b =~ /^\s*(N|SUMMARY):/ ? 1 :
538                           ($a =~ /^\s/ && $b =~ /^\S/) ? 1 :
539                           numspaces($a) == numspaces($b) ? $a cmp $b :
540                           numspaces($a) - numspaces($b) }
541                         @{$block} ),
542                   $end);
543         push @{$formatted[$#formatted]}, $_;
544       }
545     }
546
547     push @items, ${$formatted[0]}[0];
548   }
549
550   return split( /\n/, join( "\n\n", sort @items ));
551 }
552
553 # number of columns available for output:
554 # try tput without printing the shells error if not found,
555 # default to 80
556 my $columns = `which tput >/dev/null 2>/dev/null && tput 2>/dev/null && tput cols`;
557 if ($? || !$columns) {
558   $columns = 80;
559 }
560
561 if($#ARGV > 1) {
562   # error
563   Usage();
564   exit 1;
565 } elsif($#ARGV == 1) {
566   # comparison
567
568   my ($file1, $file2) = ($ARGV[0], $ARGV[1]);
569
570   my $singlewidth = int(($columns - 3) / 2);
571   $columns = $singlewidth * 2 + 3;
572   my @normal1;
573   my @normal2;
574
575   if (-d $file1 && -d $file2) {
576       # Both "files" are really directories of individual files.
577       # Don't include files in the comparison which are known
578       # to be identical because the refer to the same inode.
579       # - build map from inode to filename
580       my %files1;
581       my %files2;
582       my @content1;
583       my @content2;
584       my $inode;
585       my $fullname;
586       my $entry;
587       opendir(my $dh, $file1) || die "cannot read $file1: $!";
588       foreach $entry (grep { -f "$file1/$_" } readdir($dh)) {
589           $fullname = "$file1/$entry";
590           $inode = (stat($fullname))[1];
591           $files1{$inode} = $entry;
592       }
593       closedir($dh);
594       # - remove common files, read others
595       opendir(my $dh, $file2) || die "cannot read $file2: $!";
596       foreach $entry (grep { -f "$file2/$_" } readdir($dh)) {
597           $fullname = "$file2/$entry";
598           $inode = (stat($fullname))[1];
599           if ($files1{$inode}) {
600               delete $files1{$inode};
601           } else {
602               open(IN, "<:utf8", "$fullname") || die "$fullname: $!";
603               push @content2, <IN>;
604           }
605       }
606       # - read remaining entries from first dir
607       foreach $entry (values %files1) {
608           $fullname = "$file1/$entry";
609           open(IN, "<:utf8", "$fullname") || die "$fullname: $!";
610           push @content1, <IN>;
611       }
612       my $content1 = join("", @content1);
613       my $content2 = join("", @content2); 
614       @normal1 = Normalize($content1, $singlewidth);
615       @normal2 = Normalize($content2, $singlewidth);
616   } else {
617       if (-d $file1) {
618           open(IN1, "-|:utf8", "find $file1 -type f -print0 | xargs -0 cat") || die "$file1: $!";
619       } else {
620           open(IN1, "<:utf8", $file1) || die "$file1: $!";
621       }
622       if (-d $file2) {
623           open(IN2, "-|:utf8", "find $file2 -type f -print0 | xargs -0 cat") || die "$file2: $!";
624       } else {
625           open(IN2, "<:utf8", $file2) || die "$file2: $!";
626       }
627       my $buf1 = join("", <IN1>);
628       my $buf2 = join("", <IN2>);
629       @normal1 = Normalize($buf1, $singlewidth);
630       @normal2 = Normalize($buf2, $singlewidth);
631       close(IN1);
632       close(IN2);
633   }
634
635   # Produce output where each line is marked as old (aka remove) with o,
636   # as new (aka added) with n, and as unchanged with u at the beginning.
637   # This allows simpler processing below.
638   my $res = 0;
639   if (0) {
640     # $_ = `diff "--old-line-format=o %L" "--new-line-format=n %L" "--unchanged-line-format=u %L" "$normal1" "$normal2"`;
641     # $res = $?;
642   } else {
643     # convert into same format as diff above - this allows reusing the
644     # existing output formatting code
645     my $diffs_ref = Algorithm::Diff::sdiff(\@normal1, \@normal2);
646     @_ = ();
647     my $hunk;
648     foreach $hunk ( @{$diffs_ref} ) {
649       my ($type, $left, $right) = @{$hunk};
650       if ($type eq "-") {
651         push @_, "o $left";
652         $res = 1;
653       } elsif ($type eq "+") {
654         push @_, "n $right";
655         $res = 1;
656       } elsif ($type eq "c") {
657         push @_, "o $left";
658         push @_, "n $right";
659         $res = 1;
660       } else {
661         push @_, "u $left";
662       }
663     }
664
665     $_ = join("\n", @_);
666   }
667
668   if ($res) {
669     print $ENV{CLIENT_TEST_HEADER};
670     printf "%*s | %s\n", $singlewidth,
671            ($ENV{CLIENT_TEST_LEFT_NAME} || "before sync"),
672            ($ENV{CLIENT_TEST_RIGHT_NAME} || "after sync");
673     printf "%*s <\n", $singlewidth,
674            ($ENV{CLIENT_TEST_REMOVED} || "removed during sync");
675     printf "%*s > %s\n", $singlewidth, "",
676            ($ENV{CLIENT_TEST_ADDED} || "added during sync");
677     print "-" x $columns, "\n";
678
679     # fix confusing output like:
680     # BEGIN:VCARD                             BEGIN:VCARD
681     #                                      >  N:new;entry
682     #                                      >  FN:new
683     #                                      >  END:VCARD
684     #                                      >
685     #                                      >  BEGIN:VCARD
686     # and replace it with:
687     #                                      >  BEGIN:VCARD
688     #                                      >  N:new;entry
689     #                                      >  FN:new
690     #                                      >  END:VCARD
691     #
692     # BEGIN:VCARD                             BEGIN:VCARD
693     #
694     # With the o/n/u markup this presents itself as:
695     # u BEGIN:VCARD
696     # n N:new;entry
697     # n FN:new
698     # n END:VCARD
699     # n
700     # n BEGIN:VCARD
701     #
702     # The alternative case is also possible:
703     # o END:VCARD
704     # o 
705     # o BEGIN:VCARD
706     # o N:old;entry
707     # u END:VCARD
708
709     # case one above
710     while( s/^u BEGIN:(VCARD|VCALENDAR)\n((?:^n .*\n)+?)^n BEGIN:/n BEGIN:$1\n$2u BEGIN:/m) {}
711     # same for the other direction
712     while( s/^u BEGIN:(VCARD|VCALENDAR)\n((?:^o .*\n)+?)^o BEGIN:/o BEGIN:$1\n$2u BEGIN:/m) {}
713
714     # case two
715     while( s/^o END:(VCARD|VCALENDAR)\n((?:^o .*\n)+?)^u END:/u END:$1\n$2o END:/m) {}
716     while( s/^n END:(VCARD|VCALENDAR)\n((?:^n .*\n)+?)^u END:/u END:$1\n$2n END:/m) {}
717
718     # split at end of each record
719     my $spaces = " " x $singlewidth;
720     foreach $_ (split /(?:(?<=. END:VCARD\n)|(?<=. END:VCALENDAR\n))(?:^. \n)*/m, $_) {
721       # ignore unchanged records
722       if (!length($_) || /^((u [^\n]*\n)*(u [^\n]*?))$/s) {
723         next;
724       }
725
726       # make all lines equally long in terms of printable characters
727       s/^(.*)$/$1 . (" " x ($singlewidth + 2 - length($1)))/gme;
728
729       # convert into side-by-side output
730       my @buffer = ();
731       foreach $_ (split /\n/, $_) {
732         if (/^u (.*)/) {
733           print join(" <\n", @buffer), " <\n" if $#buffer >= 0;
734           @buffer = ();
735           print $1, "   ", $1, "\n";
736         } elsif (/^o (.*)/) {
737           # preserve in buffer for potential merging with "n "
738           push @buffer, $1;
739         } else {
740           /^n (.*)/;
741           # have line to be merged with?
742           if ($#buffer >= 0) {
743             print shift @buffer, " | ", $1, "\n";
744           } else {
745             print join(" <\n", @buffer), " <\n" if $#buffer >= 0;
746             print $spaces, " > ", $1, "\n";
747           }
748         }
749       }
750       print join(" <\n", @buffer), " <\n" if $#buffer >= 0;
751       @buffer = ();
752
753       print "-" x $columns, "\n";
754     }
755   }
756
757   # unlink($normal1);
758   # unlink($normal2);
759   exit($res ? ((defined $ENV{CLIENT_TEST_COMPARISON_FAILED}) ? int($ENV{CLIENT_TEST_COMPARISON_FAILED}) : 1) : 0);
760 } else {
761   # normalize
762   my $in;
763   if( $#ARGV >= 0 ) {
764     my $file1 = $ARGV[0];
765     if (-d $file1) {
766         open(IN, "-|:utf8", "find $file1 -type f -print0 | xargs -0 cat") || die "$file1: $!";
767     } else {
768         open(IN, "<:utf8", $file1) || die "$file1: $!";
769     }
770     $in = *IN{IO};
771   } else {
772     $in = *STDIN{IO};
773   }
774
775   my $buf = join("", <$in>);
776   print STDOUT join("\n", Normalize($buf, $columns)), "\n";
777 }