#![forbid(unsafe_code)] //! FXID effect contracts. //! //! FXID decoding and command framing are implemented as compatibility //! contracts. The create/update/emit lifecycle below is a deterministic //! reference stub until opcode timing, gates, RNG, and command-body semantics //! are backed by runtime-captured evidence. use fparkan_binary::{Cursor, DecodeError}; use std::sync::Arc; /// `FXID` `NRes` entry type. pub const FXID_KIND: u32 = 0x4449_5846; const HEADER_SIZE: usize = 60; /// FX document. #[derive(Clone, Debug)] pub struct FxDocument { bytes: Arc<[u8]>, header: FxHeader, commands: Vec, } /// FX header. #[derive(Clone, Debug, PartialEq)] pub struct FxHeader { /// Number of commands in the stream. pub command_count: u32, /// Time mode. pub time_mode: u32, /// Duration in seconds. pub duration_seconds: f32, /// Phase jitter. pub phase_jitter: f32, /// Opaque flags. pub flags: u32, /// Opaque settings id. pub settings_id: u32, /// Random spatial shift. pub random_shift: [f32; 3], /// Local pivot. pub pivot: [f32; 3], /// Base scale. pub scale: [f32; 3], } /// FX opcode. #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] pub enum FxOpcode { /// Opcode 1. Op1, /// Opcode 2. Op2, /// Opcode 3. Op3, /// Opcode 4. Op4, /// Opcode 5. Op5, /// Opcode 6. Op6, /// Opcode 7. Op7, /// Opcode 8. Op8, /// Opcode 9. Op9, /// Opcode 10. Op10, } /// FX resource reference. #[derive(Clone, Debug, Eq, PartialEq)] pub struct FxResourceRef { /// Fixed archive field bytes. pub archive_raw: [u8; 32], /// Fixed name field bytes. pub name_raw: [u8; 32], } /// FX command. #[derive(Clone, Debug, Eq, PartialEq)] pub struct FxCommand { /// Raw command word. pub word: u32, /// Decoded opcode. pub opcode: FxOpcode, /// Enabled bit. pub enabled: bool, /// Command body after the word. pub raw_body: Arc<[u8]>, /// Resource references discovered in known command layouts. pub resource_refs: Vec, } /// FX instance id. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct FxInstanceId(pub u64); /// FX seed. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct FxSeed(pub u64); /// External transform snapshot. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Transform { /// Translation. pub translation: [f32; 3], /// Rotation quaternion. pub rotation: [f32; 4], /// Scale. pub scale: [f32; 3], } impl Default for Transform { fn default() -> Self { Self { translation: [0.0; 3], rotation: [0.0, 0.0, 0.0, 1.0], scale: [1.0; 3], } } } /// Game time in ticks. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct GameTime(pub u64); /// FX reference-stub runtime state. #[derive(Clone, Debug)] pub struct FxState { /// Instance id. pub id: FxInstanceId, /// Source document. pub document: Arc, /// Seed. pub seed: FxSeed, /// Transform at creation time. pub transform: Transform, /// Last updated time. pub time: GameTime, /// RNG call count reserved for deterministic captures. pub rng_calls: u64, /// Lifecycle phase. pub lifecycle: FxLifecycle, } /// FX lifecycle phase. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum FxLifecycle { /// Running and eligible to emit. #[default] Running, /// Stopped and not eligible to emit. Stopped, /// Ended permanently for the current instance. Ended, } /// Visual FX emission produced from a command. #[derive(Clone, Debug, Eq, PartialEq)] pub struct FxPrimitive { /// Command index. pub command_index: u32, /// Opcode. pub opcode: FxOpcode, } /// Sound FX emission produced from a command. #[derive(Clone, Debug, Eq, PartialEq)] pub struct FxSoundEvent { /// Command index. pub command_index: u32, } /// FX emission. #[derive(Clone, Debug, Eq, PartialEq)] pub enum FxEmission { /// Visual primitive. Primitive(FxPrimitive), /// Sound event. Sound(FxSoundEvent), } /// FX decode/runtime error. #[derive(Debug)] pub enum FxError { /// Binary decode error. Decode(DecodeError), /// Unknown opcode. UnknownOpcode { /// Command index. index: u32, /// Raw opcode byte. opcode: u8, }, /// Command stream exceeds payload. CommandOutOfBounds { /// Command index. index: u32, /// Expected command end. expected_end: u64, /// Payload size. payload_size: u64, }, /// Resource reference cannot be framed from body. InvalidResourceRef { /// Command index. index: u32, /// Opcode. opcode: FxOpcode, }, /// A referenced dependency is missing. MissingDependency { /// Effect name or stable effect id. effect: String, /// Command index. command_index: u32, /// Archive name. archive: String, /// Resource name. name: String, }, } impl From for FxError { fn from(value: DecodeError) -> Self { Self::Decode(value) } } impl std::fmt::Display for FxError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Decode(source) => write!(f, "{source}"), Self::UnknownOpcode { index, opcode } => { write!(f, "unknown FX opcode {opcode} at command {index}") } Self::CommandOutOfBounds { index, expected_end, payload_size, } => write!( f, "FX command {index} out of bounds: expected_end={expected_end}, payload_size={payload_size}" ), Self::InvalidResourceRef { index, opcode } => { write!(f, "invalid FX resource reference in command {index} ({opcode:?})") } Self::MissingDependency { effect, command_index, archive, name, } => write!( f, "missing FX dependency: effect={effect}, command={command_index}, archive={archive}, name={name}" ), } } } impl std::error::Error for FxError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Decode(source) => Some(source), Self::UnknownOpcode { .. } | Self::CommandOutOfBounds { .. } | Self::InvalidResourceRef { .. } | Self::MissingDependency { .. } => None, } } } /// Decodes an `FXID` payload. /// /// # Errors /// /// Returns [`FxError`] when the 60-byte header, fixed-size command stream, or /// exact EOF framing is invalid. pub fn decode_fxid(bytes: Arc<[u8]>) -> Result { let mut cursor = Cursor::new(&bytes); let header = read_header(&mut cursor)?; debug_assert_eq!(cursor.offset(), HEADER_SIZE as u64); let mut commands = Vec::with_capacity( usize::try_from(header.command_count) .map_err(|_| FxError::Decode(DecodeError::IntegerOverflow))?, ); for index in 0..header.command_count { let start = cursor.offset(); let word = cursor.read_u32_le()?; let opcode_byte = (word & 0xFF) as u8; let opcode = opcode_from_byte(opcode_byte).ok_or(FxError::UnknownOpcode { index, opcode: opcode_byte, })?; let command_size = command_size(opcode); let expected_end = start .checked_add(u64::try_from(command_size).map_err(|_| DecodeError::IntegerOverflow)?) .ok_or(DecodeError::IntegerOverflow)?; if expected_end > bytes.len() as u64 { return Err(FxError::CommandOutOfBounds { index, expected_end, payload_size: bytes.len() as u64, }); } let body_size = command_size .checked_sub(4) .ok_or(DecodeError::IntegerOverflow)?; let body = cursor.read_exact(body_size)?; let raw_body = Arc::from(body.to_vec().into_boxed_slice()); let resource_refs = resource_refs(index, opcode, body)?; commands.push(FxCommand { word, opcode, enabled: ((word >> 8) & 1) != 0, raw_body, resource_refs, }); } cursor.require_eof()?; Ok(FxDocument { bytes, header, commands, }) } /// Creates a deterministic FX reference-stub instance. /// /// # Errors /// /// Currently returns [`FxError`] only for future resource/lifecycle validation /// hooks. This function does not claim original runtime parity. pub fn create_instance( document: Arc, seed: FxSeed, transform: Transform, ) -> Result { Ok(FxState { id: FxInstanceId(seed.0), document, seed, transform, time: GameTime::default(), rng_calls: 0, lifecycle: FxLifecycle::Running, }) } /// Updates reference-stub FX simulation time without emitting side effects. /// /// # Errors /// /// Reserved for future runtime-captured compatibility validation. pub fn update(state: &mut FxState, time: GameTime) -> Result<(), FxError> { state.time = time; Ok(()) } /// Emits reference-stub active commands without advancing state. /// /// # Errors /// /// Reserved for future resource/runtime-captured compatibility validation. pub fn emit(state: &FxState, out: &mut Vec) -> Result<(), FxError> { if state.lifecycle != FxLifecycle::Running { return Ok(()); } for (index, command) in state.document.commands.iter().enumerate() { if !command.enabled { continue; } let command_index = u32::try_from(index).map_err(|_| DecodeError::IntegerOverflow)?; if command.opcode == FxOpcode::Op2 { out.push(FxEmission::Sound(FxSoundEvent { command_index })); } else { out.push(FxEmission::Primitive(FxPrimitive { command_index, opcode: command.opcode, })); } } Ok(()) } /// Stops an FX instance. pub fn stop(state: &mut FxState) { state.lifecycle = FxLifecycle::Stopped; } /// Restarts a stopped FX instance from a time. pub fn restart(state: &mut FxState, time: GameTime) { state.lifecycle = FxLifecycle::Running; state.time = time; } /// Ends an FX instance permanently. pub fn end(state: &mut FxState) { state.lifecycle = FxLifecycle::Ended; } /// Validates resource references through a caller-provided dependency probe. /// /// # Errors /// /// Returns [`FxError::MissingDependency`] with effect, command, archive and /// resource name context when the probe reports a missing resource. pub fn validate_dependencies( document: &FxDocument, effect: &str, exists: impl Fn(&[u8], &[u8]) -> bool, ) -> Result<(), FxError> { for (index, command) in document.commands.iter().enumerate() { for reference in &command.resource_refs { if !exists(reference.archive_name(), reference.resource_name()) { return Err(FxError::MissingDependency { effect: effect.to_string(), command_index: u32::try_from(index) .map_err(|_| DecodeError::IntegerOverflow)?, archive: String::from_utf8_lossy(reference.archive_name()).into_owned(), name: String::from_utf8_lossy(reference.resource_name()).into_owned(), }); } } } Ok(()) } /// Builds a byte-stable capture for emitted commands. /// /// # Errors /// /// Returns [`FxError`] when emission fails. pub fn canonical_emission_capture(state: &FxState) -> Result, FxError> { let mut emissions = Vec::new(); emit(state, &mut emissions)?; let mut out = Vec::new(); for emission in emissions { match emission { FxEmission::Primitive(primitive) => { out.extend_from_slice( format!("P,{}, {:?}\n", primitive.command_index, primitive.opcode).as_bytes(), ); } FxEmission::Sound(sound) => { out.extend_from_slice(format!("S,{}\n", sound.command_index).as_bytes()); } } } Ok(out) } impl FxDocument { /// Returns original bytes. #[must_use] pub fn bytes(&self) -> &[u8] { &self.bytes } /// Returns the parsed header. #[must_use] pub fn header(&self) -> &FxHeader { &self.header } /// Returns commands in disk order. #[must_use] pub fn commands(&self) -> &[FxCommand] { &self.commands } } impl FxResourceRef { /// Archive name before first NUL, ASCII-trimmed. #[must_use] pub fn archive_name(&self) -> &[u8] { bounded_cstr(&self.archive_raw) } /// Resource name before first NUL, ASCII-trimmed. #[must_use] pub fn resource_name(&self) -> &[u8] { bounded_cstr(&self.name_raw) } } fn read_header(cursor: &mut Cursor<'_>) -> Result { Ok(FxHeader { command_count: cursor.read_u32_le()?, time_mode: cursor.read_u32_le()?, duration_seconds: cursor.read_f32_le()?, phase_jitter: cursor.read_f32_le()?, flags: cursor.read_u32_le()?, settings_id: cursor.read_u32_le()?, random_shift: [ cursor.read_f32_le()?, cursor.read_f32_le()?, cursor.read_f32_le()?, ], pivot: [ cursor.read_f32_le()?, cursor.read_f32_le()?, cursor.read_f32_le()?, ], scale: [ cursor.read_f32_le()?, cursor.read_f32_le()?, cursor.read_f32_le()?, ], }) } fn opcode_from_byte(opcode: u8) -> Option { match opcode { 1 => Some(FxOpcode::Op1), 2 => Some(FxOpcode::Op2), 3 => Some(FxOpcode::Op3), 4 => Some(FxOpcode::Op4), 5 => Some(FxOpcode::Op5), 6 => Some(FxOpcode::Op6), 7 => Some(FxOpcode::Op7), 8 => Some(FxOpcode::Op8), 9 => Some(FxOpcode::Op9), 10 => Some(FxOpcode::Op10), _ => None, } } fn command_size(opcode: FxOpcode) -> usize { match opcode { FxOpcode::Op1 => 224, FxOpcode::Op2 => 148, FxOpcode::Op3 => 200, FxOpcode::Op4 => 204, FxOpcode::Op5 => 112, FxOpcode::Op6 => 4, FxOpcode::Op7 | FxOpcode::Op9 | FxOpcode::Op10 => 208, FxOpcode::Op8 => 248, } } fn resource_refs(index: u32, opcode: FxOpcode, body: &[u8]) -> Result, FxError> { if !has_resource_ref(opcode) { return Ok(Vec::new()); } let raw = body .get(..64) .ok_or(FxError::InvalidResourceRef { index, opcode })?; let mut archive_raw = [0; 32]; let mut name_raw = [0; 32]; archive_raw.copy_from_slice(&raw[..32]); name_raw.copy_from_slice(&raw[32..64]); Ok(vec![FxResourceRef { archive_raw, name_raw, }]) } fn has_resource_ref(opcode: FxOpcode) -> bool { matches!( opcode, FxOpcode::Op2 | FxOpcode::Op3 | FxOpcode::Op4 | FxOpcode::Op5 | FxOpcode::Op7 | FxOpcode::Op8 | FxOpcode::Op9 | FxOpcode::Op10 ) } fn bounded_cstr(raw: &[u8]) -> &[u8] { let len = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len()); trim_ascii(&raw[..len]) } fn trim_ascii(bytes: &[u8]) -> &[u8] { let mut start = 0usize; let mut end = bytes.len(); while start < end && bytes[start].is_ascii_whitespace() { start += 1; } while end > start && bytes[end - 1].is_ascii_whitespace() { end -= 1; } &bytes[start..end] } #[cfg(test)] mod tests { use super::*; use fparkan_nres::ReadProfile; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; #[test] fn decodes_synthetic_opcodes_and_refs() { let mut bytes = header(2); bytes.extend_from_slice(&command_with_ref(0x0102, 148, b"sounds.lib", b"boom.wav")); bytes.extend_from_slice(&command(0x0106, 4)); let document = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx"); assert_eq!(document.header().command_count, 2); assert_eq!(document.commands()[0].opcode, FxOpcode::Op2); assert!(document.commands()[0].enabled); assert_eq!( document.commands()[0].resource_refs[0].archive_name(), b"sounds.lib" ); assert_eq!(document.commands()[1].opcode, FxOpcode::Op6); assert!(document.commands()[1].raw_body.is_empty()); } #[test] fn header_is_exactly_sixty_bytes_and_command_sizes_are_fixed() { let mut bytes = header(10); for opcode in 1..=10_u32 { bytes.extend_from_slice(&command(0x0100 | opcode, opcode_size(opcode))); } let document = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx"); assert_eq!(header(0).len(), HEADER_SIZE); assert_eq!(document.commands().len(), 10); for (index, command) in document.commands().iter().enumerate() { let opcode = u32::try_from(index + 1).expect("opcode"); assert_eq!(command.raw_body.len() + 4, opcode_size(opcode)); } } #[test] fn opcode6_four_byte_command_is_accepted() { let mut bytes = header(1); bytes.extend_from_slice(&command(0x0106, 4)); let document = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx"); assert_eq!(document.commands()[0].opcode, FxOpcode::Op6); assert!(document.commands()[0].raw_body.is_empty()); } #[test] fn rejects_unknown_opcode_at_command_index() { let mut bytes = header(1); bytes.extend_from_slice(&99_u32.to_le_bytes()); let err = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect_err("unknown opcode"); assert!(matches!( err, FxError::UnknownOpcode { index: 0, opcode: 99 } )); } #[test] fn rejects_command_count_that_exceeds_payload() { let mut bytes = header(2); bytes.extend_from_slice(&command(0x0106, 4)); let err = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect_err("out of bounds"); assert!(matches!( err, FxError::Decode(DecodeError::UnexpectedEof { .. }) | FxError::CommandOutOfBounds { .. } )); } #[test] fn rejects_trailing_bytes_after_command_stream() { let mut bytes = header(0); bytes.push(0); let err = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect_err("trailing"); assert!(matches!( err, FxError::Decode(DecodeError::TrailingBytes { .. }) )); } #[test] fn fixed_resource_refs_preserve_tails() { let mut bytes = header(1); let mut command = command_with_ref(0x0102, 148, b"sounds.lib", b"boom.wav"); command[4 + 20] = 0xAB; command[36 + 20] = 0xCD; bytes.extend_from_slice(&command); let document = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx"); let reference = &document.commands()[0].resource_refs[0]; assert_eq!(reference.archive_name(), b"sounds.lib"); assert_eq!(reference.resource_name(), b"boom.wav"); assert_eq!(reference.archive_raw[20], 0xAB); assert_eq!(reference.name_raw[20], 0xCD); } #[test] fn missing_dependency_error_contains_effect_command_archive_and_name() { let mut bytes = header(1); bytes.extend_from_slice(&command_with_ref( 0x0102, 148, b"sounds.lib", b"missing.wav", )); let document = decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx"); let err = validate_dependencies(&document, "spark", |_, _| false) .expect_err("missing dependency"); assert!(matches!( err, FxError::MissingDependency { ref effect, command_index: 0, ref archive, ref name, } if effect == "spark" && archive == "sounds.lib" && name == "missing.wav" )); assert!(err.to_string().contains("spark")); assert!(err.to_string().contains("missing.wav")); } #[test] fn update_and_emit_are_separate() { let mut bytes = header(1); bytes.extend_from_slice(&command(0x0101, 224)); let document = Arc::new(decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx")); let mut state = create_instance(document, FxSeed(7), Transform::default()).expect("state"); update(&mut state, GameTime(42)).expect("update"); let before = state.time; let mut out = Vec::new(); emit(&state, &mut out).expect("emit"); assert_eq!(state.time, before); assert_eq!(out.len(), 1); } #[test] fn create_records_seed_transform_and_start_time() { let bytes = header(0); let document = Arc::new(decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx")); let transform = Transform { translation: [1.0, 2.0, 3.0], rotation: [0.0, 0.0, 0.0, 1.0], scale: [4.0, 5.0, 6.0], }; let state = create_instance(document, FxSeed(77), transform).expect("state"); assert_eq!(state.id, FxInstanceId(77)); assert_eq!(state.seed, FxSeed(77)); assert_eq!(state.transform, transform); assert_eq!(state.time, GameTime(0)); assert_eq!(state.rng_calls, 0); assert_eq!(state.lifecycle, FxLifecycle::Running); } #[test] fn stable_command_order_and_emission_capture_are_seed_stable() { let mut bytes = header(3); bytes.extend_from_slice(&command(0x0101, 224)); bytes.extend_from_slice(&command(0x0102, 148)); bytes.extend_from_slice(&command(0x0106, 4)); let document = Arc::new(decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx")); let mut first = create_instance(document.clone(), FxSeed(5), Transform::default()).expect("first"); let mut second = create_instance(document, FxSeed(5), Transform::default()).expect("second"); update(&mut first, GameTime(9)).expect("update"); update(&mut second, GameTime(9)).expect("update"); assert_eq!( canonical_emission_capture(&first).expect("first capture"), canonical_emission_capture(&second).expect("second capture") ); assert_eq!( canonical_emission_capture(&first).expect("capture"), b"P,0, Op1\nS,1\nP,2, Op6\n" ); } #[test] fn stop_restart_end_lifecycle_controls_emission() { let mut bytes = header(1); bytes.extend_from_slice(&command(0x0101, 224)); let document = Arc::new(decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx")); let mut state = create_instance(document, FxSeed(1), Transform::default()).expect("state"); assert!(!canonical_emission_capture(&state) .expect("running") .is_empty()); stop(&mut state); assert_eq!(state.lifecycle, FxLifecycle::Stopped); assert!(canonical_emission_capture(&state) .expect("stopped") .is_empty()); restart(&mut state, GameTime(12)); assert_eq!(state.lifecycle, FxLifecycle::Running); assert_eq!(state.time, GameTime(12)); assert!(!canonical_emission_capture(&state) .expect("restarted") .is_empty()); end(&mut state); assert_eq!(state.lifecycle, FxLifecycle::Ended); assert!(canonical_emission_capture(&state) .expect("ended") .is_empty()); } #[test] fn unrelated_rng_stream_use_does_not_perturb_fx_capture() { let mut bytes = header(1); bytes.extend_from_slice(&command(0x0101, 224)); let document = Arc::new(decode_fxid(Arc::from(bytes.into_boxed_slice())).expect("fx")); let state = create_instance(document, FxSeed(3), Transform::default()).expect("state"); let before = canonical_emission_capture(&state).expect("before"); let mut unrelated = 0x1234_u64; for _ in 0..32 { unrelated = unrelated.rotate_left(7).wrapping_mul(17); } assert_ne!(unrelated, 0); assert_eq!(canonical_emission_capture(&state).expect("after"), before); } #[test] fn arbitrary_command_streams_are_bounded_and_panic_free() { for len in 0..256usize { let mut bytes = vec![0xA5; len]; if len >= HEADER_SIZE { bytes[0..4].copy_from_slice(&1_u32.to_le_bytes()); } let result = std::panic::catch_unwind(|| { let _ = decode_fxid(Arc::from(bytes.into_boxed_slice())); }); assert!(result.is_ok()); } } #[test] #[ignore = "requires licensed corpus"] fn licensed_corpus_fxid_exact_eof_and_distribution() { for (corpus, expected_count) in [("IS", 923_usize), ("IS2", 1065_usize)] { let root = corpus_root(corpus); let mut count = 0usize; let mut opcodes = BTreeMap::::new(); let mut time_modes = BTreeMap::::new(); for path in files_under(&root) { let Ok(bytes) = std::fs::read(&path) else { continue; }; let Ok(archive) = fparkan_nres::decode( Arc::from(bytes.into_boxed_slice()), ReadProfile::Compatible, ) else { continue; }; for entry in archive .entries() .iter() .filter(|entry| entry.meta().type_id == FXID_KIND) { let payload = archive.payload(entry.id()).expect("payload"); let document = decode_fxid(Arc::from(payload.to_vec().into_boxed_slice())) .unwrap_or_else(|err| { panic!("{corpus} {path:?} {:?}: {err}", entry.name_bytes()) }); count += 1; *time_modes.entry(document.header().time_mode).or_insert(0) += 1; for command in document.commands() { *opcodes.entry(command.opcode).or_insert(0) += 1; } } } assert_eq!(count, expected_count, "{corpus} FXID count"); assert!(!opcodes.contains_key(&FxOpcode::Op6), "{corpus} opcode 6"); for mode in time_modes.keys() { assert!( matches!(*mode, 0 | 1 | 2 | 4 | 5 | 14 | 15 | 16 | 17), "{corpus} unexpected time mode {mode}" ); } } } #[test] #[ignore = "requires licensed corpus"] fn licensed_corpus_fxid_emission_captures_are_approved() { for (corpus, expected_count, expected_emitting, expected_hash) in [ ("IS", 923_usize, 467_usize, 10_553_431_922_547_057_702_u64), ("IS2", 1065_usize, 532_usize, 9_217_284_592_334_143_531_u64), ] { let root = corpus_root(corpus); let mut count = 0usize; let mut emitting = 0usize; let mut hash = FNV_OFFSET; for path in files_under(&root) { let Ok(bytes) = std::fs::read(&path) else { continue; }; let Ok(archive) = fparkan_nres::decode( Arc::from(bytes.into_boxed_slice()), ReadProfile::Compatible, ) else { continue; }; for entry in archive .entries() .iter() .filter(|entry| entry.meta().type_id == FXID_KIND) { let payload = archive.payload(entry.id()).expect("payload"); let document = Arc::new( decode_fxid(Arc::from(payload.to_vec().into_boxed_slice())).unwrap_or_else( |err| panic!("{corpus} {path:?} {:?}: {err}", entry.name_bytes()), ), ); let state = create_instance(document, FxSeed(count as u64), Transform::default()) .expect("fx state"); let capture = canonical_emission_capture(&state).expect("capture"); if !capture.is_empty() { emitting += 1; } hash_bytes(&mut hash, entry.name_bytes()); hash_bytes(&mut hash, &capture); count += 1; } } assert_eq!(count, expected_count, "{corpus} FXID count"); assert_eq!(emitting, expected_emitting, "{corpus} emitting FXID count"); assert_eq!(hash, expected_hash, "{corpus} FXID capture hash"); } } fn header(command_count: u32) -> Vec { let mut out = Vec::with_capacity(HEADER_SIZE); out.extend_from_slice(&command_count.to_le_bytes()); out.extend_from_slice(&1_u32.to_le_bytes()); out.extend_from_slice(&1.0_f32.to_bits().to_le_bytes()); out.extend_from_slice(&0.0_f32.to_bits().to_le_bytes()); out.extend_from_slice(&0_u32.to_le_bytes()); out.extend_from_slice(&0_u32.to_le_bytes()); for _ in 0..9 { out.extend_from_slice(&0.0_f32.to_bits().to_le_bytes()); } assert_eq!(out.len(), HEADER_SIZE); out } fn command(word: u32, size: usize) -> Vec { let mut out = Vec::with_capacity(size); out.extend_from_slice(&word.to_le_bytes()); out.resize(size, 0); out } fn command_with_ref(word: u32, size: usize, archive: &[u8], name: &[u8]) -> Vec { let mut out = command(word, size); copy_cstr(&mut out[4..36], archive); copy_cstr(&mut out[36..68], name); out } fn opcode_size(opcode: u32) -> usize { match opcode { 1 => 224, 2 => 148, 3 => 200, 4 => 204, 5 => 112, 6 => 4, 7 | 9 | 10 => 208, 8 => 248, _ => unreachable!("test opcode"), } } fn copy_cstr(dst: &mut [u8], src: &[u8]) { let len = dst.len().saturating_sub(1).min(src.len()); dst[..len].copy_from_slice(&src[..len]); } fn corpus_root(name: &str) -> PathBuf { let variable = match name { "IS" => "FPARKAN_CORPUS_PART1_ROOT", "IS2" => "FPARKAN_CORPUS_PART2_ROOT", _ => panic!("unknown licensed corpus part: {name}"), }; let root = std::env::var_os(variable) .map(PathBuf::from) .unwrap_or_else(|| panic!("{variable} is required for licensed corpus tests")); assert!( root.is_dir(), "licensed corpus root is missing: {}", root.display() ); root } fn files_under(root: &Path) -> Vec { let mut out = Vec::new(); let mut stack = vec![root.to_path_buf()]; while let Some(path) = stack.pop() { let Ok(read_dir) = std::fs::read_dir(path) else { continue; }; for entry in read_dir.flatten() { let path = entry.path(); if path.is_dir() { stack.push(path); } else { out.push(path); } } } out.sort(); out } const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; fn hash_bytes(hash: &mut u64, bytes: &[u8]) { for byte in bytes { *hash ^= u64::from(*byte); *hash = hash.wrapping_mul(FNV_PRIME); } } }