Publish Clusterflux release candidate 6e83783

This commit is contained in:
Clusterflux release 2026-07-20 19:32:05 +02:00
parent 4d75257c6e
commit f3640643bd
16 changed files with 4667 additions and 106 deletions

View file

@ -1,18 +1,73 @@
use serde::{Deserialize, Serialize};
pub const MAX_EXTERNAL_ID_BYTES: usize = 255;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct IdParseError {
id_type: &'static str,
reason: &'static str,
}
impl IdParseError {
fn new(id_type: &'static str, reason: &'static str) -> Self {
Self { id_type, reason }
}
}
impl std::fmt::Display for IdParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} is invalid: {}", self.id_type, self.reason)
}
}
impl std::error::Error for IdParseError {}
fn validate_id(value: &str, id_type: &'static str) -> Result<(), IdParseError> {
if value.is_empty() || value.trim().is_empty() {
return Err(IdParseError::new(
id_type,
"value must not be empty or whitespace-only",
));
}
if value.len() > MAX_EXTERNAL_ID_BYTES {
return Err(IdParseError::new(
id_type,
"value exceeds the 255-byte limit",
));
}
if value.chars().any(char::is_control) {
return Err(IdParseError::new(
id_type,
"control characters are forbidden",
));
}
if !value.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(byte, b'-' | b'_' | b'.' | b':' | b'/' | b'@' | b'+')
}) {
return Err(IdParseError::new(
id_type,
"only ASCII letters, digits, '-', '_', '.', ':', '/', '@', and '+' are allowed",
));
}
Ok(())
}
macro_rules! id_type {
($name:ident) => {
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Self {
pub fn try_new(value: impl Into<String>) -> Result<Self, IdParseError> {
let value = value.into();
assert!(
!value.trim().is_empty(),
concat!(stringify!($name), " cannot be empty")
);
Self(value)
validate_id(&value, stringify!($name))?;
Ok(Self(value))
}
/// Constructs an identifier from a trusted, internally generated value.
pub fn new(value: impl Into<String>) -> Self {
Self::try_new(value).unwrap_or_else(|error| panic!("{error}"))
}
pub fn as_str(&self) -> &str {
@ -44,3 +99,39 @@ id_type!(TaskDefinitionId);
id_type!(TaskInstanceId);
id_type!(TenantId);
id_type!(UserId);
pub type DebugSessionId = ProcessId;
pub type RequestId = LaunchAttemptId;
#[cfg(test)]
mod tests {
use super::*;
fn assert_hostile_values<T>(parse: impl Fn(String) -> Result<T, IdParseError>) {
for value in [
String::new(),
" ".to_owned(),
"bad\0id".to_owned(),
"bad id".to_owned(),
"x".repeat(MAX_EXTERNAL_ID_BYTES + 1),
] {
assert!(parse(value).is_err());
}
}
#[test]
fn every_external_identifier_type_rejects_hostile_values() {
assert_hostile_values(AgentId::try_new);
assert_hostile_values(ArtifactId::try_new);
assert_hostile_values(DebugSessionId::try_new);
assert_hostile_values(NodeId::try_new);
assert_hostile_values(ProcessId::try_new);
assert_hostile_values(LaunchAttemptId::try_new);
assert_hostile_values(ProjectId::try_new);
assert_hostile_values(RequestId::try_new);
assert_hostile_values(TaskDefinitionId::try_new);
assert_hostile_values(TaskInstanceId::try_new);
assert_hostile_values(TenantId::try_new);
assert_hostile_values(UserId::try_new);
}
}

View file

@ -66,8 +66,9 @@ pub use execution::{
WasmTaskOutcome, WasmTaskResult, MAX_WASM_TASK_ENVELOPE_BYTES, WASM_TASK_ABI_VERSION,
};
pub use ids::{
AgentId, ArtifactId, LaunchAttemptId, NodeId, ProcessId, ProjectId, TaskDefinitionId,
TaskInstanceId, TenantId, UserId,
AgentId, ArtifactId, DebugSessionId, IdParseError, LaunchAttemptId, NodeId, ProcessId,
ProjectId, RequestId, TaskDefinitionId, TaskInstanceId, TenantId, UserId,
MAX_EXTERNAL_ID_BYTES,
};
pub use limits::{
LargeArgumentPolicy, LimitError, LimitKind, LogBuffer, LogRecord, ResourceLimits,