use helm_schema_ast::{Literal, TemplateExpr};
pub(crate) fn is_files_get(function: &str) -> bool {
function == "Files.Get" || function.ends_with(".Files.Get")
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum StringOperands {
None,
All,
First,
Last,
FirstAndLast,
FirstTwo,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum NilBehavior {
Tolerates,
AlwaysAborts,
DirectAccessAborts,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum OutputSemantics {
Opaque,
StringTransform,
TotalStringification,
Checksum,
TotalNumericCast,
CoercingArithmetic,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum CollectionShape {
None,
Merge,
StringSplit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ProvenanceBehavior {
Discard,
Preserve,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PredicateSemantics {
None,
String,
TypeDescriptor,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ParserSemantics {
None,
Semver,
Duration,
Url,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CollectionItemSemantics {
None,
CertificateIpList,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct FunctionSemantics {
known: bool,
pub(crate) string_operands: StringOperands,
pub(crate) nil_behavior: NilBehavior,
pub(crate) output: OutputSemantics,
pub(crate) collection: CollectionShape,
pub(crate) provenance: ProvenanceBehavior,
pub(crate) predicate: PredicateSemantics,
parser: ParserSemantics,
collection_items: CollectionItemSemantics,
}
const UNKNOWN: FunctionSemantics = FunctionSemantics {
known: false,
string_operands: StringOperands::None,
nil_behavior: NilBehavior::Tolerates,
output: OutputSemantics::Opaque,
collection: CollectionShape::None,
provenance: ProvenanceBehavior::Discard,
predicate: PredicateSemantics::None,
parser: ParserSemantics::None,
collection_items: CollectionItemSemantics::None,
};
const KNOWN: FunctionSemantics = FunctionSemantics {
known: true,
..UNKNOWN
};
impl FunctionSemantics {
const fn with_strings(self, string_operands: StringOperands) -> Self {
Self {
string_operands,
..self
}
}
const fn with_nil(self, nil_behavior: NilBehavior) -> Self {
Self {
nil_behavior,
..self
}
}
const fn with_output(self, output: OutputSemantics) -> Self {
Self { output, ..self }
}
const fn with_collection(self, collection: CollectionShape) -> Self {
Self { collection, ..self }
}
const fn with_provenance(self, provenance: ProvenanceBehavior) -> Self {
Self { provenance, ..self }
}
const fn with_predicate(self, predicate: PredicateSemantics) -> Self {
Self { predicate, ..self }
}
const fn with_parser(self, parser: ParserSemantics) -> Self {
Self { parser, ..self }
}
const fn with_collection_items(self, collection_items: CollectionItemSemantics) -> Self {
Self {
collection_items,
..self
}
}
}
#[must_use]
pub(crate) fn function_semantics(function: &str) -> FunctionSemantics {
use CollectionShape::{Merge, StringSplit};
use NilBehavior::{AlwaysAborts, DirectAccessAborts};
use OutputSemantics::{
Checksum, CoercingArithmetic, StringTransform, TotalNumericCast, TotalStringification,
};
use PredicateSemantics::{String as StringPredicate, TypeDescriptor};
use ProvenanceBehavior::Preserve;
use StringOperands::{All, First, FirstAndLast, FirstTwo, Last};
match function {
"quote" | "squote" | "toString" | "urlquery" => {
KNOWN.with_strings(All).with_output(TotalStringification)
}
"b64enc"
| "b64dec"
| "trimAll"
| "trimPrefix"
| "trimSuffix"
| "replace"
| "regexReplaceAll"
| "mustRegexReplaceAll"
| "regexReplaceAllLiteral"
| "mustRegexReplaceAllLiteral"
| "htpasswd" => KNOWN.with_strings(All).with_output(StringTransform),
"lower" | "upper" | "trunc" | "substr" | "trim" | "repeat" => {
KNOWN.with_strings(Last).with_output(StringTransform)
}
"indent" | "nindent" => KNOWN
.with_strings(Last)
.with_output(StringTransform)
.with_provenance(Preserve),
"sha1sum" | "sha256sum" | "sha512sum" | "adler32sum" => {
KNOWN.with_strings(Last).with_output(Checksum)
}
"regexMatch" | "mustRegexMatch" | "contains" | "hasPrefix" | "hasSuffix" => {
KNOWN.with_strings(All).with_predicate(StringPredicate)
}
"semverCompare" => KNOWN
.with_strings(All)
.with_predicate(StringPredicate)
.with_parser(ParserSemantics::Semver),
"mustDateModify" => KNOWN
.with_strings(First)
.with_predicate(StringPredicate)
.with_parser(ParserSemantics::Duration),
"urlParse" => KNOWN
.with_strings(All)
.with_predicate(StringPredicate)
.with_parser(ParserSemantics::Url),
"splitList" | "split" => KNOWN.with_strings(All).with_collection(StringSplit),
"splitn" => KNOWN
.with_strings(FirstAndLast)
.with_collection(StringSplit),
"regexSplit" => KNOWN.with_strings(FirstTwo).with_collection(StringSplit),
"lookup" => KNOWN.with_strings(All),
"int" => KNOWN
.with_output(TotalNumericCast)
.with_provenance(Preserve),
"int64" | "float64" => KNOWN.with_output(TotalNumericCast),
"add" | "add1" | "sub" | "mul" | "max" | "min" | "floor" | "ceil" | "round" | "addf"
| "add1f" | "subf" | "mulf" | "maxf" | "minf" => KNOWN.with_output(CoercingArithmetic),
"merge" | "mustMerge" | "mergeOverwrite" | "mustMergeOverwrite" => {
KNOWN.with_nil(DirectAccessAborts).with_collection(Merge)
}
"first" | "last" | "initial" | "rest" | "compact" | "reverse" | "append" | "push"
| "prepend" | "concat" | "slice" | "mustSlice" | "index" | "len" | "ternary" => {
KNOWN.with_nil(AlwaysAborts)
}
"uniq" | "mustUniq" | "deepCopy" | "mustDeepCopy" => {
KNOWN.with_nil(AlwaysAborts).with_provenance(Preserve)
}
"unset" => KNOWN.with_nil(DirectAccessAborts),
"hasKey" | "pick" | "omit" | "keys" | "values" | "pluck" | "get" => {
KNOWN.with_nil(DirectAccessAborts)
}
"toYaml" | "fromYaml" | "tpl" => KNOWN.with_provenance(Preserve),
"printf" => KNOWN
.with_provenance(Preserve)
.with_predicate(TypeDescriptor),
"typeOf" | "kindOf" => KNOWN.with_predicate(TypeDescriptor),
"genSignedCert" | "genSelfSignedCert" => {
KNOWN.with_collection_items(CollectionItemSemantics::CertificateIpList)
}
_ => UNKNOWN,
}
}
impl FunctionSemantics {
#[must_use]
pub(crate) const fn is_known(self) -> bool {
self.known
}
#[must_use]
pub(crate) const fn is_string_transform(self) -> bool {
matches!(
self.output,
OutputSemantics::StringTransform | OutputSemantics::TotalStringification
)
}
#[must_use]
pub(crate) const fn is_total_stringification(self) -> bool {
matches!(self.output, OutputSemantics::TotalStringification)
}
#[must_use]
pub(crate) const fn nil_aborts(self, direct_access: bool) -> bool {
match self.nil_behavior {
NilBehavior::Tolerates => false,
NilBehavior::AlwaysAborts => true,
NilBehavior::DirectAccessAborts => direct_access,
}
}
#[must_use]
pub(crate) fn string_operand_indices(self, argument_count: usize) -> Vec<usize> {
if argument_count == 0 {
return Vec::new();
}
match self.string_operands {
StringOperands::All => (0..argument_count).collect(),
StringOperands::First => vec![0],
StringOperands::Last => vec![argument_count - 1],
StringOperands::FirstAndLast if argument_count >= 3 => {
vec![0, argument_count - 1]
}
StringOperands::FirstTwo if argument_count >= 3 => vec![0, 1],
StringOperands::None | StringOperands::FirstAndLast | StringOperands::FirstTwo => {
Vec::new()
}
}
}
}
pub(crate) fn type_is_schema_type(expr: Option<&TemplateExpr>) -> Option<String> {
let TemplateExpr::Literal(Literal::String(type_name) | Literal::RawString(type_name)) =
expr?.deparen()
else {
return None;
};
go_type_schema_type(type_name).map(str::to_string)
}
#[must_use]
pub(crate) fn go_type_schema_type(type_name: &str) -> Option<&'static str> {
Some(match type_name {
"bool" | "boolean" => "boolean",
"float64" | "number" => "number",
"int" | "int64" | "integer" => "integer",
"list" | "slice" | "array" | "[]interface {}" => "array",
"map" | "dict" | "object" | "map[string]interface {}" => "object",
"string" => "string",
_ => return None,
})
}
#[must_use]
pub(crate) fn go_type_descriptor_spellings(schema_type: &str) -> &'static [&'static str] {
match schema_type {
"boolean" => &["bool"],
"integer" => &["float64", "int64"],
"number" => &["float64"],
"string" => &["string"],
"array" => &["[]interface {}", "slice"],
"object" => &["map[string]interface {}", "map"],
_ => &[],
}
}
#[must_use]
pub(crate) fn type_descriptor_call_subject<'a>(
function: &str,
args: &'a [TemplateExpr],
) -> Option<&'a TemplateExpr> {
match (function_semantics(function).predicate, args) {
(PredicateSemantics::TypeDescriptor, [subject]) if function != "printf" => Some(subject),
(PredicateSemantics::TypeDescriptor, [format_expr, subject]) if function == "printf" => {
match format_expr.deparen() {
TemplateExpr::Literal(Literal::String(format) | Literal::RawString(format))
if format == "%T" =>
{
Some(subject)
}
_ => None,
}
}
_ => None,
}
}
#[must_use]
pub(crate) fn strict_parser_operand_pattern(
semantics: FunctionSemantics,
argument_count: usize,
) -> Option<(usize, &'static str)> {
match semantics.parser {
ParserSemantics::Semver if argument_count == 2 => {
Some((
argument_count - 1,
r"^v?(0*[0-9]{1,20})(\.0*[0-9]{1,20})?(\.0*[0-9]{1,20})?(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$",
))
}
ParserSemantics::Duration if argument_count == 2 => Some((
0,
r"^[+-]?(0|((0*[0-9]{1,19}(\.[0-9]*)?|\.[0-9]+)ns|(0*[0-9]{1,16}(\.[0-9]*)?|\.[0-9]+)(us|µs|μs)|(0*[0-9]{1,13}(\.[0-9]*)?|\.[0-9]+)ms|(0*[0-9]{1,10}(\.[0-9]*)?|\.[0-9]+)s|(0*[0-9]{1,9}(\.[0-9]*)?|\.[0-9]+)m|(0*[0-9]{1,7}(\.[0-9]*)?|\.[0-9]+)h)+)$",
)),
ParserSemantics::Url if argument_count == 1 => Some((0, URL_PARSE_PATTERN)),
_ => None,
}
}
macro_rules! ipv4_pattern {
() => {
concat!(
r"((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}",
"(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"
)
};
}
macro_rules! ipv6_pattern {
() => {
concat!(
"([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}",
"|([0-9A-Fa-f]{1,4}:){1,7}:",
"|([0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}",
"|([0-9A-Fa-f]{1,4}:){1,5}(:[0-9A-Fa-f]{1,4}){1,2}",
"|([0-9A-Fa-f]{1,4}:){1,4}(:[0-9A-Fa-f]{1,4}){1,3}",
"|([0-9A-Fa-f]{1,4}:){1,3}(:[0-9A-Fa-f]{1,4}){1,4}",
"|([0-9A-Fa-f]{1,4}:){1,2}(:[0-9A-Fa-f]{1,4}){1,5}",
"|[0-9A-Fa-f]{1,4}:(:[0-9A-Fa-f]{1,4}){1,6}",
"|:(:[0-9A-Fa-f]{1,4}){1,7}",
"|::",
"|([0-9A-Fa-f]{1,4}:){6}",
ipv4_pattern!(),
"|::([0-9A-Fa-f]{1,4}:){0,5}",
ipv4_pattern!(),
"|([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}",
ipv4_pattern!(),
"|([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,3}",
ipv4_pattern!(),
"|([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,2}",
ipv4_pattern!(),
"|([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,1}",
ipv4_pattern!(),
"|([0-9A-Fa-f]{1,4}:){5}:",
ipv4_pattern!()
)
};
}
macro_rules! url_host_char {
() => {
concat!(
"(?:[A-Za-z0-9._~!$&'()*+,;=\\]<>\"\\-]",
r"|[^\u0000-\u007F]",
"|%25|%[89A-Fa-f][0-9A-Fa-f])"
)
};
}
macro_rules! url_query_fragment {
() => {
r"(?:\?[^\u0000-\u001F\u007F#]*)?(?:#(?:[^%]|%[0-9A-Fa-f]{2})*)?"
};
}
macro_rules! url_rooted {
() => {
r"/(?:(?:[^\u0000-\u001F\u007F%/?#]|%[0-9A-Fa-f]{2})(?:[^\u0000-\u001F\u007F%?#]|%[0-9A-Fa-f]{2})*)?"
};
}
macro_rules! url_authority {
() => {
concat!(
r"(?:(?:[A-Za-z0-9._~!$&'()*+,;=:@\-]|%[0-9A-Fa-f]{2})*@)?",
r"(?:\[(?:",
ipv6_pattern!(),
r")(?:%25(?:[^\u0000-\u001F\u007F%/?#\]]|%[0-9A-Fa-f]{2})+)?\](?::[0-9]*)?",
"|(?:(?:",
url_host_char!(),
"|:)*:[0-9]*|",
url_host_char!(),
"*))"
)
};
}
macro_rules! url_authority_tail {
() => {
concat!(
r"(?:/(?:[^\u0000-\u001F\u007F%?#]|%[0-9A-Fa-f]{2})*)?",
url_query_fragment!()
)
};
}
const URL_PARSE_PATTERN: &str = concat!(
"^(?:",
r"[A-Za-z][A-Za-z0-9+.\-]*://",
url_authority!(),
url_authority_tail!(),
"|//",
url_authority!(),
url_authority_tail!(),
r"|[A-Za-z][A-Za-z0-9+.\-]*:(?:[^\u0000-\u001F\u007F/?#][^\u0000-\u001F\u007F?#]*)?",
url_query_fragment!(),
r"|[A-Za-z][A-Za-z0-9+.\-]*:",
url_rooted!(),
url_query_fragment!(),
r"|(?:[^\u0000-\u001F\u007F%:/?#]|%[0-9A-Fa-f]{2})+(?:/(?:[^\u0000-\u001F\u007F%?#]|%[0-9A-Fa-f]{2})*)?",
url_query_fragment!(),
"|",
url_rooted!(),
url_query_fragment!(),
"|",
url_query_fragment!(),
")$",
);
#[must_use]
pub(crate) fn strict_collection_item_pattern(
semantics: FunctionSemantics,
index: usize,
) -> Option<&'static str> {
match (semantics.collection_items, index) {
(CollectionItemSemantics::CertificateIpList, 1) => {
Some(concat!("^(", ipv4_pattern!(), "|", ipv6_pattern!(), ")$",))
}
_ => None,
}
}