diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 2c7e49c2a6..ecf1c1c74a 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -140,6 +140,12 @@ next to the files: Files copied into `corpus/` by hand (tiff, exr, raw) must be MIT, BSD or CC0. +## Synthetic parser test fixtures + +| Path | Title | Author | Source | License | +|---|---|---|---|---| +| `crates/psd/tests/fixtures/atn/v12-*.atn` | Independently encoded ATN version-12 parser fixtures | @yoriswind | Original synthetic test data generated for PhotoCraft; wire layout documented in `crates/psd/src/atn.rs`; provenance in the adjacent README | MIT OR Apache-2.0 | + ## Embedded-vector editor source The external SVG watcher, engine/UI handoff and bounded SVG rendering in this change diff --git a/apps/photocraft-cli/src/actions.rs b/apps/photocraft-cli/src/actions.rs index 4d494c8a82..f8b900e49a 100644 --- a/apps/photocraft-cli/src/actions.rs +++ b/apps/photocraft-cli/src/actions.rs @@ -83,11 +83,11 @@ pub fn load_actions(text: &str, name: Option<&str>) -> Result<(Steps, Vec) -> Result<(Steps, Vec, Vec), ActionsError> { let bytes = std::fs::read(path).map_err(|source| ActionsError::Read { path: path.to_path_buf(), source })?; - let is_atn = path.extension().is_some_and(|e| e.eq_ignore_ascii_case("atn")) || bytes.starts_with(&[0, 0, 0, 16]); + let is_atn = path.extension().is_some_and(|e| e.eq_ignore_ascii_case("atn")) || matches!(bytes.get(..4), Some([0, 0, 0, 12 | 16])); if !is_atn { let text = String::from_utf8(bytes).map_err(|source| ActionsError::Utf8 { path: path.to_path_buf(), source })?; let (steps, set) = load_actions(&text, name)?; diff --git a/apps/photocraft-cli/tests/it/cli.rs b/apps/photocraft-cli/tests/it/cli.rs index e592d552e2..fcc93e06a0 100644 --- a/apps/photocraft-cli/tests/it/cli.rs +++ b/apps/photocraft-cli/tests/it/cli.rs @@ -1090,6 +1090,20 @@ fn batch_plays_a_photoshop_actions_file() { assert!(!o.status.success()); } +/// A version 12 Actions file (Photoshop 5, often saved on classic Mac OS without an extension) is +/// recognised by its version word like version 16, not read as a JSON actions list. +#[test] +fn extensionless_version_12_actions_file_is_read_as_atn() { + let d = tmp("atn-v12"); + let path = d.join("Legacy Actions"); + std::fs::write(&path, include_bytes!("../../../../crates/psd/tests/fixtures/atn/v12-commands.atn")).unwrap(); + let (steps, set, warnings) = photocraft_cli::load_actions_file(&path, None).unwrap(); + assert_eq!(set.len(), 1); + assert_eq!(steps, vec![("actions.play".to_string(), json!({"action": set[0].name}))]); + assert!(warnings.is_empty(), "{warnings:?}"); + std::fs::remove_dir_all(d).unwrap(); +} + /// What `photocraft_cli::run` returned and wrote, in-process. struct Invoke { code: i32, diff --git a/crates/engine/tests/it/atn_v12.rs b/crates/engine/tests/it/atn_v12.rs index 0f3426e465..d5bc6d8263 100644 --- a/crates/engine/tests/it/atn_v12.rs +++ b/crates/engine/tests/it/atn_v12.rs @@ -5,7 +5,7 @@ use serde_json::json; /// Legacy descriptor syntax must reach the same commands and parameters as version 16. #[test] fn legacy_v12_commands_import_like_v16() { - let bytes = include_bytes!("../../../psd/fuzz/corpus/atn/v12-commands.atn"); + let bytes = include_bytes!("../../../psd/tests/fixtures/atn/v12-commands.atn"); let modern = atn::write(&atn::parse(bytes).unwrap()).unwrap(); let expected = vec![ ("filter.blur.gaussianBlur".into(), json!({"radius": 2.5})), diff --git a/crates/psd/fuzz/atn-v12-seeds.md b/crates/psd/fuzz/atn-v12-seeds.md deleted file mode 100644 index 235cd69b59..0000000000 --- a/crates/psd/fuzz/atn-v12-seeds.md +++ /dev/null @@ -1,13 +0,0 @@ -The small synthetic sets in `corpus/atn/v12-*.atn` were assembled from the measured grammar -in #10603, without using PhotoCraft's parser or writer. No proprietary files or -assets are included. They also serve as inputs for `tests/it/atn_v12.rs`. - -- `v12-stop.atn`: Stop with Unicode text and a Boolean. -- `v12-values.atn`: named plug-in event, every legacy value/reference form, - bare `warp`/`list`/`time` codes, an object array with three objects and two - columns, unpaired UTF-16, opaque path/alias/data bytes, and a descriptorless step. -- `v12-commands.atn`: Gaussian Blur, Levels, Select All and Make Layer. - -`Idnt` follows the measured reference's inferred index-like layout; no real -version 12 identifier reference was available. Saving these sets produces -version 16. Keep them outside the version 16 byte-identical corpus oracle. diff --git a/crates/psd/tests/fixtures/atn/README.md b/crates/psd/tests/fixtures/atn/README.md new file mode 100644 index 0000000000..33273b9ff5 --- /dev/null +++ b/crates/psd/tests/fixtures/atn/README.md @@ -0,0 +1,30 @@ +# Synthetic ATN parser fixtures + +These seven tiny files were independently encoded field by field from the version-16 and +version-12 layouts documented in `src/atn.rs`, without calling PhotoCraft's parser or writer. They are +original test data, licensed MIT OR Apache-2.0 under the repository's licenses. They contain no user +artwork, Photoshop program code or proprietary assets. They are not Photoshop-authored +interoperability oracles. + +| File | Bytes | Purpose | SHA-256 | +|---|---:|---|---| +| `empty.atn` | 13 | Empty set and explicit big-endian version | `6fd936f91296a13b0e7456a38b139a100fd17407d1c582d4502ce76413d01539` | +| `metadata.atn` | 214 | UTF-16 surrogate pairs/unpaired units, action order, function key, modifiers, panel flags, disabled step, unknown string event, dialog options | `cec2ff5a25e003aa29e790ea6b110f06ba848f0afd7901f3f8ad4740f782ede2` | +| `descriptors.atn` | 859 | Every descriptor value variant and every reference form currently represented by the parser | `6c76502822c1f36c8060a71adb227c4540663518785bbab1b70951dac0ef58ad` | +| `float-bits.atn` | 428 | NaN payload, negative zero, infinities, and floats inside lists, objects and arrays | `0aa6278a569c777f249b305e889896132c2a87e44a24cb1a55d1d3fc0928fd0c` | +| `v12-stop.atn` | 113 | Version 12: Stop with Unicode text and a Boolean | `293462a7d5cf0043fdc5135630524699f545679088c54bad83f4a86e15160271` | +| `v12-values.atn` | 562 | Version 12: named plug-in event, every legacy value and reference form, bare `warp`/`list`/`time` codes, an object array with three objects and two columns, unpaired UTF-16, opaque path/alias/data bytes, and a descriptorless step | `23d3f377996435988367365ee5c2a15462c0ed42ce506dc4a15039387ac6a853` | +| `v12-commands.atn` | 404 | Version 12: Gaussian Blur, Levels, Select All and Make Layer | `451958b686da9aab45e65ae902c86333b7149d72021c21904e3aae8e39eb77cb` | + +`tests/it/atn_contract.rs` checks the independently encoded version-16 bytes and expected public +structures; `tests/it/atn_v12.rs` (and `photocraft-engine`'s `tests/it/atn_v12.rs`) check the +version-12 files. The version-12 files follow the version 12 layout in `src/atn.rs`, assembled from +the measured grammar in #10603. Their `Idnt` reference follows the inferred index-like layout; no +real version 12 identifier reference was available. Saving them produces version 16, so keep them +out of the version-16 byte-identical corpus oracle. + +The files also seed the `atn` fuzz target: `cargo fuzz run atn crates/psd/tests/fixtures/atn` +(from `crates/psd`: `cargo fuzz run atn tests/fixtures/atn`). + +A fixture change should explain the changed wire fields and update this table; do not regenerate +expectations merely to make a failing parser pass. diff --git a/crates/psd/fuzz/corpus/atn/v12-commands.atn b/crates/psd/tests/fixtures/atn/v12-commands.atn similarity index 100% rename from crates/psd/fuzz/corpus/atn/v12-commands.atn rename to crates/psd/tests/fixtures/atn/v12-commands.atn diff --git a/crates/psd/fuzz/corpus/atn/v12-stop.atn b/crates/psd/tests/fixtures/atn/v12-stop.atn similarity index 100% rename from crates/psd/fuzz/corpus/atn/v12-stop.atn rename to crates/psd/tests/fixtures/atn/v12-stop.atn diff --git a/crates/psd/fuzz/corpus/atn/v12-values.atn b/crates/psd/tests/fixtures/atn/v12-values.atn similarity index 100% rename from crates/psd/fuzz/corpus/atn/v12-values.atn rename to crates/psd/tests/fixtures/atn/v12-values.atn diff --git a/crates/psd/tests/it/atn_v12.rs b/crates/psd/tests/it/atn_v12.rs index d9a773d7e2..28f3921d1f 100644 --- a/crates/psd/tests/it/atn_v12.rs +++ b/crates/psd/tests/it/atn_v12.rs @@ -2,9 +2,9 @@ use photocraft_psd::PsdError; use photocraft_psd::atn; use photocraft_psd::descriptor::{Class, Descriptor, Id, MAX_DEPTH, ObjectArray, ReferenceItem as R, UnicodeString, Value as V}; -const STOP: &[u8] = include_bytes!("../../fuzz/corpus/atn/v12-stop.atn"); -const VALUES: &[u8] = include_bytes!("../../fuzz/corpus/atn/v12-values.atn"); -const COMMANDS: &[u8] = include_bytes!("../../fuzz/corpus/atn/v12-commands.atn"); +const STOP: &[u8] = include_bytes!("../fixtures/atn/v12-stop.atn"); +const VALUES: &[u8] = include_bytes!("../fixtures/atn/v12-values.atn"); +const COMMANDS: &[u8] = include_bytes!("../fixtures/atn/v12-commands.atn"); fn code(id: &[u8; 4]) -> Id { Id::Code(*id)