1 /* Copyright (C) 2014-2020 Greenbone Networks GmbH
2  *
3  * SPDX-License-Identifier: AGPL-3.0-or-later
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Affero General Public License as
7  * published by the Free Software Foundation, either version 3 of the
8  * License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU Affero General Public License for more details.
14  *
15  * You should have received a copy of the GNU Affero General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 /**
20  * @file manage_utils.c
21  * @brief Module for Greenbone Vulnerability Manager: Manage library utilities.
22  */
23 
24 #include "manage_utils.h"
25 
26 #include <assert.h> /* for assert */
27 #include <ctype.h>
28 #include <stdlib.h> /* for getenv */
29 #include <stdio.h>  /* for sscanf */
30 #include <string.h> /* for strcmp */
31 
32 #include <gvm/base/hosts.h>
33 #include <gvm/util/uuidutils.h>
34 
35 #undef G_LOG_DOMAIN
36 /**
37  * @brief GLib log domain.
38  */
39 #define G_LOG_DOMAIN "md  utils"
40 
41 /**
42  * @brief Number of seconds in a day.
43  */
44 #define SECS_PER_DAY 86400
45 
46 /**
47  * @file  manage_utils.c
48  * @brief The Greenbone Vulnerability Manager management library.
49  *
50  * Utilities used by the manage library that do not depend on anything.
51  */
52 
53 /**
54  * @brief Get the current offset from UTC of a timezone.
55  *
56  * @param[in]  zone  Timezone, or NULL for UTC.
57  *
58  * @return Seconds east of UTC.
59  */
60 long
current_offset(const char * zone)61 current_offset (const char *zone)
62 {
63   gchar *tz;
64   long offset;
65   time_t now;
66   struct tm now_broken;
67 
68   if (zone == NULL)
69     return 0;
70 
71   /* Store current TZ. */
72   tz = getenv ("TZ") ? g_strdup (getenv ("TZ")) : NULL;
73 
74   if (setenv ("TZ", zone, 1) == -1)
75     {
76       g_warning ("%s: Failed to switch to timezone", __func__);
77       if (tz != NULL)
78         setenv ("TZ", tz, 1);
79       g_free (tz);
80       return 0;
81     }
82 
83   tzset ();
84 
85   time (&now);
86   if (localtime_r (&now, &now_broken) == NULL)
87     {
88       g_warning ("%s: localtime failed", __func__);
89       if (tz != NULL)
90         setenv ("TZ", tz, 1);
91       g_free (tz);
92       return 0;
93     }
94   if (setenv ("TZ", "UTC", 1) == -1)
95     {
96       g_warning ("%s: Failed to switch to UTC", __func__);
97       if (tz != NULL)
98         setenv ("TZ", tz, 1);
99       g_free (tz);
100       return 0;
101     }
102   tzset ();
103   offset = - (now - mktime (&now_broken));
104 
105   /* Revert to stored TZ. */
106   if (tz)
107     {
108       if (setenv ("TZ", tz, 1) == -1)
109         {
110           g_warning ("%s: Failed to switch to original TZ", __func__);
111           g_free (tz);
112           return 0;
113         }
114     }
115   else
116     unsetenv ("TZ");
117 
118   g_free (tz);
119   return offset;
120 }
121 
122 /**
123  * @brief Add months to a time.
124  *
125  * @param[in]  time    Time.
126  * @param[in]  months  Months.
127  *
128  * @return Time plus given number of months.
129  */
130 time_t
add_months(time_t time,int months)131 add_months (time_t time, int months)
132 {
133   struct tm broken;
134 
135   if (localtime_r (&time, &broken) == NULL)
136     {
137       g_warning ("%s: localtime failed", __func__);
138       return 0;
139     }
140   broken.tm_mon += months;
141   return mktime (&broken);
142 }
143 
144 /**
145  * @brief Return number of hosts described by a hosts string.
146  *
147  * @param[in]  given_hosts      String describing hosts.
148  * @param[in]  exclude_hosts    String describing hosts excluded from given set.
149  * @param[in]  max_hosts        Max hosts.
150  *
151  * @return Number of hosts, or -1 on error.
152  */
153 int
manage_count_hosts_max(const char * given_hosts,const char * exclude_hosts,int max_hosts)154 manage_count_hosts_max (const char *given_hosts, const char *exclude_hosts,
155                         int max_hosts)
156 {
157   int count;
158   gvm_hosts_t *hosts;
159   gchar *clean_hosts;
160 
161   clean_hosts = clean_hosts_string (given_hosts);
162 
163   hosts = gvm_hosts_new_with_max (clean_hosts, max_hosts);
164   if (hosts == NULL)
165     {
166       g_free (clean_hosts);
167       return -1;
168     }
169 
170   if (exclude_hosts)
171     {
172       gchar *clean_exclude_hosts;
173 
174       clean_exclude_hosts = clean_hosts_string (exclude_hosts);
175       if (gvm_hosts_exclude_with_max (hosts,
176                                       clean_exclude_hosts,
177                                       max_hosts)
178           < 0)
179         {
180           g_free (clean_hosts);
181           g_free (clean_exclude_hosts);
182           return -1;
183         }
184       g_free (clean_exclude_hosts);
185     }
186 
187   count = gvm_hosts_count (hosts);
188   gvm_hosts_free (hosts);
189   g_free (clean_hosts);
190 
191   return count;
192 }
193 
194 /**
195  * @brief Get the minimum severity for a severity level.
196  *
197  * This function has a database equivalent in manage_pg_server.c.
198  * These two functions must stay in sync.
199  *
200  * @param[in] level  The name of the severity level.
201  *
202  * @return The minimum severity.
203  */
204 double
level_min_severity(const char * level)205 level_min_severity (const char *level)
206 {
207   if (strcasecmp (level, "Log") == 0)
208     return SEVERITY_LOG;
209   else if (strcasecmp (level, "False Positive") == 0)
210     return SEVERITY_FP;
211   else if (strcasecmp (level, "Error") == 0)
212     return SEVERITY_ERROR;
213 
214   if (strcasecmp (level, "high") == 0)
215     return 7.0;
216   else if (strcasecmp (level, "medium") == 0)
217     return 4.0;
218   else if (strcasecmp (level, "low") == 0)
219     return 0.1;
220   else
221     return SEVERITY_UNDEFINED;
222 }
223 
224 /**
225  * @brief Get the maximum severity for a severity level.
226  *
227  * This function has a database equivalent in manage_pg_server.c.
228  * These two functions must stay in sync.
229  *
230  * @param[in] level  The name of the severity level.
231  *
232  * @return The maximunm severity.
233  */
234 double
level_max_severity(const char * level)235 level_max_severity (const char *level)
236 {
237   if (strcasecmp (level, "Log") == 0)
238     return SEVERITY_LOG;
239   else if (strcasecmp (level, "False Positive") == 0)
240     return SEVERITY_FP;
241   else if (strcasecmp (level, "Error") == 0)
242     return SEVERITY_ERROR;
243 
244   if (strcasecmp (level, "high") == 0)
245     return 10.0;
246   else if (strcasecmp (level, "medium") == 0)
247     return 6.9;
248   else if (strcasecmp (level, "low") == 0)
249     return 3.9;
250   else
251     return SEVERITY_UNDEFINED;
252 }
253 
254 /**
255  * @brief Returns whether a host has an equal host in a hosts string.
256  *
257  * For example, 192.168.10.1 has an equal in a hosts string
258  * "192.168.10.1-5, 192.168.10.10-20" string while 192.168.10.7 doesn't.
259  *
260  * @param[in] hosts_str      Hosts string to check.
261  * @param[in] find_host_str  The host to find.
262  * @param[in] max_hosts      Maximum number of hosts allowed in hosts_str.
263  *
264  * @return 1 if host has equal in hosts_str, 0 otherwise.
265  */
266 int
hosts_str_contains(const char * hosts_str,const char * find_host_str,int max_hosts)267 hosts_str_contains (const char* hosts_str, const char* find_host_str,
268                     int max_hosts)
269 {
270   gvm_hosts_t *hosts, *find_hosts;
271 
272   hosts = gvm_hosts_new_with_max (hosts_str, max_hosts);
273   find_hosts = gvm_hosts_new_with_max (find_host_str, 1);
274 
275   if (hosts == NULL || find_hosts == NULL || find_hosts->count != 1)
276     {
277       gvm_hosts_free (hosts);
278       gvm_hosts_free (find_hosts);
279       return 0;
280     }
281 
282   int ret = gvm_host_in_hosts (find_hosts->hosts[0], NULL, hosts);
283   gvm_hosts_free (hosts);
284   gvm_hosts_free (find_hosts);
285   return ret;
286 }
287 
288 /**
289  * @brief Check whether a resource type table name is valid.
290  *
291  * @param[in]  type  Type of resource.
292  *
293  * @return 1 yes, 0 no.
294  */
295 int
valid_db_resource_type(const char * type)296 valid_db_resource_type (const char* type)
297 {
298   if (type == NULL)
299     return 0;
300 
301   return (strcasecmp (type, "alert") == 0)
302          || (strcasecmp (type, "config") == 0)
303          || (strcasecmp (type, "cpe") == 0)
304          || (strcasecmp (type, "credential") == 0)
305          || (strcasecmp (type, "cve") == 0)
306          || (strcasecmp (type, "cert_bund_adv") == 0)
307          || (strcasecmp (type, "dfn_cert_adv") == 0)
308          || (strcasecmp (type, "filter") == 0)
309          || (strcasecmp (type, "group") == 0)
310          || (strcasecmp (type, "host") == 0)
311          || (strcasecmp (type, "os") == 0)
312          || (strcasecmp (type, "note") == 0)
313          || (strcasecmp (type, "nvt") == 0)
314          || (strcasecmp (type, "ovaldef") == 0)
315          || (strcasecmp (type, "override") == 0)
316          || (strcasecmp (type, "port_list") == 0)
317          || (strcasecmp (type, "permission") == 0)
318          || (strcasecmp (type, "report") == 0)
319          || (strcasecmp (type, "report_format") == 0)
320          || (strcasecmp (type, "result") == 0)
321          || (strcasecmp (type, "role") == 0)
322          || (strcasecmp (type, "scanner") == 0)
323          || (strcasecmp (type, "schedule") == 0)
324          || (strcasecmp (type, "tag") == 0)
325          || (strcasecmp (type, "target") == 0)
326          || (strcasecmp (type, "task") == 0)
327          || (strcasecmp (type, "ticket") == 0)
328          || (strcasecmp (type, "tls_certificate") == 0)
329          || (strcasecmp (type, "user") == 0);
330 }
331 
332 /** @brief Replace any control characters in string with spaces.
333  *
334  * @param[in,out]  string  String to replace in.
335  */
336 void
blank_control_chars(char * string)337 blank_control_chars (char *string)
338 {
339   for (; *string; string++)
340     if (iscntrl (*string) && *string != '\n') *string = ' ';
341 }
342 
343 /**
344  * @brief GVM product ID.
345  */
346 #define GVM_PRODID "-//Greenbone.net//NONSGML Greenbone Security Manager " \
347                    GVMD_VERSION "//EN"
348 
349 /**
350  * @brief Try to get a built-in libical timezone from a tzid or city name.
351  *
352  * @param[in]  tzid  The tzid or Olson city name.
353  *
354  * @return The built-in timezone if found, else NULL.
355  */
356 icaltimezone*
icalendar_timezone_from_string(const char * tzid)357 icalendar_timezone_from_string (const char *tzid)
358 {
359   if (tzid)
360     {
361       icaltimezone *tz;
362 
363       tz = icaltimezone_get_builtin_timezone_from_tzid (tzid);
364       if (tz == NULL)
365         tz = icaltimezone_get_builtin_timezone (tzid);
366       return tz;
367     }
368 
369   return NULL;
370 }
371 
372 /**
373  * @brief Create an iCalendar component from old schedule data.
374  *
375  * @param[in]  first_time     The first run time.
376  * @param[in]  period         The period in seconds.
377  * @param[in]  period_months  The period in months.
378  * @param[in]  duration       The duration in seconds.
379  * @param[in]  byday_mask     The byday mask.
380  *
381  * @return  The generated iCalendar component.
382  */
383 icalcomponent *
icalendar_from_old_schedule_data(time_t first_time,time_t period,time_t period_months,time_t duration,int byday_mask)384 icalendar_from_old_schedule_data (time_t first_time,
385                                   time_t period, time_t period_months,
386                                   time_t duration,
387                                   int byday_mask)
388 {
389   gchar *uid;
390   icalcomponent *ical_new, *vevent;
391   icaltimetype dtstart, dtstamp;
392   int has_recurrence;
393   struct icalrecurrencetype recurrence;
394   struct icaldurationtype ical_duration;
395 
396   // Setup base calendar component
397   ical_new = icalcomponent_new_vcalendar ();
398   icalcomponent_add_property (ical_new, icalproperty_new_version ("2.0"));
399   icalcomponent_add_property (ical_new,
400                               icalproperty_new_prodid (GVM_PRODID));
401 
402   // Create event component
403   vevent = icalcomponent_new_vevent ();
404   icalcomponent_add_component (ical_new, vevent);
405 
406   // Generate UID for event
407   uid = gvm_uuid_make ();
408   icalcomponent_set_uid (vevent, uid);
409   g_free (uid);
410   uid = NULL;
411 
412   // Set timestamp
413   dtstamp = icaltime_current_time_with_zone (icaltimezone_get_utc_timezone ());
414   icalcomponent_set_dtstamp (vevent, dtstamp);
415 
416   // Get timezone and set first start time
417   dtstart = icaltime_from_timet_with_zone (first_time, 0,
418                                            icaltimezone_get_utc_timezone ());
419 
420   icalcomponent_set_dtstart (vevent, dtstart);
421 
422   // Get recurrence rule if applicable
423   icalrecurrencetype_clear (&recurrence);
424   if (period_months)
425     {
426       if (period_months % 12 == 0)
427         {
428           recurrence.freq = ICAL_YEARLY_RECURRENCE;
429           recurrence.interval = period_months / 12;
430         }
431       else
432         {
433           recurrence.freq = ICAL_MONTHLY_RECURRENCE;
434           recurrence.interval = period_months;
435         }
436       has_recurrence = 1;
437     }
438   else if (period)
439     {
440       if (period % 604800 == 0)
441         {
442           recurrence.freq = ICAL_WEEKLY_RECURRENCE;
443           recurrence.interval = period / 604800;
444         }
445       else if (period % 86400 == 0)
446         {
447           recurrence.freq = ICAL_DAILY_RECURRENCE;
448           recurrence.interval = period / 86400;
449         }
450       else if (period % 3600 == 0)
451         {
452           recurrence.freq = ICAL_HOURLY_RECURRENCE;
453           recurrence.interval = period / 3600;
454         }
455       else if (period % 60 == 0)
456         {
457           recurrence.freq = ICAL_MINUTELY_RECURRENCE;
458           recurrence.interval = period / 60;
459         }
460       else
461         {
462           recurrence.freq = ICAL_SECONDLY_RECURRENCE;
463           recurrence.interval = period;
464         }
465 
466       has_recurrence = 1;
467     }
468   else
469     has_recurrence = 0;
470 
471   // Add set by_day and add the RRULE if applicable
472   if (has_recurrence)
473     {
474       if (byday_mask)
475         {
476           int ical_day, array_pos;
477 
478           // iterate over libical days starting at 1 for Sunday.
479           array_pos = 0;
480           for (ical_day = 1; ical_day <= 7; ical_day++)
481             {
482               int mask_bit;
483               // Convert to GVM byday mask bit index starting at 0 for Monday.
484               mask_bit = (ical_day == 1) ? 1 : (ical_day - 2);
485               if (byday_mask & (1 << mask_bit))
486                 {
487                   recurrence.by_day[array_pos] = ical_day;
488                   array_pos ++;
489                 }
490             }
491         }
492 
493       icalcomponent_add_property (vevent,
494                                   icalproperty_new_rrule (recurrence));
495     }
496 
497   // Add duration
498   if (duration)
499     {
500       ical_duration = icaldurationtype_from_int (duration);
501       icalcomponent_set_duration (vevent, ical_duration);
502     }
503 
504   return ical_new;
505 }
506 
507 /**
508  * @brief Simplify an VEVENT iCal component.
509  *
510  * @param[in]  vevent          The VEVENT component to simplify.
511  * @param[in]  zone            Timezone.
512  * @param[out] error           Output of iCal errors or warnings.
513  * @param[out] warnings_buffer GString buffer to write warnings to.
514  *
515  * @return  A newly allocated, simplified VEVENT component.
516  */
517 static icalcomponent *
icalendar_simplify_vevent(icalcomponent * vevent,icaltimezone * zone,gchar ** error,GString * warnings_buffer)518 icalendar_simplify_vevent (icalcomponent *vevent, icaltimezone *zone,
519                            gchar **error, GString *warnings_buffer)
520 {
521   icalproperty *error_prop;
522   gchar *uid;
523   icalcomponent *vevent_simplified;
524   icaltimetype original_dtstart, dtstart, dtstamp;
525   struct icaldurationtype duration;
526   icalproperty *rrule_prop, *rdate_prop, *exdate_prop, *exrule_prop;
527 
528   // Only handle VEVENT components
529   assert (icalcomponent_isa (vevent) == ICAL_VEVENT_COMPONENT);
530 
531   // Check for errors
532   icalrestriction_check (vevent);
533   error_prop = icalcomponent_get_first_property (vevent,
534                                                  ICAL_XLICERROR_PROPERTY);
535   if (error_prop)
536     {
537       if (error)
538         *error = g_strdup_printf ("Error in VEVENT: %s",
539                                   icalproperty_get_xlicerror (error_prop));
540       return NULL;
541     }
542 
543   // Get mandatory first start time
544   original_dtstart = icalcomponent_get_dtstart (vevent);
545   if (icaltime_is_null_time (original_dtstart))
546     {
547       if (error)
548         *error = g_strdup_printf ("VEVENT must have a dtstart property");
549       return NULL;
550     }
551 
552   dtstart = icaltime_convert_to_zone (original_dtstart, zone);
553 
554   // Get duration or try to calculate it from end time
555   duration = icalcomponent_get_duration (vevent);
556   if (icaldurationtype_is_null_duration (duration))
557     {
558       icaltimetype original_dtend;
559       original_dtend = icalcomponent_get_dtend (vevent);
560 
561       if (icaltime_is_null_time (original_dtend))
562         {
563           duration = icaldurationtype_null_duration ();
564         }
565       else
566         {
567           icaltimetype dtend_zone;
568 
569           dtend_zone = icaltime_convert_to_zone (original_dtend, zone);
570 
571           duration = icaltime_subtract (dtend_zone, dtstart);
572         }
573     }
574 
575   /*
576    * Try to get only the first recurrence rule and ignore any others.
577    * Technically there can be multiple ones but behavior is undefined in
578    *  the iCalendar specification.
579    */
580   rrule_prop = icalcomponent_get_first_property (vevent,
581                                                  ICAL_RRULE_PROPERTY);
582 
583   // Warn about EXRULE being deprecated
584   exrule_prop = icalcomponent_get_first_property (vevent,
585                                                   ICAL_EXRULE_PROPERTY);
586   if (exrule_prop)
587     {
588       g_string_append_printf (warnings_buffer,
589                               "<warning>"
590                               "VEVENT contains the deprecated EXRULE property,"
591                               " which will be ignored."
592                               "</warning>");
593     }
594 
595   // Create new, simplified VEVENT from collected data.
596   vevent_simplified = icalcomponent_new_vevent ();
597   icalcomponent_set_dtstart (vevent_simplified, dtstart);
598   icalcomponent_set_duration (vevent_simplified, duration);
599   if (rrule_prop)
600     {
601       icalproperty *prop_clone = icalproperty_new_clone (rrule_prop);
602       icalcomponent_add_property (vevent_simplified, prop_clone);
603     }
604 
605   // Simplify and copy RDATE properties
606   rdate_prop = icalcomponent_get_first_property (vevent,
607                                                  ICAL_RDATE_PROPERTY);
608   while (rdate_prop)
609     {
610       struct icaldatetimeperiodtype old_datetimeperiod, new_datetimeperiod;
611       icalproperty *new_rdate;
612 
613       old_datetimeperiod = icalproperty_get_rdate (rdate_prop);
614 
615       // Reduce period to a simple date or datetime.
616       new_datetimeperiod.period = icalperiodtype_null_period ();
617       if (icalperiodtype_is_null_period (old_datetimeperiod.period))
618         {
619           new_datetimeperiod.time
620             = icaltime_convert_to_zone (old_datetimeperiod.time, zone);
621         }
622       else
623         {
624           new_datetimeperiod.time
625             = icaltime_convert_to_zone (old_datetimeperiod.period.start, zone);
626         }
627       new_rdate = icalproperty_new_rdate (new_datetimeperiod);
628       icalcomponent_add_property (vevent_simplified, new_rdate);
629 
630       rdate_prop
631         = icalcomponent_get_next_property (vevent, ICAL_RDATE_PROPERTY);
632     }
633 
634   // Copy EXDATE properties
635   exdate_prop = icalcomponent_get_first_property (vevent,
636                                                   ICAL_EXDATE_PROPERTY);
637   while (exdate_prop)
638     {
639       icaltimetype original_exdate_time, exdate_time;
640       icalproperty *prop_clone;
641 
642       original_exdate_time = icalproperty_get_exdate (exdate_prop);
643       exdate_time
644         = icaltime_convert_to_zone (original_exdate_time, zone);
645 
646       prop_clone = icalproperty_new_exdate (exdate_time);
647       icalcomponent_add_property (vevent_simplified, prop_clone);
648 
649       exdate_prop
650         = icalcomponent_get_next_property (vevent, ICAL_EXDATE_PROPERTY);
651     }
652 
653   // Generate UID for event
654   uid = gvm_uuid_make ();
655   icalcomponent_set_uid (vevent_simplified, uid);
656   g_free (uid);
657   uid = NULL;
658 
659   // Set timestamp
660   dtstamp = icaltime_current_time_with_zone (zone);
661   icalcomponent_set_dtstamp (vevent_simplified, dtstamp);
662 
663   return vevent_simplified;
664 }
665 
666 /**
667  * @brief Error return for icalendar_from_string.
668  */
669 #define ICAL_RETURN_ERROR(message)              \
670   do                                            \
671     {                                           \
672       if (error)                                \
673         *error = message;                       \
674       icalcomponent_free (ical_parsed);         \
675       icalcomponent_free (ical_new);            \
676       g_string_free (warnings_buffer, TRUE);    \
677       return NULL;                              \
678     }                                           \
679   while (0)
680 
681 /**
682  * @brief Creates a new, simplified VCALENDAR component from a string.
683  *
684  * @param[in]  ical_string  The ical_string to create the component from.
685  * @param[in]  zone         Timezone.
686  * @param[out] error        Output of iCal errors or warnings.
687  *
688  * @return  A newly allocated, simplified VCALENDAR component.
689  */
690 icalcomponent *
icalendar_from_string(const char * ical_string,icaltimezone * zone,gchar ** error)691 icalendar_from_string (const char *ical_string, icaltimezone *zone,
692                        gchar **error)
693 {
694   icalcomponent *ical_new, *ical_parsed, *timezone_component;
695   icalproperty *error_prop;
696   GString *warnings_buffer;
697   int vevent_count = 0;
698   int other_component_count = 0;
699   icalcompiter ical_iter;
700 
701   // Parse the iCalendar string
702   ical_parsed = icalcomponent_new_from_string (ical_string);
703   if (ical_parsed == NULL)
704     {
705       if (error)
706         *error = g_strdup_printf ("Could not parse iCalendar string");
707       return NULL;
708     }
709 
710   // Check for errors
711   icalrestriction_check (ical_parsed);
712   error_prop = icalcomponent_get_first_property (ical_parsed,
713                                                  ICAL_XLICERROR_PROPERTY);
714   if (error_prop)
715     {
716       if (error)
717         *error = g_strdup_printf ("Error in root component: %s",
718                                   icalproperty_get_xlicerror (error_prop));
719       icalcomponent_free (ical_parsed);
720       return NULL;
721     }
722 
723   // Create buffers and new VCALENDAR
724   warnings_buffer = g_string_new ("");
725 
726   ical_new = icalcomponent_new_vcalendar ();
727   icalcomponent_add_property (ical_new, icalproperty_new_version ("2.0"));
728   icalcomponent_add_property (ical_new,
729                               icalproperty_new_prodid (GVM_PRODID));
730 
731   timezone_component
732     = icalcomponent_new_clone (icaltimezone_get_component (zone));
733   icalcomponent_add_component (ical_new, timezone_component);
734 
735   switch (icalcomponent_isa (ical_parsed))
736     {
737       case ICAL_NO_COMPONENT:
738         // The text must contain valid iCalendar component
739         ICAL_RETURN_ERROR
740             (g_strdup_printf ("String contains no iCalendar component"));
741         break;
742       case ICAL_XROOT_COMPONENT:
743       case ICAL_VCALENDAR_COMPONENT:
744         // Check multiple components
745         ical_iter = icalcomponent_begin_component (ical_parsed,
746                                                    ICAL_ANY_COMPONENT);
747         icalcomponent *subcomp;
748         while ((subcomp = icalcompiter_deref (&ical_iter)))
749           {
750             icalcomponent *new_vevent;
751             switch (icalcomponent_isa (subcomp))
752               {
753                 case ICAL_VEVENT_COMPONENT:
754                   // Copy and simplify only the first VEVENT, ignoring all
755                   //  following ones.
756                   if (vevent_count == 0)
757                     {
758                       new_vevent = icalendar_simplify_vevent
759                                       (subcomp,
760                                        zone,
761                                        error,
762                                        warnings_buffer);
763                       if (new_vevent == NULL)
764                         ICAL_RETURN_ERROR (*error);
765                       icalcomponent_add_component (ical_new, new_vevent);
766                     }
767                   vevent_count ++;
768                   break;
769                 case ICAL_VTIMEZONE_COMPONENT:
770                   // Timezones are collected separately
771                   break;
772                 case ICAL_VJOURNAL_COMPONENT:
773                 case ICAL_VTODO_COMPONENT:
774                   // VJOURNAL and VTODO components are ignored
775                   other_component_count ++;
776                   break;
777                 default:
778                   // Unexpected components
779                   ICAL_RETURN_ERROR
780                       (g_strdup_printf ("Unexpected component type: %s",
781                                         icalcomponent_kind_to_string
782                                             (icalcomponent_isa (subcomp))));
783               }
784             icalcompiter_next (&ical_iter);
785           }
786 
787         if (vevent_count == 0)
788           {
789             ICAL_RETURN_ERROR
790                 (g_strdup_printf ("iCalendar string must contain a VEVENT"));
791           }
792         else if (vevent_count > 1)
793           {
794             g_string_append_printf (warnings_buffer,
795                                     "<warning>"
796                                     "iCalendar contains %d VEVENT components"
797                                     " but only the first one will be used"
798                                     "</warning>",
799                                     vevent_count);
800           }
801 
802         if (other_component_count)
803           {
804             g_string_append_printf (warnings_buffer,
805                                     "<warning>"
806                                     "iCalendar contains %d VTODO and/or"
807                                     " VJOURNAL component(s) which will be"
808                                     " ignored"
809                                     "</warning>",
810                                     other_component_count);
811           }
812         break;
813       case ICAL_VEVENT_COMPONENT:
814         {
815           icalcomponent *new_vevent;
816 
817           new_vevent = icalendar_simplify_vevent (ical_parsed,
818                                                   zone,
819                                                   error,
820                                                   warnings_buffer);
821           if (new_vevent == NULL)
822             ICAL_RETURN_ERROR (*error);
823           icalcomponent_add_component (ical_new, new_vevent);
824         }
825         break;
826       default:
827         ICAL_RETURN_ERROR
828             (g_strdup_printf ("iCalendar string must be a VCALENDAR or VEVENT"
829                               " component or consist of multiple elements."));
830         break;
831     }
832 
833   icalcomponent_free (ical_parsed);
834 
835   if (error)
836     *error = g_string_free (warnings_buffer, FALSE);
837   else
838     g_string_free (warnings_buffer, TRUE);
839 
840   return ical_new;
841 }
842 
843 /**
844  * @brief Approximate the recurrence of a VCALENDAR as classic schedule data.
845  * The VCALENDAR must have simplified with icalendar_from_string for this to
846  *  work reliably.
847  *
848  * @param[in]  vcalendar       The VCALENDAR component to get the data from.
849  * @param[out] period          Output of the period in seconds.
850  * @param[out] period_months   Output of the period in months.
851  * @param[out] byday_mask      Output of the GVM byday mask.
852  *
853  * @return 0 success, 1 invalid vcalendar.
854  */
855 int
icalendar_approximate_rrule_from_vcalendar(icalcomponent * vcalendar,time_t * period,time_t * period_months,int * byday_mask)856 icalendar_approximate_rrule_from_vcalendar (icalcomponent *vcalendar,
857                                             time_t *period,
858                                             time_t *period_months,
859                                             int *byday_mask)
860 {
861   icalcomponent *vevent;
862   icalproperty *rrule_prop;
863 
864 
865   assert (period);
866   assert (period_months);
867   assert (byday_mask);
868 
869   *period = 0;
870   *period_months = 0;
871   *byday_mask = 0;
872 
873   // Component must be a VCALENDAR
874   if (vcalendar == NULL
875       || icalcomponent_isa (vcalendar) != ICAL_VCALENDAR_COMPONENT)
876     return 1;
877 
878   // Process only the first VEVENT
879   // Others should be removed by icalendar_from_string
880   vevent = icalcomponent_get_first_component (vcalendar,
881                                               ICAL_VEVENT_COMPONENT);
882   if (vevent == NULL)
883     return -1;
884 
885   // Process only first RRULE.
886   rrule_prop = icalcomponent_get_first_property (vevent,
887                                                  ICAL_RRULE_PROPERTY);
888   if (rrule_prop)
889     {
890       struct icalrecurrencetype recurrence;
891       recurrence = icalproperty_get_rrule (rrule_prop);
892       int array_pos;
893 
894       // Get period or period_months
895       switch (recurrence.freq)
896         {
897           case ICAL_YEARLY_RECURRENCE:
898             *period_months = recurrence.interval * 12;
899             break;
900           case ICAL_MONTHLY_RECURRENCE:
901             *period_months = recurrence.interval;
902             break;
903           case ICAL_WEEKLY_RECURRENCE:
904             *period = recurrence.interval * 604800;
905             break;
906           case ICAL_DAILY_RECURRENCE:
907             *period = recurrence.interval * 86400;
908             break;
909           case ICAL_HOURLY_RECURRENCE:
910             *period = recurrence.interval * 3600;
911             break;
912           case ICAL_MINUTELY_RECURRENCE:
913             *period = recurrence.interval * 60;
914             break;
915           case ICAL_SECONDLY_RECURRENCE:
916             *period = recurrence.interval;
917           case ICAL_NO_RECURRENCE:
918             break;
919           default:
920             return -1;
921         }
922 
923       /*
924        * Try to approximate byday mask
925        * - libical days start at 1 for Sunday.
926        * - GVM byday mask bit index starts at 0 for Monday -> Sunday = 6
927        */
928       array_pos = 0;
929       while (recurrence.by_day[array_pos] != ICAL_RECURRENCE_ARRAY_MAX)
930         {
931           int ical_day = icalrecurrencetype_day_day_of_week
932                             (recurrence.by_day[array_pos]);
933           int mask_bit = -1;
934 
935           if (ical_day == 1)
936             mask_bit = 6;
937           else if (ical_day)
938             mask_bit = ical_day - 2;
939 
940           if (mask_bit != -1)
941             {
942               *byday_mask |= (1 << mask_bit);
943             }
944           array_pos ++;
945         }
946     }
947 
948   return 0;
949 }
950 
951 /**
952  * @brief Collect the times of EXDATE or RDATE properties from an VEVENT.
953  * The returned GPtrArray will contain pointers to icaltimetype structs, which
954  *  will be freed with g_ptr_array_free.
955  *
956  * @param[in]  vevent  The VEVENT component to collect times.
957  * @param[in]  type    The property to get the times from.
958  *
959  * @return  GPtrArray with pointers to collected times or NULL on error.
960  */
961 static GPtrArray*
icalendar_times_from_vevent(icalcomponent * vevent,icalproperty_kind type)962 icalendar_times_from_vevent (icalcomponent *vevent, icalproperty_kind type)
963 {
964   GPtrArray* times;
965   icalproperty *date_prop;
966 
967   if (icalcomponent_isa (vevent) != ICAL_VEVENT_COMPONENT
968       || (type != ICAL_EXDATE_PROPERTY && type != ICAL_RDATE_PROPERTY))
969     return NULL;
970 
971   times = g_ptr_array_new_with_free_func (g_free);
972 
973   date_prop = icalcomponent_get_first_property (vevent, type);
974   while (date_prop)
975     {
976       icaltimetype *time;
977       time = g_malloc0 (sizeof (icaltimetype));
978       if (type == ICAL_EXDATE_PROPERTY)
979         {
980           *time = icalproperty_get_exdate (date_prop);
981         }
982       else if (type == ICAL_RDATE_PROPERTY)
983         {
984           struct icaldatetimeperiodtype datetimeperiod;
985           datetimeperiod = icalproperty_get_rdate (date_prop);
986           // Assume periods have been converted to date or datetime
987           *time = datetimeperiod.time;
988         }
989       g_ptr_array_insert (times, -1, time);
990       date_prop = icalcomponent_get_next_property (vevent, type);
991     }
992 
993   return times;
994 }
995 
996 /**
997  * @brief  Tests if an icaltimetype matches one in a GPtrArray.
998  * When an icaltimetype is a date, only the date must match, otherwise both
999  *  date and time must match.
1000  *
1001  * @param[in]  time         The icaltimetype to try to find a match of.
1002  * @param[in]  times_array  Array of pointers to check for a matching time.
1003  *
1004  * @return  Whether a match was found.
1005  */
1006 static gboolean
icalendar_time_matches_array(icaltimetype time,GPtrArray * times_array)1007 icalendar_time_matches_array (icaltimetype time, GPtrArray *times_array)
1008 {
1009   gboolean found = FALSE;
1010   int index;
1011 
1012   if (times_array == NULL)
1013     return FALSE;
1014 
1015   for (index = 0;
1016        found == FALSE && index < times_array->len;
1017        index++)
1018     {
1019       int compare_result;
1020       icaltimetype *array_time = g_ptr_array_index (times_array, index);
1021 
1022       if (array_time->is_date)
1023         compare_result = icaltime_compare_date_only (time, *array_time);
1024       else
1025         compare_result = icaltime_compare (time, *array_time);
1026 
1027       if (compare_result == 0)
1028         found = TRUE;
1029     }
1030   return found;
1031 }
1032 
1033 /**
1034  * @brief  Get the next or previous time from a list of RDATEs.
1035  *
1036  * @param[in]  rdates         The list of RDATEs.
1037  * @param[in]  tz             The icaltimezone to use.
1038  * @param[in]  ref_time_ical  The reference time (usually the current time).
1039  * @param[in]  periods_offset 0 for next, -1 for previous from/before reference.
1040  *
1041  * @return  The next or previous time as time_t.
1042  */
1043 static time_t
icalendar_next_time_from_rdates(GPtrArray * rdates,icaltimetype ref_time_ical,icaltimezone * tz,int periods_offset)1044 icalendar_next_time_from_rdates (GPtrArray *rdates,
1045                                  icaltimetype ref_time_ical,
1046                                  icaltimezone *tz,
1047                                  int periods_offset)
1048 {
1049   int index;
1050   time_t ref_time, closest_time;
1051   int old_diff;
1052 
1053   closest_time = 0;
1054   ref_time = icaltime_as_timet_with_zone (ref_time_ical, tz);
1055   if (periods_offset < 0)
1056     old_diff = INT_MIN;
1057   else
1058     old_diff = INT_MAX;
1059 
1060   for (index = 0; index < rdates->len; index++)
1061     {
1062       icaltimetype *iter_time_ical;
1063       time_t iter_time;
1064       int time_diff;
1065 
1066       iter_time_ical = g_ptr_array_index (rdates, index);
1067       iter_time = icaltime_as_timet_with_zone (*iter_time_ical, tz);
1068       time_diff = iter_time - ref_time;
1069 
1070       // Cases: previous (offset -1): latest before reference
1071       //        next     (offset  0): earliest after reference
1072       if ((periods_offset == -1 && time_diff < 0 && time_diff > old_diff)
1073           || (periods_offset == 0 && time_diff > 0 && time_diff < old_diff))
1074         {
1075           closest_time = iter_time;
1076           old_diff = time_diff;
1077         }
1078     }
1079 
1080   return closest_time;
1081 }
1082 
1083 /**
1084  * @brief Calculate the next time of a recurrence
1085  *
1086  * @param[in]  recurrence     The recurrence rule to evaluate.
1087  * @param[in]  dtstart        The start time of the recurrence.
1088  * @param[in]  reference_time The reference time (usually the current time).
1089  * @param[in]  tz             The icaltimezone to use.
1090  * @param[in]  exdates        GList of EXDATE dates or datetimes to skip.
1091  * @param[in]  rdates         GList of RDATE datetimes to include.
1092  * @param[in]  periods_offset 0 for next, -1 for previous from/before reference.
1093  *
1094  * @return  The next time.
1095  */
1096 static time_t
icalendar_next_time_from_recurrence(struct icalrecurrencetype recurrence,icaltimetype dtstart,icaltimetype reference_time,icaltimezone * tz,GPtrArray * exdates,GPtrArray * rdates,int periods_offset)1097 icalendar_next_time_from_recurrence (struct icalrecurrencetype recurrence,
1098                                      icaltimetype dtstart,
1099                                      icaltimetype reference_time,
1100                                      icaltimezone *tz,
1101                                      GPtrArray *exdates,
1102                                      GPtrArray *rdates,
1103                                      int periods_offset)
1104 {
1105   icalrecur_iterator *recur_iter;
1106   icaltimetype recur_time, prev_time, next_time;
1107   time_t rdates_time;
1108 
1109   // Start iterating over rule-based times
1110   recur_iter = icalrecur_iterator_new (recurrence, dtstart);
1111   recur_time = icalrecur_iterator_next (recur_iter);
1112 
1113   if (icaltime_is_null_time (recur_time))
1114     {
1115       // Use DTSTART if there are no recurrence rule times
1116       if (icaltime_compare (dtstart, reference_time) < 0)
1117         {
1118           prev_time = dtstart;
1119           next_time = icaltime_null_time ();
1120         }
1121       else
1122         {
1123           prev_time = icaltime_null_time ();
1124           next_time = dtstart;
1125         }
1126     }
1127   else
1128     {
1129       /* Handle rule-based recurrence times:
1130        * Get the first rule-based recurrence time, skipping ahead in case
1131        *  DTSTART is excluded by EXDATEs.  */
1132 
1133       while (icaltime_is_null_time (recur_time) == FALSE
1134              && icalendar_time_matches_array (recur_time, exdates))
1135         {
1136           recur_time = icalrecur_iterator_next (recur_iter);
1137         }
1138 
1139       // Set the first recur_time as either the previous or next time.
1140       if (icaltime_compare (recur_time, reference_time) < 0)
1141         {
1142           prev_time = recur_time;
1143         }
1144       else
1145         {
1146           prev_time = icaltime_null_time ();
1147         }
1148 
1149       /* Iterate over rule-based recurrences up to first time after
1150        * reference time */
1151       while (icaltime_is_null_time (recur_time) == FALSE
1152              && icaltime_compare (recur_time, reference_time) < 0)
1153         {
1154           if (icalendar_time_matches_array (recur_time, exdates) == FALSE)
1155             prev_time = recur_time;
1156 
1157           recur_time = icalrecur_iterator_next (recur_iter);
1158         }
1159 
1160       // Skip further ahead if last recurrence time is in EXDATEs
1161       while (icaltime_is_null_time (recur_time) == FALSE
1162              && icalendar_time_matches_array (recur_time, exdates))
1163         {
1164           recur_time = icalrecur_iterator_next (recur_iter);
1165         }
1166 
1167       // Select last recur_time as the next_time
1168       next_time = recur_time;
1169     }
1170 
1171   // Get time from RDATEs
1172   rdates_time = icalendar_next_time_from_rdates (rdates, reference_time, tz,
1173                                                  periods_offset);
1174 
1175   // Select appropriate time as the RRULE time, compare it to the RDATEs time
1176   //  and return the appropriate time.
1177   if (periods_offset == -1)
1178     {
1179       time_t rrule_time;
1180 
1181       rrule_time = icaltime_as_timet_with_zone (prev_time, tz);
1182       if (rdates_time == 0 || rrule_time - rdates_time > 0)
1183         return rrule_time;
1184       else
1185         return rdates_time;
1186     }
1187   else
1188     {
1189       time_t rrule_time;
1190 
1191       rrule_time = icaltime_as_timet_with_zone (next_time, tz);
1192       if (rdates_time == 0 || rrule_time - rdates_time < 0)
1193         return rrule_time;
1194       else
1195         return rdates_time;
1196     }
1197 }
1198 
1199 /**
1200  * @brief  Get the next or previous due time from a VCALENDAR component.
1201  * The VCALENDAR must have simplified with icalendar_from_string for this to
1202  *  work reliably.
1203  *
1204  * @param[in]  vcalendar       The VCALENDAR component to get the time from.
1205  * @param[in]  default_tzid    Timezone id to use if none is set in the iCal.
1206  * @param[in]  periods_offset  0 for next, -1 for previous from/before now.
1207  *
1208  * @return The next or previous time as a time_t.
1209  */
1210 time_t
icalendar_next_time_from_vcalendar(icalcomponent * vcalendar,const char * default_tzid,int periods_offset)1211 icalendar_next_time_from_vcalendar (icalcomponent *vcalendar,
1212                                     const char *default_tzid,
1213                                     int periods_offset)
1214 {
1215   icalcomponent *vevent;
1216   icaltimetype dtstart, dtstart_with_tz, ical_now;
1217   icaltimezone *tz;
1218   icalproperty *rrule_prop;
1219   struct icalrecurrencetype recurrence;
1220   GPtrArray *exdates, *rdates;
1221   time_t next_time = 0;
1222 
1223   // Only offsets -1 and 0 will work properly
1224   if (periods_offset < -1 || periods_offset > 0)
1225     return 0;
1226 
1227   // Component must be a VCALENDAR
1228   if (vcalendar == NULL
1229       || icalcomponent_isa (vcalendar) != ICAL_VCALENDAR_COMPONENT)
1230     return 0;
1231 
1232   // Process only the first VEVENT
1233   // Others should be removed by icalendar_from_string
1234   vevent = icalcomponent_get_first_component (vcalendar,
1235                                               ICAL_VEVENT_COMPONENT);
1236   if (vevent == NULL)
1237     return 0;
1238 
1239   // Get start time and timezone
1240   dtstart = icalcomponent_get_dtstart (vevent);
1241   if (icaltime_is_null_time (dtstart))
1242     return 0;
1243 
1244   tz = (icaltimezone*) icaltime_get_timezone (dtstart);
1245   if (tz == NULL)
1246     {
1247       tz = icalendar_timezone_from_string (default_tzid);
1248       if (tz == NULL)
1249         tz = icaltimezone_get_utc_timezone ();
1250     }
1251 
1252   dtstart_with_tz = dtstart;
1253   // Set timezone in case the original DTSTART did not have any set.
1254   icaltime_set_timezone (&dtstart_with_tz, tz);
1255 
1256   // Get current time
1257   ical_now = icaltime_current_time_with_zone (tz);
1258   // Set timezone explicitly because icaltime_current_time_with_zone doesn't.
1259   icaltime_set_timezone (&ical_now, tz);
1260   if (ical_now.zone == NULL)
1261     {
1262       ical_now.zone = tz;
1263     }
1264 
1265   // Get EXDATEs and RDATEs
1266   exdates = icalendar_times_from_vevent (vevent, ICAL_EXDATE_PROPERTY);
1267   rdates = icalendar_times_from_vevent (vevent, ICAL_RDATE_PROPERTY);
1268 
1269   // Try to get the recurrence from the RRULE property
1270   rrule_prop = icalcomponent_get_first_property (vevent, ICAL_RRULE_PROPERTY);
1271   if (rrule_prop)
1272     recurrence = icalproperty_get_rrule (rrule_prop);
1273   else
1274     icalrecurrencetype_clear (&recurrence);
1275 
1276   // Calculate next time.
1277   next_time = icalendar_next_time_from_recurrence (recurrence,
1278                                                    dtstart_with_tz,
1279                                                    ical_now, tz,
1280                                                    exdates, rdates,
1281                                                    periods_offset);
1282 
1283   // Cleanup
1284   g_ptr_array_free (exdates, TRUE);
1285   g_ptr_array_free (rdates, TRUE);
1286 
1287   return next_time;
1288 }
1289 
1290 /**
1291  * @brief  Get the next or previous due time from a VCALENDAR string.
1292  * The string must be a VCALENDAR simplified with icalendar_from_string for
1293  *  this to work reliably.
1294  *
1295  * @param[in]  ical_string     The VCALENDAR string to get the time from.
1296  * @param[in]  default_tzid    Timezone id to use if none is set in the iCal.
1297  * @param[in]  periods_offset  0 for next, -1 for previous from/before now.
1298  *
1299  * @return The next or previous time as a time_t.
1300  */
1301 time_t
icalendar_next_time_from_string(const char * ical_string,const char * default_tzid,int periods_offset)1302 icalendar_next_time_from_string (const char *ical_string,
1303                                  const char *default_tzid,
1304                                  int periods_offset)
1305 {
1306   time_t next_time;
1307   icalcomponent *ical_parsed;
1308 
1309   ical_parsed = icalcomponent_new_from_string (ical_string);
1310   next_time = icalendar_next_time_from_vcalendar (ical_parsed, default_tzid,
1311                                                   periods_offset);
1312   icalcomponent_free (ical_parsed);
1313   return next_time;
1314 }
1315 
1316 /**
1317  * @brief  Get the duration VCALENDAR component.
1318  * The VCALENDAR must have simplified with icalendar_from_string for this to
1319  *  work reliably.
1320  *
1321  * @param[in]  vcalendar       The VCALENDAR component to get the time from.
1322  *
1323  * @return The duration in seconds.
1324  */
1325 int
icalendar_duration_from_vcalendar(icalcomponent * vcalendar)1326 icalendar_duration_from_vcalendar (icalcomponent *vcalendar)
1327 {
1328   icalcomponent *vevent;
1329   struct icaldurationtype duration;
1330 
1331   // Component must be a VCALENDAR
1332   if (vcalendar == NULL
1333       || icalcomponent_isa (vcalendar) != ICAL_VCALENDAR_COMPONENT)
1334     return 0;
1335 
1336   // Process only the first VEVENT
1337   // Others should be removed by icalendar_from_string
1338   vevent = icalcomponent_get_first_component (vcalendar,
1339                                               ICAL_VEVENT_COMPONENT);
1340   if (vevent == NULL)
1341     return 0;
1342 
1343   // Get the duration
1344   duration = icalcomponent_get_duration (vevent);
1345 
1346   // Convert to time_t
1347   return icaldurationtype_as_int (duration);
1348 }
1349 
1350 /**
1351  * @brief  Get the first time from a VCALENDAR component.
1352  * The VCALENDAR must have simplified with icalendar_from_string for this to
1353  *  work reliably.
1354  *
1355  * @param[in]  vcalendar       The VCALENDAR component to get the time from.
1356  * @param[in]  default_tz      Timezone to use if none is set in the iCal.
1357  *
1358  * @return The first time as a time_t.
1359  */
1360 time_t
icalendar_first_time_from_vcalendar(icalcomponent * vcalendar,icaltimezone * default_tz)1361 icalendar_first_time_from_vcalendar (icalcomponent *vcalendar,
1362                                      icaltimezone *default_tz)
1363 {
1364   icalcomponent *vevent;
1365   icaltimetype dtstart;
1366   icaltimezone *tz;
1367 
1368   // Component must be a VCALENDAR
1369   if (vcalendar == NULL
1370       || icalcomponent_isa (vcalendar) != ICAL_VCALENDAR_COMPONENT)
1371     return 0;
1372 
1373   // Process only the first VEVENT
1374   // Others should be removed by icalendar_from_string
1375   vevent = icalcomponent_get_first_component (vcalendar,
1376                                               ICAL_VEVENT_COMPONENT);
1377   if (vevent == NULL)
1378     return 0;
1379 
1380   // Get start time and timezone
1381   dtstart = icalcomponent_get_dtstart (vevent);
1382   if (icaltime_is_null_time (dtstart))
1383     return 0;
1384 
1385   tz = (icaltimezone*) icaltime_get_timezone (dtstart);
1386   if (tz == NULL)
1387     tz = default_tz;
1388 
1389   // Convert to time_t
1390   return icaltime_as_timet_with_zone (dtstart, tz);
1391 }
1392 
1393 /**
1394  * @brief Cleans up a hosts string, removing extra zeroes from IPv4 addresses.
1395  *
1396  * @param[in]  hosts  The hosts string to clean.
1397  *
1398  * @return  The newly allocated, cleaned up hosts string.
1399  */
1400 gchar *
clean_hosts_string(const char * hosts)1401 clean_hosts_string (const char *hosts)
1402 {
1403   gchar **hosts_split, **item;
1404   GString *new_hosts;
1405   GRegex *ipv4_match_regex, *ipv4_replace_regex;
1406 
1407   if (hosts == NULL)
1408     return NULL;
1409 
1410   /*
1411    * Regular expression for matching candidates for IPv4 addresses
1412    * (four groups of digits separated by a dot "."),
1413    * with optional extensions for ranges:
1414    * - Another IP address candidate, separated with a hyphen "-"
1415    *   (e.g. "192.168.123.001-192.168.123.005)"
1416    * - A final group of digits, separated with a hyphen "-"
1417    *   (short form address range, e.g. "192.168.123.001-005)
1418    * - A final group of digits, separated with a slash "-"
1419    *   (CIDR notation, e.g. "192.168.123.001/027)
1420    */
1421   ipv4_match_regex
1422     = g_regex_new ("^[0-9]+(?:\\.[0-9]+){3}"
1423                    "(?:\\/[0-9]+|-[0-9]+(?:(?:\\.[0-9]+){3})?)?$",
1424                    0, 0, NULL);
1425   /*
1426    * Regular expression matching leading zeroes in groups of digits
1427    * separated by dots or other characters.
1428    * First line matches zeroes before non-zero numbers, e.g. "000" in "000120"
1429    * Second line matches groups of all zeroes except one, e.g. "00" in "000"
1430    */
1431   ipv4_replace_regex
1432     = g_regex_new ("(?<=\\D|^)(0+)(?=(?:(?:[1-9]\\d*)(?:\\D|$)))"
1433                    "|(?<=\\D|^)(0+)(?=0(?:\\D|$))",
1434                    0, 0, NULL);
1435   new_hosts = g_string_new ("");
1436 
1437   hosts_split = g_strsplit (hosts, ",", -1);
1438   item = hosts_split;
1439   while (*item)
1440     {
1441       g_strstrip (*item);
1442       if (g_regex_match (ipv4_match_regex, *item, 0, 0))
1443         {
1444           // IPv4 address, address range or CIDR notation
1445           gchar *new_item;
1446           /* Remove leading zeroes in each group of digits by replacing them
1447            * with empty strings,
1448            * e.g. "000.001.002.003-004" becomes "0.1.2.3-4"
1449            */
1450           new_item = g_regex_replace (ipv4_replace_regex,
1451                                       *item, -1, 0, "", 0, NULL);
1452           g_string_append (new_hosts, new_item);
1453           g_free (new_item);
1454         }
1455       else
1456         g_string_append (new_hosts, *item);
1457 
1458       if (*(item + 1))
1459         g_string_append (new_hosts, ", ");
1460       item++;
1461     }
1462   g_strfreev (hosts_split);
1463 
1464   g_regex_unref (ipv4_match_regex);
1465   g_regex_unref (ipv4_replace_regex);
1466 
1467   return g_string_free (new_hosts, FALSE);
1468 }
1469