diff --git a/crates/lib/video_info/src/bin/video_info_parser.rs b/crates/lib/video_info/src/bin/video_info_parser.rs index 85424f5b7d..84f68d598c 100644 --- a/crates/lib/video_info/src/bin/video_info_parser.rs +++ b/crates/lib/video_info/src/bin/video_info_parser.rs @@ -85,6 +85,7 @@ fn print_info(filename: &str, info: &SeedanceInfo) { &info.generated_at_utc.map(|t| t.to_rfc3339()).unwrap_or_else(|| "(unparseable)".to_string()), ); opt("log id", &info.log_id); + opt("log id (decoded hex)", &info.log_id_decoded_hex); opt("digital source type", &info.digital_source_type); opt("claim generator", &info.claim_generator); opt("claim generator version", &info.claim_generator_version); diff --git a/crates/lib/video_info/src/seedance_info.rs b/crates/lib/video_info/src/seedance_info.rs index 2343b7e9b4..594e78f80f 100644 --- a/crates/lib/video_info/src/seedance_info.rs +++ b/crates/lib/video_info/src/seedance_info.rs @@ -73,9 +73,17 @@ pub struct SeedanceInfo { /// Parsed [`generated_at`](Self::generated_at), when it is valid RFC 3339. pub generated_at_utc: Option>, - /// Opaque per-generation log / request id (e.g. `"ATIAA7b8D_iKjF32GukAAAAA"`). + /// Per-generation log / request id, base64url-encoded + /// (e.g. `"ATIAA7b8D_iKjF32GukAAAAA"`). pub log_id: Option, + /// [`log_id`](Self::log_id) base64url-decoded to hex (18 bytes). It is a + /// structured id: byte 0 = version (`01`), byte 1 = region/platform + /// (`0x32` = Volcengine, `0x33` = BytePlus), bytes 2–3 constant (`0003`), + /// bytes 4–13 a per-generation value, bytes 14–17 zero padding. The region + /// byte independently corroborates [`platform`](Self::platform). + pub log_id_decoded_hex: Option, + /// IPTC digital source type URL — always trained-algorithmic-media for these. pub digital_source_type: Option, @@ -155,6 +163,12 @@ impl SeedanceInfo { let (model_brand, model_version, is_fast) = parse_model_name(&model_name); + let log_id = text_after_key(data, b"log_id"); + let log_id_decoded_hex = log_id + .as_deref() + .and_then(decode_base64url) + .map(|bytes| bytes.iter().map(|b| format!("{:02x}", b)).collect::()); + let software_agent_version = text_after_key_from(data, b"version", vendor_end); let claim_generator = find(data, b"c2pa-rs").map(|_| "c2pa-rs".to_string()); @@ -176,7 +190,8 @@ impl SeedanceInfo { is_fast, generated_at, generated_at_utc, - log_id: text_after_key(data, b"log_id"), + log_id, + log_id_decoded_hex, digital_source_type: text_after_key(data, b"digitalSourceType"), claim_generator, claim_generator_version, @@ -358,6 +373,37 @@ fn find_cert_serial(data: &[u8]) -> Option { Some(bytes.iter().map(|b| format!("{:02X}", b)).collect()) } +/// Minimal base64url (RFC 4648 §5) decoder — no external dependency. Ignores +/// `=` padding; returns `None` on any invalid character. +fn decode_base64url(input: &str) -> Option> { + fn sextet(c: u8) -> Option { + match c { + b'A'..=b'Z' => Some(c - b'A'), + b'a'..=b'z' => Some(c - b'a' + 26), + b'0'..=b'9' => Some(c - b'0' + 52), + b'-' => Some(62), + b'_' => Some(63), + _ => None, + } + } + let chars: Vec = input.bytes().filter(|&b| b != b'=').collect(); + let mut out = Vec::with_capacity(chars.len() * 3 / 4); + for chunk in chars.chunks(4) { + let mut buf = [0u8; 4]; + for (i, &c) in chunk.iter().enumerate() { + buf[i] = sextet(c)?; + } + out.push((buf[0] << 2) | (buf[1] >> 4)); + if chunk.len() >= 3 { + out.push((buf[1] << 4) | (buf[2] >> 2)); + } + if chunk.len() >= 4 { + out.push((buf[2] << 6) | buf[3]); + } + } + Some(out) +} + // ── Byte search ── fn find(haystack: &[u8], needle: &[u8]) -> Option { @@ -451,6 +497,7 @@ mod tests { assert_eq!(info.generated_at, "2026-06-19T01:32:58Z"); assert!(info.generated_at_utc.is_some()); assert_eq!(info.log_id.as_deref(), Some("ATIAA7b8D_iKjF32GukAAAAA")); + assert_eq!(info.log_id_decoded_hex.as_deref(), Some("01320003b6fc0ff88a8c5df61ae900000000")); assert_eq!(info.claim_generator.as_deref(), Some("c2pa-rs")); assert_eq!(info.claim_generator_version.as_deref(), Some("0.78.4")); assert_eq!(info.manifest_id.as_deref(), Some("urn:c2pa:40a5f6fe-b88a-4a2c-ae72-f76a1e5a6012")); @@ -474,6 +521,8 @@ mod tests { assert_eq!(info.signer_email.as_deref(), Some("certificate@byteplus.com")); assert_eq!(info.signer_org_id.as_deref(), Some("NTRSG-202024632W")); assert_eq!(info.signer_country.as_deref(), Some("SG")); + // BytePlus region byte is 0x33 (vs 0x32 for Volcengine). + assert_eq!(info.log_id_decoded_hex.as_deref(), Some("01330003b6f7e89f1c2d93b6894900000000")); } #[test]