This file is indexed.

/usr/lib/thunderbird-addons/extensions/{e2fda1a4-762b-4020-b5ad-a41df1933103}/calendar-js/calRecurrenceInfo.js is in xul-ext-lightning 1:24.4.0+build1-0ubuntu1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

Components.utils.import("resource://calendar/modules/calUtils.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

function getRidKey(dt) {
    if (!dt) {
        return null;
    }
    var tz = dt.timezone;
    if (!tz.isUTC && !tz.isFloating) {
        dt = dt.getInTimezone(UTC());
    }
    return dt.icalString;
}

function calRecurrenceInfo() {
    this.mRecurrenceItems = [];
    this.mExceptionMap = {};

    this.wrappedJSObject = this;
}

const calRecurrenceInfoClassID = Components.ID("{04027036-5884-4a30-b4af-f2cad79f6edf}");
const calRecurrenceInfoInterfaces = [Components.interfaces.calIRecurrenceInfo];
calRecurrenceInfo.prototype = {
    mImmutable: false,
    mBaseItem: null,
    mRecurrenceItems: null,
    mPositiveRules: null,
    mNegativeRules: null,
    mExceptionMap: null,

    classID: calRecurrenceInfoClassID,
    QueryInterface: XPCOMUtils.generateQI(calRecurrenceInfoInterfaces),
    classInfo: XPCOMUtils.generateCI({
        classID: calRecurrenceInfoClassID,
        contractID: "@mozilla.org/calendar/recurrence-info;1",
        classDescription: "Calendar Recurrence Info",
        interfaces: calRecurrenceInfoInterfaces,
    }),

    /**
     * Helpers
     */
    ensureBaseItem: function cRI_ensureBaseItem() {
        if (!this.mBaseItem) {
            throw Components.results.NS_ERROR_NOT_INITIALIZED;
        }
    },
    ensureMutable: function cRI_ensureMutable() {
        if (this.mImmutable) {
            throw Components.results.NS_ERROR_OBJECT_IS_IMMUTABLE;
        }
    },
    ensureSortedRecurrenceRules: function cRI_ensureSortedRecurrenceRules() {
        if (!this.mPositiveRules || !this.mNegativeRules) {
            this.mPositiveRules = [];
            this.mNegativeRules = [];
            for each (var ritem in this.mRecurrenceItems) {
                if (ritem.isNegative) {
                    this.mNegativeRules.push(ritem);
                } else {
                    this.mPositiveRules.push(ritem);
                }
            }
        }
    },

    /**
     * Mutability bits
     */
    get isMutable() {
        return !this.mImmutable;
    },
    makeImmutable: function cRI_makeImmutable() {
        if (this.mImmutable) {
            return;
        }

        for each (let ritem in this.mRecurrenceItems) {
            if (ritem.isMutable) {
                ritem.makeImmutable();
            }
        }

        for each (let item in this.mExceptionMap) {
            if (item.isMutable) {
                item.makeImmutable();
            }
        }

        this.mImmutable = true;
    },

    clone: function cRI_clone() {
        var cloned = new calRecurrenceInfo();
        cloned.mBaseItem = this.mBaseItem;

        var clonedItems = [];
        for each (var ritem in this.mRecurrenceItems) {
            clonedItems.push(ritem.clone());
        }
        cloned.mRecurrenceItems = clonedItems;

        var clonedExceptions = {};
        for (var exitem in this.mExceptionMap) {
            clonedExceptions[exitem] = this.mExceptionMap[exitem].cloneShallow(this.mBaseItem);
        }
        cloned.mExceptionMap = clonedExceptions;

        return cloned;
    },

    /*
     * calIRecurrenceInfo
     */
    get item() {
        return this.mBaseItem;
    },
    set item(value) {
        this.ensureMutable();

        value = calTryWrappedJSObject(value);
        this.mBaseItem = value;
        // patch exception's parentItem:
        for each (let exitem in this.mExceptionMap) {
            exitem.parentItem = value;
        }
    },

    get isFinite() {
        this.ensureBaseItem();

        for each (let ritem in this.mRecurrenceItems) {
            if (!ritem.isFinite) {
                return false;
            }
        }
        return true;
    },

    getRecurrenceItems: function cRI_getRecurrenceItems(aCount) {
        this.ensureBaseItem();

        aCount.value = this.mRecurrenceItems.length;
        return this.mRecurrenceItems;
    },

    setRecurrenceItems: function cRI_setRecurrenceItems(aCount, aItems) {
        this.ensureBaseItem();
        this.ensureMutable();

        // XXX should we clone these?
        this.mRecurrenceItems = aItems;
        this.mPositiveRules = null;
        this.mNegativeRules = null;
    },

    countRecurrenceItems: function cRI_countRecurrenceItems() {
        this.ensureBaseItem();

        return this.mRecurrenceItems.length;
    },

    getRecurrenceItemAt: function cRI_getRecurrenceItemAt(aIndex) {
        this.ensureBaseItem();

        if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length) {
            throw Components.results.NS_ERROR_INVALID_ARG;
        }

        return this.mRecurrenceItems[aIndex];
    },

    appendRecurrenceItem: function cRI_appendRecurrenceItem(aItem) {
        this.ensureBaseItem();
        this.ensureMutable();
        this.ensureSortedRecurrenceRules();

        this.mRecurrenceItems.push(aItem);
        if (aItem.isNegative) {
            this.mNegativeRules.push(aItem);
        } else {
            this.mPositiveRules.push(aItem);
        }
    },

    deleteRecurrenceItemAt: function cRI_deleteRecurrenceItemAt(aIndex) {
        this.ensureBaseItem();
        this.ensureMutable();

        if (aIndex < 0 || aIndex >= this.mRecurrenceItems.length) {
            throw Components.results.NS_ERROR_INVALID_ARG;
        }

        if (this.mRecurrenceItems[aIndex].isNegative) {
            this.mNegativeRules = null;
        } else {
            this.mPositiveRules = null;
        }

        this.mRecurrenceItems.splice(aIndex, 1);
    },

    deleteRecurrenceItem: function cRI_deleteRecurrenceItem(aItem) {
        // Because xpcom objects can be wrapped in various ways, testing for
        // mere == sometimes returns false even when it should be true.  Use
        // the interface pointer returned by sip to avoid that problem.
        var sip1 = Components.classes["@mozilla.org/supports-interface-pointer;1"]
                            .createInstance(Components.interfaces.nsISupportsInterfacePointer);
        sip1.data = aItem;
        sip1.dataIID = Components.interfaces.calIRecurrenceItem;

        var pos;
        if ((pos = this.mRecurrenceItems.indexOf(sip1.data)) > -1) {
            this.deleteRecurrenceItemAt(pos);
        } else {
            throw Components.results.NS_ERROR_INVALID_ARG;
        }
    },

    insertRecurrenceItemAt: function cRI_insertRecurrenceItemAt(aItem, aIndex) {
        this.ensureBaseItem();
        this.ensureMutable();
        this.ensureSortedRecurrenceRules();

        if (aIndex < 0 || aIndex > this.mRecurrenceItems.length) {
            throw Components.results.NS_ERROR_INVALID_ARG;
        }

        if (aItem.isNegative) {
            this.mNegativeRules.push(aItem);
        } else {
            this.mPositiveRules.push(aItem);
        }

        this.mRecurrenceItems.splice(aIndex, 0, aItem);
    },

    clearRecurrenceItems: function cRI_clearRecurrenceItems() {
        this.ensureBaseItem();
        this.ensureMutable();

        this.mRecurrenceItems = [];
        this.mPositiveRules = [];
        this.mNegativeRules = [];
    },

    /*
     * calculations
     */
    getNextOccurrence: function cRI_getNextOccurrence(aTime) {
        this.ensureBaseItem();
        this.ensureSortedRecurrenceRules();

        var startDate = this.mBaseItem.recurrenceStartDate;
        var dates = [];

        var nextOccurrences = [];
        var invalidOccurrences;
        var negMap = {};
        var minOccRid;

        // Go through all negative rules to create a map of occurrences that
        // should be skipped when going through occurrences.
        for each (var ritem in this.mNegativeRules) {
            // TODO Infinite rules (i.e EXRULE) are not taken into account,
            // because its very performance hungry and could potentially
            // lead to a deadlock (i.e RRULE is canceled out by an EXRULE).
            // This is ok for now, since EXRULE is deprecated anyway.
            if (ritem.isFinite) {
                // Get all occurrences starting at our recurrence start date.
                // This is fine, since there will never be an EXDATE that
                // occurrs before the event started and its illegal to EXDATE an
                // RDATE.
                var rdates = ritem.getOccurrences(startDate,
                                                  startDate,
                                                  null,
                                                  0,
                                                  {});
                // Map all negative dates.
                for each (var r in rdates) {
                    negMap[getRidKey(r)] = true;
                }
            } else {
                WARN("Item '" + this.mBaseItem.title + "'" +
                     (this.mBaseItem.calendar ? " (" + this.mBaseItem.calendar.name + ")" : "") +
                     " has an infinite negative rule (EXRULE)");
            }
        }

        var bailCounter = 0;
        do {
            invalidOccurrences = 0;
            // Go through all positive rules and get the next recurrence id
            // according to that rule. If for all rules the rid is "invalid",
            // (i.e an EXDATE removed it, or an exception moved it somewhere
            // else), then get the respective next rid.
            //
            // If in a loop at least one rid is valid (i.e not an exception, not
            // an exdate, is after aTime), then remember the lowest one.
            for (var i = 0; i < this.mPositiveRules.length; i++) {
                let rDateInstance = cal.wrapInstance(this.mPositiveRules[i], Components.interfaces.calIRecurrenceDate);
                let rRuleInstance = cal.wrapInstance(this.mPositiveRules[i], Components.interfaces.calIRecurrenceRule);
                if (rDateInstance) {
                    // RDATEs are special. there is only one date in this rule,
                    // so no need to search anything.
                    let rdate = rDateInstance.date;
                    if (!nextOccurrences[i] && rdate.compare(aTime) > 0) {
                        // The RDATE falls into range, save it.
                        nextOccurrences[i] = rdate;
                    } else {
                        // The RDATE doesn't fall into range. This rule will
                        // always be invalid, since it can't give out a date.
                        nextOccurrences[i] = null;
                        invalidOccurrences++;
                    }
                } else if (rRuleInstance) {
                    // RRULEs must not start searching before |startDate|, since
                    // the pattern is only valid afterwards. If an occurrence
                    // was found in a previous round, we can go ahead and start
                    // searching from that occurrence.
                    var searchStart = nextOccurrences[i] || startDate;

                    // Search for the next occurrence after aTime. If the last
                    // round was invalid, then in this round we need to search
                    // after nextOccurrences[i] to make sure getNextOccurrence()
                    // doesn't find the same occurrence again.
                    var searchDate =
                        (nextOccurrences[i] && nextOccurrences[i].compare(aTime) > 0 ?
                            nextOccurrences[i] :
                            aTime);

                    nextOccurrences[i] = rRuleInstance
                                             .getNextOccurrence(searchStart, searchDate);
                }

                // As decided in bug 734245, an EXDATE of type DATE shall also match a DTSTART of type DATE-TIME
                let nextKey = getRidKey(nextOccurrences[i]);
                let isInExceptionMap = nextKey && (this.mExceptionMap[nextKey.substring(0,8)] ||
                                                   this.mExceptionMap[nextKey]);
                let isInNegMap = nextKey && (negMap[nextKey.substring(0,8)] ||
                                             negMap[nextKey]);
                if (nextKey && (isInNegMap || isInExceptionMap)) {
                    // If the found recurrence id points to either an exception
                    // (will handle later) or an EXDATE, then nextOccurrences[i]
                    // is invalid and we might need to try again next round.
                    invalidOccurrences++;
                } else if (nextOccurrences[i]) {
                    // We have a valid recurrence id (not an exception, not an
                    // EXDATE, falls into range). We only need to save the
                    // earliest occurrence after aTime (checking for aTime is
                    // not needed, since getNextOccurrence() above returns only
                    // occurrences after aTime).
                    if (!minOccRid || minOccRid.compare(nextOccurrences[i]) > 0) {
                        minOccRid = nextOccurrences[i];
                    }
                }
            }

            // To make sure users don't just report bugs like "the application
            // hangs", bail out after 100 runs. If this happens, it is most
            // likely a bug.
            if (bailCounter++ > 100) {
                ERROR("Could not find next occurrence after 100 runs!");
                return null;
            }

            // We counted how many positive rules found out that their next
            // candidate is invalid. If all rules produce invalid next
            // occurrences, a second round is needed.
        } while (invalidOccurrences == this.mPositiveRules.length);

        // Since we need to compare occurrences by date, save the rid found
        // above also as a date. This works out because above we skipped
        // exceptions.
        var minOccDate = minOccRid;

        // Scan exceptions for any dates earlier than the above found
        // minOccDate, but still after aTime.
        for each (var exc in this.mExceptionMap) {
            var start = exc.recurrenceStartDate;
            if (start.compare(aTime) > 0 &&
                (!minOccDate || start.compare(minOccDate) <= 0)) {
                // This exception is earlier, save its rid (for getting the
                // occurrence later on) and its date (for comparing to other
                // exceptions).
                minOccRid = exc.recurrenceId;
                minOccDate = start;
            }
        }

        // If we found a recurrence id any time above, then return the
        // occurrence for it.
        return (minOccRid ? this.getOccurrenceFor(minOccRid) : null);
    },

    getPreviousOccurrence: function cRI_getPreviousOccurrence(aTime) {
        // TODO libical currently does not provide us with easy means of
        // getting the previous occurrence. This could be fixed to improve
        // performance greatly. Filed as libical feature request 1944020.

        // HACK We never know how early an RDATE might be before the actual
        // recurrence start. Since rangeStart cannot be null for recurrence
        // items like calIRecurrenceRule, we need to work around by supplying a
        // very early date. Again, this might have a high performance penalty.
        var early = createDateTime();
        early.icalString = "00000101T000000Z";

        var rids = this.calculateDates(early,
                                       aTime,
                                       0);
        // The returned dates are sorted, so the last one is a good
        // candidate, if it exists.
        return (rids.length > 0 ? this.getOccurrenceFor(rids[rids.length - 1].id) : null);
    },

    // internal helper function;
    calculateDates: function cRI_calculateDates(aRangeStart,
                                                aRangeEnd,
                                                aMaxCount) {
        this.ensureBaseItem();
        this.ensureSortedRecurrenceRules();

        function ridDateSortComptor(a,b) {
            return a.rstart.compare(b.rstart);
        }

        // workaround for UTC- timezones
        var rangeStart = ensureDateTime(aRangeStart);
        var rangeEnd = ensureDateTime(aRangeEnd);

        // If aRangeStart falls in the middle of an occurrence, libical will
        // not return that occurrence when we go and ask for an
        // icalrecur_iterator_new.  This actually seems fairly rational, so
        // instead of hacking libical, I'm going to move aRangeStart back far
        // enough to make sure we get the occurrences we might miss.
        var searchStart = rangeStart.clone();
        var baseDuration = this.mBaseItem.duration;
        if (baseDuration) {
            var duration = baseDuration.clone();
            duration.isNegative = true;
            searchStart.addDuration(duration);
        }

        var startDate = this.mBaseItem.recurrenceStartDate;
        if (startDate == null) {
            // Todo created by other apps may have a saved recurrence but
            // start and due dates disabled.  Since no recurrenceStartDate,
            // treat as undated task.
            return [];
        }

        var dates = [];

        // toss in exceptions first. Save a map of all exceptions ids, so we
        // don't add the wrong occurrences later on.
        var occurrenceMap = {};
        for (var ex in this.mExceptionMap) {
            var item = this.mExceptionMap[ex];
            var occDate = checkIfInRange(item, aRangeStart, aRangeEnd, true);
            occurrenceMap[ex] = true;
            if (occDate) {
                binaryInsert(dates, { id: item.recurrenceId, rstart: occDate }, ridDateSortComptor);
            }
        }

        // DTSTART/DUE is always part of the (positive) expanded set:
        // DTSTART always equals RECURRENCE-ID for items expanded from RRULE
        var baseOccDate = checkIfInRange(this.mBaseItem, aRangeStart, aRangeEnd, true);
        var baseOccDateKey = getRidKey(baseOccDate);
        if (baseOccDate && !occurrenceMap[baseOccDateKey]) {
            occurrenceMap[baseOccDateKey] = true;
            binaryInsert(dates, { id: baseOccDate, rstart: baseOccDate }, ridDateSortComptor);
        }

        // if both range start and end are specified, we ask for all of the occurrences,
        // to make sure we catch all possible exceptions.  If aRangeEnd isn't specified,
        // then we have to ask for aMaxCount, and hope for the best.
        var maxCount;
        if (rangeStart && rangeEnd) {
            maxCount = 0;
        } else {
            maxCount = aMaxCount;
        }

        // Apply positive rules
        for each (let ritem in this.mPositiveRules) {
            var cur_dates = ritem.getOccurrences(startDate,
                                                 searchStart,
                                                 rangeEnd,
                                                 maxCount, {});
            if (cur_dates.length == 0) {
                continue;
            }

            // if positive, we just add these date to the existing set,
            // but only if they're not already there

            var index = 0;
            var len = cur_dates.length;

            // skip items before rangeStart due to searchStart libical hack:
            if (rangeStart && baseDuration) {
                for (; index < len; ++index) {
                    var date = cur_dates[index].clone();
                    date.addDuration(baseDuration);
                    if (rangeStart.compare(date) < 0) {
                        break;
                    }
                }
            }
            for (; index < len; ++index) {
                var date = cur_dates[index];
                var dateKey = getRidKey(date);
                if (occurrenceMap[dateKey]) {
                    // Don't add occurrences twice (i.e exception was
                    // already added before)
                    continue;
                }
                // TODO if cur_dates[] is also sorted, then this binary
                // search could be optimized further
                binaryInsert(dates, { id: date, rstart: date }, ridDateSortComptor);
                occurrenceMap[dateKey] = true;
            }
        }

        // Apply negative rules
        for each (let ritem in this.mNegativeRules) {
            var cur_dates = ritem.getOccurrences(startDate,
                                                 searchStart,
                                                 rangeEnd,
                                                 maxCount, {});
            if (cur_dates.length == 0) {
                continue;
            }

            // XXX: i'm pretty sure negative dates can't really have exceptions
            // (like, you can't make a date "real" by defining an RECURRENCE-ID which
            // is an EXDATE, and then giving it a real DTSTART) -- so we don't
            // check exceptions here
            for each (let dateToRemove in cur_dates) {
                let dateToRemoveKey = getRidKey(dateToRemove);
                if (dateToRemove.isDate) {
                    // As decided in bug 734245, an EXDATE of type DATE shall also match a DTSTART of type DATE-TIME
                    let toRemove = [];
                    for (let occurenceKey in occurrenceMap) {
                        if (occurrenceMap[occurenceKey] && occurenceKey.substring(0,8) == dateToRemoveKey) {
                            dates = dates.filter(function (d) { return d.id.compare(dateToRemove) != 0; });
                            toRemove.push(occurenceKey)
                        }
                    }
                    for (let i=0; i < toRemove.length; i++) {
                        delete occurrenceMap[toRemove[i]];
                    }
                } else if (occurrenceMap[dateToRemoveKey]) {
                    // TODO PERF Theoretically we could use occurrence map
                    // to construct the array of occurrences. Right now I'm
                    // just using the occurrence map to skip the filter
                    // action if the occurrence isn't there anyway.
                    dates = dates.filter(function (d) { return d.id.compare(dateToRemove) != 0; });
                    delete occurrenceMap[dateToRemoveKey];
                }
            }
        }

        // The list was already sorted above, chop anything over aMaxCount, if
        // specified.
        if (aMaxCount && dates.length > aMaxCount) {
            dates = dates.slice(0, aMaxCount);
        }

        return dates;
    },

    getOccurrenceDates: function cRI_getOccurrenceDates(aRangeStart,
                                                        aRangeEnd,
                                                        aMaxCount,
                                                        aCount) {
        var dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount);
        dates = dates.map(function(d) { return d.rstart; });
        aCount.value = dates.length;
        return dates;
    },

    getOccurrences: function cRI_getOccurrences(aRangeStart,
                                                aRangeEnd,
                                                aMaxCount,
                                                aCount) {
        let results = [];
        let dates = this.calculateDates(aRangeStart, aRangeEnd, aMaxCount);
        if (dates.length) {
            let count;
            if (aMaxCount) {
                count = Math.min(aMaxCount, dates.length);
            } else {
                count = dates.length;
            }

            for (let i = 0; i < count; i++) {
                results.push(this.getOccurrenceFor(dates[i].id));
            }
        }

        aCount.value = results.length;
        return results;
    },

    getOccurrenceFor: function cRI_getOccurrenceFor(aRecurrenceId) {
        let proxy = this.getExceptionFor(aRecurrenceId);
        if (!proxy) {
            return this.item.createProxy(aRecurrenceId);
        }
        return proxy;
    },

    removeOccurrenceAt: function cRI_removeOccurrenceAt(aRecurrenceId) {
        this.ensureBaseItem();
        this.ensureMutable();

        var d = Components.classes["@mozilla.org/calendar/recurrence-date;1"]
                          .createInstance(Components.interfaces.calIRecurrenceDate);
        d.isNegative = true;
        d.date = aRecurrenceId.clone();

        this.removeExceptionFor(d.date);

        this.appendRecurrenceItem(d);
    },

    restoreOccurrenceAt: function cRI_restoreOccurrenceAt(aRecurrenceId) {
        this.ensureBaseItem();
        this.ensureMutable();
        this.ensureSortedRecurrenceRules();

        for (var i = 0; i < this.mRecurrenceItems.length; i++) {
            let wrappedItem = cal.wrapInstance(this.mRecurrenceItems[i], Components.interfaces.calIRecurrenceDate);
            if(wrappedItem) {
                let rd = wrappedItem;
                if (rd.isNegative && rd.date.compare(aRecurrenceId) == 0) {
                    return this.deleteRecurrenceItemAt(i);
                }
            }
        }

        throw Components.results.NS_ERROR_INVALID_ARG;
    },

    //
    // exceptions
    //

    //
    // Some notes:
    //
    // The way I read ICAL, RECURRENCE-ID is used to specify a
    // particular instance of a recurring event, according to the
    // RRULEs/RDATEs/etc. specified in the base event.  If one of
    // these is to be changed ("an exception"), then it can be
    // referenced via the UID of the original event, and a
    // RECURRENCE-ID of the start time of the instance to change.
    // This, to me, means that an event where one of the instances has
    // changed to a different time has a RECURRENCE-ID of the original
    // start time, and a DTSTART/DTEND representing the new time.
    //
    // ITIP, however, seems to want something different -- you're
    // supposed to use UID/RECURRENCE-ID to select from the current
    // set of occurrences of an event.  If you change the DTSTART for
    // an instance, you're supposed to use the old (original) DTSTART
    // as the RECURRENCE-ID, and put the new time as the DTSTART.
    // However, after that change, to refer to that instance in the
    // future, you have to use the modified DTSTART as the
    // RECURRENCE-ID.  This madness is described in ITIP end of
    // section 3.7.1.
    //
    // This implementation does the first approach (RECURRENCE-ID will
    // never change even if DTSTART for that instance changes), which
    // I think is the right thing to do for CalDAV; I don't know what
    // we'll do for incoming ITIP events though.
    //
    modifyException: function cRI_modifyException(anItem, aTakeOverOwnership) {
        this.ensureBaseItem();

        anItem = calTryWrappedJSObject(anItem);

        if (anItem.parentItem.calendar != this.mBaseItem.calendar &&
            anItem.parentItem.id != this.mBaseItem.id)
        {
            ERROR("recurrenceInfo::addException: item parentItem != this.mBaseItem (calendar/id)!");
            throw Components.results.NS_ERROR_INVALID_ARG;
        }

        if (anItem.recurrenceId == null) {
            ERROR("recurrenceInfo::addException: item with null recurrenceId!");
            throw Components.results.NS_ERROR_INVALID_ARG;
        }

        var itemtoadd;
        if (aTakeOverOwnership && anItem.isMutable) {
            itemtoadd = anItem;
            itemtoadd.parentItem = this.mBaseItem;
        } else {
            itemtoadd = anItem.cloneShallow(this.mBaseItem);
        }

        // we're going to assume that the recurrenceId is valid here,
        // because presumably the item came from one of our functions

        var exKey = getRidKey(itemtoadd.recurrenceId);
        this.mExceptionMap[exKey] = itemtoadd;
    },

    getExceptionFor: function cRI_getExceptionFor(aRecurrenceId) {
        this.ensureBaseItem();
        // Interface calIRecurrenceInfo specifies result be null if not found.
        // To avoid strict "reference to undefined property" warning, appending
        // "|| null" gives explicit result in case where property undefined
        // (or false, 0, null, or "", but here it should never be those values).
        return this.mExceptionMap[getRidKey(aRecurrenceId)] || null;
    },

    removeExceptionFor: function cRI_removeExceptionFor(aRecurrenceId) {
        this.ensureBaseItem();
        delete this.mExceptionMap[getRidKey(aRecurrenceId)];
    },

    getExceptionIds: function cRI_getExceptionIds(aCount) {
        this.ensureBaseItem();

        var ids = [];
        for each (var item in this.mExceptionMap) {
            ids.push(item.recurrenceId);
        }

        aCount.value = ids.length;
        return ids;
    },

    // changing the startdate of an item needs to take exceptions into account.
    // in case we're about to modify a parentItem (aka 'folded' item), we need
    // to modify the recurrenceId's of all possibly existing exceptions as well.
    onStartDateChange: function cRI_onStartDateChange(aNewStartTime, aOldStartTime) {
        // passing null for the new starttime would indicate an error condition,
        // since having a recurrence without a starttime is invalid.
        cal.ASSERT(aNewStartTime, "invalid arg!", true);

        // no need to check for changes if there's no previous starttime.
        if (!aOldStartTime) {
            return;
        }

        // convert both dates to UTC since subtractDate is not timezone aware.
        let timeDiff = aNewStartTime.getInTimezone(UTC()).subtractDate(aOldStartTime.getInTimezone(UTC()));

        let rdates = {};

        // take RDATE's and EXDATE's into account.
        const kCalIRecurrenceDate = Components.interfaces.calIRecurrenceDate;
        let ritems = this.getRecurrenceItems({});
        for each (let ritem in ritems) {
            let rDateInstance = cal.wrapInstance(ritem, kCalIRecurrenceDate);
            let rRuleInstance = cal.wrapInstance(ritem, Components.interfaces.calIRecurrenceRule);
            if (rDateInstance) {
                ritem = rDateInstance;
                let date = ritem.date;
                date.addDuration(timeDiff);
                if (!ritem.isNegative) {
                    rdates[getRidKey(date)] = date;
                }
                ritem.date = date;
            } else if (rRuleInstance) {
                ritem = rRuleInstance;
                if (!ritem.isByCount) {
                    let untilDate = ritem.untilDate;
                    if (untilDate) {
                        untilDate.addDuration(timeDiff);
                        ritem.untilDate = untilDate;
                    }
                }
            }
        }

        let startTimezone = aNewStartTime.timezone;
        let modifiedExceptions = [];
        for each (let exid in this.getExceptionIds({})) {
            let ex = this.getExceptionFor(exid);
            if (ex) {
                ex = ex.clone();
                // track RECURRENCE-IDs in DTSTART's or RDATE's timezone,
                // otherwise those won't match any longer w.r.t DST:
                let rid = ex.recurrenceId;
                let rdate = rdates[getRidKey(rid)];
                rid = rid.getInTimezone(rdate ? rdate.timezone : startTimezone);
                rid.addDuration(timeDiff);
                ex.recurrenceId = rid;
                cal.shiftItem(ex, timeDiff);
                modifiedExceptions.push(ex);
                this.removeExceptionFor(exid);
            }
        }
        for each (let modifiedEx in modifiedExceptions) {
            this.modifyException(modifiedEx, true);
        }
    },

    onIdChange: function cRI_onIdChange(aNewId) {
        // patch all overridden items' id:
        for each (let item in this.mExceptionMap) {
            item.id = aNewId;
        }
    }
};