/* * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "template_string.h" #include "template_string_private.h" #include "services/common/i18n/i18n.h" #include "syscall/syscall.h" #include "system/passert.h" #include "util/attributes.h" #include "util/math.h" #include "util/size.h" #include "util/string.h" #include #include #include #include #include #include #include #include #define MAX_FILTER_NAME_LENGTH 16 #define SUPPORT_MONTH 0 #define SUPPORT_YEAR 0 typedef enum { PredicateCondition_Invalid, // This order is important; >= must be next after >, and <= must be next after <. // This is so that the parser can just add 1 to the enum value when it finds the = character. PredicateCondition_L, PredicateCondition_LE, PredicateCondition_G, PredicateCondition_GE, } PredicateCondition; typedef enum { FormatUnits_None, FormatUnits_Abbreviated, FormatUnits_Full, } FormatUnits; typedef struct { const char name[MAX_FILTER_NAME_LENGTH]; // Filters must manually advance state->position to the parenthesis that indicates the end of // the filter arguments. void (*cb)(TemplateStringState *state); } FilterImplementation; static void prv_filter_format(TemplateStringState *state); static void prv_filter_time_until(TemplateStringState *state); static void prv_filter_time_since(TemplateStringState *state); static void prv_filter_end(TemplateStringState *state); static const FilterImplementation s_filter_impls[] = { { "format", prv_filter_format, }, { "time_until", prv_filter_time_until, }, { "time_since", prv_filter_time_since, }, { "end", prv_filter_end, }, }; static void prv_handle_escape_character(TemplateStringState *state) { if (*state->position == '\\') { state->position++; if (*state->position == '\0') { state->error->status = TemplateStringErrorStatus_InvalidEscapeCharacter; } } } static bool prv_predicate_check(char ch) { return ((ch == '>') || (ch == '<')); } static bool prv_format_string_ending(char ch) { return ((ch == ',') || (ch == ')')); } static bool prv_predicate_valid_splitter(char ch) { return ((ch == ':') || prv_format_string_ending(ch)); } T_STATIC intmax_t prv_template_predicate_time(TemplateStringState *state) { bool negative = false; if (*state->position == '-') { negative = true; state->position++; } bool total_value_valid = false; intmax_t total_value = 0; while (!total_value_valid || !prv_predicate_valid_splitter(*state->position)) { const char *endpos; int value = strtol(state->position, (char **)&endpos, 10); if (endpos == state->position) { state->error->status = TemplateStringErrorStatus_InvalidTimeUnit; return 0; } state->position = endpos; int multiplier = 1; switch (*state->position) { // NOTE: This number of seconds is a hack! See PBL-39903 #if SUPPORT_YEAR case 'y': multiplier = 365 * SECONDS_PER_DAY; break; #endif // NOTE: This number of seconds is a hack! See PBL-39903 #if SUPPORT_MONTH case 'm': multiplier = 30 * SECONDS_PER_DAY; break; #endif case 'd': multiplier = SECONDS_PER_DAY; break; case 'H': multiplier = SECONDS_PER_HOUR; break; case 'M': multiplier = SECONDS_PER_MINUTE; break; case 'S': multiplier = 1; break; default: state->error->status = TemplateStringErrorStatus_InvalidTimeUnit; return 0; } state->position++; total_value += value * multiplier; total_value_valid = true; } if (negative) { total_value = -total_value; } return total_value; } T_STATIC bool prv_template_predicate_match(TemplateStringState *state, PredicateCondition *cond, intmax_t *value) { *cond = PredicateCondition_Invalid; if (*state->position == '<') { *cond = PredicateCondition_L; } else if (*state->position == '>') { *cond = PredicateCondition_G; } else { WTF; } state->position++; if (*state->position == '=') { (*cond)++; state->position++; } else if (!isdigit(*state->position)) { state->error->status = TemplateStringErrorStatus_InvalidTimeUnit; return false; } *value = prv_template_predicate_time(state); if (state->error->status != TemplateStringErrorStatus_Success) { return false; } switch (*cond) { case PredicateCondition_G: return (state->filter_state > *value); case PredicateCondition_GE: return (state->filter_state >= *value); case PredicateCondition_L: return (state->filter_state < *value); case PredicateCondition_LE: return (state->filter_state <= *value); default: WTF; } } static const char * const s_Tstrings[3][3] = { { "H", "M", "S", }, { "aH", "aM", "aS", }, { "uH", "uM", "uS", }, }; static const char * const s_splitters[3][2] = { /// The first separator in `::` { i18n_ctx_noop("TmplStringSep", ":"), /// The second separator in `::` i18n_ctx_noop("TmplStringSep", ":"), }, /// The first separator in ` hr min sec` { i18n_ctx_noop("TmplStringSep", " "), /// The second separator in ` hr min sec` i18n_ctx_noop("TmplStringSep", " "), }, /// The first separator in ` hours, minutes, and seconds` { i18n_ctx_noop("TmplStringSep", ", "), /// The second separator in ` hours, minutes, and seconds` i18n_ctx_noop("TmplStringSep", ", and "), }, }; /* Flag truth table. fmt = >= 1 hour >= 1 minute other %T = %H:%0M:%0S %M:%0S %S %uT = %uH, %uM, and %uS %uM, and %uS %uS %aT = %aH %aM %aS %aM %aS %aS %0T = %0H:%0M:%0S %0M:%0S %0S %fT = %fH:%0M:%0S %fM:%0S %fS %0uT = %0uH, %0uM, and %0uS %0uM, and %0uS %0uS %0aT = %0aH %0aM %0aS %0aM %0aS %0aS %0fT = %0fH:%0M:%0S %0fM:%0S %0fS %fuT = %fuH, %uM, and %uS %fuM, and %uS %fuS %faT = %faH %aM %aS %faM %aS %faS %0fuT = %f0uH, %0uM, and %0uS %f0uM, and %0uS %f0uS %0faT = %f0aH %0aM %0aS %f0aM %0aS %f0aS */ /* 0 flag adds 0 flag to all sub-specs f flag adds f flag to first sub-spec ua. are unique */ static void prv_append_string_i18n(TemplateStringState *state, const char *str) { size_t len = sys_i18n_get_length(str); if (state->output_remaining < len) { len = state->output_remaining; } if (!len) { return; } // len needs +1 in order for i18n_get_with_buffer to write all the characters we want. // It's ok that it writes the NUL at the end because we've already reserved that space. sys_i18n_get_with_buffer(str, state->output, len + 1); state->output += len; state->output_remaining -= len; } static void prv_append_number(TemplateStringState *state, const char *fmt, int value) { size_t len = snprintf(NULL, 0, fmt, value); if (state->output_remaining < len) { len = state->output_remaining; } if (!len) { return; } // len needs +1 in order for snprintf to write all the characters we want. // It's ok that it writes the NUL at the end because we've already reserved that space. snprintf(state->output, len + 1, fmt, value); state->output += len; state->output_remaining -= len; } static void prv_append_char(TemplateStringState *state, char c) { if (state->output_remaining >= 1) { *state->output++ = c; state->output_remaining--; } } static const char * const s_second_strings[3][2] = { /// Singular suffix for seconds with no units { i18n_ctx_noop("TmplStringSing", ""), /// Plural suffix for seconds with no units i18n_ctx_noop("TmplStringPlur", "")}, /// Singular suffix for seconds with abbreviated units { i18n_ctx_noop("TmplStringSing", " sec"), /// Plural suffix for seconds with abbreviated units i18n_ctx_noop("TmplStringPlur", " sec")}, /// Singular suffix for seconds with full units { i18n_ctx_noop("TmplStringSing", " second"), /// Plural suffix for seconds with full units i18n_ctx_noop("TmplStringPlur", " seconds")}, }; static const char * const s_minute_strings[3][2] = { /// Singular suffix for minutes with no units { i18n_ctx_noop("TmplStringSing", ""), /// Plural suffix for minutes with no units i18n_ctx_noop("TmplStringPlur", "")}, /// Singular suffix for minutes with abbreviated units { i18n_ctx_noop("TmplStringSing", " min"), /// Plural suffix for minutes with abbreviated units i18n_ctx_noop("TmplStringPlur", " min")}, /// Singular suffix for minutes with full units { i18n_ctx_noop("TmplStringSing", " minute"), /// Plural suffix for minutes with full units i18n_ctx_noop("TmplStringPlur", " minutes")}, }; static const char * const s_hour_strings[3][2] = { /// Singular suffix for hours with no units { i18n_ctx_noop("TmplStringSing", ""), /// Plural suffix for hours with no units i18n_ctx_noop("TmplStringPlur", "")}, /// Singular suffix for hours with abbreviated units { i18n_ctx_noop("TmplStringSing", " hr"), /// Plural suffix for hours with abbreviated units i18n_ctx_noop("TmplStringPlur", " hr")}, /// Singular suffix for hours with full units { i18n_ctx_noop("TmplStringSing", " hour"), /// Plural suffix for hours with full units i18n_ctx_noop("TmplStringPlur", " hours")}, }; static const char * const s_day_strings[3][2] = { /// Singular suffix for days with no units { i18n_ctx_noop("TmplStringSing", ""), /// Plural suffix for days with no units i18n_ctx_noop("TmplStringPlur", "")}, /// Singular suffix for days with abbreviated units { i18n_ctx_noop("TmplStringSing", " d"), /// Plural suffix for days with abbreviated units i18n_ctx_noop("TmplStringPlur", " d")}, /// Singular suffix for days with full units { i18n_ctx_noop("TmplStringSing", " day"), /// Plural suffix for days with full units i18n_ctx_noop("TmplStringPlur", " days")}, }; #if SUPPORT_MONTH static const char * const s_month_strings[3][2] = { /// Singular suffix for months with no units { i18n_ctx_noop("TmplStringSing", ""), /// Plural suffix for months with no units i18n_ctx_noop("TmplStringPlur", "")}, /// Singular suffix for months with abbreviated units { i18n_ctx_noop("TmplStringSing", " mo"), /// Plural suffix for months with abbreviated units i18n_ctx_noop("TmplStringPlur", " mo")}, /// Singular suffix for months with full units { i18n_ctx_noop("TmplStringSing", " month"), /// Plural suffix for months with full units i18n_ctx_noop("TmplStringPlur", " months")}, }; #endif #if SUPPORT_YEAR static const char * const s_year_strings[3][2] = { /// Singular suffix for years with no units { i18n_ctx_noop("TmplStringSing", ""), /// Plural suffix for years with no units i18n_ctx_noop("TmplStringPlur", "")}, /// Singular suffix for years with abbreviated units { i18n_ctx_noop("TmplStringSing", " yr"), /// Plural suffix for years with abbreviated units i18n_ctx_noop("TmplStringPlur", " yr")}, /// Singular suffix for years with full units { i18n_ctx_noop("TmplStringSing", " year"), /// Plural suffix for years with full units i18n_ctx_noop("TmplStringPlur", " years")}, }; #endif static void prv_do_conversion(TemplateStringState *state, intmax_t value, int divide, int mod, const char * const suffix_strings[3][2], FormatUnits add_units, bool zero_pad, bool should_mod) { int remain = (value % divide); if (!state->time_was_until) { // We want to go in reverse for 'since' remain = divide - remain; } else { // Add 1 because the next eval time is how long until the result changes. remain++; } if (state->eval_cond) { if (remain < state->eval_cond->eval_time) { state->eval_cond->eval_time = remain; } } value /= divide; if (should_mod && (mod != 0)) { value %= mod; } prv_append_number(state, zero_pad ? "%02d" : "%d", value); prv_append_string_i18n(state, suffix_strings[add_units][value != 1]); } // This is a recursive function, so watch out! // The recursion happens on the %R and %T cases only, and will only recurse once. // So when adding stack variables, realize the stack usage may be doubled! T_STATIC const char *prv_template_format_specifier(TemplateStringState *state, const char *input, intmax_t value) { if (*input == '%') { // Escaped % prv_append_char(state, *input); input++; return input; } FormatUnits add_units = FormatUnits_None; bool zero_pad = false; bool modulus = true; bool checking_flags = true; while (checking_flags) { switch (*input) { case 'a': add_units = FormatUnits_Abbreviated; break; case 'u': add_units = FormatUnits_Full; break; case '-': value = -value; break; case '0': zero_pad = true; break; case 'f': modulus = false; break; default: checking_flags = false; break; } if (checking_flags) { input++; } } if (value < 0) { prv_append_char(state, '-'); value = -value; } int macro_units = 2; if (value >= SECONDS_PER_MINUTE) { macro_units--; } if (value >= SECONDS_PER_HOUR) { macro_units--; } int macro_end = 3; // conversion specifiers switch (*input) { #if SUPPORT_YEAR case 'y': // year // NOTE: This number of seconds to divide by is a hack! See PBL-39903 prv_do_conversion(state, value, 365 * SECONDS_PER_DAY, 100, s_year_strings, add_units, zero_pad, modulus); break; #endif #if SUPPORT_MONTH case 'm': // month // NOTE: This number of seconds to divide by is a hack! See PBL-39903 prv_do_conversion(state, value, 30 * SECONDS_PER_DAY, 12, s_month_strings, add_units, zero_pad, modulus); break; #endif case 'd': // day // NOTE: This number of modulus is a hack! See PBL-39903 #if SUPPORT_MONTH prv_do_conversion(state, value, SECONDS_PER_DAY, 30, s_day_strings, add_units, zero_pad, modulus); #else prv_do_conversion(state, value, SECONDS_PER_DAY, 0, s_day_strings, add_units, zero_pad, modulus); #endif break; case 'H': // hour prv_do_conversion(state, value, SECONDS_PER_HOUR, HOURS_PER_DAY, s_hour_strings, add_units, zero_pad, modulus); break; case 'M': // minute prv_do_conversion(state, value, SECONDS_PER_MINUTE, MINUTES_PER_HOUR, s_minute_strings, add_units, zero_pad, modulus); break; case 'S': // second prv_do_conversion(state, value, 1, SECONDS_PER_MINUTE, s_second_strings, add_units, zero_pad, modulus); break; case 'R': // H:M // R is mostly the same as T, just without seconds. macro_end--; // fall-thru case 'T': { // H:M:S char macro_spec[16]; // Always show the last unit, even if it's 0. macro_units = MIN(macro_units, macro_end - 1); for (int i = macro_units; i < macro_end; i++) { char *macro_ptr = macro_spec; if (zero_pad || ((i != macro_units) && (add_units == FormatUnits_None))) { *macro_ptr++ = '0'; } if (!modulus && (i == macro_units)) { *macro_ptr++ = 'f'; } strcpy(macro_ptr, s_Tstrings[add_units][i]); macro_ptr += strlen(s_Tstrings[add_units][i]); *macro_ptr = '\0'; prv_template_format_specifier(state, macro_spec, value); if (i != macro_end - 1) { prv_append_string_i18n(state, s_splitters[add_units][i >= macro_end - 2]); } } break; } default: state->error->status = TemplateStringErrorStatus_InvalidConversionSpecifier; return input; } // Skip the conversion specifier. input++; return input; } static bool prv_format_predicate(TemplateStringState *state, bool previously_matched) { bool match = true; if (!prv_predicate_check(*state->position)) { return match; } PredicateCondition predicate_cond; intmax_t predicate_value = 0; // If this is a predicate, we need to evaluate it for a match. match = prv_template_predicate_match(state, &predicate_cond, &predicate_value); if (state->error->status != TemplateStringErrorStatus_Success) { return false; } // Predicate matcher will only leave on :,) or error, so this should never trip. if (!prv_predicate_valid_splitter(*state->position)) { WTF; } int wait_time; // Need to handle predicates differently based on whether the value is incrementing or // decrementing over time. PredicateCondition cond_to_expire; PredicateCondition cond_to_valid; if (!state->time_was_until) { // Value increments over time, so a < will expire and a > will become valid. cond_to_expire = PredicateCondition_L; cond_to_valid = PredicateCondition_G; wait_time = predicate_value - state->filter_state; } else { // Value decrements over time, so a < will become valid and a > will expire. cond_to_expire = PredicateCondition_G; cond_to_valid = PredicateCondition_L; wait_time = state->filter_state - predicate_value; } if (!previously_matched && match && ((predicate_cond == cond_to_expire) || (predicate_cond == cond_to_expire + 1))) { // This predicate could expire over time. // If the conditional is equal, add 1 to the wait time, because the equals case stays // valid on the specified value. if (predicate_cond == cond_to_expire + 1) { wait_time++; } } else if (!match && ((predicate_cond == cond_to_valid) || (predicate_cond == cond_to_valid + 1))) { // This predicate could become valid over time. // If the conditional is not equal, add 1 to the wait time, because only the equals case // becomes valid on the specified value. if (predicate_cond == cond_to_valid) { wait_time++; } } else { wait_time = INT_MAX; } if (state->eval_cond) { if (wait_time < state->eval_cond->eval_time) { state->eval_cond->eval_time = wait_time; } } // Only characters possible here are :,) if (*state->position == ':') { state->position++; } return match; } static void prv_format_process_format_string(TemplateStringState *state, char delimiter) { // Predicate matched (or is default case), so let's parse the string. while ((*state->position != delimiter) && (*state->position != '\0')) { if (*state->position != '%') { // Not a format character prv_handle_escape_character(state); if (state->error->status != TemplateStringErrorStatus_Success) { return; } prv_append_char(state, *state->position); state->position++; } else { // Skip over the % state->position++; state->position = prv_template_format_specifier(state, state->position, state->filter_state); if (state->error->status != TemplateStringErrorStatus_Success) { return; } } } if (*state->position == '\0') { state->error->status = TemplateStringErrorStatus_MissingClosingQuote; return; } // Skip the delimiter state->position++; } static void prv_format_skip_format_string(TemplateStringState *state, char delimiter) { // No match, so let's move along to the next one. while ((*state->position != delimiter) && (*state->position != '\0')) { prv_handle_escape_character(state); if (state->error->status != TemplateStringErrorStatus_Success) { return; } state->position++; } if (*state->position == '\0') { state->error->status = TemplateStringErrorStatus_MissingClosingQuote; return; } // Skip the delimiter state->position++; } static void prv_filter_format(TemplateStringState *state) { bool match; bool previously_matched = false; bool did_output = false; // We need to iterate all the way through for finding the proper 'next' time. while (*state->position != ')') { match = prv_format_predicate(state, previously_matched); if (state->error->status != TemplateStringErrorStatus_Success) { return; } // A force-default case if (prv_format_string_ending(*state->position)) { state->position++; continue; } // Get the delimiter being used. const char delimiter = *state->position; if ((delimiter != '\'') && (delimiter != '"')) { state->error->status = TemplateStringErrorStatus_MissingOpeningQuote; return; } state->position++; if (match && !previously_matched) { prv_format_process_format_string(state, delimiter); did_output = true; previously_matched = true; } else { prv_format_skip_format_string(state, delimiter); } if (state->error->status != TemplateStringErrorStatus_Success) { return; } if (!prv_format_string_ending(*state->position)) { state->error->status = TemplateStringErrorStatus_InvalidArgumentSeparator; return; } else if (*state->position == ',') { state->position++; if (*state->position == ')') { state->error->status = TemplateStringErrorStatus_MissingArgument; return; } } } if (!did_output) { // If no output was generated, it's an error. state->error->status = TemplateStringErrorStatus_CantResolve; } // format() must be the last filter, and ends the sequence. state->filters_complete = true; } static void prv_filter_time_until(TemplateStringState *state) { char *endptr; time_t target_time = strtol(state->position, &endptr, 10); if (*endptr != ')') { state->error->status = TemplateStringErrorStatus_MissingClosingParen; return; } state->position = endptr; state->filter_state = target_time - state->vars->current_time; state->time_was_until = true; } static void prv_filter_time_since(TemplateStringState *state) { prv_filter_time_until(state); if (state->error->status != TemplateStringErrorStatus_Success) { return; } state->filter_state = -state->filter_state; state->time_was_until = false; } static void prv_filter_end(TemplateStringState *state) { state->filters_complete = true; } T_STATIC void prv_template_evaluate_filter(TemplateStringState *state, const char *filter_name, const char *parameters_start) { for (size_t i = 0; i < ARRAY_LENGTH(s_filter_impls); i++) { if (strcmp(s_filter_impls[i].name, filter_name) == 0) { state->position = parameters_start; s_filter_impls[i].cb(state); return; } } state->error->status = TemplateStringErrorStatus_UnknownFilter; } static void prv_template_eval(TemplateStringState *state) { while ((*state->position != '}') && (*state->position != '\0')) { if (state->filters_complete) { state->error->status = TemplateStringErrorStatus_FormatBeforeLast; return; } // Find the filter's opening paren const char *filter_name_paren = strchr(state->position, '('); if (!filter_name_paren) { state->error->status = TemplateStringErrorStatus_MissingOpeningParen; return; } // Copy out the filter name char filter_name[MAX_FILTER_NAME_LENGTH]; size_t len = MIN(MAX_FILTER_NAME_LENGTH - 1, filter_name_paren - state->position); strncpy(filter_name, state->position, len); filter_name[len] = '\0'; prv_template_evaluate_filter(state, filter_name, filter_name_paren + 1); if (state->error->status != TemplateStringErrorStatus_Success) { return; } if (*state->position != ')') { state->error->status = TemplateStringErrorStatus_MissingClosingParen; return; } // Advance pointer to the character after the filter state->position++; if (*state->position == '|') { state->position++; continue; } else if (*state->position == '}') { continue; } else { state->error->status = TemplateStringErrorStatus_MissingClosingBrace; return; } } // Must end on a closing brace. if (*state->position != '}') { state->error->status = TemplateStringErrorStatus_MissingClosingBrace; return; } // Did not generate an output. if (!state->filters_complete) { state->error->status = TemplateStringErrorStatus_NoResultGenerated; return; } // Skip past the closing brace. state->position++; } bool template_string_evaluate(const char *input_template_string, char *output, size_t output_size, TemplateStringEvalConditions *eval_cond, const TemplateStringVars *vars, TemplateStringError *error) { TemplateStringState state = { .position = input_template_string, .output = output, .output_remaining = output_size, .eval_cond = eval_cond, .vars = vars, .error = error, .time_was_until = false, .filter_state = 0, }; if (!state.position || !state.vars || !state.error) { if (state.error) { state.error->status = TemplateStringErrorStatus_InvalidParameter; } return false; } // We have no output space, so don't bother trying to write anything. // By unifying these states, we can just check `output_remaining` against zero for writing. if (!state.output || !state.output_remaining) { state.output = NULL; state.output_remaining = 0; } else { // Subtract 1 for the null terminator. state.output_remaining--; } if (state.eval_cond) { state.eval_cond->eval_time = INT_MAX; state.eval_cond->force_eval_on_time = false; } state.error->status = TemplateStringErrorStatus_Success; while (*state.position != '\0') { if (*state.position != '{') { prv_handle_escape_character(&state); if (state.error->status != TemplateStringErrorStatus_Success) { break; } prv_append_char(&state, *state.position); state.position++; } else { // Template state.position++; prv_template_eval(&state); if (state.error->status != TemplateStringErrorStatus_Success) { break; } state.time_was_until = false; state.filter_state = 0; state.filters_complete = false; } } if (state.error->status != TemplateStringErrorStatus_Success) { // get the position index. state.error->index_in_string = state.position - input_template_string; } // Null terminator if (state.output) { *state.output = '\0'; } if (state.eval_cond) { // Adjust eval_time if it never got set. if (state.eval_cond->eval_time == INT_MAX) { // If we never set the re-evaluation time, set `eval_time` to 0. // This is the value we specified for "we don't need to re-evaluate". state.eval_cond->eval_time = 0; } else { // `eval_time` is an absolute timestamp, so add the input time to the relative time offset. state.eval_cond->eval_time += state.vars->current_time; state.eval_cond->force_eval_on_time = true; } } return (state.error->status == TemplateStringErrorStatus_Success); }