use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use sha2::{Digest, Sha256};
use uuid::Uuid;
use crate::error::missing_parameter;
pub fn require_str<'a>(
input: &'a serde_json::Value,
key: &str,
) -> Result<&'a str, awsim_core::AwsError> {
input
.get(key)
.and_then(|v| v.as_str())
.ok_or_else(|| missing_parameter(key))
}
pub fn opt_str<'a>(input: &'a serde_json::Value, key: &str) -> Option<&'a str> {
input.get(key).and_then(|v| v.as_str())
}
pub fn opt_u64(input: &serde_json::Value, key: &str) -> Option<u64> {
input.get(key).and_then(|v| v.as_u64())
}
pub fn opt_bool(input: &serde_json::Value, key: &str) -> Option<bool> {
input.get(key).and_then(|v| v.as_bool())
}
pub fn now_iso8601() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let (y, mo, d, h, min, s) = unix_to_ymd_hms(secs);
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{min:02}:{s:02}.000+0000")
}
fn unix_to_ymd_hms(secs: u64) -> (u64, u64, u64, u64, u64, u64) {
let s = secs % 60;
let mins = secs / 60;
let min = mins % 60;
let hours = mins / 60;
let h = hours % 24;
let days = hours / 24;
let (y, doy) = days_to_year(days);
let (mo, d) = doy_to_month_day(doy, is_leap(y));
(y, mo, d, h, min, s)
}
fn is_leap(y: u64) -> bool {
(y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
}
fn days_to_year(mut days: u64) -> (u64, u64) {
let mut y = 1970u64;
loop {
let dy = if is_leap(y) { 366 } else { 365 };
if days < dy {
return (y, days);
}
days -= dy;
y += 1;
}
}
fn doy_to_month_day(doy: u64, leap: bool) -> (u64, u64) {
let months: &[u64] = if leap {
&[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
&[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut rem = doy;
for (i, &days) in months.iter().enumerate() {
if rem < days {
return ((i + 1) as u64, rem + 1);
}
rem -= days;
}
(12, 31)
}
pub fn new_uuid() -> String {
Uuid::new_v4().to_string()
}
pub fn decode_zip(b64: &str) -> Result<(Vec<u8>, String, u64), awsim_core::AwsError> {
let bytes = BASE64.decode(b64).map_err(|e| {
awsim_core::AwsError::bad_request(
"InvalidParameterValueException",
format!("Invalid base64 ZipFile: {e}"),
)
})?;
let hash = sha256_base64(&bytes);
let size = bytes.len() as u64;
Ok((bytes, hash, size))
}
pub const VALID_RUNTIMES: &[&str] = &[
"nodejs18.x",
"nodejs20.x",
"nodejs22.x",
"python3.10",
"python3.11",
"python3.12",
"python3.13",
"java11",
"java17",
"java21",
"dotnet6",
"dotnet8",
"ruby3.2",
"ruby3.3",
"provided.al2",
"provided.al2023",
];
pub fn validate_runtime(runtime: &str) -> Result<(), awsim_core::AwsError> {
if VALID_RUNTIMES.contains(&runtime) {
return Ok(());
}
Err(awsim_core::AwsError::bad_request(
"InvalidParameterValueException",
format!(
"Value {runtime} at 'runtime' failed to satisfy constraint: \
Member must satisfy enum value set: [{}]",
VALID_RUNTIMES.join(", ")
),
))
}
pub fn validate_handler(handler: &str) -> Result<(), awsim_core::AwsError> {
if handler.is_empty() {
return Err(awsim_core::AwsError::bad_request(
"InvalidParameterValueException",
"Handler must not be empty",
));
}
if handler.len() > 128 {
return Err(awsim_core::AwsError::bad_request(
"InvalidParameterValueException",
"Handler must be at most 128 characters",
));
}
if handler.chars().any(char::is_whitespace) {
return Err(awsim_core::AwsError::bad_request(
"InvalidParameterValueException",
"Handler must not contain whitespace",
));
}
Ok(())
}
pub fn sha256_base64(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
BASE64.encode(hasher.finalize())
}
pub fn validate_qualifier(qualifier: &str) -> Result<(), awsim_core::AwsError> {
if qualifier == "$LATEST" {
return Ok(());
}
if qualifier.chars().all(|c| c.is_ascii_digit()) && !qualifier.is_empty() {
return Ok(());
}
if (1..=128).contains(&qualifier.len())
&& let Some(first) = qualifier.chars().next()
&& first.is_ascii_alphabetic()
&& qualifier
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Ok(());
}
Err(awsim_core::AwsError::bad_request(
"InvalidParameterValueException",
format!(
"Qualifier `{qualifier}` is not valid. Must be $LATEST, a numeric version, \
or an alias matching [a-zA-Z][a-zA-Z0-9-_]{{0,127}}."
),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_qualifier_accepts_dollar_latest() {
validate_qualifier("$LATEST").unwrap();
}
#[test]
fn validate_qualifier_accepts_numeric_version() {
validate_qualifier("1").unwrap();
validate_qualifier("42").unwrap();
}
#[test]
fn validate_qualifier_accepts_alias_name() {
validate_qualifier("prod").unwrap();
validate_qualifier("blue-green_1").unwrap();
}
#[test]
fn validate_qualifier_rejects_leading_digit_or_dash() {
assert!(validate_qualifier("-prod").is_err());
assert!(validate_qualifier("1prod").is_err());
}
#[test]
fn validate_qualifier_rejects_invalid_chars() {
assert!(validate_qualifier("has space").is_err());
assert!(validate_qualifier("dot.name").is_err());
assert!(validate_qualifier("").is_err());
}
#[test]
fn validate_qualifier_rejects_over_128_chars() {
let long: String = "a".repeat(129);
assert!(validate_qualifier(&long).is_err());
}
}