diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index 9662bcd9e8..ef86d73c4b 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -7597,6 +7597,166 @@ static FnCallResult FnCallStrftime(ARG_UNUSED EvalContext *ctx, /*********************************************************************/ +static const char *date_paths[] = { + "/usr/bin/date", + "/bin/date", + NULL +}; + +static const char *LocateDateBinary() +{ + for (size_t i = 0; date_paths[i] != NULL; i++) + { + const char *path = date_paths[i]; + + if (IsExecutable(path)) + { + return path; + } + } + return NULL; +} + +static int ParseDate(const char *input_string, time_t *out) +{ + const char *date_path = LocateDateBinary(); + + if (date_path == NULL) + { + Log(LOG_LEVEL_ERR, "Unable to find 'date' binary"); + return -1; + } + + char buffer[MAX_INPUT]; + int n = snprintf(buffer, sizeof(buffer), "--date=%s", input_string); + + if (n < 0 || (size_t) n >= sizeof(buffer)) { + Log(LOG_LEVEL_ERR, "Truncation error: input string '%.10s...' is too long (%d >= %zu)", + input_string, n, sizeof(buffer)); + return -1; + } + + const char *argv[] = {date_path, buffer, "+%s", NULL}; + FILE *fd = cf_popen_exact_args(argv, "r", true); + + if (fd == NULL) + { + Log(LOG_LEVEL_ERR, "Couldn't run date \"--date='%s' +%%s\"", input_string); + return -1; + } + + size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1 , fd); + buffer[bytes_read] = '\0'; + + if (bytes_read == 0) + { + if (ferror(fd)) + { + Log(LOG_LEVEL_ERR, "Error reading output for '%s'", input_string); + } + else if (feof(fd)) + { + Log(LOG_LEVEL_DEBUG, "No output read for '%s'", input_string); + } + fclose(fd); + return -1; + } + fclose(fd); + + int ret = StringToLong(buffer, (long *) out); + if (ret != 0) + { + LogStringToLongError(buffer, "ParseDate", ret); + return -1; + } + + return 0; +} + +static FnCallResult FnCallStrToTime(ARG_UNUSED EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) +{ + assert(fp != NULL); + + const char *input_string = RlistScalarValue(finalargs); + time_t result; + int ret = ParseDate(input_string, &result); + + if (ret != 0) + { + Log(LOG_LEVEL_ERR, "'%s': Invalid date '%s'", fp->name, input_string); + return FnFailure(); + } + + return FnReturnF("%ld", result); +} + +/*********************************************************************/ + +static FnCallResult FnCallFileOlderThan(ARG_UNUSED EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) +{ + assert(fp != NULL); + + if (finalargs == NULL) + { + Log(LOG_LEVEL_ERR, "Function '%s' requires path as first argument", + fp->name); + return FnFailure(); + } + const char *filename = RlistScalarValue(finalargs); + + if (finalargs->next == NULL) + { + Log(LOG_LEVEL_ERR, "Function '%s' requires date or date offset as second argument", + fp->name); + return FnFailure(); + } + const char *date = RlistScalarValue(finalargs->next); + const char *option = (finalargs->next->next != NULL) ? RlistScalarValue(finalargs->next->next) : "modification"; + + struct stat statbuf; + + if (stat(filename, &statbuf) != 0) + { + Log(LOG_LEVEL_ERR, "'%s': Couldn't stat '%s'", fp->name, filename); + return FnFailure(); + } + + time_t file_ts; + + if (StringEqual_IgnoreCase(option, "modification") || StringEqual_IgnoreCase(option, "modif") ) + { + file_ts = statbuf.st_mtime; + } + else if (StringEqual_IgnoreCase(option, "access")) + { + file_ts = statbuf.st_atime; + } + else if (StringEqual_IgnoreCase(option, "change")) + { + file_ts = statbuf.st_ctime; + } + else + { + ProgrammingError("Unknown option for %s\n", fp->name); + } + + time_t input_time; + int ret = ParseDate(date, &input_time); + + if (ret != 0) + { + Log(LOG_LEVEL_ERR, "'%s': Couldn't parse '%s'", fp->name, date); + return FnFailure(); + } + + time_t now = time(NULL); + time_t offset = input_time - now; // convert date to an offset + + return FnReturnContext(file_ts + offset <= now); +} + +/*********************************************************************/ + static FnCallResult FnCallEval(EvalContext *ctx, ARG_UNUSED const Policy *policy, const FnCall *fp, const Rlist *finalargs) { if (finalargs == NULL) @@ -11427,6 +11587,12 @@ static const FnCallArg STRFTIME_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; +static const FnCallArg STRTOTIME_ARGS[] = +{ + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "String to parse"}, + {NULL, CF_DATA_TYPE_NONE, NULL} +}; + static const FnCallArg STRING_REPLACE_ARGS[] = { {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Source string"}, @@ -11655,6 +11821,14 @@ static const FnCallArg ISREADABLE_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; +static const FnCallArg FILE_OLDER_THAN_ARGS[] = +{ + {CF_ABSPATHRANGE, CF_DATA_TYPE_STRING, "Path to file"}, + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Date string"}, + {"modification,modif,access,change", CF_DATA_TYPE_OPTION, "file timespec"}, + {NULL, CF_DATA_TYPE_NONE, NULL} +}; + static const FnCallArg DATATYPE_ARGS[] = { {CF_ANYSTRING, CF_DATA_TYPE_STRING, "Variable identifier"}, @@ -12003,6 +12177,8 @@ const FnCallType CF_FNCALL_TYPES[] = FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("strftime", CF_DATA_TYPE_STRING, STRFTIME_ARGS, &FnCallStrftime, "Format a date and time string", FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), + FnCallTypeNew("strtotime", CF_DATA_TYPE_INT, STRTOTIME_ARGS, &FnCallStrToTime, "Parse a timestamp from a string", + FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("sublist", CF_DATA_TYPE_STRING_LIST, SUBLIST_ARGS, &FnCallSublist, "Returns arg3 element from either the head or the tail (according to arg2) of list or array or data container arg1.", FNCALL_OPTION_COLLECTING, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("sysctlvalue", CF_DATA_TYPE_STRING, SYSCTLVALUE_ARGS, &FnCallSysctlValue, "Returns a value for sysctl key arg1 pair", @@ -12103,6 +12279,9 @@ const FnCallType CF_FNCALL_TYPES[] = FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(2, 3)), FnCallTypeNew("isreadable", CF_DATA_TYPE_CONTEXT, ISREADABLE_ARGS, &FnCallIsReadable, "Check if file is readable. Timeout immediately or after optional timeout interval", FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(1, 2)), + FnCallTypeNew("file_older_than", CF_DATA_TYPE_CONTEXT, FILE_OLDER_THAN_ARGS, &FnCallFileOlderThan, "Check if file is older than a time offset or date.", + FNCALL_OPTION_VARARG, FNCALL_CATEGORY_FILES, SYNTAX_STATUS_NORMAL, ARGC(2, 3)), + // Datatype functions FnCallTypeNew("type", CF_DATA_TYPE_STRING, DATATYPE_ARGS, &FnCallDatatype, "Get type description as string", diff --git a/libpromises/pipes.h b/libpromises/pipes.h index 5961ed0140..f5756e45b6 100644 --- a/libpromises/pipes.h +++ b/libpromises/pipes.h @@ -53,6 +53,8 @@ FILE *cf_popensetuid(const char *command, const Seq *arglist, const char *type, FILE *cf_popen_sh(const char *command, const char *type); FILE *cf_popen_sh_select(const char *command, const char *type, OutputSelect output_select); FILE *cf_popen_shsetuid(const char *command, const char *type, uid_t uid, gid_t gid, char *chdirv, char *chrootv, int background); +FILE *cf_popen_exact_args_select(const char **argv, const char *type, OutputSelect output_select); +FILE *cf_popen_exact_args(const char **argv, const char *type, bool capture_stderr); int cf_pclose(FILE *pp); void cf_pclose_nowait(FILE *pp); bool PipeToPid(pid_t *pid, FILE *pp); diff --git a/libpromises/pipes_unix.c b/libpromises/pipes_unix.c index a9aece3778..1e9a1b7ba7 100644 --- a/libpromises/pipes_unix.c +++ b/libpromises/pipes_unix.c @@ -371,18 +371,16 @@ IOData cf_popen_full_duplex(const char *command, bool capture_stderr, bool requi } } -FILE *cf_popen_select(const char *command, const char *type, OutputSelect output_select) +// do not use with user input +FILE *cf_popen_exact_args_select(const char **argv, const char *type, OutputSelect output_select) { int pd[2]; pid_t pid; FILE *pp = NULL; - char **argv = ArgSplitCommand(command, NULL); - pid = CreatePipeAndFork(type, pd); if (pid == (pid_t) -1) { - ArgFree(argv); return NULL; } @@ -427,7 +425,6 @@ FILE *cf_popen_select(const char *command, const char *type, OutputSelect output if ((pp = fdopen(pd[0], type)) == NULL) { cf_pwait(pid); - ArgFree(argv); return NULL; } break; @@ -439,13 +436,11 @@ FILE *cf_popen_select(const char *command, const char *type, OutputSelect output if ((pp = fdopen(pd[1], type)) == NULL) { cf_pwait(pid); - ArgFree(argv); return NULL; } } ChildrenFDSet(fileno(pp), pid); - ArgFree(argv); return pp; } @@ -453,6 +448,24 @@ FILE *cf_popen_select(const char *command, const char *type, OutputSelect output return NULL; } +// do not use with user input +FILE *cf_popen_exact_args(const char **argv, const char *type, bool capture_stderr) +{ + return cf_popen_exact_args_select( + argv, + type, + capture_stderr ? OUTPUT_SELECT_BOTH : OUTPUT_SELECT_STDOUT); +} + +FILE *cf_popen_select(const char *command, const char *type, OutputSelect output_select) +{ + char **argv = ArgSplitCommand(command, NULL); + FILE *ret = cf_popen_exact_args_select(argv, type, output_select); + ArgFree(argv); + + return ret; +} + FILE *cf_popen(const char *command, const char *type, bool capture_stderr) { return cf_popen_select( diff --git a/tests/acceptance/01_vars/02_functions/file_older_than.cf b/tests/acceptance/01_vars/02_functions/file_older_than.cf new file mode 100644 index 0000000000..4f0a24041a --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/file_older_than.cf @@ -0,0 +1,64 @@ +####################################################### +# +# Test file_older_than function +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent init +{ + files: + "$(G.testdir)/somefile.txt" + create => "true", + content => "Hello world!"; +} + +####################################################### +bundle agent test +{ + classes: + "long_time_past" + expression => file_older_than( + "$(G.testdir)/somefile.txt", "100 years ago" + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "long_time_future" + expression => not( + file_older_than("$(G.testdir)/somefile.txt", "100 years") + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "newer_than_a_week" + expression => file_older_than( + "$(G.testdir)/somefile.txt", "-1 week", "modif" + ), + if => fileexists("$(G.testdir)/somefile.txt"); + + "ok" + expression => and( + "long_time_past", "long_time_future", "newer_than_a_week" + ), + if => fileexists("$(G.testdir)/somefile.txt"), + scope => "namespace"; +} + +####################################################### +bundle agent check +{ + files: + "$(G.testdir)/somefile.txt" delete => tidy; + + reports: + ok:: + "$(this.promise_filename) Pass"; + + !ok:: + "$(this.promise_filename) FAIL"; +} diff --git a/tests/acceptance/01_vars/02_functions/strtotime.cf b/tests/acceptance/01_vars/02_functions/strtotime.cf new file mode 100644 index 0000000000..1b6d5eb98a --- /dev/null +++ b/tests/acceptance/01_vars/02_functions/strtotime.cf @@ -0,0 +1,29 @@ +####################################################### +# +# Test strtotime function +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent test +{ + vars: + "some_date" int => strtotime("2009-09-21T00:00:00Z"); + "epoch" int => int(1253491200); + + classes: + "ok" expression => strcmp("$(epoch)", "$(some_date)"); + + reports: + ok:: + "$(this.promise_filename) Pass"; + + !ok:: + "$(this.promise_filename) FAIL"; +}