mirror of
https://github.com/google/pebble.git
synced 2025-05-04 00:41:40 -04:00
869 lines
28 KiB
C
869 lines
28 KiB
C
/*
|
|
* 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 <ctype.h>
|
|
#include <limits.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#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 `<hour>:<minute>:<second>`
|
|
{ i18n_ctx_noop("TmplStringSep", ":"),
|
|
/// The second separator in `<hour>:<minute>:<second>`
|
|
i18n_ctx_noop("TmplStringSep", ":"), },
|
|
/// The first separator in `<hour> hr <minute> min <second> sec`
|
|
{ i18n_ctx_noop("TmplStringSep", " "),
|
|
/// The second separator in `<hour> hr <minute> min <second> sec`
|
|
i18n_ctx_noop("TmplStringSep", " "), },
|
|
/// The first separator in `<hour> hours, <minute> minutes, and <second> seconds`
|
|
{ i18n_ctx_noop("TmplStringSep", ", "),
|
|
/// The second separator in `<hour> hours, <minute> minutes, and <second> 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);
|
|
}
|