3 # Copyright (C) 2008 Funambol, Inc.
4 # Copyright (C) 2008-2009 Patrick Ohly <patrick.ohly@gmx.de>
5 # Copyright (C) 2009 Intel Corporation
8 # <left file> <right file>
9 # Either normalizes a file or compares two of them in a side-by-side
12 # Checks environment variables:
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.
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.
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
32 # CLIENT_TEST_COMPARISON_FAILED=1
33 # Overrides the default error code when changes are found.
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.
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.
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
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/;
66 # TODO: this hack ensures that any synchronization is limited to
67 # properties supported by Synthesis. Remove this again.
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/;
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";
92 return join(",", sort(split(/,/, $list)));
101 foreach my $val (split (/;/, $values)) {
102 push(@res, $prop, ":", $val, $eol);
104 return join("", @res);
107 # parameters: text, width to use for reformatted lines
108 # returns list of lines without line breaks
117 foreach $_ ( split( /(?:(?<=\nEND:VCARD)|(?<=\nEND:VCALENDAR))\n*/ ) ) {
118 # undo line continuation
120 # ignore charset specifications, assume UTF-8
121 s/;CHARSET="?UTF-8"?//g;
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;
128 # exact order of categories is irrelevant
129 s/^CATEGORIES:(\S+)/"CATEGORIES:" . sortlist($1)/mge;
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) {}
134 # the distinction between an empty and a missing property
135 # is vague and handled differently, so ignore empty properties
138 # use separate TYPE= fields
139 while( s/^(\w*[^:\n]*);TYPE=(\w*),(\w*)/$1;TYPE=$2;TYPE=$3/mg ) {}
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 ) {}
144 # replace parameters with a sorted parameter list
145 s!^([^;:\n]*);(.*?):!$1 . ";" . join(';',sort(split(/;/, $2))) . ":"!meg;
147 # EXDATE;VALUE=DATE is the default, no need to show it
148 s/^EXDATE;VALUE=DATE:/EXDATE:/mg;
150 # multiple EXDATEs may be joined into one, use separate properties as normal form
151 s/^(EXDATE[^:]*):(.*)(\r?\n)/splitvalue($1, $2, $3)/mge;
153 # sort value lists of specific properties
154 s!^(RRULE.*):(.*)!$1 . ":" . join(';',sort(split(/;/, $2)))!meg;
156 # INTERVAL=1 is the default and thus can be removed
157 s/^RRULE(.*?);INTERVAL=1(;|$)/RRULE$1$2/mg;
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) {}
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;
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;
197 # mailto is case insensitive
198 s/^((ATTENDEE|ORGANIZER).*):[Mm][Aa][Ii][Ll][Tt][Oo]:/$1:mailto:/mg;
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;
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;
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;
216 s/^X-EVOLUTION-(SPOUSE|MANAGER|ASSISTANT|ANNIVERSARY)/X-$1/gm;
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;
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;
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;
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
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;
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;
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;
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;
261 if ($synthesis || $mobical) {
262 # only preserves ORG "Company", but loses "Department" and "Office"
263 s/^ORG:([^;:\n]+)(;[^\n]*)/ORG:$1/mg;
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;
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;
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;
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
297 s/^ADR(.*?)\:(.*)/$type=($1 || ""); @_ = split(\/(?<!\\);\/, $2); "ADR:;;" . ($_[2] || "") . ";" . ($_[3] || "") . ";" . ($_[4] || "") . ";" . ($_[5] || "") . ";" . ($_[6] || "")/gme;
298 # TYPE=CAR not supported
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/) {
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;
320 # VALARM not supported
321 s/^BEGIN:VALARM.*?END:VALARM\r?\n?//msg;
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;
329 s/^ORG:([^;:\n]*);.*/ORG:$1/gm;
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;
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/;
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;
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;
359 #some new properties are added by funambol server
360 s/^(CLASS:PUBLIC|PERCENT-COMPLETE:0).*\r?\n?//gm;
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;
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;
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;
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) {}
394 if (/^BEGIN:VTODO/m) {
396 s/^(PRIORITY)(;[^:;\n]*)*:.*\r?\n?/$1:[...]\n/gm;
398 s/^(STATUS|DTSTART|CATEGORIES)(;[^:;\n]*)*:.*\r?\n?//gm;
401 #Testing with phones using vcalendar, do not support UID
402 s/^(UID|CLASS|SEQUENCE|TRANSP)(;[^:;\n]*)*:.*\r?\n?//gm;
406 if (/^BEGIN:VCARD/m) {
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) {}
414 # does not preserve X-EVOLUTION-UI-SLOT=
415 s/^(\w+)([^:\n]*);X-EVOLUTION-UI-SLOT=\d+/$1$2/mg;
417 # does not preserve third ADR
418 s/^ADR:Test Box #3.*\n\r?//mg;
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;
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;
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;
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;
450 s/^URL([^\n:]*);TYPE=HOME/URL$1/mg;
451 s/^EMAIL([^\n:]*);TYPE=HOME/EMAIL$1/mg;
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;
468 if (/^BEGIN:VTODO/m ) {
469 s/^(UID|SEQUENCE|URL|CLASS|PRIORITY)(;[^:;\n]*)*:.*\r?\n?//gm;
470 s/^PERCENT-COMPLETE:0\r?\n?//gm;
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;
476 # some workrounds here for mobical's bug
477 s/^(FN|BDAY)(;[^:;\n]*)*:.*\r?\n?//gm;
479 if (/^BEGIN:VEVENT/m ) {
480 s/^(UID|SEQUENCE|CLASS|TRANSP|RECURRENCE-ID|ATTENDEE|ORGANIZER|AALARM|DALARM)(;[^:;\n]*)*:.*\r?\n?//gm;
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;
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;
493 # treat X-MOZILLA-HTML=FALSE as if the property didn't exist
494 s/^X-MOZILLA-HTML:FALSE\r?\n?//gm;
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
506 foreach $_ (split /\n/, $_) {
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]}, $_;
520 my $block = pop @formatted;
521 my $begin = shift @{$block};
522 my $end = pop @{$block};
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.
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) }
543 push @{$formatted[$#formatted]}, $_;
547 push @items, ${$formatted[0]}[0];
550 return split( /\n/, join( "\n\n", sort @items ));
553 # number of columns available for output:
554 # try tput without printing the shells error if not found,
556 my $columns = `which tput >/dev/null 2>/dev/null && tput 2>/dev/null && tput cols`;
557 if ($? || !$columns) {
565 } elsif($#ARGV == 1) {
568 my ($file1, $file2) = ($ARGV[0], $ARGV[1]);
570 my $singlewidth = int(($columns - 3) / 2);
571 $columns = $singlewidth * 2 + 3;
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
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;
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};
602 open(IN, "<:utf8", "$fullname") || die "$fullname: $!";
603 push @content2, <IN>;
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>;
612 my $content1 = join("", @content1);
613 my $content2 = join("", @content2);
614 @normal1 = Normalize($content1, $singlewidth);
615 @normal2 = Normalize($content2, $singlewidth);
618 open(IN1, "-|:utf8", "find $file1 -type f -print0 | xargs -0 cat") || die "$file1: $!";
620 open(IN1, "<:utf8", $file1) || die "$file1: $!";
623 open(IN2, "-|:utf8", "find $file2 -type f -print0 | xargs -0 cat") || die "$file2: $!";
625 open(IN2, "<:utf8", $file2) || die "$file2: $!";
627 my $buf1 = join("", <IN1>);
628 my $buf2 = join("", <IN2>);
629 @normal1 = Normalize($buf1, $singlewidth);
630 @normal2 = Normalize($buf2, $singlewidth);
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.
640 # $_ = `diff "--old-line-format=o %L" "--new-line-format=n %L" "--unchanged-line-format=u %L" "$normal1" "$normal2"`;
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);
648 foreach $hunk ( @{$diffs_ref} ) {
649 my ($type, $left, $right) = @{$hunk};
653 } elsif ($type eq "+") {
656 } elsif ($type eq "c") {
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";
679 # fix confusing output like:
680 # BEGIN:VCARD BEGIN:VCARD
686 # and replace it with:
692 # BEGIN:VCARD BEGIN:VCARD
694 # With the o/n/u markup this presents itself as:
702 # The alternative case is also possible:
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) {}
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) {}
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) {
726 # make all lines equally long in terms of printable characters
727 s/^(.*)$/$1 . (" " x ($singlewidth + 2 - length($1)))/gme;
729 # convert into side-by-side output
731 foreach $_ (split /\n/, $_) {
733 print join(" <\n", @buffer), " <\n" if $#buffer >= 0;
735 print $1, " ", $1, "\n";
736 } elsif (/^o (.*)/) {
737 # preserve in buffer for potential merging with "n "
741 # have line to be merged with?
743 print shift @buffer, " | ", $1, "\n";
745 print join(" <\n", @buffer), " <\n" if $#buffer >= 0;
746 print $spaces, " > ", $1, "\n";
750 print join(" <\n", @buffer), " <\n" if $#buffer >= 0;
753 print "-" x $columns, "\n";
759 exit($res ? ((defined $ENV{CLIENT_TEST_COMPARISON_FAILED}) ? int($ENV{CLIENT_TEST_COMPARISON_FAILED}) : 1) : 0);
764 my $file1 = $ARGV[0];
766 open(IN, "-|:utf8", "find $file1 -type f -print0 | xargs -0 cat") || die "$file1: $!";
768 open(IN, "<:utf8", $file1) || die "$file1: $!";
775 my $buf = join("", <$in>);
776 print STDOUT join("\n", Normalize($buf, $columns)), "\n";