1 /*
2   Copyright 2021 Northern.tech AS
3 
4   This file is part of CFEngine 3 - written and maintained by Northern.tech AS.
5 
6   This program is free software; you can redistribute it and/or modify it
7   under the terms of the GNU General Public License as published by the
8   Free Software Foundation; version 3.
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 General Public License for more details.
14 
15   You should have received a copy of the GNU General Public License
16   along with this program; if not, write to the Free Software
17   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
18 
19   To the extent this program is licensed as part of the Enterprise
20   versions of CFEngine, the applicable Commercial Open Source License
21   (COSL) may apply to this file if you as a licensee so wish it. See
22   included file COSL.txt.
23 */
24 
25 #include <platform.h>
26 
27 #include <stdlib.h>
28 #include <stdio.h>
29 
30 #include <logging.h>
31 #include <eval_context.h>       /* ToChangesChroot() */
32 #include <file_lib.h>           /* IsAbsoluteFileName() */
33 #include <dir.h>                /* DirOpen(),...*/
34 #include <string_lib.h>         /* StringEqual() */
35 #include <string_sequence.h>    /* ReadLenPrefixedString() */
36 #include <changes_chroot.h>     /* CHROOT_CHANGES_LIST_FILE */
37 #include <known_dirs.h>         /* GetBinDir() */
38 #include <files_names.h>        /* JoinPaths() */
39 #include <pipes.h>              /* cf_popen(), cf_pclose() */
40 #include <set.h>                /* StringSet */
41 #include <map.h>                /* StringMap */
42 #include <csv_parser.h>         /* GetCsvLineNext() */
43 
44 #include <simulate_mode.h>
45 
46 #define DELIM_CHAR '='
47 
48 /* Taken from coreutils/lib/stat-macros.h. */
49 #define CHMOD_MODE_BITS (S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO)
50 
PrintDelimiter()51 static inline void PrintDelimiter()
52 {
53     char *columns = getenv("COLUMNS");
54     int n_columns = 0;
55     if (columns != NULL)
56     {
57         n_columns = atoi(columns);
58     }
59     n_columns = MAX(n_columns, 80) - 5;
60     for (int i = n_columns; i > 0; i--)
61     {
62         putchar(DELIM_CHAR);
63     }
64     putchar('\n');
65 }
66 
67 #ifndef __MINGW32__
ManifestStatInfo(const struct stat * st)68 static void ManifestStatInfo(const struct stat *st)
69 {
70     assert(st != NULL);
71 
72     /* Inspired by the output from the 'stat' command */
73     char mode_str[10] = {
74         st->st_mode & S_IRUSR ? 'r' : '-',
75         st->st_mode & S_IWUSR ? 'w' : '-',
76         (st->st_mode & S_ISUID
77          ? (st->st_mode & S_IXUSR ? 's' : 'S')
78          : (st->st_mode & S_IXUSR ? 'x' : '-')),
79         st->st_mode & S_IRGRP ? 'r' : '-',
80         st->st_mode & S_IWGRP ? 'w' : '-',
81         (st->st_mode & S_ISGID
82          ? (st->st_mode & S_IXGRP ? 's' : 'S')
83          : (st->st_mode & S_IXGRP ? 'x' : '-')),
84         st->st_mode & S_IROTH ? 'r' : '-',
85         st->st_mode & S_IWOTH ? 'w' : '-',
86         (st->st_mode & S_ISVTX
87          ? (st->st_mode & S_IXOTH ? 't' : 'T')
88          : (st->st_mode & S_IXOTH ? 'x' : '-')),
89         '\0'
90     };
91     printf("Size: %ju\n", (uintmax_t) st->st_size);
92     printf("Access: (%04o/%s)  ", st->st_mode & CHMOD_MODE_BITS, mode_str);
93 
94     errno = 0;
95     struct passwd *owner = getpwuid(st->st_uid);
96     if (owner != NULL)
97     {
98         printf("Uid: (%ju/%s)   ", (uintmax_t) st->st_uid, owner->pw_name);
99     }
100     else
101     {
102         if (errno == 0)
103         {
104             Log(LOG_LEVEL_ERR, "Failed to get file owner name: non-existent user id '%ju'",
105                 (uintmax_t) st->st_uid);
106         }
107         else
108         {
109             Log(LOG_LEVEL_ERR, "Failed to get file owner name: %s",
110                 GetErrorStr());
111         }
112     }
113 
114     errno = 0;
115     struct group *group = getgrgid(st->st_gid);
116     if (group != NULL) {
117         printf("Gid: (%ju/%s)\n", (uintmax_t) st->st_gid, group->gr_name);
118     }
119     else
120     {
121         if (errno == 0)
122         {
123             Log(LOG_LEVEL_ERR, "Failed to get file group name: non-existent group id '%ju'",
124                 (uintmax_t) st->st_gid);
125         }
126         else
127         {
128             Log(LOG_LEVEL_ERR, "Failed to get file group name: %s",
129                 GetErrorStr());
130         }
131     }
132 
133 #define MAX_TIMESTAMP_SIZE (sizeof("2020-10-05 12:56:18 +0200"))
134     char buf[MAX_TIMESTAMP_SIZE] = {0};
135 
136     size_t ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
137                           localtime((time_t*) &(st->st_atime)));
138     assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
139     printf("Access: %s\n", buf);
140 
141     ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
142                    localtime((time_t*) &(st->st_mtime)));
143     assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
144     printf("Modify: %s\n", buf);
145 
146     ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
147                    localtime((time_t*) &(st->st_ctime)));
148     assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
149     printf("Change: %s\n", buf);
150 }
151 #else  /* !__MINGW32__ */
ManifestStatInfo(const struct stat * st)152 static void ManifestStatInfo(const struct stat *st)
153 {
154     assert(st != NULL);
155 
156     /* Inspired by the output from the 'stat' command */
157     char mode_str[10] = {
158         st->st_mode & S_IRUSR ? 'r' : '-',
159         st->st_mode & S_IWUSR ? 'w' : '-',
160         st->st_mode & S_IXUSR ? 'x' : '-',
161         st->st_mode & S_IRGRP ? 'r' : '-',
162         st->st_mode & S_IWGRP ? 'w' : '-',
163         st->st_mode & S_IXGRP ? 'x' : '-',
164         st->st_mode & S_IROTH ? 'r' : '-',
165         st->st_mode & S_IWOTH ? 'w' : '-',
166         st->st_mode & S_IXOTH ? 'x' : '-',
167         '\0'
168     };
169     printf("Size: %ju\n", (uintmax_t) st->st_size);
170     printf("Access: (%04o/%s)  ", st->st_mode, mode_str);
171     printf("Uid: %ju   ", (uintmax_t)st->st_uid);
172     printf("Gid: %ju\n", (uintmax_t)st->st_gid);
173 
174 #define MAX_TIMESTAMP_SIZE (sizeof("2020-10-05 12:56:18 +0200"))
175     char buf[MAX_TIMESTAMP_SIZE] = {0};
176 
177     size_t ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
178                           localtime((time_t*) &(st->st_atime)));
179     assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
180     printf("Access: %s\n", buf);
181 
182     ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
183                    localtime((time_t*) &(st->st_mtime)));
184     assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
185     printf("Modify: %s\n", buf);
186 
187     ret = strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %z",
188                    localtime((time_t*) &(st->st_ctime)));
189     assert((ret > 0) && (ret < MAX_TIMESTAMP_SIZE));
190     printf("Change: %s\n", buf);
191 }
192 #endif  /* !__MINGW32__ */
193 
194 #ifndef __MINGW32__
ManifestLinkTarget(const char * link_path,bool chrooted)195 static inline void ManifestLinkTarget(const char *link_path, bool chrooted)
196 {
197     char target[PATH_MAX] = {0};
198     if (readlink(link_path, target, sizeof(target)) > 0)
199     {
200         const char *real_target = target;
201         if (chrooted && IsAbsoluteFileName(target))
202         {
203             real_target = ToNormalRoot(target);
204         }
205         printf("Target: '%s'\n", real_target);
206     }
207     else
208     {
209         printf("Invalid target\n");
210     }
211 }
212 #endif  /* !__MINGW32__ */
213 
ManifestFileContents(const char * path)214 static inline void ManifestFileContents(const char *path)
215 {
216     FILE *f = fopen(path, "r");
217     if (f != NULL)
218     {
219         puts("Contents of the file:");
220     }
221     else
222     {
223         Log(LOG_LEVEL_ERR, "Failed to open file for reading: %s", GetErrorStr());
224         return;
225     }
226 
227     bool binary = false;
228     char last_char = '\n';
229 
230     char buf[CF_BUFSIZE];
231     size_t n_read;
232     bool done = false;
233     while (!done && ((n_read = fread(buf, 1, sizeof(buf), f)) > 0))
234     {
235         bool is_ascii = true;
236         for (size_t i = 0; is_ascii && (i < n_read); i++)
237         {
238             is_ascii = isascii(buf[i]);
239         }
240         last_char = buf[n_read - 1];
241         if (is_ascii)
242         {
243             size_t offset = 0;
244             size_t to_write = n_read;
245             while (to_write > 0)
246             {
247                 size_t n_written = fwrite(buf + offset, 1, to_write, stdout);
248                 if (n_written > 0)
249                 {
250                     to_write -= n_written;
251                     offset += n_written;
252                 }
253                 else
254                 {
255                     Log(LOG_LEVEL_ERR, "Failed to print contents of the file");
256                     break;
257                 }
258             }
259         }
260         else
261         {
262             puts("File contains non-ASCII data");
263             binary = true;
264             done = true;
265         }
266     }
267     if (!binary && (last_char != '\n'))
268     {
269         puts("\n\\no newline at the end of file");
270     }
271     if (!done && !feof(f))
272     {
273         Log(LOG_LEVEL_ERR, "Failed to print contents of the file");
274     }
275     fclose(f);
276 }
277 
ManifestDirectoryListing(const char * path)278 static inline void ManifestDirectoryListing(const char *path)
279 {
280     Dir *dir = DirOpen(path);
281     if (dir == NULL)
282     {
283         Log(LOG_LEVEL_ERR, "Failed to open the directory: %s", GetErrorStr());
284         return;
285     }
286     else
287     {
288         puts("Directory contents:");
289     }
290     for (const struct dirent *dir_p = DirRead(dir);
291          dir_p != NULL;
292          dir_p = DirRead(dir))
293     {
294         if (StringEqual(dir_p->d_name, ".") ||
295             StringEqual(dir_p->d_name, ".."))
296         {
297             continue;
298         }
299         else
300         {
301             puts(dir_p->d_name);
302         }
303     }
304     DirClose(dir);
305 }
306 
GetFileTypeDescription(mode_t st_mode)307 static inline const char *GetFileTypeDescription(mode_t st_mode)
308 {
309     switch (st_mode & S_IFMT) {
310     case S_IFBLK:
311         return "block device";
312     case S_IFCHR:
313         return "character device";
314     case S_IFDIR:
315         return "directory";
316     case S_IFIFO:
317         return "FIFO/pipe";
318 #ifndef __MINGW32__
319     case S_IFLNK:
320         return "symbolic link";
321 #endif
322     case S_IFREG:
323         return "regular file";
324     case S_IFSOCK:
325         return "socket";
326     default:
327         debug_abort_if_reached();
328         return "unknown";
329     }
330 }
331 
ManifestFileDetails(const char * path,struct stat * st,bool chrooted)332 static inline void ManifestFileDetails(const char *path, struct stat *st, bool chrooted)
333 {
334     assert(st != NULL);
335 
336     switch (st->st_mode & S_IFMT) {
337     case S_IFREG:
338         puts(""); /* blank line */
339         ManifestFileContents(path);
340         break;
341 #ifndef __MINGW32__
342     case S_IFLNK:
343         puts(""); /* blank line */
344         ManifestLinkTarget(path, chrooted);
345         break;
346 #endif
347     case S_IFDIR:
348         puts(""); /* blank line */
349         ManifestDirectoryListing(path);
350         break;
351     default:
352         /* nothing to do for other types */
353         break;
354     }
355 }
356 
ManifestFile(const char * path,bool chrooted)357 bool ManifestFile(const char *path, bool chrooted)
358 {
359     PrintDelimiter();
360     const char *real_path = path;
361     if (chrooted)
362     {
363         real_path = ToChangesChroot(path);
364     }
365 
366     /* TODO: handle renames */
367     struct stat st;
368     if (lstat(real_path, &st) == -1)
369     {
370         printf("'%s' no longer exists\n", path);
371         return true;
372     }
373 
374     printf("'%s' is a %s\n", path, GetFileTypeDescription(st.st_mode));
375     ManifestStatInfo(&st);
376     ManifestFileDetails(real_path, &st, chrooted);
377 
378     return true;
379 }
380 
ManifestRename(const char * orig_name,const char * new_name)381 bool ManifestRename(const char *orig_name, const char *new_name)
382 {
383     PrintDelimiter();
384     printf("'%s' is the new name of '%s'\n", new_name, orig_name);
385     return true;
386 }
387 
RunDiff(const char * path1,const char * path2)388 static bool RunDiff(const char *path1, const char *path2)
389 {
390     char diff_path[PATH_MAX];
391     strncpy(diff_path, GetBinDir(), sizeof(diff_path));
392     JoinPaths(diff_path, sizeof(diff_path), "diff");
393 
394     /* We use the '--label' option to override the paths in the output, for example:
395      * --- original /etc/motd.d/cfengine
396      * +++ changed  /etc/motd.d/cfengine
397      * @@ -1,1 +1,1 @@
398      * -One line
399      * +New line
400      */
401     char *command;
402     int ret = xasprintf(&command, "%s -u --label 'original %s' --label 'changed  %s' '%s' '%s'",
403                         diff_path, path1, path1, path1, path2);
404     assert(ret != -1); /* should never happen */
405 
406     FILE *f = cf_popen(command, "r", true);
407 
408     char buf[CF_BUFSIZE];
409     size_t n_read;
410     bool failure = false;
411     while (!failure && ((n_read = fread(buf, 1, sizeof(buf), f)) > 0))
412     {
413         size_t offset = 0;
414         size_t to_write = n_read;
415         while (to_write > 0)
416         {
417             size_t n_written = fwrite(buf + offset, 1, to_write, stdout);
418             if (n_written > 0)
419             {
420                 to_write -= n_written;
421                 offset += n_written;
422             }
423             else
424             {
425                 Log(LOG_LEVEL_ERR, "Failed to print results from 'diff' for '%s' and '%s'",
426                     path1, path2);
427                 failure = true;
428                 break;
429             }
430         }
431     }
432     if (!feof(f))
433     {
434         Log(LOG_LEVEL_ERR, "Failed to read output from the 'diff' utility");
435         cf_pclose(f);
436         free(command);
437         return false;
438     }
439     ret = cf_pclose(f);
440     free(command);
441     if (ret == 2)
442     {
443         Log(LOG_LEVEL_ERR, "'diff -u %s %s' failed", path1, path2);
444         return false;
445     }
446     return !failure;
447 }
448 
DiffFile(const char * path)449 static bool DiffFile(const char *path)
450 {
451     const char *chrooted_path = ToChangesChroot(path);
452 
453     struct stat st_orig;
454     struct stat st_chrooted;
455     if (lstat(path, &st_orig) == -1)
456     {
457         /* Original final doesn't exist, must be a new file in the changes
458          * chroot, let's just manifest it instead of running 'diff' on it. */
459         ManifestFile(path, true);
460         return true;
461     }
462 
463     PrintDelimiter();
464     if (lstat(chrooted_path, &st_chrooted) == -1)
465     {
466         /* TODO: should this do print info about the original file? */
467         printf("'%s' no longer exists\n", path);
468         return true;
469     }
470 
471     if ((st_orig.st_mode & S_IFMT) != (st_chrooted.st_mode & S_IFMT))
472     {
473         /* File type changed. */
474         printf("'%s' changed type from %s to %s\n", path,
475                GetFileTypeDescription(st_orig.st_mode),
476                GetFileTypeDescription(st_chrooted.st_mode));
477         ManifestStatInfo(&st_chrooted);
478         ManifestFileDetails(path, &st_chrooted, true);
479         return true;
480     }
481     else
482     {
483         switch (st_chrooted.st_mode & S_IFMT) {
484         case S_IFREG:
485         case S_IFDIR:
486             return RunDiff(path, chrooted_path);
487         default:
488             printf("'%s' is a %s\n", path, GetFileTypeDescription(st_chrooted.st_mode));
489             ManifestStatInfo(&st_chrooted);
490             ManifestFileDetails(path, &st_chrooted, true);
491             return true;
492         }
493     }
494 }
495 
ManifestRenamedFiles()496 bool ManifestRenamedFiles()
497 {
498     bool success = true;
499 
500     const char *renamed_files_file = ToChangesChroot(CHROOT_RENAMES_LIST_FILE);
501     if (access(renamed_files_file, F_OK) == 0)
502     {
503         int fd = safe_open(renamed_files_file, O_RDONLY);
504         if (fd == -1)
505         {
506             Log(LOG_LEVEL_ERR, "Failed to open the file with list of renamed files: %s", GetErrorStr());
507             success = false;
508         }
509 
510         Log(LOG_LEVEL_INFO, "Manifesting renamed files (in the changes chroot)");
511         bool done = false;
512         while (!done)
513         {
514             /* The CHROOT_RENAMES_LIST_FILE contains lines where two consecutive
515              * lines represent the original and the new name of a file (see
516              * RecordFileRenamedInChroot(). */
517 
518             /* TODO: read into a PATH_MAX buffers */
519             char *orig_name;
520             int ret = ReadLenPrefixedString(fd, &orig_name);
521             if (ret > 0)
522             {
523                 char *new_name;
524                 ret = ReadLenPrefixedString(fd, &new_name);
525                 if (ret > 0)
526                 {
527                     success = (success && ManifestRename(orig_name, new_name));
528                     free(new_name);
529                 }
530                 else
531                 {
532                     /* If there was the line with the original name, there
533                      * must be a line with the new name. */
534                     Log(LOG_LEVEL_ERR, "Invalid data about renamed files");
535                     success = false;
536                     done = true;
537                 }
538                 free(orig_name);
539             }
540             else if (ret == 0)
541             {
542                 /* EOF */
543                 done = true;
544             }
545             else
546             {
547                 Log(LOG_LEVEL_ERR, "Failed to read the list of changed files");
548                 success = false;
549                 done = true;
550             }
551         }
552         close(fd);
553     }
554     return success;
555 }
556 
AuditChangedFiles(EvalMode mode,StringSet ** audited_files)557 bool AuditChangedFiles(EvalMode mode, StringSet **audited_files)
558 {
559     assert((mode == EVAL_MODE_SIMULATE_MANIFEST) ||
560            (mode == EVAL_MODE_SIMULATE_MANIFEST_FULL) ||
561            (mode == EVAL_MODE_SIMULATE_DIFF));
562 
563     bool success = true;
564 
565     /* Renames are part of the EVAL_MODE_SIMULATE_MANIFEST audit output which
566      * shows changes. */
567     if (mode != EVAL_MODE_SIMULATE_MANIFEST_FULL)
568     {
569         success = ManifestRenamedFiles();
570     }
571 
572     const char *action;
573     const char *action_ing;
574     const char *file_category;
575     const char *files_list_file;
576     if (mode == EVAL_MODE_SIMULATE_MANIFEST)
577     {
578         action = "manifest";
579         action_ing = "Manifesting";
580         file_category = "changed";
581         files_list_file = ToChangesChroot(CHROOT_CHANGES_LIST_FILE);
582     }
583     else if (mode == EVAL_MODE_SIMULATE_MANIFEST_FULL)
584     {
585         action = "manifest";
586         action_ing = "Manifesting";
587         file_category = "unmodified";
588         files_list_file = ToChangesChroot(CHROOT_KEPT_LIST_FILE);
589     }
590     else
591     {
592         action = "show diff for";
593         action_ing = "Showing diff for";
594         file_category = "changed";
595         files_list_file = ToChangesChroot(CHROOT_CHANGES_LIST_FILE);
596     }
597 
598     /* If the file doesn't exist, there were no changes recorded. */
599     if (access(files_list_file, F_OK) != 0)
600     {
601         Log(LOG_LEVEL_INFO, "No %s files to %s", file_category, action);
602         return true;
603     }
604 
605     int fd = safe_open(files_list_file, O_RDONLY);
606     if (fd == -1)
607     {
608         Log(LOG_LEVEL_ERR, "Failed to open the file with list of %s files: %s", file_category, GetErrorStr());
609         return false;
610     }
611 
612     Log(LOG_LEVEL_INFO, "%s %s files (in the changes chroot)", action_ing, file_category);
613     if (*audited_files == NULL)
614     {
615         *audited_files = StringSetNew();
616     }
617     bool done = false;
618     while (!done)
619     {
620         /* TODO: read into a PATH_MAX buffer */
621         char *path;
622         int ret = ReadLenPrefixedString(fd, &path);
623         if (ret > 0)
624         {
625             /* Each file should only be audited once. */
626             if (!StringSetContains(*audited_files, path))
627             {
628                 if ((mode == EVAL_MODE_SIMULATE_MANIFEST) ||
629                     (mode == EVAL_MODE_SIMULATE_MANIFEST_FULL))
630                 {
631                     success = (success && ManifestFile(path, true));
632                 }
633                 else
634                 {
635                     success = (success && DiffFile(path));
636                 }
637                 StringSetAdd(*audited_files, path);
638             }
639             else
640             {
641                 free(path);
642             }
643         }
644         else if (ret == 0)
645         {
646             /* EOF */
647             done = true;
648         }
649         else
650         {
651             Log(LOG_LEVEL_ERR, "Failed to read the list of %s files", file_category);
652             success = false;
653             done = true;
654         }
655     }
656     close(fd);
657     return success;
658 }
659 
ManifestChangedFiles(StringSet ** audited_files)660 bool ManifestChangedFiles(StringSet **audited_files)
661 {
662     return AuditChangedFiles(EVAL_MODE_SIMULATE_MANIFEST, audited_files);
663 }
664 
ManifestAllFiles(StringSet ** audited_files)665 bool ManifestAllFiles(StringSet **audited_files)
666 {
667     return AuditChangedFiles(EVAL_MODE_SIMULATE_MANIFEST_FULL, audited_files);
668 }
669 
DiffChangedFiles(StringSet ** audited_files)670 bool DiffChangedFiles(StringSet **audited_files)
671 {
672     return AuditChangedFiles(EVAL_MODE_SIMULATE_DIFF, audited_files);
673 }
674 
675 
676 typedef struct PkgOperationRecord_ {
677     char *msg;
678     char *pkg_ver;
679 } PkgOperationRecord;
680 
PkgOperationRecordNew(char * msg,char * pkg_ver)681 static PkgOperationRecord *PkgOperationRecordNew(char *msg, char *pkg_ver)
682 {
683     PkgOperationRecord *ret = xmalloc(sizeof(PkgOperationRecord));
684     ret->msg = msg;
685     ret->pkg_ver = pkg_ver;
686 
687     return ret;
688 }
689 
PkgOperationRecordDestroy(PkgOperationRecord * pkg_op)690 static void PkgOperationRecordDestroy(PkgOperationRecord *pkg_op)
691 {
692     if (pkg_op != NULL)
693     {
694         free(pkg_op->msg);
695         free(pkg_op->pkg_ver);
696         free(pkg_op);
697     }
698 }
699 
PkgVersionIsGreater(const char * ver1,const char * ver2)700 static inline bool PkgVersionIsGreater(const char *ver1, const char *ver2)
701 {
702     /* Empty/missing versions should be handled separately based on the
703      * operations they appear in */
704     assert(!NULL_OR_EMPTY(ver1) && !NULL_OR_EMPTY(ver2));
705 
706     /* "latest" is greater than any version */
707     if (StringEqual(ver1, "latest"))
708     {
709         return true;
710     }
711 
712     /* TODO: do real version comparison */
713     return (StringSafeCompare(ver1, ver2) == 1);
714 }
715 
GetPkgOperationMsg(ChrootPkgOperationCode op,const char * pkg_name,const char * pkg_arch,const char * pkg_ver)716 static inline char *GetPkgOperationMsg(ChrootPkgOperationCode op, const char *pkg_name, const char *pkg_arch, const char *pkg_ver)
717 {
718     const char *op_str = "";
719     switch (op)
720     {
721     case CHROOT_PKG_OPERATION_CODE_INSTALL:
722         op_str = "installed";
723         break;
724     case CHROOT_PKG_OPERATION_CODE_REMOVE:
725         op_str = "removed";
726         break;
727     case CHROOT_PKG_OPERATION_CODE_PRESENT:
728         op_str = "present";
729         break;
730     case CHROOT_PKG_OPERATION_CODE_ABSENT:
731         op_str = "absent";
732         break;
733     default:
734         debug_abort_if_reached();
735     }
736 
737     char *msg;
738     if (!NULL_OR_EMPTY(pkg_arch) && !NULL_OR_EMPTY(pkg_ver))
739     {
740         xasprintf(&msg, "Package '%s-%s [%s]' would be %s\n", pkg_name, pkg_arch, pkg_ver, op_str);
741     }
742     else if (!NULL_OR_EMPTY(pkg_arch))
743     {
744         xasprintf(&msg, "Package '%s-%s' would be %s\n", pkg_name, pkg_arch, op_str);
745     }
746     else if (!NULL_OR_EMPTY(pkg_ver))
747     {
748         xasprintf(&msg, "Package '%s [%s]' would be %s\n", pkg_name, pkg_ver, op_str);
749     }
750     else
751     {
752         xasprintf(&msg, "Package '%s' would be %s\n", pkg_name, op_str);
753     }
754 
755     return msg;
756 }
757 
DiffPkgOperations()758 bool DiffPkgOperations()
759 {
760     const char *pkgs_ops_csv_file = ToChangesChroot(CHROOT_PKGS_OPS_FILE);
761     if (access(pkgs_ops_csv_file, F_OK) != 0)
762     {
763         Log(LOG_LEVEL_INFO, "No package operations done by the agent run");
764         return true;
765     }
766 
767     FILE *csv_file = safe_fopen(pkgs_ops_csv_file, "r");
768 
769     if (csv_file == NULL)
770     {
771         Log(LOG_LEVEL_ERR, "Failed to open the file with package operations records");
772         return false;
773     }
774 
775     Map *installed = MapNew(StringHash_untyped, StringEqual_untyped, free, (MapDestroyDataFn) PkgOperationRecordDestroy);
776     Map *removed = MapNew(StringHash_untyped, StringEqual_untyped, free, (MapDestroyDataFn) PkgOperationRecordDestroy);
777     char *line;
778     while ((line = GetCsvLineNext(csv_file)) != NULL)
779     {
780         Seq *fields = SeqParseCsvString(line);
781         free(line);
782         if ((fields == NULL) || (SeqLength(fields) != 4))
783         {
784             Log(LOG_LEVEL_ERR, "Invalid package operation record: '%s'", line);
785             SeqDestroy(fields);
786             continue;
787         }
788 
789         /* See RecordPkgOperationInChroot() */
790         const char *op       = SeqAt(fields, 0);
791         const char *pkg_name = SeqAt(fields, 1);
792         const char *pkg_ver  = SeqAt(fields, 2);
793         const char *pkg_arch = SeqAt(fields, 3);
794 
795         /* These two must always be set properly. */
796         assert(!NULL_OR_EMPTY(op));
797         assert(!NULL_OR_EMPTY(pkg_name));
798 
799         /* We need to have a key for the map encoding package name and
800          * architecture. Let's use the sequence "-_-" as a separator to avoid
801          * collisions with possible weird package names. */
802         char *name_arch;
803         xasprintf(&name_arch, "%s-_-%s", pkg_name, pkg_arch ? pkg_arch : "");
804 
805         if (*op == CHROOT_PKG_OPERATION_CODE_PRESENT)
806         {
807             /* 'present' operation means that the package was requested to be present and it already
808              * was, so installation didn't happen. However, package operations are not properly
809              * simulated, so the package may have been simulate-removed before and the 'present'
810              * operation would in reality mean the package would be installed back. It's only
811              * recorded as a 'present' operation because the removal doesn't happen in simulate mode
812              * and so the package is still seen as present in the system (package cache).
813              *
814              * This means that a 'present' operation after 'remove' operation results in no
815              * difference (the package would be installed back) so the potential message about the
816              * removal should be removed. */
817             MapRemove(removed, name_arch);
818         }
819         else if (*op == CHROOT_PKG_OPERATION_CODE_ABSENT)
820         {
821             /* The same logic as above applies here for an originally absent package that is
822              * installed and then reported as absent again. No diff to report, just remove the
823              * message about the package installation. */
824 
825             /* However, if a different specific version is reported as absent than the version that
826              * would have been installed, this removal would not remove the installed package
827              * (because of version mismatch). */
828             if (NULL_OR_EMPTY(pkg_ver))
829             {
830                 MapRemove(installed, name_arch);
831             }
832             else
833             {
834                 PkgOperationRecord *record = MapGet(installed, name_arch);
835                 if ((record != NULL) && StringEqual(pkg_ver, record->pkg_ver))
836                 {
837                     /* Matching version being removed -> cancel the installation */
838                     MapRemove(installed, name_arch);
839                 }
840             }
841         }
842         else if (*op == CHROOT_PKG_OPERATION_CODE_INSTALL)
843         {
844             /* Package would be installed if there is no previous 'install' operation record with a
845              * higher version.
846              *                                   OR
847              * Package would be removed and now it would be installed. However, if the 'install'
848              * operation had the same version as what is already present in the system (remove in
849              * simulation mode doesn't remove the package), it would be reported as 'present'
850              * operation. So 'install' operation must mean a newer version than what's present would
851              * be installed. */
852 
853             PkgOperationRecord *prev_record = MapGet(installed, name_arch);
854             if ((prev_record == NULL) || PkgVersionIsGreater(pkg_ver, prev_record->pkg_ver))
855             {
856                 char *msg = GetPkgOperationMsg(CHROOT_PKG_OPERATION_CODE_INSTALL,
857                                                pkg_name, pkg_arch, pkg_ver);
858                 PkgOperationRecord *record = PkgOperationRecordNew(msg, SafeStringDuplicate(pkg_ver));
859                 MapInsert(installed, name_arch, record);
860                 name_arch = NULL; /* name_arch is now owned by the map (as a key) */
861             }
862 
863             /* Package installation cancels a previous removal (if any). */
864             MapRemove(removed, name_arch);
865         }
866         else
867         {
868             assert(*op == CHROOT_PKG_OPERATION_CODE_REMOVE); /* The only option not covered above. */
869 
870             /* If there is a previous 'remove' operation record with version specified, prefer that
871              * message over a new message without version specification as the net result would be
872              * the package being removed, in the version that was pressent. */
873             PkgOperationRecord *prev_record = MapGet(removed, name_arch);
874             bool insert_new_msg = ((prev_record == NULL) || (NULL_OR_EMPTY(prev_record->pkg_ver)));
875 
876             /* If there is a previous 'install' operation and now there is a 'remove' operation it
877              * means that the package was initially present, then updated by the 'install' operation
878              * and now it is attempted to be removed. If the 'remove' operation specifies a version
879              * then in case the versions match, the net result would be no change (install and
880              * remove). If there is a mismatch between the versions, the installation would happen,
881              * but the removal would fail. If no version is specified for the 'remove' operation. it
882              * would remove the installed package. */
883             if (NULL_OR_EMPTY(pkg_ver))
884             {
885                 /* No version specified, remove the installation message (if any). */
886                 MapRemove(installed, name_arch);
887             }
888             else
889             {
890                 PkgOperationRecord *inst_record = MapGet(installed, name_arch);
891                 if ((inst_record != NULL) && (StringEqual(pkg_ver, inst_record->pkg_ver)))
892                 {
893                     MapRemove(installed, name_arch);
894                 }
895                 else
896                 {
897                     /* Keeping the install message, the removal would make no change. */
898                     insert_new_msg = false;
899                 }
900             }
901 
902             if (insert_new_msg)
903             {
904                 char *msg = GetPkgOperationMsg(CHROOT_PKG_OPERATION_CODE_REMOVE,
905                                                pkg_name, pkg_arch, pkg_ver);
906                 PkgOperationRecord *record = PkgOperationRecordNew(msg, SafeStringDuplicate(pkg_ver));
907                 MapInsert(removed, name_arch, record);
908                 name_arch = NULL; /* name_arch is now owned by the map (as a key) */
909             }
910         }
911         SeqDestroy(fields);
912         free(name_arch);
913     }
914     fclose(csv_file);
915 
916     if ((MapSize(installed) == 0) && (MapSize(removed) == 0))
917     {
918         Log(LOG_LEVEL_INFO, "No differences in installed packages to report");
919 
920         MapDestroy(installed);
921         MapDestroy(removed);
922 
923         return true;
924     }
925 
926     Log(LOG_LEVEL_INFO, "Showing differences in installed packages");
927     MapIterator i = MapIteratorInit(installed);
928     MapKeyValue *item;
929     while ((item = MapIteratorNext(&i)))
930     {
931         PkgOperationRecord *value = item->value;
932         const char *msg = value->msg;
933         puts(msg);
934     }
935     i = MapIteratorInit(removed);
936     while ((item = MapIteratorNext(&i)))
937     {
938         PkgOperationRecord *value = item->value;
939         const char *msg = value->msg;
940         puts(msg);
941     }
942 
943     MapDestroy(installed);
944     MapDestroy(removed);
945 
946     return true;
947 }
948 
ManifestPkgOperations()949 bool ManifestPkgOperations()
950 {
951     const char *pkgs_ops_csv_file = ToChangesChroot(CHROOT_PKGS_OPS_FILE);
952     if (access(pkgs_ops_csv_file, F_OK) != 0)
953     {
954         Log(LOG_LEVEL_INFO, "No package operations done by the agent run");
955         return true;
956     }
957 
958     FILE *csv_file = safe_fopen(pkgs_ops_csv_file, "r");
959 
960     if (csv_file == NULL)
961     {
962         Log(LOG_LEVEL_ERR, "Failed to open the file with package operations records");
963         return false;
964     }
965 
966     Map *present = MapNew(StringHash_untyped, StringEqual_untyped, free, (MapDestroyDataFn) PkgOperationRecordDestroy);
967     Map *absent = MapNew(StringHash_untyped, StringEqual_untyped, free, (MapDestroyDataFn) PkgOperationRecordDestroy);
968     char *line;
969     while ((line = GetCsvLineNext(csv_file)) != NULL)
970     {
971         Seq *fields = SeqParseCsvString(line);
972         free(line);
973         if ((fields == NULL) || (SeqLength(fields) != 4))
974         {
975             Log(LOG_LEVEL_ERR, "Invalid package operation record: '%s'", line);
976             SeqDestroy(fields);
977             continue;
978         }
979 
980         /* See RecordPkgOperationInChroot() */
981         const char *op       = SeqAt(fields, 0);
982         const char *pkg_name = SeqAt(fields, 1);
983         const char *pkg_ver  = SeqAt(fields, 2);
984         const char *pkg_arch = SeqAt(fields, 3);
985 
986         /* These two must always be set properly. */
987         assert(!NULL_OR_EMPTY(op));
988         assert(!NULL_OR_EMPTY(pkg_name));
989 
990         /* We need to have a key for the map encoding package name and
991          * architecture. Let's use the sequence "-_-" as a separator to avoid
992          * collisions with possible weird package names. */
993         char *name_arch;
994         xasprintf(&name_arch, "%s-_-%s", pkg_name, pkg_arch ? pkg_arch : "");
995 
996         if ((*op == CHROOT_PKG_OPERATION_CODE_INSTALL) ||
997             (*op == CHROOT_PKG_OPERATION_CODE_PRESENT))
998         {
999             /* If there is a previous install/present operation, we want to choose the message with
1000              * the higher version or the message which has a specific version (if any). */
1001             PkgOperationRecord *prev_record = MapGet(present, name_arch);
1002             if ((prev_record == NULL) ||
1003                 (NULL_OR_EMPTY(prev_record->pkg_ver) && !NULL_OR_EMPTY(pkg_ver)) ||
1004                 (!NULL_OR_EMPTY(pkg_ver) && !NULL_OR_EMPTY(prev_record->pkg_ver) &&
1005                  PkgVersionIsGreater(pkg_ver, prev_record->pkg_ver)))
1006             {
1007                 char *msg = GetPkgOperationMsg(CHROOT_PKG_OPERATION_CODE_PRESENT,
1008                                                pkg_name, pkg_arch, pkg_ver);
1009                 PkgOperationRecord *record = PkgOperationRecordNew(msg, SafeStringDuplicate(pkg_ver));
1010                 MapInsert(present, name_arch, record);
1011                 name_arch = NULL; /* name_arch is now owned by the map (as a key) */
1012             }
1013 
1014             /* Cancels any previous remove/absent message. */
1015             MapRemove(absent, name_arch);
1016         }
1017         else
1018         {
1019             assert((*op == CHROOT_PKG_OPERATION_CODE_REMOVE) ||
1020                    (*op == CHROOT_PKG_OPERATION_CODE_ABSENT));
1021 
1022             /* If there is a 'present' message with a different version than the version specified
1023              * here (if any), we know that the removal would fail and so it should not be
1024              * reported. */
1025             PkgOperationRecord *present_record = MapGet(present, name_arch);
1026             if ((present_record == NULL) ||
1027                 NULL_OR_EMPTY(present_record->pkg_ver) || NULL_OR_EMPTY(pkg_ver) ||
1028                 StringEqual(present_record->pkg_ver, pkg_ver))
1029             {
1030                 /* Remove the 'present' message now that we know the removal would/should work. */
1031                 MapRemove(present, name_arch);
1032 
1033                 /* If there is a previous 'absent' message, we want to use the message with no
1034                  * version specification (if any) because it's more generic. */
1035                 PkgOperationRecord *prev_record = MapGet(absent, name_arch);
1036                 if ((prev_record == NULL) ||
1037                     (!NULL_OR_EMPTY(prev_record->pkg_ver) && NULL_OR_EMPTY(pkg_ver)))
1038                 {
1039                     char *msg = GetPkgOperationMsg(CHROOT_PKG_OPERATION_CODE_ABSENT,
1040                                                    pkg_name, pkg_arch, pkg_ver);
1041                     PkgOperationRecord *record = PkgOperationRecordNew(msg, SafeStringDuplicate(pkg_ver));
1042                     MapInsert(absent, name_arch, record);
1043                     name_arch = NULL; /* name_arch is now owned by the map (as a key) */
1044                 }
1045             }
1046         }
1047         SeqDestroy(fields);
1048         free(name_arch);
1049     }
1050     fclose(csv_file);
1051 
1052     /* If there were package operations (the file with the records exists, which is checked above),
1053      * there must be something to manifest. Otherwise, there's a flaw in the logic above,
1054      * manipulating the maps. */
1055     assert((MapSize(present) != 0) || (MapSize(absent) != 0));
1056 
1057     Log(LOG_LEVEL_INFO, "Manifesting present and absent packages");
1058     MapIterator i = MapIteratorInit(present);
1059     MapKeyValue *item;
1060     while ((item = MapIteratorNext(&i)))
1061     {
1062         PkgOperationRecord *value = item->value;
1063         const char *msg = value->msg;
1064         puts(msg);
1065     }
1066     i = MapIteratorInit(absent);
1067     while ((item = MapIteratorNext(&i)))
1068     {
1069         PkgOperationRecord *value = item->value;
1070         const char *msg = value->msg;
1071         puts(msg);
1072     }
1073 
1074     MapDestroy(present);
1075     MapDestroy(absent);
1076 
1077     return true;
1078 }
1079