#!/usr/bin/perl -w # Copyright © 2013 Jamie Zawinski # # Permission to use, copy, modify, distribute, and sell this software and its # documentation for any purpose is hereby granted without fee, provided that # the above copyright notice appear in all copies and that both that # copyright notice and this permission notice appear in supporting # documentation. No representations are made about the suitability of this # software for any purpose. It is provided "as is" without express or # implied warranty. # # Compare two ICS files, and emit one with the events that are in the first # but not the second. # # Events are considered the same if they have the same UID. # With --url, URLs are compared instead of UIDs. # # Note: the generated file will have freshly-generated UIDs! # # Created: 10-Mar-2013. require 5; use diagnostics; use strict; use open ":encoding(utf8)"; my $progname = $0; $progname =~ s@.*/@@g; my $version = q{ $Revision: 1.1 $ }; $version =~ s/^[^\d]+([\d.]+).*/$1/; my $verbose = 0; sub ics_diff($$$$) { my ($fa, $fb, $o, $urlp) = @_; open (my $in, '<', "$fa") || error ("$fa: $!"); local $/ = undef; # read entire file my $bodya = <$in>; close $in; open ($in, '<', "$fb") || error ("$fb: $!"); local $/ = undef; # read entire file my $bodyb = <$in>; close $in; $bodya =~ s/^(BEGIN:VEVENT)/\001$1/gmi; $bodyb =~ s/^(BEGIN:VEVENT)/\001$1/gmi; $bodya =~ s/\nEND:VCALENDAR.*$//si; $bodyb =~ s/\nEND:VCALENDAR.*$//si; my @a = split (/\001/, $bodya); my @b = split (/\001/, $bodyb); my $head = shift @a; shift @b; my $fallback = 'UID'; my $field = ($urlp ? 'URL' : $fallback); my %a; my ($counta, $countb, $countc) = (0, 0, 0); foreach my $e (@a) { my ($key) = ($e =~ m/^$field(?:;[^:\n]*)?:(.*)$/mi); ($key) = ($e =~ m/^$fallback(?:;[^:\n]*)?:(.*)$/mi) unless defined($key); error ("$fa: no $fallback in event $counta") unless $key; $a{$key} = $e; print STDERR "$progname: $fa: $counta: $key\n" if ($verbose); $counta++; } print STDERR "$progname: $fa: $counta events\n"; foreach my $e (@b) { my ($key) = ($e =~ m/^$field(?:;[^:\n]*)?:(.*)$/mi); ($key) = ($e =~ m/^$fallback(?:;[^:\n]*)?:(.*)$/mi) unless defined($key); error ("$fb: no $fallback in event $countb") unless $key; $a{$key} = undef; print STDERR "$progname: $fb: $countb: $key\n" if ($verbose); $countb++; } print STDERR "$progname: $fb: $countb events\n"; my @bodyc = (); foreach my $key (keys(%a)) { my $e = $a{$key}; next unless $e; # I guess we need to give them fresh UIDs? # If we don't, iCloud freaks the fuck out with 403 errors. # my $uid = sprintf("icsdiff_%08x", rand(0xFFFFFFFF)); $e =~ s/^(UID:).*$/$1$uid/gm; push @bodyc, $e; print STDERR "$progname: $o: $countc: $key\n" if ($verbose); $countc++; } @bodyc = sort { my ($k1) = ($a =~ m/^DTSTART\b.*?:(.*)$/mi); my ($k2) = ($b =~ m/^DTSTART\b.*?:(.*)$/mi); return $k1 cmp $k2; } @bodyc; my $title = $o; $title =~ s@^.*/@@gs; $title =~ s@\.[^.]+$@@gs; $head =~ s/^(X-WR-CALNAME:).*/$1$title/mi; my $bodyc = $head . join('', @bodyc) . "END:VCALENDAR\n"; $bodyc =~ s/\r\n/\n/gs; $bodyc =~ s/\r/\n/gs; $bodyc =~ s/\n/\r\n/gs; open (my $out, '>', "$o") || error ("$o: $!"); print {$out} $bodyc; close $out; print STDERR "$progname: wrote $o ($countc events)\n"; } sub error($) { my ($err) = @_; print STDERR "$progname: $err\n"; exit 1; } sub usage() { print STDERR "usage: $progname [--verbose] [--url ] A.ics B.ics OUT.ics\n"; exit 1; } sub main() { my ($a, $b, $o); my $urlp = 0; while ($#ARGV >= 0) { $_ = shift @ARGV; if (m/^--?verbose$/) { $verbose++; } elsif (m/^-v+$/) { $verbose += length($_)-1; } elsif (m/^--?url$/) { $urlp = 1; } elsif (m/^-./) { usage; } elsif (!defined($a)) { $a = $_; } elsif (!defined($b)) { $b = $_; } elsif (!defined($o)) { $o = $_; } else { usage; } } usage unless (defined($o)); ics_diff ($a, $b, $o, $urlp); } main(); exit 0;