use std::collections::BTreeSet;
use serde::{Deserialize, Deserializer, de::Error};
use serde_json::Number;
pub(super) fn present<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
T::deserialize(deserializer).map(Some)
}
use crate::types::MAX_SAFE_JSON_INTEGER as MAX_SAFE_INTEGER;
const MAX_INPUTS: usize = 64;
const MAX_IDENTIFIER_BYTES: usize = 64;
const MAX_PATH_UNITS: usize = 1024;
fn integer<'de, D>(deserializer: D, max: u64, message: &str) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
let number = Number::deserialize(deserializer)?;
let value = number.as_f64().ok_or_else(|| D::Error::custom(message))?;
if value < 1.0 || value > max as f64 || value.fract() != 0.0 {
return Err(D::Error::custom(message));
}
Ok(value as u64)
}
pub(super) fn positive_integer<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
integer(
deserializer,
MAX_SAFE_INTEGER,
"Expected a positive safe integer (1–9007199254740991).",
)
.map(Some)
}
pub const MAX_TIMEOUT_MS: u64 = 2_147_483_647;
pub(super) fn timeout_number<E: Error>(number: Number) -> Result<std::time::Duration, E> {
let value = number
.as_f64()
.ok_or_else(|| E::custom("Invalid timeout_ms."))?;
if value < 1.0 || value > MAX_TIMEOUT_MS as f64 || value.fract() != 0.0 {
return Err(E::custom(
"timeout_ms must be an integer from 1 through 2147483647.",
));
}
Ok(std::time::Duration::from_millis(value as u64))
}
pub(super) fn timeout<'de, D>(deserializer: D) -> Result<Option<std::time::Duration>, D::Error>
where
D: Deserializer<'de>,
{
timeout_number::<D::Error>(Number::deserialize(deserializer)?).map(Some)
}
pub(crate) mod milliseconds {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S: Serializer>(
value: &Option<Duration>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(value) => serializer.serialize_u128(value.as_millis()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Duration>, D::Error> {
Option::<serde_json::Number>::deserialize(deserializer)?
.map(super::timeout_number::<D::Error>)
.transpose()
}
}
pub(crate) fn identifier(value: &str, label: &str) -> Result<(), String> {
if value.is_empty()
|| value.len() > MAX_IDENTIFIER_BYTES
|| !value.as_bytes()[0].is_ascii_alphanumeric()
|| !value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
{
return Err(format!(
"{label} must match [A-Za-z0-9][A-Za-z0-9_-]{{0,63}}."
));
}
Ok(())
}
pub(super) fn text(value: &str, label: &str) -> Result<(), String> {
if value.trim().is_empty() {
return Err(format!("{label} must be nonblank text."));
}
Ok(())
}
pub(super) fn tags(values: &[String]) -> Result<(), String> {
if values.len() > MAX_INPUTS || values.iter().collect::<BTreeSet<_>>().len() != values.len() {
return Err("tags must contain at most 64 unique strings.".into());
}
for value in values {
text(value, "Artifact tag")?;
if value.bytes().any(|byte| byte.is_ascii_control()) {
return Err("Artifact tags must not contain ASCII control characters.".into());
}
}
Ok(())
}
pub(super) fn script(command: &str, args: &[crate::scope::Argument]) -> Result<(), String> {
text(command, "Script command")?;
if command.bytes().any(|byte| byte.is_ascii_control())
|| args.iter().any(|arg| arg.to_string().contains('\0'))
{
return Err(
"A script requires a fixed command without control characters and string args without NUL."
.into(),
);
}
Ok(())
}
pub(crate) fn path(value: &str) -> Result<(), String> {
if value.encode_utf16().count() > MAX_PATH_UNITS
|| crate::platform::paths::drive_rooted(value)
|| value
.bytes()
.any(|byte| byte.is_ascii_control() || byte == b'\\')
|| value
.split('/')
.any(|part| part.is_empty() || part == "." || part == "..")
{
return Err("Execution input must be a safe project-relative path.".into());
}
Ok(())
}
pub(super) fn paths(values: &[crate::config::LogicalPath], label: &str) -> Result<(), String> {
if values.len() > MAX_INPUTS || values.iter().collect::<BTreeSet<_>>().len() != values.len() {
return Err(format!(
"{label} must contain at most 64 unique project-relative paths."
));
}
for value in values {
path(value).map_err(|message| format!("{label}: {message}"))?;
}
Ok(())
}