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 <files_lib.h>
26 
27 #include <files_interfaces.h>
28 #include <files_names.h>
29 #include <files_copy.h>
30 #include <item_lib.h>
31 #include <promises.h>
32 #include <matching.h>
33 #include <misc_lib.h>
34 #include <dir.h>
35 #include <policy.h>
36 #include <string_lib.h>
37 #include <eval_context.h>       /* MakingChanges(), RecordFailure() */
38 #include <actuator.h>           /* PromiseResultUpdate() */
39 
40 
41 static Item *ROTATED = NULL; /* GLOBAL_X */
42 
43 
44 /*********************************************************************/
45 
PurgeItemList(Item ** list,char * name)46 void PurgeItemList(Item **list, char *name)
47 {
48     Item *ip, *copy = NULL;
49     struct stat sb;
50 
51     CopyList(&copy, *list);
52 
53     for (ip = copy; ip != NULL; ip = ip->next)
54     {
55         if (stat(ip->name, &sb) == -1)
56         {
57             Log(LOG_LEVEL_VERBOSE,
58                 "Purging file '%s' from '%s' list as it no longer exists",
59                 ip->name, name);
60             DeleteItemLiteral(list, ip->name);
61         }
62     }
63 
64     DeleteItemList(copy);
65 }
66 
FileWriteOver(char * filename,char * contents)67 bool FileWriteOver(char *filename, char *contents)
68 {
69     FILE *fp = safe_fopen_create_perms(filename, "w", CF_PERMS_DEFAULT);
70 
71     if(fp == NULL)
72     {
73         return false;
74     }
75 
76     size_t bytes_to_write = strlen(contents);
77 
78     size_t bytes_written = fwrite(contents, 1, bytes_to_write, fp);
79 
80     bool res = true;
81 
82     if(bytes_written != bytes_to_write)
83     {
84         res = false;
85     }
86 
87     if(fclose(fp) != 0)
88     {
89         res = false;
90     }
91 
92     return res;
93 }
94 
95 
96 /*********************************************************************/
97 
98 static bool MakeParentDirectoryImpl(EvalContext *ctx, const Promise *pp, const Attributes *attr,
99                                     PromiseResult *result, const char *parentandchild,
100                                     bool force, bool internal, bool *created);
101 
MakeParentDirectory(const char * parentandchild,bool force,bool * created)102 bool MakeParentDirectory(const char *parentandchild, bool force, bool *created)
103 {
104     /* just use the complex function with no promise info */
105     return MakeParentDirectoryImpl(NULL, NULL, NULL, NULL,
106                                    parentandchild, force, false, created);
107 }
108 
MakeParentInternalDirectory(const char * parentandchild,bool force,bool * created)109 bool MakeParentInternalDirectory(const char *parentandchild, bool force, bool *created)
110 {
111     /* just use the complex function with no promise info */
112     return MakeParentDirectoryImpl(NULL, NULL, NULL, NULL,
113                                    parentandchild, force, true, created);
114 }
115 
MakeParentDirectoryForPromise(EvalContext * ctx,const Promise * pp,const Attributes * attr,PromiseResult * result,const char * parentandchild,bool force,bool * created)116 bool MakeParentDirectoryForPromise(EvalContext *ctx, const Promise *pp, const Attributes *attr,
117                                    PromiseResult *result, const char *parentandchild,
118                                    bool force, bool *created)
119 {
120     return MakeParentDirectoryImpl(ctx, pp, attr, result, parentandchild, force, false, created);
121 }
122 
MakeParentDirectoryImpl(EvalContext * ctx,const Promise * pp,const Attributes * attr,PromiseResult * result,const char * parentandchild,bool force,bool internal,bool * created)123 static bool MakeParentDirectoryImpl(EvalContext *ctx, const Promise *pp, const Attributes *attr,
124                                     PromiseResult *result, const char *parentandchild,
125                                     bool force, bool internal, bool *created)
126 {
127     char *sp;
128     char currentpath[CF_BUFSIZE];
129     char pathbuf[CF_BUFSIZE];
130     struct stat statbuf;
131     mode_t mask;
132     int rootlen;
133 
134     const char *changes_parentandchild = parentandchild;
135     if (!internal && ChrootChanges())
136     {
137         changes_parentandchild = ToChangesChroot(parentandchild);
138     }
139 
140     const bool have_promise_info = ((ctx != NULL) && (pp != NULL) && (attr != NULL) && (result != NULL));
141 
142     if (created != NULL)
143     {
144         *created = false;
145     }
146 
147 #ifdef __APPLE__
148 /* Keeps track of if dealing w. resource fork */
149     int rsrcfork;
150 
151     rsrcfork = 0;
152 
153     char *tmpstr;
154 #endif
155 
156     Log(LOG_LEVEL_DEBUG, "Trying to create a parent directory%s for: %s",
157         force ? " (force applied)" : "",
158         parentandchild);
159 
160     if (!IsAbsoluteFileName(parentandchild))
161     {
162         Log(LOG_LEVEL_ERR,
163             "Will not create directories for a relative filename: %s",
164             parentandchild);
165         return false;
166     }
167 
168     strlcpy(pathbuf, changes_parentandchild, CF_BUFSIZE);   /* local copy */
169 
170 #ifdef __APPLE__
171     if (strstr(pathbuf, _PATH_RSRCFORKSPEC) != NULL)
172     {
173         rsrcfork = 1;
174     }
175 #endif
176 
177 /* skip link name */
178 
179     sp = (char *) LastFileSeparator(pathbuf);                /* de-constify */
180 
181     if (sp == NULL)
182     {
183         sp = pathbuf;
184     }
185     *sp = '\0';
186 
187     DeleteSlash(pathbuf);
188 
189     if (lstat(pathbuf, &statbuf) != -1)
190     {
191         if (S_ISLNK(statbuf.st_mode))
192         {
193             Log(LOG_LEVEL_VERBOSE, "'%s' is a symbolic link, not a directory",
194                 pathbuf);
195         }
196 
197         if (force)              /* force in-the-way directories aside */
198         {
199             struct stat dir;
200             stat(pathbuf, &dir);
201 
202             /* If the target directory exists as a directory, no problem. */
203             /* If the target directory exists but is not a directory, then
204              * rename it to ".cf-moved": */
205             if (!S_ISDIR(dir.st_mode))
206             {
207                 struct stat sbuf;
208 
209                 strcpy(currentpath, pathbuf);
210                 DeleteSlash(currentpath);
211                 /* TODO overflow check! */
212                 strlcat(currentpath, ".cf-moved", sizeof(currentpath));
213                 Log(LOG_LEVEL_VERBOSE,
214                     "Moving obstructing file/link %s to %s to make directory",
215                     pathbuf, currentpath);
216 
217                 if (have_promise_info &&
218                     !MakingChanges(ctx, pp, attr, result,
219                                    "move obstructing file/link '%s' to '%s' to make directories for '%s'",
220                                    pathbuf, currentpath, parentandchild))
221                 {
222                     return true;
223                 }
224 
225                 /* Remove possibly pre-existing ".cf-moved" backup object. */
226                 if (lstat(currentpath, &sbuf) != -1)
227                 {
228                     if (S_ISDIR(sbuf.st_mode))                 /* directory */
229                     {
230                         if (!DeleteDirectoryTree(currentpath))
231                         {
232                             Log(LOG_LEVEL_WARNING,
233                                 "Failed to remove directory '%s' while trying to remove a backup",
234                                 currentpath);
235                         }
236                     }
237                     else                                 /* not a directory */
238                     {
239                         if (unlink(currentpath) == -1)
240                         {
241                             Log(LOG_LEVEL_WARNING,
242                                 "Couldn't remove file/link '%s' while trying to remove a backup"
243                                 " (unlink: %s)", currentpath, GetErrorStr());
244                         }
245                     }
246                 }
247 
248                 /* And then rename the current object to ".cf-moved". */
249                 if (rename(pathbuf, currentpath) == -1)
250                 {
251                     if (have_promise_info)
252                     {
253                         RecordFailure(ctx, pp, attr,
254                                       "Couldn't rename '%s' to .cf-moved (rename: %s)",
255                                       pathbuf, GetErrorStr());
256                         *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
257                     }
258                     else
259                     {
260                         Log(LOG_LEVEL_ERR,
261                             "Couldn't rename '%s' to .cf-moved (rename: %s)",
262                             pathbuf, GetErrorStr());
263                     }
264                     return false;
265                 }
266                 else if (have_promise_info)
267                 {
268                     RecordChange(ctx, pp, attr,
269                                  "Moved obstructing file/link '%s' to '%s' to make directories for '%s'",
270                                  pathbuf, currentpath, parentandchild);
271                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_CHANGE);
272                 }
273             }
274         }
275         else
276         {
277             if (!S_ISLNK(statbuf.st_mode) && !S_ISDIR(statbuf.st_mode))
278             {
279                 if (have_promise_info)
280                 {
281                     RecordFailure(ctx, pp, attr,
282                                   "The object '%s' is not a directory."
283                                   " Cannot make a new directory without deleting it.",
284                                   pathbuf);
285                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
286                 }
287                 else
288                 {
289                     Log(LOG_LEVEL_ERR, "The object '%s' is not a directory."
290                         " Cannot make a new directory without deleting it.",
291                         pathbuf);
292                 }
293                 return false;
294             }
295         }
296     }
297 
298 /* Now we make directories descending from the root folder down to the leaf */
299 
300     currentpath[0] = '\0';
301 
302     rootlen = RootDirLength(changes_parentandchild);
303     /* currentpath is not NULL terminated on purpose! */
304     strncpy(currentpath, changes_parentandchild, rootlen);
305 
306     for (size_t z = rootlen; changes_parentandchild[z] != '\0'; z++)
307     {
308         const char c = changes_parentandchild[z];
309 
310         /* Copy up to the next separator. */
311         if (!IsFileSep(c))
312         {
313             currentpath[z] = c;
314             continue;
315         }
316 
317         const char path_file_separator = c;
318         currentpath[z]                 = '\0';
319 
320         /* currentpath is complete path for each of the parent directories.  */
321 
322         if (currentpath[0] == '\0')
323         {
324             /* We are at dir "/" of an absolute path, no need to create. */
325         }
326         /* WARNING: on Windows stat() fails if path has a trailing slash! */
327         else if (stat(currentpath, &statbuf) == -1)
328         {
329             if (!have_promise_info ||
330                 MakingChanges(ctx, pp, attr, result,
331                               "make directory '%s' for '%s'", currentpath, parentandchild))
332             {
333                 mask = umask(0);
334 
335                 if (mkdir(currentpath, DEFAULTMODE) == -1)
336                 {
337                     if (errno != EEXIST)
338                     {
339                         if (have_promise_info)
340                         {
341                             RecordFailure(ctx, pp, attr, "Failed to make directory: %s (mkdir: %s)",
342                                           currentpath, GetErrorStr());
343                             *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
344                         }
345                         else
346                         {
347                             Log(LOG_LEVEL_ERR,
348                                 "Failed to make directory: %s (mkdir: %s)",
349                                 currentpath, GetErrorStr());
350                         }
351                         umask(mask);
352                         return false;
353                     }
354                 }
355                 else
356                 {
357                     if (created != NULL)
358                     {
359                         *created = true;
360                     }
361                 }
362                 umask(mask);
363             }
364         }
365         else
366         {
367             if (!S_ISDIR(statbuf.st_mode))
368             {
369 #ifdef __APPLE__
370                 /* Ck if rsrc fork */
371                 if (rsrcfork)
372                 {
373                     tmpstr = xmalloc(CF_BUFSIZE);
374                     strlcpy(tmpstr, currentpath, CF_BUFSIZE);
375                     strncat(tmpstr, _PATH_FORKSPECIFIER, CF_BUFSIZE);
376 
377                     /* CFEngine removed terminating slashes */
378                     DeleteSlash(tmpstr);
379 
380                     if (strncmp(tmpstr, pathbuf, CF_BUFSIZE) == 0)
381                     {
382                         free(tmpstr);
383                         return true;
384                     }
385                     free(tmpstr);
386                 }
387 #endif
388                 if (have_promise_info)
389                 {
390                     RecordFailure(ctx, pp, attr,
391                                   "Cannot make %s - %s is not a directory!",
392                                   pathbuf, currentpath);
393                     *result = PromiseResultUpdate(*result, PROMISE_RESULT_FAIL);
394                 }
395                 else
396                 {
397                     Log(LOG_LEVEL_ERR,
398                         "Cannot make %s - %s is not a directory!",
399                         pathbuf, currentpath);
400                 }
401                 return false;
402             }
403         }
404 
405         currentpath[z] = path_file_separator;
406     }
407 
408     Log(LOG_LEVEL_DEBUG, "Directory for '%s' exists. Okay", parentandchild);
409     return true;
410 }
411 
LoadFileAsItemList(Item ** liststart,const char * file,EditDefaults edits)412 bool LoadFileAsItemList(Item **liststart, const char *file, EditDefaults edits)
413 {
414     {
415         struct stat statbuf;
416         if (stat(file, &statbuf) == -1)
417         {
418             Log(LOG_LEVEL_VERBOSE, "The proposed file '%s' could not be loaded. (stat: %s)", file, GetErrorStr());
419             return false;
420         }
421 
422         if (edits.maxfilesize != 0 && statbuf.st_size > edits.maxfilesize)
423         {
424             Log(LOG_LEVEL_INFO, "File '%s' is bigger than the edit limit. max_file_size = %jd > %d bytes", file,
425                   (intmax_t) statbuf.st_size, edits.maxfilesize);
426             return false;
427         }
428 
429         if (!S_ISREG(statbuf.st_mode))
430         {
431             Log(LOG_LEVEL_INFO, "%s is not a plain file", file);
432             return false;
433         }
434     }
435 
436     FILE *fp = safe_fopen(file, "rt");
437     if (!fp)
438     {
439         Log(LOG_LEVEL_INFO, "Couldn't read file '%s' for editing. (fopen: %s)", file, GetErrorStr());
440         return false;
441     }
442 
443     Buffer *concat = BufferNew();
444 
445     size_t line_size = CF_BUFSIZE;
446     char *line = xmalloc(line_size);
447     bool result = true;
448 
449     for (;;)
450     {
451         ssize_t num_read = CfReadLine(&line, &line_size, fp);
452         if (num_read == -1)
453         {
454             if (!feof(fp))
455             {
456                 Log(LOG_LEVEL_ERR,
457                     "Unable to read contents of file: %s (fread: %s)",
458                     file, GetErrorStr());
459                 result = false;
460             }
461             break;
462         }
463 
464         if (edits.joinlines && *(line + strlen(line) - 1) == '\\')
465         {
466             *(line + strlen(line) - 1) = '\0';
467 
468             BufferAppend(concat, line, num_read);
469         }
470         else
471         {
472             BufferAppend(concat, line, num_read);
473             if (!feof(fp) || (BufferSize(concat) > 0))
474             {
475                 AppendItem(liststart, BufferData(concat), NULL);
476             }
477         }
478 
479         BufferClear(concat);
480     }
481 
482     free(line);
483     BufferDestroy(concat);
484     fclose(fp);
485     return result;
486 }
487 
TraverseDirectoryTreeInternal(const char * base_path,const char * current_path,int (* callback)(const char *,const struct stat *,void *),void * user_data)488 bool TraverseDirectoryTreeInternal(const char *base_path,
489                                    const char *current_path,
490                                    int (*callback)(const char *, const struct stat *, void *),
491                                    void *user_data)
492 {
493     Dir *dirh = DirOpen(base_path);
494     if (!dirh)
495     {
496         if (errno == ENOENT)
497         {
498             return true;
499         }
500 
501         Log(LOG_LEVEL_INFO, "Unable to open directory '%s' during traversal of directory tree '%s' (opendir: %s)",
502             current_path, base_path, GetErrorStr());
503         return false;
504     }
505 
506     bool failed = false;
507     for (const struct dirent *dirp = DirRead(dirh); dirp != NULL; dirp = DirRead(dirh))
508     {
509         if (!strcmp(dirp->d_name, ".") || !strcmp(dirp->d_name, ".."))
510         {
511             continue;
512         }
513 
514         char sub_path[CF_BUFSIZE];
515         snprintf(sub_path, CF_BUFSIZE, "%s" FILE_SEPARATOR_STR "%s", current_path, dirp->d_name);
516 
517         struct stat lsb;
518         if (lstat(sub_path, &lsb) == -1)
519         {
520             if (errno == ENOENT)
521             {
522                 /* File disappeared on its own */
523                 continue;
524             }
525 
526             Log(LOG_LEVEL_VERBOSE, "Unable to stat file '%s' during traversal of directory tree '%s' (lstat: %s)",
527                 current_path, base_path, GetErrorStr());
528             failed = true;
529         }
530         else
531         {
532             if (S_ISDIR(lsb.st_mode))
533             {
534                 if (!TraverseDirectoryTreeInternal(base_path, sub_path, callback, user_data))
535                 {
536                     failed = true;
537                 }
538             }
539             else
540             {
541                 if (callback(sub_path, &lsb, user_data) == -1)
542                 {
543                     failed = true;
544                 }
545             }
546         }
547     }
548 
549     DirClose(dirh);
550     return !failed;
551 }
552 
TraverseDirectoryTree(const char * path,int (* callback)(const char *,const struct stat *,void *),void * user_data)553 bool TraverseDirectoryTree(const char *path,
554                            int (*callback)(const char *, const struct stat *, void *),
555                            void *user_data)
556 {
557     return TraverseDirectoryTreeInternal(path, path, callback, user_data);
558 }
559 
560 typedef struct
561 {
562     unsigned char buffer[1024];
563     const char **extensions_filter;
564     EVP_MD_CTX *crypto_context;
565     unsigned char **digest;
566 } HashDirectoryTreeState;
567 
HashDirectoryTreeCallback(const char * filename,ARG_UNUSED const struct stat * sb,void * user_data)568 int HashDirectoryTreeCallback(const char *filename, ARG_UNUSED const struct stat *sb, void *user_data)
569 {
570     HashDirectoryTreeState *state = user_data;
571     bool ignore = true;
572     for (size_t i = 0; state->extensions_filter[i]; i++)
573     {
574         if (StringEndsWith(filename, state->extensions_filter[i]))
575         {
576             ignore = false;
577             break;
578         }
579     }
580 
581     if (ignore)
582     {
583         return 0;
584     }
585 
586     FILE *file = fopen(filename, "rb");
587     if (!file)
588     {
589         Log(LOG_LEVEL_ERR, "Cannot open file for hashing '%s'. (fopen: %s)", filename, GetErrorStr());
590         return -1;
591     }
592 
593     size_t len = 0;
594     char buffer[1024];
595     while ((len = fread(buffer, 1, 1024, file)))
596     {
597         EVP_DigestUpdate(state->crypto_context, state->buffer, len);
598     }
599 
600     fclose(file);
601     return 0;
602 }
603 
HashDirectoryTree(const char * path,const char ** extensions_filter,EVP_MD_CTX * crypto_context)604 bool HashDirectoryTree(const char *path,
605                        const char **extensions_filter,
606                        EVP_MD_CTX *crypto_context)
607 {
608     HashDirectoryTreeState state;
609     memset(state.buffer, 0, 1024);
610     state.extensions_filter = extensions_filter;
611     state.crypto_context = crypto_context;
612 
613     return TraverseDirectoryTree(path, HashDirectoryTreeCallback, &state);
614 }
615 
RotateFiles(const char * name,int number)616 void RotateFiles(const char *name, int number)
617 {
618     int i, fd;
619     struct stat statbuf;
620     char from[CF_BUFSIZE], to[CF_BUFSIZE];
621 
622     if (IsItemIn(ROTATED, name))
623     {
624         return;
625     }
626 
627     PrependItem(&ROTATED, name, NULL);
628 
629     if (stat(name, &statbuf) == -1)
630     {
631         Log(LOG_LEVEL_VERBOSE, "No access to file %s", name);
632         return;
633     }
634 
635     for (i = number - 1; i > 0; i--)
636     {
637         snprintf(from, CF_BUFSIZE, "%s.%d", name, i);
638         snprintf(to, CF_BUFSIZE, "%s.%d", name, i + 1);
639 
640         if (rename(from, to) == -1)
641         {
642             Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
643         }
644 
645         snprintf(from, CF_BUFSIZE, "%s.%d.gz", name, i);
646         snprintf(to, CF_BUFSIZE, "%s.%d.gz", name, i + 1);
647 
648         if (rename(from, to) == -1)
649         {
650             Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
651         }
652 
653         snprintf(from, CF_BUFSIZE, "%s.%d.Z", name, i);
654         snprintf(to, CF_BUFSIZE, "%s.%d.Z", name, i + 1);
655 
656         if (rename(from, to) == -1)
657         {
658             Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
659         }
660 
661         snprintf(from, CF_BUFSIZE, "%s.%d.bz", name, i);
662         snprintf(to, CF_BUFSIZE, "%s.%d.bz", name, i + 1);
663 
664         if (rename(from, to) == -1)
665         {
666             Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
667         }
668 
669         snprintf(from, CF_BUFSIZE, "%s.%d.bz2", name, i);
670         snprintf(to, CF_BUFSIZE, "%s.%d.bz2", name, i + 1);
671 
672         if (rename(from, to) == -1)
673         {
674             Log(LOG_LEVEL_DEBUG, "Rename failed in RotateFiles '%s' -> '%s'", name, from);
675         }
676     }
677 
678     snprintf(to, CF_BUFSIZE, "%s.1", name);
679 
680     if (CopyRegularFileDisk(name, to) == false)
681     {
682         Log(LOG_LEVEL_DEBUG, "Copy failed in RotateFiles '%s' -> '%s'", name, to);
683         return;
684     }
685 
686     safe_chmod(to, statbuf.st_mode);
687     if (safe_chown(to, statbuf.st_uid, statbuf.st_gid))
688     {
689         UnexpectedError("Failed to chown %s", to);
690     }
691     safe_chmod(name, 0600);       /* File must be writable to empty .. */
692 
693     if ((fd = safe_creat(name, statbuf.st_mode)) == -1)
694     {
695         Log(LOG_LEVEL_ERR, "Failed to create new '%s' in disable(rotate). (create: %s)",
696             name, GetErrorStr());
697     }
698     else
699     {
700         if (safe_chown(name, statbuf.st_uid, statbuf.st_gid))  /* NT doesn't have fchown */
701         {
702             UnexpectedError("Failed to chown '%s'", name);
703         }
704         fchmod(fd, statbuf.st_mode);
705         close(fd);
706     }
707 }
708 
709 #ifndef __MINGW32__
710 
CreateEmptyFile(char * name)711 void CreateEmptyFile(char *name)
712 {
713     if (unlink(name) == -1)
714     {
715         if (errno != ENOENT)
716         {
717             Log(LOG_LEVEL_DEBUG, "Unable to remove existing file '%s'. (unlink: %s)", name, GetErrorStr());
718         }
719     }
720 
721     int tempfd = safe_open(name, O_CREAT | O_EXCL | O_WRONLY);
722     if (tempfd < 0)
723     {
724         Log(LOG_LEVEL_ERR, "Couldn't open a file '%s'. (open: %s)", name, GetErrorStr());
725     }
726 
727     close(tempfd);
728 }
729 
730 #endif
731