ics - types.go
1 package ics
2
3 import (
4 "encoding/base64"
5 "errors"
6 "fmt"
7 "math"
8 "net/url"
9 "strconv"
10 "strings"
11 "time"
12 "unicode/utf8"
13
14 "vimagination.zapto.org/parser"
15 )
16
17 const dateTimeFormat = "20060102T150405Z"
18
19 // Binary is inline binary data
20 type Binary []byte
21
22 func (b *Binary) decode(params map[string]string, data string) error {
23 if params["ENCODING"] != "BASE64" {
24 return ErrInvalidEncoding
25 }
26 cb, err := base64.StdEncoding.DecodeString(data)
27 *b = cb
28 if err != nil {
29 return fmt.Errorf("error while decoding binary data: %w", err)
30 }
31 return nil
32 }
33
34 func (b Binary) aencode(w writer) {
35 w.WriteString(";ENCODING=BASE64:")
36 b.encode(w)
37 }
38
39 func (b Binary) encode(w writer) {
40 e := base64.NewEncoder(base64.StdEncoding, w)
41 e.Write(b)
42 e.Close()
43 }
44
45 func (Binary) valid() error {
46 return nil
47 }
48
49 // Boolean is true or false
50 type Boolean bool
51
52 func (b *Boolean) decode(_ map[string]string, data string) error {
53 cb, err := strconv.ParseBool(data)
54 *b = Boolean(cb)
55 if err != nil {
56 return ErrInvalidBoolean
57 }
58 return nil
59 }
60
61 var (
62 booleanTrue = [...]byte{'T', 'R', 'U', 'E'}
63 booleanFalse = [...]byte{'F', 'A', 'L', 'S', 'E'}
64 )
65
66 func (b Boolean) aencode(w writer) {
67 w.WriteString(":")
68 b.encode(w)
69 }
70
71 func (b Boolean) encode(w writer) {
72 if b {
73 w.Write(booleanTrue[:])
74 } else {
75 w.Write(booleanFalse[:])
76 }
77 }
78
79 func (Boolean) valid() error {
80 return nil
81 }
82
83 // CalendarAddress contains a calendar user address
84 type CalendarAddress struct {
85 url.URL
86 }
87
88 func (c *CalendarAddress) decode(_ map[string]string, data string) error {
89 cu, err := url.Parse(data)
90 if err != nil {
91 return fmt.Errorf("error parsing CalendarAddress: %w", err)
92 }
93 c.URL = *cu
94 return nil
95 }
96
97 func (c CalendarAddress) aencode(w writer) {
98 w.WriteString(":")
99 c.encode(w)
100 }
101
102 func (c CalendarAddress) encode(w writer) {
103 w.WriteString(c.URL.String())
104 }
105
106 func (CalendarAddress) valid() error {
107 return nil
108 }
109
110 // Date is a Calendar Data
111 type Date struct {
112 time.Time
113 }
114
115 func (d *Date) decode(_ map[string]string, data string) error {
116 t, err := time.Parse(dateTimeFormat[:8], data)
117 if err != nil {
118 return fmt.Errorf("error parsing Date: %w", err)
119 }
120 d.Time = t
121 return nil
122 }
123
124 func (d Date) aencode(w writer) {
125 w.WriteString(":")
126 d.encode(w)
127 }
128
129 func (d Date) encode(w writer) {
130 b := make([]byte, 0, 8)
131 w.Write(d.AppendFormat(b, dateTimeFormat[:8]))
132 }
133
134 func (d Date) valid() error {
135 if d.IsZero() {
136 return ErrInvalidTime
137 }
138 return nil
139 }
140
141 // DateTime is a Calendar Date and Time
142 type DateTime struct {
143 time.Time
144 }
145
146 func (d *DateTime) decode(params map[string]string, data string) error {
147 if tz, ok := params["TZID"]; ok {
148 l, err := time.LoadLocation(tz)
149 if err != nil {
150 return fmt.Errorf("error loading timezone for DateTime: %w", err)
151 }
152 t, err := time.ParseInLocation(dateTimeFormat[:15], data, l)
153 if err != nil {
154 return fmt.Errorf("error parsing time in location for DateTime: %w", err)
155 }
156 d.Time = t
157 } else if len(data) > 0 && data[len(data)-1] == 'Z' {
158 t, err := time.ParseInLocation(dateTimeFormat, data, time.UTC)
159 if err != nil {
160 return fmt.Errorf("error parsing time in UTC for DateTime: %w", err)
161 }
162 d.Time = t
163 } else {
164 t, err := time.ParseInLocation(dateTimeFormat[:15], data, time.Local)
165 if err != nil {
166 return fmt.Errorf("error parsing local time for DateTime: %w", err)
167 }
168 d.Time = t
169 }
170 return nil
171 }
172
173 func (d DateTime) aencode(w writer) {
174 writeTimezone(w, d.Time)
175 w.WriteString(":")
176 d.encode(w)
177 }
178
179 func (d DateTime) encode(w writer) {
180 b := make([]byte, 0, 16)
181 switch d.Location() {
182 case time.UTC:
183 b = d.AppendFormat(b, dateTimeFormat)
184 case time.Local:
185 b = d.AppendFormat(b, dateTimeFormat[:15])
186 default:
187 b = d.AppendFormat(b, dateTimeFormat[:15])
188 }
189 w.Write(b)
190 }
191
192 func (d DateTime) valid() error {
193 if d.IsZero() {
194 return ErrInvalidTime
195 }
196 return nil
197 }
198
199 // Duration is a duration of time
200 type Duration struct {
201 Negative bool
202 Weeks, Days, Hours, Minutes, Seconds uint
203 }
204
205 func (d *Duration) decode(_ map[string]string, data string) error {
206 t := parser.NewStringTokeniser(data)
207 if t.Accept("-") {
208 d.Negative = true
209 } else {
210 t.Accept("+")
211 }
212 if !t.Accept("P") {
213 return ErrInvalidDuration
214 }
215 var level uint8
216 for t.Peek() != -1 {
217 if t.Accept("T") {
218 level = 1
219 }
220 t.Get()
221 mode := t.AcceptRun("0123456789")
222 n, err := strconv.ParseUint(t.Get(), 10, 0)
223 num := uint(n)
224 if err != nil {
225 return fmt.Errorf("error decoding duration: %w", err)
226 }
227 switch mode {
228 case 'W':
229 if level > 0 {
230 return ErrInvalidDuration
231 }
232 t.Accept("W")
233 if t.Peek() != -1 {
234 return ErrInvalidDuration
235 }
236 d.Weeks = num
237 return nil
238 case 'D':
239 if level > 0 {
240 return ErrInvalidDuration
241 }
242 t.Accept("D")
243 d.Days = num
244 level = 1
245 case 'H':
246 if level > 1 {
247 return ErrInvalidDuration
248 }
249 t.Accept("H")
250 d.Hours = num
251 level = 2
252 case 'M':
253 if level > 2 {
254 return ErrInvalidDuration
255 }
256 t.Accept("M")
257 d.Minutes = num
258 level = 3
259 case 'S':
260 if level > 3 {
261 return ErrInvalidDuration
262 }
263 t.Accept("S")
264 if t.Peek() != -1 {
265 return ErrInvalidDuration
266 }
267 d.Seconds = num
268 default:
269 return ErrInvalidDuration
270 }
271 }
272 if level == 0 {
273 return ErrInvalidDuration
274 }
275 return nil
276 }
277
278 func itoa(n uint) []byte {
279 if n == 0 {
280 return []byte{'0'}
281 }
282 var digits [20]byte
283 pos := 20
284 for ; n > 0; n /= 10 {
285 pos--
286 digits[pos] = '0' + byte(n%10)
287 }
288 return digits[pos:]
289 }
290
291 func (d Duration) aencode(w writer) {
292 w.WriteString(":")
293 d.encode(w)
294 }
295
296 func (d Duration) encode(w writer) {
297 data := make([]byte, 0, 64)
298 if d.Negative {
299 data = append(data, '-')
300 }
301 data = append(data, 'P')
302 if d.Weeks != 0 {
303 data = append(data, itoa(d.Weeks)...)
304 data = append(data, 'W')
305 } else {
306 if d.Days != 0 {
307 data = append(data, itoa(d.Days)...)
308 data = append(data, 'D')
309 }
310 if d.Days == 0 || (d.Hours != 0 || d.Minutes != 0 || d.Seconds != 0) {
311 data = append(data, 'T')
312 if d.Hours != 0 {
313 data = append(data, itoa(d.Hours)...)
314 data = append(data, 'H')
315 if d.Minutes != 0 || d.Seconds != 0 {
316 data = append(data, itoa(d.Minutes)...)
317 data = append(data, 'M')
318 if d.Seconds != 0 {
319 data = append(data, itoa(d.Seconds)...)
320 data = append(data, 'S')
321 }
322 }
323 } else if d.Minutes != 0 {
324 data = append(data, itoa(d.Minutes)...)
325 data = append(data, 'M')
326 if d.Seconds != 0 {
327 data = append(data, itoa(d.Seconds)...)
328 data = append(data, 'S')
329 }
330 } else {
331 data = append(data, itoa(d.Seconds)...)
332 data = append(data, 'S')
333 }
334 }
335 }
336 w.Write(data)
337 }
338
339 func (Duration) valid() error {
340 return nil
341 }
342
343 // Float contains a real-number value
344 type Float float64
345
346 func (f *Float) decode(_ map[string]string, data string) error {
347 cf, err := strconv.ParseFloat(data, 64)
348 if err != nil {
349 return fmt.Errorf("error parsing Float: %w", err)
350 }
351 *f = Float(cf)
352 return nil
353 }
354
355 func (f Float) aencode(w writer) {
356 w.WriteString(":")
357 f.encode(w)
358 }
359
360 func (f Float) encode(w writer) {
361 w.WriteString(strconv.FormatFloat(float64(f), 'f', -1, 64))
362 }
363
364 func (f Float) valid() error {
365 d := float64(f)
366 if !math.IsNaN(d) && !math.IsInf(d, 0) {
367 return ErrInvalidFloat
368 }
369 return nil
370 }
371
372 // TFloat is a pair of float values used for coords
373 type TFloat [2]float64
374
375 func (t *TFloat) decode(_ map[string]string, data string) error {
376 fs := strings.SplitN(data, ";", 2)
377 if len(fs) != 2 {
378 return ErrInvalidTFloat
379 }
380 var err error
381 (*t)[0], err = strconv.ParseFloat(fs[0], 64)
382 if err != nil {
383 return fmt.Errorf("error parsing TFloat[0]: %w", err)
384 }
385 (*t)[1], err = strconv.ParseFloat(fs[1], 64)
386 if err != nil {
387 return fmt.Errorf("error parsing TFloat[1]: %w", err)
388 }
389 return nil
390 }
391
392 func (t TFloat) aencode(w writer) {
393 w.WriteString(":")
394 t.encode(w)
395 }
396
397 func (t TFloat) encode(w writer) {
398 w.WriteString(strconv.FormatFloat(t[0], 'f', -1, 64))
399 w.WriteString(";")
400 w.WriteString(strconv.FormatFloat(t[1], 'f', -1, 64))
401 }
402
403 func (t TFloat) valid() error {
404 d := float64(t[0])
405 if !math.IsNaN(d) && !math.IsInf(d, 0) {
406 return ErrInvalidFloat
407 }
408 d = float64(t[1])
409 if !math.IsNaN(d) && !math.IsInf(d, 0) {
410 return ErrInvalidFloat
411 }
412 return nil
413 }
414
415 // Integer is a signed integer value
416 type Integer int32
417
418 func (i *Integer) decode(_ map[string]string, data string) error {
419 ci, err := strconv.ParseInt(data, 10, 32)
420 if err != nil {
421 return err
422 }
423 *i = Integer(ci)
424 return nil
425 }
426
427 func (i Integer) aencode(w writer) {
428 w.WriteString(":")
429 i.encode(w)
430 }
431
432 func (i Integer) encode(w writer) {
433 w.WriteString(strconv.FormatInt(int64(i), 10))
434 }
435
436 func (Integer) valid() error {
437 return nil
438 }
439
440 // Period represents a precise period of time/
441 //
442 // Only one of End or Duration will be used. If Period.End.IsZero() is true,
443 // then it uses Period.Duration
444 type Period struct {
445 Start, End DateTime
446 Duration Duration
447 }
448
449 func (p *Period) decode(params map[string]string, data string) error {
450 i := strings.IndexByte(data, '/')
451 if i == -1 || len(data) == i+1 {
452 return ErrInvalidPeriod
453 }
454 err := p.Start.decode(params, data[:i])
455 if err != nil {
456 return fmt.Errorf("error while decoding Period Start: %w", err)
457 }
458 if data[i+1] == 'P' || data[i+1] == '+' {
459 err = p.Duration.decode(params, data[i+1:])
460 if err != nil {
461 return fmt.Errorf("error while decoding Period: %w", err)
462 }
463 return nil
464 }
465 err = p.End.decode(params, data[i+1:])
466 if err != nil {
467 return fmt.Errorf("error while decoding Period End: %w", err)
468 }
469 return nil
470 }
471
472 func (p Period) aencode(w writer) {
473 writeTimezone(w, p.Start.Time)
474 w.WriteString(":")
475 p.encode(w)
476 }
477
478 func (p Period) encode(w writer) {
479 p.Start.encode(w)
480 w.Write([]byte{'/'})
481 if p.End.IsZero() {
482 p.Duration.encode(w)
483 } else {
484 p.End.encode(w)
485 }
486 }
487
488 func (p Period) valid() error {
489 if p.Start.IsZero() {
490 return ErrInvalidPeriodStart
491 }
492 if p.End.IsZero() {
493 if p.Duration.Negative {
494 return ErrInvalidPeriodDuration
495 }
496 } else if !p.End.After(p.Start.Time) || p.Start.Location() != p.End.Location() {
497 return ErrInvalidPeriodEnd
498 }
499 return nil
500 }
501
502 // Frequency represents the Recurrence frequency
503 type Frequency uint8
504
505 // Frequency constant values
506 const (
507 Secondly Frequency = iota
508 Minutely
509 Hourly
510 Daily
511 Weekly
512 Monthly
513 Yearly
514 )
515
516 // WeekDay is a numeric representation of a Day of the Week
517 type WeekDay uint8
518
519 // Weekday constant values
520 const (
521 UnknownDay WeekDay = iota
522 Sunday
523 Monday
524 Tuesday
525 Wednesday
526 Thursday
527 Friday
528 Saturday
529 )
530
531 // Month is a numeric representation of a Month of the Year
532 type Month uint8
533
534 // Month Constant Values
535 const (
536 UnknownMonth Month = iota
537 January
538 February
539 March
540 April
541 May
542 June
543 July
544 August
545 September
546 October
547 November
548 December
549 )
550
551 // DayRecur is used to reprent the nth day in a time period, be it 2nd Monday
552 // in a Month, or 31st Friday in a year, etc.
553 type DayRecur struct {
554 Day WeekDay
555 Occurrence int8
556 }
557
558 // Recur contains a recurrence rule specification
559 type Recur struct {
560 Frequency Frequency
561 WeekStart WeekDay
562 UntilTime bool
563 Until time.Time
564 Count uint64
565 Interval uint64
566 BySecond []uint8
567 ByMinute []uint8
568 ByHour []uint8
569 ByDay []DayRecur
570 ByMonthDay []int8
571 ByYearDay []int16
572 ByWeekNum []int8
573 ByMonth []Month
574 BySetPos []int16
575 }
576
577 func (r *Recur) decode(params map[string]string, data string) error {
578 var freq bool
579 for _, rule := range strings.Split(data, ";") {
580 parts := strings.SplitN(rule, "=", 2)
581 if len(parts) != 2 {
582 return ErrInvalidRecur
583 }
584 switch parts[0] {
585 case "FREQ":
586 switch parts[1] {
587 case "SECONDLY":
588 r.Frequency = Secondly
589 case "MINUTELY":
590 r.Frequency = Minutely
591 case "HOURLY":
592 r.Frequency = Hourly
593 case "DAILY":
594 r.Frequency = Daily
595 case "WEEKLY":
596 r.Frequency = Weekly
597 case "MONTHLY":
598 r.Frequency = Monthly
599 case "YEARLY":
600 r.Frequency = Yearly
601 default:
602 return ErrInvalidRecur
603 }
604 freq = true
605 case "UNTIL":
606 if r.Count > 0 {
607 return ErrInvalidRecur
608 }
609 if len(parts[1]) > 10 {
610 var d DateTime
611 if err := d.decode(params, parts[1]); err != nil {
612 return ErrInvalidRecur
613 }
614 r.Until = d.Time
615 r.UntilTime = true
616 } else {
617 var d Date
618 if err := d.decode(params, parts[1]); err != nil {
619 return ErrInvalidRecur
620 }
621 r.Until = d.Time
622 r.UntilTime = false
623 }
624 case "COUNT":
625 if !r.Until.IsZero() {
626 return ErrInvalidRecur
627 }
628 n, err := strconv.ParseUint(parts[1], 10, 64)
629 if err != nil {
630 return ErrInvalidRecur
631 }
632 r.Count = n
633 case "INTERVAL":
634 if r.Interval > 0 {
635 return ErrInvalidRecur
636 }
637 n, err := strconv.ParseUint(parts[1], 10, 64)
638 if err != nil {
639 return ErrInvalidRecur
640 }
641 r.Interval = n
642 case "BYSECOND":
643 if r.BySecond != nil {
644 return ErrInvalidRecur
645 }
646 seconds := strings.Split(parts[1], ",")
647 secondList := make([]uint8, len(seconds))
648 for n, second := range seconds {
649 i, err := strconv.ParseUint(second, 10, 8)
650 if err != nil || i > 60 {
651 return ErrInvalidRecur
652 }
653 secondList[n] = uint8(i)
654 }
655 r.BySecond = secondList
656 case "BYMINUTE":
657 if r.ByMinute != nil {
658 return ErrInvalidRecur
659 }
660 minutes := strings.Split(parts[1], ",")
661 minuteList := make([]uint8, len(minutes))
662 for n, minute := range minutes {
663 i, err := strconv.ParseUint(minute, 10, 8)
664 if err != nil || i > 59 {
665 return ErrInvalidRecur
666 }
667 minuteList[n] = uint8(i)
668 }
669 r.ByMinute = minuteList
670 case "BYHOUR":
671 if r.ByHour != nil {
672 return ErrInvalidRecur
673 }
674 hours := strings.Split(parts[1], ",")
675 hourList := make([]uint8, len(hours))
676 for n, hour := range hours {
677 i, err := strconv.ParseUint(hour, 10, 8)
678 if err != nil || i > 23 {
679 return ErrInvalidRecur
680 }
681 hourList[n] = uint8(i)
682 }
683 r.ByHour = hourList
684 case "BYDAY":
685 if r.ByDay != nil {
686 return ErrInvalidRecur
687 }
688 days := strings.Split(parts[1], ",")
689 dayList := make([]DayRecur, len(days))
690 for n, day := range days {
691 neg := false
692 numCheck := true
693 if len(day) < 2 {
694 return ErrInvalidRecur
695 }
696 if day[0] == '+' {
697 day = day[1:]
698 } else if day[0] == '-' {
699 neg = true
700 numCheck = false
701 day = day[1:]
702 if len(day) < 2 {
703 return ErrInvalidRecur
704 }
705 }
706 var num int8
707 if day[0] >= '0' && day[0] <= '9' {
708 numCheck = true
709 num = int8(day[0] - '0')
710 day = day[1:]
711 if day[0] >= '0' && day[0] <= '9' {
712 num *= 10
713 num += int8(day[0] - '0')
714 day = day[1:]
715 }
716 if num == 0 || num > 53 {
717 return ErrInvalidRecur
718 }
719 if neg {
720 num = -num
721 }
722 }
723 if !numCheck || len(day) != 2 {
724 return ErrInvalidRecur
725 }
726 switch day {
727 case "SU":
728 dayList[n].Day = Sunday
729 case "MO":
730 dayList[n].Day = Monday
731 case "TU":
732 dayList[n].Day = Tuesday
733 case "WE":
734 dayList[n].Day = Wednesday
735 case "TH":
736 dayList[n].Day = Thursday
737 case "FR":
738 dayList[n].Day = Friday
739 case "SA":
740 dayList[n].Day = Saturday
741 default:
742 return ErrInvalidRecur
743 }
744 dayList[n].Occurrence = num
745 }
746 r.ByDay = dayList
747 case "BYMONTHDAY":
748 if r.ByMonthDay != nil {
749 return ErrInvalidRecur
750 }
751 monthDays := strings.Split(parts[1], ",")
752 monthDayList := make([]int8, len(monthDays))
753 for n, monthDay := range monthDays {
754 i, err := strconv.ParseInt(monthDay, 10, 8)
755 if err != nil || i == 0 || i > 31 || i < -31 {
756 return ErrInvalidRecur
757 }
758 monthDayList[n] = int8(i)
759 }
760 r.ByMonthDay = monthDayList
761 case "BYYEARDAY":
762 if r.ByYearDay != nil {
763 return ErrInvalidRecur
764 }
765 yearDays := strings.Split(parts[1], ",")
766 yearDayList := make([]int16, len(yearDays))
767 for n, yearDay := range yearDays {
768 i, err := strconv.ParseInt(yearDay, 10, 16)
769 if err != nil || i == 0 || i > 366 || i < -366 {
770 return ErrInvalidRecur
771 }
772 yearDayList[n] = int16(i)
773 }
774 r.ByYearDay = yearDayList
775 case "BYWEEKNO":
776 if r.ByWeekNum != nil {
777 return ErrInvalidRecur
778 }
779 weekNums := strings.Split(parts[1], ",")
780 weekNumList := make([]int8, len(weekNums))
781 for n, weekNum := range weekNums {
782 i, err := strconv.ParseInt(weekNum, 10, 8)
783 if err != nil || i == 0 || i > 53 || i < -53 {
784 return ErrInvalidRecur
785 }
786 weekNumList[n] = int8(i)
787 }
788 r.ByWeekNum = weekNumList
789 case "BYMONTH":
790 if r.ByMonth != nil {
791 return ErrInvalidRecur
792 }
793 months := strings.Split(parts[1], ",")
794 monthList := make([]Month, len(months))
795 for n, month := range months {
796 i, err := strconv.ParseUint(month, 10, 8)
797 if err != nil || i == 0 || i > 12 {
798 return ErrInvalidRecur
799 }
800 monthList[n] = Month(i)
801 }
802 r.ByMonth = monthList
803 case "BYSETPOS":
804 if r.BySetPos != nil {
805 return ErrInvalidRecur
806 }
807 setPoss := strings.Split(parts[1], ",")
808 setPosList := make([]int16, len(setPoss))
809 for n, setPos := range setPoss {
810 i, err := strconv.ParseInt(setPos, 10, 16)
811 if err != nil || i == 0 || i > 366 || i < -366 {
812 return ErrInvalidRecur
813 }
814 setPosList[n] = int16(i)
815 }
816 r.BySetPos = setPosList
817 case "WKST":
818 if r.WeekStart != UnknownDay {
819 return ErrInvalidRecur
820 }
821 switch parts[1] {
822 case "SU":
823 r.WeekStart = Sunday
824 case "MO":
825 r.WeekStart = Monday
826 case "TU":
827 r.WeekStart = Tuesday
828 case "WE":
829 r.WeekStart = Wednesday
830 case "TH":
831 r.WeekStart = Thursday
832 case "FR":
833 r.WeekStart = Friday
834 case "SA":
835 r.WeekStart = Saturday
836 default:
837 return ErrInvalidRecur
838 }
839 default:
840 return ErrInvalidRecur
841 }
842 }
843 if !freq {
844 return ErrInvalidRecur
845 }
846 return nil
847 }
848
849 func (r *Recur) aencode(w writer) {
850 writeTimezone(w, r.Until)
851 w.WriteString(":")
852 r.encode(w)
853 }
854
855 func (r *Recur) encode(w writer) {
856 comma := []byte{','}
857 switch r.Frequency {
858 case Secondly:
859 w.WriteString("FREQ=SECONDLY")
860 case Minutely:
861 w.WriteString("FREQ=MINUTELY")
862 case Hourly:
863 w.WriteString("FREQ=HOURLY")
864 case Daily:
865 w.WriteString("FREQ=DAILY")
866 case Weekly:
867 w.WriteString("FREQ=WEEKLY")
868 case Monthly:
869 w.WriteString("FREQ=MONTHLY")
870 case Yearly:
871 w.WriteString("FREQ=YEARLY")
872 default:
873 w.WriteString("FREQ=SECONDLY")
874 }
875 if r.Count != 0 {
876 w.WriteString(";COUNT=")
877 w.WriteString(strconv.FormatUint(r.Count, 10))
878 } else if !r.Until.IsZero() {
879 w.WriteString(";UNTIL=")
880 if r.UntilTime {
881 d := DateTime{r.Until}
882 d.encode(w)
883 } else {
884 d := Date{r.Until}
885 d.encode(w)
886 }
887 }
888 if r.Interval != 0 {
889 w.WriteString(";INTERVAL=")
890 w.WriteString(strconv.FormatUint(r.Interval, 10))
891 }
892 if len(r.BySecond) > 0 {
893 w.WriteString(";BYSECOND=")
894 for n, second := range r.BySecond {
895 if n > 0 {
896 w.Write(comma)
897 }
898 w.WriteString(strconv.FormatUint(uint64(second), 10))
899 }
900 }
901 if len(r.ByMinute) > 0 {
902 w.WriteString(";BYMINUTE=")
903 for n, minute := range r.ByMinute {
904 if n > 0 {
905 w.Write(comma)
906 }
907 w.WriteString(strconv.FormatUint(uint64(minute), 10))
908 }
909 }
910 if len(r.ByHour) > 0 {
911 w.WriteString(";BYHOUR=")
912 for n, hour := range r.ByHour {
913 if n > 0 {
914 w.Write(comma)
915 }
916 w.WriteString(strconv.FormatUint(uint64(hour), 10))
917 }
918 }
919 if len(r.ByDay) > 0 {
920 w.WriteString(";BYDAY=")
921 for n, day := range r.ByDay {
922 if n > 0 {
923 w.Write(comma)
924 }
925 if day.Occurrence != 0 {
926 w.WriteString(strconv.FormatInt(int64(day.Occurrence), 10))
927 }
928 switch day.Day {
929 case Sunday:
930 w.Write([]byte{'S', 'U'})
931 case Monday:
932 w.Write([]byte{'M', 'O'})
933 case Tuesday:
934 w.Write([]byte{'T', 'U'})
935 case Wednesday:
936 w.Write([]byte{'W', 'E'})
937 case Thursday:
938 w.Write([]byte{'T', 'H'})
939 case Friday:
940 w.Write([]byte{'F', 'R'})
941 case Saturday:
942 w.Write([]byte{'S', 'A'})
943 }
944 }
945 }
946 if len(r.ByMonthDay) > 0 {
947 w.WriteString(";BYMONTHDAY=")
948 for n, month := range r.ByMonthDay {
949 if n > 0 {
950 w.Write(comma)
951 }
952 w.WriteString(strconv.FormatInt(int64(month), 10))
953 }
954 }
955 if len(r.ByYearDay) > 0 {
956 w.WriteString(";BYYEARDAY=")
957 for n, year := range r.ByYearDay {
958 if n > 0 {
959 w.Write(comma)
960 }
961 w.WriteString(strconv.FormatInt(int64(year), 10))
962 }
963 }
964 if len(r.ByWeekNum) > 0 {
965 w.WriteString(";BYWEEKNO=")
966 for n, week := range r.ByWeekNum {
967 if n > 0 {
968 w.Write(comma)
969 }
970 w.WriteString(strconv.FormatInt(int64(week), 10))
971 }
972 }
973 if len(r.ByMonth) > 0 {
974 w.WriteString(";BYMONTH=")
975 for n, month := range r.ByMonth {
976 if n > 0 {
977 w.Write(comma)
978 }
979 w.WriteString(strconv.FormatUint(uint64(month), 10))
980 }
981 }
982 if len(r.BySetPos) > 0 {
983 w.WriteString(";BYSETPOS=")
984 for n, setPos := range r.BySetPos {
985 if n > 0 {
986 w.Write(comma)
987 }
988 w.WriteString(strconv.FormatInt(int64(setPos), 10))
989 }
990 }
991 if r.WeekStart != UnknownDay {
992 w.WriteString(";WKST=")
993 switch r.WeekStart {
994 case Sunday:
995 w.Write([]byte{'S', 'U'})
996 case Monday:
997 w.Write([]byte{'M', 'O'})
998 case Tuesday:
999 w.Write([]byte{'T', 'U'})
1000 case Wednesday:
1001 w.Write([]byte{'W', 'E'})
1002 case Thursday:
1003 w.Write([]byte{'T', 'H'})
1004 case Friday:
1005 w.Write([]byte{'F', 'R'})
1006 case Saturday:
1007 w.Write([]byte{'S', 'A'})
1008 }
1009 }
1010 }
1011
1012 func (r *Recur) valid() error {
1013 switch r.Frequency {
1014 case Secondly, Minutely, Hourly, Daily, Weekly, Monthly, Yearly:
1015 default:
1016 return ErrInvalidRecurFrequency
1017 }
1018 for _, second := range r.BySecond {
1019 if second > 60 {
1020 return ErrInvalidRecurBySecond
1021 }
1022 }
1023 for _, minute := range r.ByMinute {
1024 if minute > 59 {
1025 return ErrInvalidRecurByMinute
1026 }
1027 }
1028 for _, hour := range r.ByHour {
1029 if hour > 23 {
1030 return ErrInvalidRecurByHour
1031 }
1032 }
1033 for _, day := range r.ByDay {
1034 switch day.Day {
1035 case Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday:
1036 default:
1037 return ErrInvalidRecurByDay
1038 }
1039 }
1040 for _, monthDay := range r.ByMonthDay {
1041 if monthDay == 0 || monthDay > 31 || monthDay < -31 {
1042 return ErrInvalidRecurByMonthDay
1043 }
1044 }
1045 for _, yearDay := range r.ByYearDay {
1046 if yearDay == 0 || yearDay > 366 || yearDay < -366 {
1047 return ErrInvalidRecurByYearDay
1048 }
1049 }
1050 for _, week := range r.ByWeekNum {
1051 if week == 0 || week > 53 || week < -53 {
1052 return ErrInvalidRecurByWeekNum
1053 }
1054 }
1055 for _, month := range r.ByMonth {
1056 if month == 0 || month > 12 {
1057 return ErrInvalidRecurByMonth
1058 }
1059 }
1060 for _, setPos := range r.BySetPos {
1061 if setPos == 0 || setPos > 366 || setPos < -366 {
1062 return ErrInvalidRecurBySetPos
1063 }
1064 }
1065 if r.WeekStart != UnknownDay {
1066 switch r.WeekStart {
1067 case Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday:
1068 default:
1069 return ErrInvalidRecurWeekStart
1070 }
1071 }
1072 return nil
1073 }
1074
1075 // Text contains human-readable text
1076 type Text string
1077
1078 func (t *Text) decode(_ map[string]string, data string) error {
1079 st := parser.NewStringTokeniser(data)
1080 *t = Text(decodeText(st))
1081 if st.Peek() != -1 {
1082 return ErrInvalidText
1083 }
1084 return nil
1085 }
1086
1087 func decodeText(t parser.Tokeniser) string {
1088 var d []byte
1089 var ru [4]byte
1090 Loop:
1091 for {
1092 c := t.ExceptRun("\\,;")
1093 d = append(d, t.Get()...)
1094 switch c {
1095 case '\\':
1096 t.Accept("\\")
1097 switch c := t.Peek(); c {
1098 case '\\':
1099 d = append(d, '\\')
1100 case ';':
1101 d = append(d, ';')
1102 case ',':
1103 d = append(d, ',')
1104 case 'N', 'n':
1105 d = append(d, '\n')
1106 default:
1107 d = append(d, '\\')
1108 l := utf8.EncodeRune(ru[:], c)
1109 d = append(d, ru[:l]...)
1110 }
1111 t.Except("")
1112 t.Get()
1113 default:
1114 break Loop
1115 }
1116 }
1117 return string(d)
1118 }
1119
1120 func (t *Text) aencode(w writer) {
1121 w.WriteString(":")
1122 t.encode(w)
1123 }
1124
1125 func (t Text) encode(w writer) {
1126 d := make([]byte, 0, len(t)+256)
1127 var ru [4]byte
1128 for _, c := range t {
1129 switch c {
1130 case '\\':
1131 d = append(d, '\\', '\\')
1132 case '\n':
1133 d = append(d, '\\', 'n')
1134 case ';':
1135 d = append(d, '\\', ';')
1136 case ',':
1137 d = append(d, '\\', ',')
1138 default:
1139 l := utf8.EncodeRune(ru[:], c)
1140 d = append(d, ru[:l]...)
1141 }
1142 }
1143 w.Write(d)
1144 }
1145
1146 func (t Text) valid() error {
1147 if strings.ContainsAny(string(t), nonsafeChars[:31]) {
1148 return ErrInvalidText
1149 }
1150 return nil
1151 }
1152
1153 // MText contains multiple text values
1154 type MText []Text
1155
1156 func (t *MText) decode(_ map[string]string, data string) error {
1157 st := parser.NewStringTokeniser(data)
1158 Loop:
1159 for {
1160 *t = append(*t, Text(decodeText(st)))
1161 switch st.Peek() {
1162 case -1:
1163 break Loop
1164 case ',':
1165 st.Accept(",")
1166 st.Get()
1167 default:
1168 return ErrInvalidText
1169 }
1170 }
1171 return nil
1172 }
1173
1174 func (t MText) aencode(w writer) {
1175 w.WriteString(":")
1176 t.encode(w)
1177 }
1178
1179 func (t MText) encode(w writer) {
1180 for n, tx := range t {
1181 if n > 0 {
1182 w.WriteString(",")
1183 }
1184 tx.encode(w)
1185 }
1186 }
1187
1188 func (t MText) valid() error {
1189 for _, m := range t {
1190 if err := m.valid(); err != nil {
1191 return err
1192 }
1193 }
1194 return nil
1195 }
1196
1197 // Time contains a precise time
1198 type Time struct {
1199 time.Time
1200 }
1201
1202 func (t *Time) decode(params map[string]string, data string) error {
1203 if tz, ok := params["TZID"]; ok {
1204 l, err := time.LoadLocation(tz)
1205 if err != nil {
1206 return fmt.Errorf("error loading timezone for Time: %w", err)
1207 }
1208 ct, err := time.ParseInLocation(dateTimeFormat[9:15], data, l)
1209 if err != nil {
1210 return fmt.Errorf("error parsing time in location for Time: %w", err)
1211 }
1212 t.Time = ct
1213 } else if len(data) > 0 && data[len(data)-1] == 'Z' {
1214 ct, err := time.ParseInLocation(dateTimeFormat[9:], data, time.UTC)
1215 if err != nil {
1216 return fmt.Errorf("error parsing time in UTC for Time: %w", err)
1217 }
1218 t.Time = ct
1219 } else {
1220 ct, err := time.ParseInLocation(dateTimeFormat[9:15], data, time.Local)
1221 if err != nil {
1222 return fmt.Errorf("error parsing local time for Time: %w", err)
1223 }
1224 t.Time = ct
1225 }
1226 return nil
1227 }
1228
1229 func (t Time) aencode(w writer) {
1230 writeTimezone(w, t.Time)
1231 w.WriteString(":")
1232 t.encode(w)
1233 }
1234
1235 func (t Time) encode(w writer) {
1236 b := make([]byte, 0, 7)
1237 switch t.Location() {
1238 case time.UTC:
1239 b = t.AppendFormat(b, dateTimeFormat[9:])
1240 case time.Local:
1241 b = t.AppendFormat(b, dateTimeFormat[9:15])
1242 default:
1243 b = t.AppendFormat(b, dateTimeFormat[9:15])
1244 }
1245 w.Write(b)
1246 }
1247
1248 func (t Time) valid() error {
1249 if t.IsZero() {
1250 return ErrInvalidTime
1251 }
1252 return nil
1253 }
1254
1255 // URI contains a reference to another piece of data
1256 type URI struct {
1257 url.URL
1258 }
1259
1260 func (u *URI) decode(_ map[string]string, data string) error {
1261 cu, err := url.Parse(data)
1262 if err != nil {
1263 return fmt.Errorf("error decoding URI: %w", err)
1264 }
1265 u.URL = *cu
1266 return nil
1267 }
1268
1269 func (u URI) aencode(w writer) {
1270 w.WriteString(":")
1271 u.encode(w)
1272 }
1273
1274 func (u URI) encode(w writer) {
1275 w.WriteString(u.URL.String())
1276 }
1277
1278 func (URI) valid() error {
1279 return nil
1280 }
1281
1282 // UTCOffset contains the offset from UTC to local time
1283 type UTCOffset int
1284
1285 func (u *UTCOffset) decode(_ map[string]string, data string) error {
1286 t := parser.NewStringTokeniser(data)
1287 neg := false
1288 if t.Accept("-") {
1289 neg = true
1290 } else {
1291 t.Accept("+")
1292 }
1293 t.Get()
1294 if !t.Accept("0123456789") || !t.Accept("0123456789") {
1295 return ErrInvalidOffset
1296 }
1297 h, _ := strconv.ParseInt(t.Get(), 10, 32)
1298 if !t.Accept("0123456789") || !t.Accept("0123456789") {
1299 return ErrInvalidOffset
1300 }
1301 m, _ := strconv.ParseInt(t.Get(), 10, 32)
1302 if m >= 60 {
1303 return ErrInvalidOffset
1304 }
1305 var s int64
1306 if t.Accept("0123456789") {
1307 if !t.Accept("0123456789") || t.Peek() != -1 {
1308 return ErrInvalidOffset
1309 }
1310 s, _ = strconv.ParseInt(t.Get(), 10, 32)
1311 if s >= 60 {
1312 return ErrInvalidOffset
1313 }
1314 } else if t.Peek() != -1 {
1315 return ErrInvalidOffset
1316 }
1317 *u = UTCOffset(3600*h + 60*m + s)
1318 if neg {
1319 if *u == 0 {
1320 return ErrInvalidOffset
1321 }
1322 *u = -(*u)
1323 }
1324 return nil
1325 }
1326 func (u UTCOffset) aencode(w writer) {
1327 w.WriteString(":")
1328 u.encode(w)
1329 }
1330
1331 func (u UTCOffset) encode(w writer) {
1332 o := int64(u)
1333 b := make([]byte, 0, 7)
1334 if o < 0 {
1335 b = append(b, '-')
1336 o = -o
1337 }
1338 s := byte(o % 60)
1339 o /= 60
1340 m := byte(o % 60)
1341 h := byte(o / 60)
1342 if h > 99 {
1343 h = 0
1344 }
1345 b = append(b, '0'+h/10)
1346 b = append(b, '0'+h%10)
1347 b = append(b, '0'+m/10)
1348 b = append(b, '0'+m%10)
1349 if s > 0 {
1350 b = append(b, '0'+s/10)
1351 b = append(b, '0'+s%10)
1352 }
1353 w.Write(b)
1354 }
1355
1356 func (UTCOffset) valid() error {
1357 return nil
1358 }
1359
1360 func writeTimezone(w writer, t time.Time) {
1361 switch l := t.Location(); l {
1362 case time.UTC, time.Local:
1363 default:
1364 w.WriteString(";TZID=")
1365 w.WriteString(l.String())
1366 }
1367 }
1368
1369 // Errors
1370 var (
1371 ErrInvalidEncoding = errors.New("invalid Binary encoding")
1372 ErrInvalidPeriod = errors.New("invalid Period")
1373 ErrInvalidDuration = errors.New("invalid Duration")
1374 ErrInvalidText = errors.New("invalid encoded text")
1375 ErrInvalidBoolean = errors.New("invalid Boolean")
1376 ErrInvalidOffset = errors.New("invalid UTC Offset")
1377 ErrInvalidRecur = errors.New("invalid Recur")
1378 ErrInvalidTime = errors.New("invalid Time")
1379 ErrInvalidFloat = errors.New("invalid float")
1380 ErrInvalidTFloat = errors.New("invalid number of floats")
1381 ErrInvalidPeriodStart = errors.New("invalid start of Period")
1382 ErrInvalidPeriodDuration = errors.New("invalid Period duration")
1383 ErrInvalidPeriodEnd = errors.New("invalid end of Period")
1384 ErrInvalidRecurFrequency = errors.New("invalid Recur frequency")
1385 ErrInvalidRecurBySecond = errors.New("invalid Recur BySecond")
1386 ErrInvalidRecurByMinute = errors.New("invalid Recur ByMinute")
1387 ErrInvalidRecurByHour = errors.New("invalid Recur ByHour")
1388 ErrInvalidRecurByDay = errors.New("invalid Recur ByDay")
1389 ErrInvalidRecurByMonthDay = errors.New("invalid Recur ByMonthDay")
1390 ErrInvalidRecurByYearDay = errors.New("invalid Recur ByYearDay")
1391 ErrInvalidRecurByWeekNum = errors.New("invalid Recur ByWeekNum")
1392 ErrInvalidRecurByMonth = errors.New("invalid Recur ByMonth")
1393 ErrInvalidRecurBySetPos = errors.New("invalid Recur BySetPos")
1394 ErrInvalidRecurWeekStart = errors.New("invalid Recur WeekStart")
1395 )
1396
1397 const (
1398 cBinary = "Binary"
1399 cCalendarAddress = "CalendarAddress"
1400 cDate = "Date"
1401 cDateTime = "DateTime"
1402 cMText = "MText"
1403 cPeriod = "Period"
1404 cText = "Text"
1405 cAlarm = "Alarm"
1406 )