From cc711027da7559c3e28687a3a2e5bf0988bb37f0 Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Thu, 13 Aug 2026 10:43:26 -0400 Subject: [PATCH] Parse workflow version IDs case-insensitively MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WorkflowVersionId bolted a lowercase-only byte scan onto BlobHash parsing, giving the same 64-hex concept two parse behaviors across entry points. Identity is the decoded 32-byte digest and canonical serialization always emits lowercase, so accepting either case on input is lossless — the stored-blob canonicality check still rejects non-canonical bytes independently. Delegate straight to BlobHash. Co-Authored-By: Claude Fable 5 --- .../fabro-types/src/workflow_version_id.rs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/foundation/fabro-types/src/workflow_version_id.rs b/lib/foundation/fabro-types/src/workflow_version_id.rs index f3a618482..4bf597f47 100644 --- a/lib/foundation/fabro-types/src/workflow_version_id.rs +++ b/lib/foundation/fabro-types/src/workflow_version_id.rs @@ -35,18 +35,13 @@ impl From for String { } #[derive(Clone, Copy, Debug, PartialEq, Eq, Error)] -#[error("workflow version ID must be exactly 64 lowercase hexadecimal characters")] +#[error("workflow version ID must be exactly 64 hexadecimal characters")] pub struct WorkflowVersionIdParseError; impl FromStr for WorkflowVersionId { type Err = WorkflowVersionIdParseError; fn from_str(value: &str) -> Result { - // `BlobHash` enforces length and hex charset but accepts uppercase digits; - // the canonical wire form is lowercase only. - if value.bytes().any(|byte| byte.is_ascii_uppercase()) { - return Err(WorkflowVersionIdParseError); - } value .parse::() .map(Self) @@ -75,11 +70,14 @@ mod tests { } #[test] - fn parse_and_serde_require_lowercase_hex() { + fn parse_accepts_any_case_and_serializes_lowercase() { let value = BlobHash::new(b"workflow").to_string(); let id: WorkflowVersionId = value.parse().unwrap(); assert_eq!(serde_json::to_value(id).unwrap(), value); - assert!(value.to_uppercase().parse::().is_err()); + assert_eq!( + value.to_uppercase().parse::().unwrap(), + id + ); for invalid in [ String::new(), "0".repeat(63), @@ -88,9 +86,11 @@ mod tests { ] { assert!(invalid.parse::().is_err()); } - assert!( + assert_eq!( serde_json::from_value::(serde_json::json!(value.to_uppercase())) - .is_err() + .unwrap() + .to_string(), + value ); } }