diff --git a/crates/openjd-model/src/job/create_job/ranges.rs b/crates/openjd-model/src/job/create_job/ranges.rs index 71cdf49d..75a84dda 100644 --- a/crates/openjd-model/src/job/create_job/ranges.rs +++ b/crates/openjd-model/src/job/create_job/ranges.rs @@ -209,15 +209,9 @@ fn resolve_int_range( &openjd_expr::FormatStringOptions::new().with_path_format(PathFormat::Posix), ) { match val { + // Range expressions are not length-capped; only the list + // forms are. See `EffectiveLimits::max_task_param_range_len`. ExprValue::RangeExpr(r) => { - if r.len() > limits.max_task_param_range_len { - return Err(ModelError::DecodeValidation(format!( - "Task parameter '{}' range exceeds {} elements ({} elements)", - param_name, - limits.max_task_param_range_len, - r.len() - ))); - } return Ok(job::TaskParamRange::RangeExpr(r)); } val if val.is_list() => { @@ -254,14 +248,6 @@ fn resolve_int_range( let range_expr: RangeExpr = resolved .parse() .map_err(|e: openjd_expr::ExpressionError| ModelError::Expression(e))?; - if range_expr.len() > limits.max_task_param_range_len { - return Err(ModelError::DecodeValidation(format!( - "Task parameter '{}' range exceeds {} elements ({} elements)", - param_name, - limits.max_task_param_range_len, - range_expr.len() - ))); - } Ok(job::TaskParamRange::RangeExpr(range_expr)) } } diff --git a/crates/openjd-model/src/template/validate_v2023_09/mod.rs b/crates/openjd-model/src/template/validate_v2023_09/mod.rs index 257f3d6a..99a9daf6 100644 --- a/crates/openjd-model/src/template/validate_v2023_09/mod.rs +++ b/crates/openjd-model/src/template/validate_v2023_09/mod.rs @@ -38,6 +38,16 @@ pub struct EffectiveLimits { /// raises the job-template limit to 200. pub max_env_template_param_count: usize, pub max_filename_len: usize, + /// Maximum elements in a task parameter's *list*-form range — + /// `` (§3.4.1.1), `` (§3.4.1.2) and + /// `` (§3.4.1.3). + /// + /// Do not apply this to an `` expansion. §3.4.1.1.1 + /// constrains that form only by "no two ranges may overlap", and its stated + /// purpose is expressing frame ranges succinctly — capping the expansion + /// rejects the form's primary use case and pre-empts the host service's own + /// task-count limits, which it may raise per account. A host that wants to + /// bound fan-out has `CallerLimits::max_task_count`. pub max_task_param_range_len: usize, pub max_task_param_string_len: usize, pub max_job_param_string_len: usize, diff --git a/crates/openjd-model/src/template/validate_v2023_09/structure.rs b/crates/openjd-model/src/template/validate_v2023_09/structure.rs index c77ad407..219a7330 100644 --- a/crates/openjd-model/src/template/validate_v2023_09/structure.rs +++ b/crates/openjd-model/src/template/validate_v2023_09/structure.rs @@ -1053,18 +1053,15 @@ fn validate_task_param_range( } } IntRange::Expression(expr) => { + // Grammar only. `max_task_param_range_len` deliberately does not + // apply to an expression's expansion — see its doc comment. let raw = expr.raw(); if !raw.contains("{{") { - match raw.parse::() { - Ok(range) => { - if range.len() > limits.max_task_param_range_len { - errors.add(path, format!("INT parameter '{}' range expression expands to {} elements (max {}).", tp.name, range.len(), limits.max_task_param_range_len)); - } - } - Err(e) => errors.add( + if let Err(e) = raw.parse::() { + errors.add( path, format!("INT parameter '{}' range expression error: {e}", tp.name), - ), + ); } } } diff --git a/crates/openjd-model/tests/integration/test_chunk_int.rs b/crates/openjd-model/tests/integration/test_chunk_int.rs index dbebc311..db3398ab 100644 --- a/crates/openjd-model/tests/integration/test_chunk_int.rs +++ b/crates/openjd-model/tests/integration/test_chunk_int.rs @@ -856,3 +856,31 @@ fn chunks_parameter_name() { let tasks: Vec<_> = iter.collect(); assert_eq!(tasks.len(), 2); } + +// ══════════════════════════════════════════════════════════════ +// Range length — §3.4 caps the list form only +// ══════════════════════════════════════════════════════════════ + +/// §3.4.1.1.1 `` states no element cap, so a CHUNK[INT] range +/// expression may expand past `max_task_param_range_len`. +#[test] +fn range_expression_expansion_is_not_capped() { + for range in ["1-1025", "1-5000", "1-100000:2"] { + decode_ok(&chunk_job(&format!( + r#"{{"name": "foo", "type": "CHUNK[INT]", "range": "{range}", "chunks": {{"defaultTaskCount": 10, "rangeConstraint": "CONTIGUOUS"}}}}"# + ))); + } +} + +/// §3.4.1.1 item 4 caps the list form at 1024 elements. That cap stays. +#[test] +fn range_list_is_still_capped() { + let values: Vec = (0..1025).map(|v| v.to_string()).collect(); + check_err( + &chunk_job(&format!( + r#"{{"name": "foo", "type": "CHUNK[INT]", "range": [{}], "chunks": {{"defaultTaskCount": 10, "rangeConstraint": "CONTIGUOUS"}}}}"#, + values.join(",") + )), + &["range exceeds 1024 elements"], + ); +} diff --git a/crates/openjd-model/tests/integration/test_parameter_space.rs b/crates/openjd-model/tests/integration/test_parameter_space.rs index 1958c65e..dbff6335 100644 --- a/crates/openjd-model/tests/integration/test_parameter_space.rs +++ b/crates/openjd-model/tests/integration/test_parameter_space.rs @@ -739,3 +739,57 @@ fn combination_expr_leading_star_rejected() { "Leading star in combination should be rejected" ); } + +// ══════════════════════════════════════════════════════════════ +// Task parameter range length — §3.4 caps the list forms only +// ══════════════════════════════════════════════════════════════ + +/// §3.4.1.1.1 `` states no element cap, so the expansion of a +/// range expression must not be bounded by `max_task_param_range_len`. The +/// form exists to express frame ranges, which routinely exceed 1024 values. +#[test] +fn int_range_expression_expansion_is_not_capped() { + for range in ["1-1024", "1-1025", "1-5000", "1-100000:2"] { + decode_ok(&job_with_param_space(&format!( + r#"{{"taskParameterDefinitions": [{{"name": "Frame", "type": "INT", "range": "{range}"}}]}}"# + ))); + } +} + +/// A malformed range expression is still rejected — dropping the length cap +/// must not drop grammar validation. +#[test] +fn int_range_expression_grammar_is_still_validated() { + check_err( + &job_with_param_space( + r#"{"taskParameterDefinitions": [{"name": "Frame", "type": "INT", "range": "1-10,5-15"}]}"#, + ), + &["range expression error"], + ); +} + +/// §3.4.1.1 item 4 caps `` at 1024 elements. That cap stays. +#[test] +fn int_range_list_is_still_capped() { + let values: Vec = (0..1025).map(|v| v.to_string()).collect(); + check_err( + &job_with_param_space(&format!( + r#"{{"taskParameterDefinitions": [{{"name": "Frame", "type": "INT", "range": [{}]}}]}}"#, + values.join(",") + )), + &["range exceeds 1024 elements"], + ); +} + +/// §3.4.1.3 caps `` too. +#[test] +fn string_range_list_is_still_capped() { + let values: Vec = (0..1025).map(|v| format!("\"v{v}\"")).collect(); + check_err( + &job_with_param_space(&format!( + r#"{{"taskParameterDefinitions": [{{"name": "S", "type": "STRING", "range": [{}]}}]}}"#, + values.join(",") + )), + &["range exceeds 1024 elements"], + ); +} diff --git a/crates/openjd-model/tests/integration/test_step_param_space_iter.rs b/crates/openjd-model/tests/integration/test_step_param_space_iter.rs index 60befa6a..19c2300b 100644 --- a/crates/openjd-model/tests/integration/test_step_param_space_iter.rs +++ b/crates/openjd-model/tests/integration/test_step_param_space_iter.rs @@ -905,7 +905,6 @@ fn test_single_param_getitem() { #[test] fn lazy_param_space_range_expr_within_limit() { - // max_task_param_range_len is 1024 for all configs (not raised by FB1) let template = yaml_val( r#" specificationVersion: "jobtemplate-2023-09" diff --git a/specs/model/parameters.md b/specs/model/parameters.md index dc065152..91285750 100644 --- a/specs/model/parameters.md +++ b/specs/model/parameters.md @@ -187,6 +187,12 @@ Numeric limits derived from context. FEATURE_BUNDLE_1 raises many limits: | `max_command_len` | 1024 | 1024 | | `max_description_len` | 2048 | 2048 | +`max_task_param_range_len` applies to the **list** forms of a task parameter +range only — `` (§3.4.1.1), `` (§3.4.1.2) and +`` (§3.4.1.3). §3.4.1.1.1 `` states no element +cap, so an expression's expansion is unbounded here; a host that needs to bound +fan-out uses `CallerLimits::max_task_count`. + ### EffectiveRules Structural rules derived from context: