use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Value;
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::Result;
use core::fmt::Write as _;
use chrono::{DateTime, Duration, FixedOffset, Utc};
use super::helpers::as_str;
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("azure.policy.fn.date_time_add", (fn_date_time_add, 3));
m.insert(
"azure.policy.fn.date_time_from_epoch",
(fn_date_time_from_epoch, 1),
);
m.insert(
"azure.policy.fn.date_time_to_epoch",
(fn_date_time_to_epoch, 1),
);
m.insert("azure.policy.fn.add_days", (fn_add_days, 0));
}
fn parse_datetime(s: &str) -> Option<DateTime<FixedOffset>> {
if s.len() > 10 && s.as_bytes().get(10).copied() == Some(b' ') {
if let Ok(dt) = DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%:z") {
return Some(dt);
}
if let Ok(dt) = DateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f%:z") {
return Some(dt);
}
if let Some(stripped) = s.strip_suffix('Z').or_else(|| s.strip_suffix('z')) {
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(stripped, "%Y-%m-%d %H:%M:%S")
{
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some(utc.fixed_offset());
}
if let Ok(naive) =
chrono::NaiveDateTime::parse_from_str(stripped, "%Y-%m-%d %H:%M:%S%.f")
{
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some(utc.fixed_offset());
}
}
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some(utc.fixed_offset());
}
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f") {
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some(utc.fixed_offset());
}
}
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(dt);
}
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some(utc.fixed_offset());
}
if let Ok(naive) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
let utc = DateTime::<Utc>::from_naive_utc_and_offset(naive, Utc);
return Some(utc.fixed_offset());
}
None
}
fn format_datetime(dt: &DateTime<FixedOffset>) -> String {
if dt.offset().local_minus_utc() == 0 {
dt.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string()
} else {
dt.format("%Y-%m-%dT%H:%M:%S%.f%:z").to_string()
}
}
fn parse_iso8601_duration(s: &str) -> Option<Duration> {
let (s, negative) = s.strip_prefix('-').map_or((s, false), |rest| (rest, true));
let s = s.strip_prefix('P')?;
let mut total_seconds: i64 = 0;
let mut in_time = false;
let mut num_buf = String::new();
for ch in s.chars() {
match ch {
'T' => {
if !num_buf.is_empty() {
return None;
}
in_time = true;
}
'0'..='9' | '.' => {
num_buf.push(ch);
}
'Y' if !in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 365.0 * 86400.0))?;
num_buf.clear();
}
'M' if !in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 30.0 * 86400.0))?;
num_buf.clear();
}
'W' if !in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 7.0 * 86400.0))?;
num_buf.clear();
}
'D' if !in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 86400.0))?;
num_buf.clear();
}
'H' if in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 3600.0))?;
num_buf.clear();
}
'M' if in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n * 60.0))?;
num_buf.clear();
}
'S' if in_time => {
let n: f64 = num_buf.parse().ok()?;
total_seconds = total_seconds.checked_add(f64_as_i64(n))?;
num_buf.clear();
}
_ => return None,
}
}
if !num_buf.is_empty() {
return None;
}
let dur = Duration::seconds(if negative {
total_seconds.checked_neg()?
} else {
total_seconds
});
Some(dur)
}
fn fn_date_time_add(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(base_str) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(duration_str) = args.get(1).and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(base_dt) = parse_datetime(base_str) else {
return Ok(Value::Undefined);
};
let Some(duration) = parse_iso8601_duration(duration_str) else {
return Ok(Value::Undefined);
};
let result = base_dt
.checked_add_signed(duration)
.ok_or_else(|| anyhow::anyhow!("dateTimeAdd: datetime overflow"))?;
let output = match args.get(2).and_then(as_str) {
Some(fmt) => format_datetime_dotnet(&result, fmt)?,
None => format_datetime(&result),
};
Ok(Value::from(output))
}
fn fn_date_time_from_epoch(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(epoch) = args.first().and_then(extract_i64) else {
return Ok(Value::Undefined);
};
let Some(dt) = DateTime::<Utc>::from_timestamp(epoch, 0) else {
return Ok(Value::Undefined);
};
Ok(Value::from(dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()))
}
fn fn_date_time_to_epoch(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(s) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(dt) = parse_datetime(s) else {
return Ok(Value::Undefined);
};
Ok(Value::from(dt.timestamp()))
}
fn fn_add_days(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(base_str) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let Some(days) = args.get(1).and_then(extract_i64) else {
return Ok(Value::Undefined);
};
let Some(base_dt) = parse_datetime(base_str) else {
return Ok(Value::Undefined);
};
let duration = Duration::days(days);
let result = base_dt
.checked_add_signed(duration)
.ok_or_else(|| anyhow::anyhow!("addDays: datetime overflow"))?;
Ok(Value::from(format_datetime(&result)))
}
fn extract_i64(v: &Value) -> Option<i64> {
match *v {
Value::Number(ref n) => n.as_i64(),
_ => None,
}
}
#[expect(clippy::as_conversions)]
const fn f64_as_i64(x: f64) -> i64 {
x as i64
}
enum FmtSegment {
Chrono(String),
Frac(usize),
AmPmShort,
TzHoursNoPad,
TzHoursPad,
}
enum StandardFormat {
Expansion(String),
UtcNormalized(String),
NotStandard,
}
fn format_datetime_dotnet(dt: &DateTime<FixedOffset>, dotnet_fmt: &str) -> Result<String> {
let (effective_fmt_owned, format_dt);
let effective_fmt = match resolve_standard_format(dotnet_fmt)? {
StandardFormat::Expansion(s) => {
effective_fmt_owned = s;
&effective_fmt_owned
}
StandardFormat::UtcNormalized(s) => {
format_dt = dt.with_timezone(&Utc).fixed_offset();
effective_fmt_owned = s;
let is_utc = true;
let segments = dotnet_to_segments(&effective_fmt_owned, is_utc);
return Ok(render_segments(&format_dt, &segments));
}
StandardFormat::NotStandard => dotnet_fmt,
};
let is_utc = dt.offset().local_minus_utc() == 0;
let segments = dotnet_to_segments(effective_fmt, is_utc);
Ok(render_segments(dt, &segments))
}
fn render_segments(dt: &DateTime<FixedOffset>, segments: &[FmtSegment]) -> String {
let mut out = String::new();
for seg in segments {
match *seg {
FmtSegment::Chrono(ref fmt) => {
out.push_str(&dt.format(fmt).to_string());
}
FmtSegment::Frac(n) => {
let nanos = dt.format("%f").to_string();
let truncated: String = nanos.chars().take(n).collect();
out.push_str(&truncated);
}
FmtSegment::AmPmShort => {
let full = dt.format("%p").to_string();
if let Some(c) = full.chars().next() {
out.push(c);
}
}
FmtSegment::TzHoursNoPad => {
let hours = dt.offset().local_minus_utc() / 3600;
if hours >= 0 {
out.push('+');
}
let _ = write!(out, "{hours}");
}
FmtSegment::TzHoursPad => {
let secs = dt.offset().local_minus_utc();
let hours = secs / 3600;
if secs >= 0 {
let _ = write!(out, "+{hours:02}");
} else {
let _ = write!(out, "-{:02}", hours.wrapping_neg());
}
}
}
}
out
}
fn resolve_standard_format(fmt: &str) -> Result<StandardFormat> {
if fmt.len() != 1 {
return Ok(StandardFormat::NotStandard);
}
Ok(match fmt {
"d" => StandardFormat::Expansion("MM/dd/yyyy".into()),
"D" => StandardFormat::Expansion("dddd, dd MMMM yyyy".into()),
"t" => StandardFormat::Expansion("HH:mm".into()),
"T" => StandardFormat::Expansion("HH:mm:ss".into()),
"g" => StandardFormat::Expansion("MM/dd/yyyy HH:mm".into()),
"G" => StandardFormat::Expansion("MM/dd/yyyy HH:mm:ss".into()),
"M" | "m" => StandardFormat::Expansion("MMMM dd".into()),
"o" | "O" => StandardFormat::Expansion("yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK".into()),
"R" | "r" => StandardFormat::UtcNormalized("ddd, dd MMM yyyy HH':'mm':'ss 'GMT'".into()),
"s" => StandardFormat::Expansion("yyyy'-'MM'-'dd'T'HH':'mm':'ss".into()),
"u" => StandardFormat::UtcNormalized("yyyy'-'MM'-'dd HH':'mm':'ss'Z'".into()),
"U" => StandardFormat::UtcNormalized("dddd, dd MMMM yyyy HH:mm:ss".into()),
"Y" | "y" => StandardFormat::Expansion("yyyy MMMM".into()),
"f" => StandardFormat::Expansion("dddd, dd MMMM yyyy HH:mm".into()),
"F" => StandardFormat::Expansion("dddd, dd MMMM yyyy HH:mm:ss".into()),
_ => anyhow::bail!(
"dateTimeAdd: unrecognised standard format specifier '{}'",
fmt
),
})
}
fn dotnet_to_segments(fmt: &str, is_utc: bool) -> Vec<FmtSegment> {
let mut segments: Vec<FmtSegment> = Vec::new();
let mut chrono_buf = String::new();
let chars: Vec<char> = fmt.chars().collect();
let len = chars.len();
let mut i: usize = 0;
macro_rules! flush {
() => {
if !chrono_buf.is_empty() {
segments.push(FmtSegment::Chrono(core::mem::take(&mut chrono_buf)));
}
};
}
while i < len {
let ch = chars.get(i).copied().unwrap_or('\0');
let remaining = len.saturating_sub(i);
match ch {
'\\' if remaining > 1 => {
i = i.wrapping_add(1);
let next = chars.get(i).copied().unwrap_or('\0');
chrono_buf.push(next);
i = i.wrapping_add(1);
}
'\'' => {
i = i.wrapping_add(1);
while i < len {
let c = chars.get(i).copied().unwrap_or('\0');
if c == '\'' {
i = i.wrapping_add(1);
break;
}
chrono_buf.push(c);
i = i.wrapping_add(1);
}
}
'y' if remaining >= 4 && matches_run(&chars, i, 'y', 4) => {
chrono_buf.push_str("%Y");
i = i.wrapping_add(4);
}
'y' if remaining >= 2 && matches_run(&chars, i, 'y', 2) => {
chrono_buf.push_str("%y");
i = i.wrapping_add(2);
}
'M' if remaining >= 4 && matches_run(&chars, i, 'M', 4) => {
chrono_buf.push_str("%B");
i = i.wrapping_add(4);
}
'M' if remaining >= 3 && matches_run(&chars, i, 'M', 3) => {
chrono_buf.push_str("%b");
i = i.wrapping_add(3);
}
'M' if remaining >= 2 && matches_run(&chars, i, 'M', 2) => {
chrono_buf.push_str("%m");
i = i.wrapping_add(2);
}
'M' => {
chrono_buf.push_str("%-m");
i = i.wrapping_add(1);
}
'd' if remaining >= 4 && matches_run(&chars, i, 'd', 4) => {
chrono_buf.push_str("%A");
i = i.wrapping_add(4);
}
'd' if remaining >= 3 && matches_run(&chars, i, 'd', 3) => {
chrono_buf.push_str("%a");
i = i.wrapping_add(3);
}
'd' if remaining >= 2 && matches_run(&chars, i, 'd', 2) => {
chrono_buf.push_str("%d");
i = i.wrapping_add(2);
}
'd' => {
chrono_buf.push_str("%-d");
i = i.wrapping_add(1);
}
'H' if remaining >= 2 && matches_run(&chars, i, 'H', 2) => {
chrono_buf.push_str("%H");
i = i.wrapping_add(2);
}
'H' => {
chrono_buf.push_str("%-H");
i = i.wrapping_add(1);
}
'h' if remaining >= 2 && matches_run(&chars, i, 'h', 2) => {
chrono_buf.push_str("%I");
i = i.wrapping_add(2);
}
'h' => {
chrono_buf.push_str("%-I");
i = i.wrapping_add(1);
}
'm' if remaining >= 2 && matches_run(&chars, i, 'm', 2) => {
chrono_buf.push_str("%M");
i = i.wrapping_add(2);
}
'm' => {
chrono_buf.push_str("%-M");
i = i.wrapping_add(1);
}
's' if remaining >= 2 && matches_run(&chars, i, 's', 2) => {
chrono_buf.push_str("%S");
i = i.wrapping_add(2);
}
's' => {
chrono_buf.push_str("%-S");
i = i.wrapping_add(1);
}
'f' => {
let mut count: usize = 0;
while i < len && chars.get(i).copied() == Some('f') {
count = count.wrapping_add(1);
i = i.wrapping_add(1);
}
flush!();
segments.push(FmtSegment::Frac(count.min(9)));
}
't' if remaining >= 2 && matches_run(&chars, i, 't', 2) => {
chrono_buf.push_str("%p");
i = i.wrapping_add(2);
}
't' => {
flush!();
segments.push(FmtSegment::AmPmShort);
i = i.wrapping_add(1);
}
'K' => {
if is_utc {
chrono_buf.push('Z');
} else {
chrono_buf.push_str("%:z");
}
i = i.wrapping_add(1);
}
'z' if remaining >= 3 && matches_run(&chars, i, 'z', 3) => {
chrono_buf.push_str("%:z");
i = i.wrapping_add(3);
}
'z' if remaining >= 2 && matches_run(&chars, i, 'z', 2) => {
flush!();
segments.push(FmtSegment::TzHoursPad);
i = i.wrapping_add(2);
}
'z' => {
flush!();
segments.push(FmtSegment::TzHoursNoPad);
i = i.wrapping_add(1);
}
_ => {
chrono_buf.push(ch);
i = i.wrapping_add(1);
}
}
}
flush!();
segments
}
fn matches_run(chars: &[char], start: usize, ch: char, count: usize) -> bool {
(0..count).all(|offset| chars.get(start.wrapping_add(offset)).copied() == Some(ch))
}