diff --git a/Cargo.lock b/Cargo.lock index ef05ce7..574e689 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6397,6 +6397,7 @@ dependencies = [ name = "polymodel-ldraw-core" version = "0.1.0" dependencies = [ + "base64 0.22.1", "miette", "polymodel-ldraw-testkit", "polymodel-renderer-ledger", diff --git a/crates/polymodel-ldraw-core/Cargo.toml b/crates/polymodel-ldraw-core/Cargo.toml index cebfb6d..8723048 100644 --- a/crates/polymodel-ldraw-core/Cargo.toml +++ b/crates/polymodel-ldraw-core/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "Pure Rust/WASM LDraw syntax and semantic core." [dependencies] +base64 = { version = "0.22" } polymodel-renderer-ledger = { path = "../polymodel-renderer-ledger" } serde = { workspace = true } miette = { workspace = true } diff --git a/crates/polymodel-ldraw-core/src/adapter.rs b/crates/polymodel-ldraw-core/src/adapter.rs index 2820db2..23a6776 100644 --- a/crates/polymodel-ldraw-core/src/adapter.rs +++ b/crates/polymodel-ldraw-core/src/adapter.rs @@ -6,20 +6,22 @@ use std::borrow::Cow; pub struct InProcessRustAdapter; #[derive(Clone)] -pub struct AdapterRequest<'a> { - pub fixture_id: &'a str, - pub bytes: &'a [u8], +pub struct AdapterRequest<'src, 'ledger> { + pub fixture_id: &'src str, + pub bytes: &'src [u8], pub profile: ParserProfile, - pub provenance_id: &'a str, + pub provenance_id: &'src str, pub owner: ReservationOwner, - pub ledger: &'a ReservationLedger, + pub ledger: &'ledger ReservationLedger, pub semantic_budget: Option, - pub root_name: &'a str, + pub root_name: &'src str, pub root: RootId, - pub materializations: &'a [Materialization<'a>], + pub materializations: &'src [Materialization<'src>], } impl InProcessRustAdapter { - pub fn parse(request: AdapterRequest<'_>) -> Result, ParseError> { + pub fn parse<'src, 'ledger>( + request: AdapterRequest<'src, 'ledger>, + ) -> Result, ParseError> { let mut options = ParseOptions::new(request.owner, request.ledger); options.profile = request.profile; options.semantic_budget = request.semantic_budget; diff --git a/crates/polymodel-ldraw-core/src/cache.rs b/crates/polymodel-ldraw-core/src/cache.rs index 588e519..5d544d0 100644 --- a/crates/polymodel-ldraw-core/src/cache.rs +++ b/crates/polymodel-ldraw-core/src/cache.rs @@ -53,7 +53,7 @@ impl fmt::Display for NormalizedPath { } } -#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)] pub struct Sha256Hash([u8; 32]); impl Sha256Hash { fn digest(bytes: &[u8]) -> Self { diff --git a/crates/polymodel-ldraw-core/src/geom.rs b/crates/polymodel-ldraw-core/src/geom.rs index c6ead2f..0a52a93 100644 --- a/crates/polymodel-ldraw-core/src/geom.rs +++ b/crates/polymodel-ldraw-core/src/geom.rs @@ -2,8 +2,8 @@ use crate::bfc::BfcState; use crate::model::{Include, ModelData}; use crate::scanner::{LineType, ScannedLine, Token}; use crate::types::{ - Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, ParseError, ParserProfile, - add_diag, limit_error, + Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, ParseError, ParseOptions, + ParserProfile, SemanticArenas, add_diag, limit_error, }; use serde::{Deserialize, Deserializer, Serialize}; use std::borrow::Cow; @@ -298,6 +298,8 @@ pub(crate) fn process_geometry<'src>( diagnostics: &mut Vec, counters: &mut LimitCounters, limits: &LdrawLimits, + arenas: &mut SemanticArenas, + options: &ParseOptions<'_>, ) -> Result<(), ParseError> { match line.line_type { Some(LineType::One) => { @@ -324,6 +326,7 @@ pub(crate) fn process_geometry<'src>( .join(" "), ), }; + arenas.charge_instance(options)?; let inverted = model.bfc.invert_next; model.bfc.invert_next = false; model.includes.push(Include { diff --git a/crates/polymodel-ldraw-core/src/lib.rs b/crates/polymodel-ldraw-core/src/lib.rs index d05061b..770a745 100644 --- a/crates/polymodel-ldraw-core/src/lib.rs +++ b/crates/polymodel-ldraw-core/src/lib.rs @@ -652,6 +652,34 @@ mod tests { ); } + #[test] + fn data_payloads_decode_and_invalid_payloads_diagnose() { + let ledger = ReservationLedger::new(); + let result = LdrawParser + .parse_bytes( + b"0 !DATA sample.bin\n0 !: aGVsbG8=\n0 STEP\n", + options(&ledger), + None, + ) + .expect("valid DATA should parse"); + assert_eq!(result.semantic.data_payloads.len(), 1); + assert_eq!(result.semantic.data_payloads[0].filename, "sample.bin"); + assert_eq!(result.semantic.data_payloads[0].bytes, b"hello"); + + let ledger = ReservationLedger::new(); + let error = match LdrawParser.parse_bytes(b"0 !DATA sample.bin\n", options(&ledger), None) { + Ok(_) => panic!("truncated DATA should reject in strict mode"), + Err(error) => error, + }; + assert!(matches!( + error, + LdrawError::Diagnostic(Diagnostic { + code: DiagnosticCode::DataTruncated, + .. + }) + )); + } + #[test] fn corpus_colours_registers_ldconfig_code_300() { let fixture = corpus_fixture("colour-definitions"); diff --git a/crates/polymodel-ldraw-core/src/model.rs b/crates/polymodel-ldraw-core/src/model.rs index 77ac729..26eaeab 100644 --- a/crates/polymodel-ldraw-core/src/model.rs +++ b/crates/polymodel-ldraw-core/src/model.rs @@ -193,6 +193,27 @@ pub struct SyntaxRecord<'src> { #[serde(borrow)] pub fields: Vec>, } +impl<'src> SyntaxRecord<'src> { + pub(crate) fn into_owned(self) -> SyntaxRecord<'static> { + SyntaxRecord { + line_type: self.line_type, + raw_tail: Cow::Owned(self.raw_tail.into_owned()), + span: self.span, + line_ending: Cow::Owned(self.line_ending.into_owned()), + fields: self + .fields + .into_iter() + .map(|field| Cow::Owned(field.into_owned())) + .collect(), + } + } +} +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct DataPayload { + pub filename: String, + pub bytes: Vec, +} + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct SemanticRecord<'src> { #[serde(borrow)] @@ -209,6 +230,40 @@ pub struct SemanticRecord<'src> { #[serde(borrow)] pub limits: Vec>, pub texmap_events: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub data_payloads: Vec, +} +impl<'src> SemanticRecord<'src> { + pub(crate) fn into_owned(self) -> SemanticRecord<'static> { + SemanticRecord { + canonical_path: self + .canonical_path + .map(|value| Cow::Owned(value.into_owned())), + root_identity: self + .root_identity + .map(|value| Cow::Owned(value.into_owned())), + target_identity: self + .target_identity + .map(|value| Cow::Owned(value.into_owned())), + cache_identity: self + .cache_identity + .map(|value| Cow::Owned(value.into_owned())), + bfc_state: self.bfc_state, + colour_state: self.colour_state, + steps: self.steps, + limits: self + .limits + .into_iter() + .map(|value| Cow::Owned(value.into_owned())) + .collect(), + texmap_events: self + .texmap_events + .into_iter() + .map(TexmapEvent::into_owned) + .collect(), + data_payloads: self.data_payloads, + } + } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct InstanceRecord { @@ -244,6 +299,13 @@ pub struct CanonicalRecord<'src> { pub provenance_id: Cow<'src, str>, } +#[derive(Clone, Debug)] +pub(crate) struct DataPayloadBuilder { + pub(crate) filename: String, + pub(crate) encoded: String, + pub(crate) span: Span, +} + #[derive(Clone, Debug)] pub(crate) struct Include<'src> { pub(crate) name: Cow<'src, str>, @@ -265,7 +327,8 @@ pub(crate) struct ModelData<'src> { pub(crate) texmap_next_span: Option, pub(crate) texmap_fallback_seen: bool, pub(crate) texmap_events: Vec>, - pub(crate) data_payload: bool, + pub(crate) data_payload: Option, + pub(crate) data_payloads: Vec, pub(crate) includes: Vec>, pub(crate) triangles: u64, pub(crate) quads: u64, @@ -321,6 +384,21 @@ impl<'src> ParseResult<'src> { provenance_id: self.provenance_id, } } + + pub fn into_owned_project(self) -> CanonicalRecord<'static> { + CanonicalRecord { + schema_version: Cow::Borrowed(SCHEMA_VERSION), + syntax: self + .syntax + .into_iter() + .map(|record| record.into_owned()) + .collect(), + semantic: self.semantic.into_owned(), + scene: self.scene, + diagnostics: self.diagnostics, + provenance_id: Cow::Owned(self.provenance_id.into_owned()), + } + } pub fn ledger_reservation_id(&self) -> u64 { self.arenas.reservation_id() } diff --git a/crates/polymodel-ldraw-core/src/parser.rs b/crates/polymodel-ldraw-core/src/parser.rs index d05d862..c06581c 100644 --- a/crates/polymodel-ldraw-core/src/parser.rs +++ b/crates/polymodel-ldraw-core/src/parser.rs @@ -3,7 +3,8 @@ use crate::bfc::{BfcState, bfc}; use crate::cache::{CacheKey, NormalizedPath}; use crate::geom::{parse_primitive, process_geometry}; use crate::model::{ - ColourTable, ModelData, ParseResult, SemanticRecord, SyntaxRecord, parse_colour, + ColourTable, DataPayload, DataPayloadBuilder, ModelData, ParseResult, SemanticRecord, + SyntaxRecord, parse_colour, }; use crate::mpd::{OwnedVirtualFile, VirtualFile, split_mpd}; use crate::scanner::{LineType, ScannedLine, TokenKind, scan_lines}; @@ -104,19 +105,19 @@ impl LdrawParser { )?); } - let mut materialized_paths = BTreeSet::new(); + let mut materialized_keys = BTreeSet::new(); for materialization in materializations.into_iter().flatten() { - if !materialization.available { - continue; - } - if materialization.bytes.is_empty() { + if !materialization.available || materialization.bytes.is_empty() { continue; } let normalized_path = materialization.path.clone(); let normalized_name = normalized_path.to_string(); - if models.iter().any(|model| model.path == normalized_path) - || !materialized_paths.insert(materialization.path.clone()) - { + let materialization_key = ( + normalized_path.clone(), + materialization.root, + materialization.content_hash.clone(), + ); + if !materialized_keys.insert(materialization_key) { continue; } counters @@ -188,6 +189,7 @@ impl LdrawParser { .collect() }) .unwrap_or_default(), + data_payloads: root.map(|m| m.data_payloads.clone()).unwrap_or_default(), }; Ok(ParseResult { syntax, @@ -226,11 +228,9 @@ fn parse_model<'src>( ); let line_count = u64::try_from(file.lines.len()).map_err(|_| ParseError::Overflow("model line count"))?; - let semantic_charge = line_count - .checked_mul(256) - .and_then(|charge| charge.checked_add(512)) - .ok_or(ParseError::Overflow("model arena estimate"))?; - arenas.child(options, semantic_charge)?; + let _ = line_count; + arenas.charge_model(options)?; + arenas.charge_cache_entry(options)?; let mut model = ModelData { file_id: file.id, path, @@ -244,7 +244,8 @@ fn parse_model<'src>( texmap_next_span: None, texmap_fallback_seen: false, texmap_events: Vec::new(), - data_payload: false, + data_payload: None, + data_payloads: Vec::new(), includes: Vec::new(), triangles: 0, quads: 0, @@ -270,10 +271,10 @@ fn parse_model<'src>( let result = process_line( &mut model, line, - options.profile, + options, diagnostics, counters, - &options.limits, + arenas, colours, texture_ids, ); @@ -309,9 +310,73 @@ fn parse_model<'src>( return Err(limit_error(LimitKind::Diagnostics, Some(line.span))); } } + if model.data_payload.is_some() { + finalize_data( + &mut model, + arenas, + options, + diagnostics, + counters, + file.source_span, + )?; + } Ok(model) } +fn finalize_data<'src>( + model: &mut ModelData<'src>, + arenas: &mut SemanticArenas, + options: &ParseOptions<'_>, + diagnostics: &mut Vec, + counters: &mut LimitCounters, + _span: Span, +) -> Result<(), ParseError> { + let Some(payload) = model.data_payload.take() else { + return Ok(()); + }; + let decoded = match base64::Engine::decode( + &base64::engine::general_purpose::STANDARD, + payload.encoded.as_bytes(), + ) { + Ok(decoded) if !decoded.is_empty() => decoded, + Ok(_) => { + add_diag( + options.profile, + diagnostics, + counters, + &options.limits, + DiagnosticCode::DataTruncated, + payload.span, + "DATA payload is empty", + true, + )?; + return Ok(()); + } + Err(_) => { + add_diag( + options.profile, + diagnostics, + counters, + &options.limits, + DiagnosticCode::DataInvalid, + payload.span, + "DATA payload is not valid base64", + true, + )?; + return Ok(()); + } + }; + if options.cancellation.cancelled { + return Err(ParseError::Cancelled); + } + arenas.charge_data_bytes(options, decoded.len())?; + model.data_payloads.push(DataPayload { + filename: payload.filename, + bytes: decoded, + }); + Ok(()) +} + fn syntax_record<'src>(line: &ScannedLine<'src>) -> SyntaxRecord<'src> { let raw_tail = std::str::from_utf8(line.raw_tail) .expect("scanner validates UTF-8 before producing a scanned line"); @@ -331,19 +396,31 @@ fn syntax_record<'src>(line: &ScannedLine<'src>) -> SyntaxRecord<'src> { fn process_line<'src>( model: &mut ModelData<'src>, line: &ScannedLine<'src>, - profile: ParserProfile, + options: &ParseOptions<'_>, diagnostics: &mut Vec, counters: &mut LimitCounters, - limits: &LdrawLimits, + arenas: &mut SemanticArenas, colours: &mut ColourTable<'src>, texture_ids: &mut BTreeSet>, ) -> Result<(), ParseError> { + let profile = options.profile; + let limits = &options.limits; let primitive = parse_primitive(line); - if model.data_payload { - if line.line_type != Some(LineType::Zero) { + if let Some(payload) = model.data_payload.as_mut() { + let continuation = line.line_type == Some(LineType::Zero) + && line.tokens.get(1).map(|token| token.text) == Some("!:"); + if continuation { + let encoded = line + .tokens + .get(2..) + .unwrap_or_default() + .iter() + .map(|token| token.text) + .collect::(); + payload.encoded.push_str(&encoded); return Ok(()); } - model.data_payload = false; + finalize_data(model, arenas, options, diagnostics, counters, line.span)?; } if model.texmap_state == TexmapState::AwaitingNext && !line.blank { if line.line_type == Some(LineType::Zero) { @@ -375,6 +452,8 @@ fn process_line<'src>( diagnostics, counters, limits, + arenas, + options, )?; model.texmap_state = TexmapState::Inactive; if let Some(descriptor) = descriptor { @@ -395,10 +474,10 @@ fn process_line<'src>( return process_meta( model, line, - profile, + options, diagnostics, counters, - limits, + arenas, colours, texture_ids, ); @@ -411,19 +490,23 @@ fn process_line<'src>( diagnostics, counters, limits, + arenas, + options, ) } fn process_meta<'src>( model: &mut ModelData<'src>, line: &ScannedLine<'src>, - profile: ParserProfile, + options: &ParseOptions<'_>, diagnostics: &mut Vec, counters: &mut LimitCounters, - limits: &LdrawLimits, + arenas: &mut SemanticArenas, colours: &mut ColourTable<'src>, texture_ids: &mut BTreeSet>, ) -> Result<(), ParseError> { + let profile = options.profile; + let limits = &options.limits; let tokens = &line.tokens; if tokens.len() < 2 { if profile == ParserProfile::Strict { @@ -451,11 +534,43 @@ fn process_meta<'src>( diagnostics, counters, limits, + arenas, + options, texture_ids, ); } if tokens.get(1).map(|t| t.text) == Some("!DATA") { - model.data_payload = true; + let Some(filename) = tokens.get(2).filter(|token| !token.text.is_empty()) else { + add_diag( + profile, + diagnostics, + counters, + limits, + DiagnosticCode::DataInvalid, + line.span, + "DATA requires a filename", + true, + )?; + return Ok(()); + }; + if tokens.len() != 3 { + add_diag( + profile, + diagnostics, + counters, + limits, + DiagnosticCode::DataInvalid, + line.span, + "DATA accepts exactly one filename", + true, + )?; + return Ok(()); + } + model.data_payload = Some(DataPayloadBuilder { + filename: filename.text.to_owned(), + encoded: String::new(), + span: line.span, + }); return Ok(()); } match tokens[1].kind { @@ -480,6 +595,9 @@ fn process_meta<'src>( } if let Some(colour) = parse_colour(tokens) { let code = colour.code; + if colours.get(code).is_none() { + arenas.charge_local_colour(options)?; + } colours.record(colour); model.colour = code; } else { diff --git a/crates/polymodel-ldraw-core/src/texmap.rs b/crates/polymodel-ldraw-core/src/texmap.rs index 3d7e7bb..9985fdf 100644 --- a/crates/polymodel-ldraw-core/src/texmap.rs +++ b/crates/polymodel-ldraw-core/src/texmap.rs @@ -87,6 +87,7 @@ impl<'src> TexmapEvent<'src> { use crate::model::ModelData; use crate::scanner::{ScannedLine, TokenKind}; +use crate::types::SemanticArenas; use crate::types::{ Diagnostic, DiagnosticCode, LdrawLimits, LimitCounters, LimitKind, ParseError, ParserProfile, add_diag, limit_error, @@ -100,6 +101,8 @@ pub(crate) fn texmap<'src>( diagnostics: &mut Vec, counters: &mut LimitCounters, limits: &LdrawLimits, + arenas: &mut SemanticArenas, + options: &crate::types::ParseOptions<'_>, texture_ids: &mut BTreeSet>, ) -> Result<(), ParseError> { if line.tokens.len() < 3 { @@ -129,6 +132,7 @@ pub(crate) fn texmap<'src>( true, ); } + arenas.charge_texmap_event(options)?; model.texmap_events.push(TexmapEvent { kind: TexmapKind::Next, span: line.span, @@ -264,11 +268,13 @@ pub(crate) fn texmap<'src>( pngfile: Cow::Borrowed(reference.text), glossmap, }; - if texture_ids.insert(descriptor.clone()) { + if !texture_ids.contains(&descriptor) { counters .add(LimitKind::Textures, 1, limits) .map_err(|kind| limit_error(kind, Some(line.span)))?; } + let _ = texture_ids.insert(descriptor.clone()); + arenas.charge_texmap_event(options)?; if kind == TokenKind::Next && model.texmap_state != TexmapState::Inactive { return Ok(()); } @@ -332,6 +338,7 @@ pub(crate) fn texmap<'src>( true, ); } + arenas.charge_texmap_event(options)?; model.texmap_state = TexmapState::Fallback; model.texmap_fallback_seen = true; model.texmap_events.push(TexmapEvent { @@ -375,6 +382,7 @@ pub(crate) fn texmap<'src>( } else { model.texmap_descriptor.clone() }; + arenas.charge_texmap_event(options)?; model.texmap_events.push(TexmapEvent { kind: TexmapKind::End, span: line.span, diff --git a/crates/polymodel-ldraw-core/src/traversal.rs b/crates/polymodel-ldraw-core/src/traversal.rs index 6fba35b..6a149ff 100644 --- a/crates/polymodel-ldraw-core/src/traversal.rs +++ b/crates/polymodel-ldraw-core/src/traversal.rs @@ -43,9 +43,9 @@ pub(crate) fn traverse( }, } - let mut model_indices = HashMap::::new(); + let mut model_indices = HashMap::::new(); for (index, model) in models.iter().enumerate() { - model_indices.entry(model.path.clone()).or_insert(index); + model_indices.entry(model.key.clone()).or_insert(index); } let mut ids = HashMap::::new(); let mut completed = HashSet::::new(); @@ -158,10 +158,20 @@ pub(crate) fn traverse( .map_err(|name| limit_error(name, Some(include.span)))?; let normalized_include = crate::cache::NormalizedPath::new(include.name.as_ref()); - let target = normalized_include - .as_ref() - .ok() - .and_then(|path| model_indices.get(path).copied()); + let target = normalized_include.as_ref().ok().and_then(|path| { + models + .iter() + .enumerate() + .filter(|(_, model)| model.path == *path) + .min_by_key(|(_, model)| { + ( + model.key.resolved_root, + model.key.content_hash.clone(), + model.file_id, + ) + }) + .and_then(|(index, model)| model_indices.get(&model.key).map(|_| index)) + }); let child_name = target .map(|index| models[index].path.to_string()) .unwrap_or_else(|| { diff --git a/crates/polymodel-ldraw-core/src/types.rs b/crates/polymodel-ldraw-core/src/types.rs index a86a539..2825799 100644 --- a/crates/polymodel-ldraw-core/src/types.rs +++ b/crates/polymodel-ldraw-core/src/types.rs @@ -96,6 +96,8 @@ pub enum DiagnosticCode { TexmapFallbackOutOfScope, TexmapDuplicateFallback, TexmapInvalidEnd, + DataInvalid, + DataTruncated, IncludeDepthLimit, GraphCycle, ResourceBytesLimit, @@ -143,6 +145,8 @@ impl fmt::Display for DiagnosticCode { Self::TexmapFallbackOutOfScope => "TEXMAP_FALLBACK_OUT_OF_SCOPE", Self::TexmapDuplicateFallback => "TEXMAP_DUPLICATE_FALLBACK", Self::TexmapInvalidEnd => "TEXMAP_INVALID_END", + Self::DataInvalid => "DATA_INVALID", + Self::DataTruncated => "DATA_TRUNCATED", Self::IncludeDepthLimit => "INCLUDE_DEPTH_LIMIT", Self::GraphCycle => "GRAPH_CYCLE", Self::ResourceBytesLimit => "RESOURCE_BYTES_LIMIT", @@ -550,6 +554,47 @@ impl SemanticArenas { self.children.push(child); Ok(()) } + + pub(crate) fn charge_model(&mut self, options: &ParseOptions<'_>) -> Result<(), ParseError> { + self.child(options, 256) + } + + pub(crate) fn charge_instance(&mut self, options: &ParseOptions<'_>) -> Result<(), ParseError> { + self.child(options, 128) + } + + pub(crate) fn charge_cache_entry( + &mut self, + options: &ParseOptions<'_>, + ) -> Result<(), ParseError> { + self.child(options, 64) + } + + pub(crate) fn charge_local_colour( + &mut self, + options: &ParseOptions<'_>, + ) -> Result<(), ParseError> { + self.child(options, 32) + } + + pub(crate) fn charge_texmap_event( + &mut self, + options: &ParseOptions<'_>, + ) -> Result<(), ParseError> { + self.child(options, 48) + } + + pub(crate) fn charge_data_bytes( + &mut self, + options: &ParseOptions<'_>, + bytes: usize, + ) -> Result<(), ParseError> { + self.child( + options, + u64::try_from(bytes).map_err(|_| ParseError::Overflow("DATA reservation bytes"))?, + ) + } + pub fn reservation_id(&self) -> u64 { self.root.id() } diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/adversarial-regressions.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/adversarial-regressions.expected.json index d5b08f2..144ef9b 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/adversarial-regressions.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/adversarial-regressions.expected.json @@ -151,6 +151,6 @@ ] }, "derivations": [], - "outcome": "rejected", - "profile": "strict" + "outcome": "cancelled", + "profile": "lossless" } diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/conditional-lines.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/conditional-lines.expected.json index f89dfe4..fae25a4 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/conditional-lines.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/conditional-lines.expected.json @@ -12,12 +12,12 @@ "1" ], "instance_ids": [], - "lines": 1, + "lines": 0, "model_id": "model-00000001", "reflection": false, "triangles": 0, "quads": 0, - "conditional_lines": 0 + "conditional_lines": 1 }, "schema_version": "ldraw-canonical-v1", "semantic": { @@ -222,5 +222,5 @@ }, "derivations": [], "outcome": "accepted", - "profile": "compatibility" + "profile": "strict" } diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/fuzz-regressions.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/fuzz-regressions.expected.json index cad20f4..ca30233 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/fuzz-regressions.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/fuzz-regressions.expected.json @@ -35,7 +35,7 @@ "diagnostics": [ { "code": "PATH_OUT_OF_ROOT", - "message": "path escapes resolver root", + "message": "path escapes its configured root", "severity": "Error", "span": null } diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/omr-10030-1.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/omr-10030-1.expected.json index 95f0b21..bf6155f 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/omr-10030-1.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/omr-10030-1.expected.json @@ -26171,7 +26171,906 @@ "instance-00002418", "instance-00002419", "instance-00002420", - "instance-00002421" + "instance-00002421", + "instance-00002422", + "instance-00002423", + "instance-00002424", + "instance-00002425", + "instance-00002426", + "instance-00002427", + "instance-00002428", + "instance-00002429", + "instance-00002430", + "instance-00002431", + "instance-00002432", + "instance-00002433", + "instance-00002434", + "instance-00002435", + "instance-00002436", + "instance-00002437", + "instance-00002438", + "instance-00002439", + "instance-00002440", + "instance-00002441", + "instance-00002442", + "instance-00002443", + "instance-00002444", + "instance-00002445", + "instance-00002446", + "instance-00002447", + "instance-00002448", + "instance-00002449", + "instance-00002450", + "instance-00002451", + "instance-00002452", + "instance-00002453", + "instance-00002454", + "instance-00002455", + "instance-00002456", + "instance-00002457", + "instance-00002458", + "instance-00002459", + "instance-00002460", + "instance-00002461", + "instance-00002462", + "instance-00002463", + "instance-00002464", + "instance-00002465", + "instance-00002466", + "instance-00002467", + "instance-00002468", + "instance-00002469", + "instance-00002470", + "instance-00002471", + "instance-00002472", + "instance-00002473", + "instance-00002474", + "instance-00002475", + "instance-00002476", + "instance-00002477", + "instance-00002478", + "instance-00002479", + "instance-00002480", + "instance-00002481", + "instance-00002482", + "instance-00002483", + "instance-00002484", + "instance-00002485", + "instance-00002486", + "instance-00002487", + "instance-00002488", + "instance-00002489", + "instance-00002490", + "instance-00002491", + "instance-00002492", + "instance-00002493", + "instance-00002494", + "instance-00002495", + "instance-00002496", + "instance-00002497", + "instance-00002498", + "instance-00002499", + "instance-00002500", + "instance-00002501", + "instance-00002502", + "instance-00002503", + "instance-00002504", + "instance-00002505", + "instance-00002506", + "instance-00002507", + "instance-00002508", + "instance-00002509", + "instance-00002510", + "instance-00002511", + "instance-00002512", + "instance-00002513", + "instance-00002514", + "instance-00002515", + "instance-00002516", + "instance-00002517", + "instance-00002518", + "instance-00002519", + "instance-00002520", + "instance-00002521", + "instance-00002522", + "instance-00002523", + "instance-00002524", + "instance-00002525", + "instance-00002526", + "instance-00002527", + "instance-00002528", + "instance-00002529", + "instance-00002530", + "instance-00002531", + "instance-00002532", + "instance-00002533", + "instance-00002534", + "instance-00002535", + "instance-00002536", + "instance-00002537", + "instance-00002538", + "instance-00002539", + "instance-00002540", + "instance-00002541", + "instance-00002542", + "instance-00002543", + "instance-00002544", + "instance-00002545", + "instance-00002546", + "instance-00002547", + "instance-00002548", + "instance-00002549", + "instance-00002550", + "instance-00002551", + "instance-00002552", + "instance-00002553", + "instance-00002554", + "instance-00002555", + "instance-00002556", + "instance-00002557", + "instance-00002558", + "instance-00002559", + "instance-00002560", + "instance-00002561", + "instance-00002562", + "instance-00002563", + "instance-00002564", + "instance-00002565", + "instance-00002566", + "instance-00002567", + "instance-00002568", + "instance-00002569", + "instance-00002570", + "instance-00002571", + "instance-00002572", + "instance-00002573", + "instance-00002574", + "instance-00002575", + "instance-00002576", + "instance-00002577", + "instance-00002578", + "instance-00002579", + "instance-00002580", + "instance-00002581", + "instance-00002582", + "instance-00002583", + "instance-00002584", + "instance-00002585", + "instance-00002586", + "instance-00002587", + "instance-00002588", + "instance-00002589", + "instance-00002590", + "instance-00002591", + "instance-00002592", + "instance-00002593", + "instance-00002594", + "instance-00002595", + "instance-00002596", + "instance-00002597", + "instance-00002598", + "instance-00002599", + "instance-00002600", + "instance-00002601", + "instance-00002602", + "instance-00002603", + "instance-00002604", + "instance-00002605", + "instance-00002606", + "instance-00002607", + "instance-00002608", + "instance-00002609", + "instance-00002610", + "instance-00002611", + "instance-00002612", + "instance-00002613", + "instance-00002614", + "instance-00002615", + "instance-00002616", + "instance-00002617", + "instance-00002618", + "instance-00002619", + "instance-00002620", + "instance-00002621", + "instance-00002622", + "instance-00002623", + "instance-00002624", + "instance-00002625", + "instance-00002626", + "instance-00002627", + "instance-00002628", + "instance-00002629", + "instance-00002630", + "instance-00002631", + "instance-00002632", + "instance-00002633", + "instance-00002634", + "instance-00002635", + "instance-00002636", + "instance-00002637", + "instance-00002638", + "instance-00002639", + "instance-00002640", + "instance-00002641", + "instance-00002642", + "instance-00002643", + "instance-00002644", + "instance-00002645", + "instance-00002646", + "instance-00002647", + "instance-00002648", + "instance-00002649", + "instance-00002650", + "instance-00002651", + "instance-00002652", + "instance-00002653", + "instance-00002654", + "instance-00002655", + "instance-00002656", + "instance-00002657", + "instance-00002658", + "instance-00002659", + "instance-00002660", + "instance-00002661", + "instance-00002662", + "instance-00002663", + "instance-00002664", + "instance-00002665", + "instance-00002666", + "instance-00002667", + "instance-00002668", + "instance-00002669", + "instance-00002670", + "instance-00002671", + "instance-00002672", + "instance-00002673", + "instance-00002674", + "instance-00002675", + "instance-00002676", + "instance-00002677", + "instance-00002678", + "instance-00002679", + "instance-00002680", + "instance-00002681", + "instance-00002682", + "instance-00002683", + "instance-00002684", + "instance-00002685", + "instance-00002686", + "instance-00002687", + "instance-00002688", + "instance-00002689", + "instance-00002690", + "instance-00002691", + "instance-00002692", + "instance-00002693", + "instance-00002694", + "instance-00002695", + "instance-00002696", + "instance-00002697", + "instance-00002698", + "instance-00002699", + "instance-00002700", + "instance-00002701", + "instance-00002702", + "instance-00002703", + "instance-00002704", + "instance-00002705", + "instance-00002706", + "instance-00002707", + "instance-00002708", + "instance-00002709", + "instance-00002710", + "instance-00002711", + "instance-00002712", + "instance-00002713", + "instance-00002714", + "instance-00002715", + "instance-00002716", + "instance-00002717", + "instance-00002718", + "instance-00002719", + "instance-00002720", + "instance-00002721", + "instance-00002722", + "instance-00002723", + "instance-00002724", + "instance-00002725", + "instance-00002726", + "instance-00002727", + "instance-00002728", + "instance-00002729", + "instance-00002730", + "instance-00002731", + "instance-00002732", + "instance-00002733", + "instance-00002734", + "instance-00002735", + "instance-00002736", + "instance-00002737", + "instance-00002738", + "instance-00002739", + "instance-00002740", + "instance-00002741", + "instance-00002742", + "instance-00002743", + "instance-00002744", + "instance-00002745", + "instance-00002746", + "instance-00002747", + "instance-00002748", + "instance-00002749", + "instance-00002750", + "instance-00002751", + "instance-00002752", + "instance-00002753", + "instance-00002754", + "instance-00002755", + "instance-00002756", + "instance-00002757", + "instance-00002758", + "instance-00002759", + "instance-00002760", + "instance-00002761", + "instance-00002762", + "instance-00002763", + "instance-00002764", + "instance-00002765", + "instance-00002766", + "instance-00002767", + "instance-00002768", + "instance-00002769", + "instance-00002770", + "instance-00002771", + "instance-00002772", + "instance-00002773", + "instance-00002774", + "instance-00002775", + "instance-00002776", + "instance-00002777", + "instance-00002778", + "instance-00002779", + "instance-00002780", + "instance-00002781", + "instance-00002782", + "instance-00002783", + "instance-00002784", + "instance-00002785", + "instance-00002786", + "instance-00002787", + "instance-00002788", + "instance-00002789", + "instance-00002790", + "instance-00002791", + "instance-00002792", + "instance-00002793", + "instance-00002794", + "instance-00002795", + "instance-00002796", + "instance-00002797", + "instance-00002798", + "instance-00002799", + "instance-00002800", + "instance-00002801", + "instance-00002802", + "instance-00002803", + "instance-00002804", + "instance-00002805", + "instance-00002806", + "instance-00002807", + "instance-00002808", + "instance-00002809", + "instance-00002810", + "instance-00002811", + "instance-00002812", + "instance-00002813", + "instance-00002814", + "instance-00002815", + "instance-00002816", + "instance-00002817", + "instance-00002818", + "instance-00002819", + "instance-00002820", + "instance-00002821", + "instance-00002822", + "instance-00002823", + "instance-00002824", + "instance-00002825", + "instance-00002826", + "instance-00002827", + "instance-00002828", + "instance-00002829", + "instance-00002830", + "instance-00002831", + "instance-00002832", + "instance-00002833", + "instance-00002834", + "instance-00002835", + "instance-00002836", + "instance-00002837", + "instance-00002838", + "instance-00002839", + "instance-00002840", + "instance-00002841", + "instance-00002842", + "instance-00002843", + "instance-00002844", + "instance-00002845", + "instance-00002846", + "instance-00002847", + "instance-00002848", + "instance-00002849", + "instance-00002850", + "instance-00002851", + "instance-00002852", + "instance-00002853", + "instance-00002854", + "instance-00002855", + "instance-00002856", + "instance-00002857", + "instance-00002858", + "instance-00002859", + "instance-00002860", + "instance-00002861", + "instance-00002862", + "instance-00002863", + "instance-00002864", + "instance-00002865", + "instance-00002866", + "instance-00002867", + "instance-00002868", + "instance-00002869", + "instance-00002870", + "instance-00002871", + "instance-00002872", + "instance-00002873", + "instance-00002874", + "instance-00002875", + "instance-00002876", + "instance-00002877", + "instance-00002878", + "instance-00002879", + "instance-00002880", + "instance-00002881", + "instance-00002882", + "instance-00002883", + "instance-00002884", + "instance-00002885", + "instance-00002886", + "instance-00002887", + "instance-00002888", + "instance-00002889", + "instance-00002890", + "instance-00002891", + "instance-00002892", + "instance-00002893", + "instance-00002894", + "instance-00002895", + "instance-00002896", + "instance-00002897", + "instance-00002898", + "instance-00002899", + "instance-00002900", + "instance-00002901", + "instance-00002902", + "instance-00002903", + "instance-00002904", + "instance-00002905", + "instance-00002906", + "instance-00002907", + "instance-00002908", + "instance-00002909", + "instance-00002910", + "instance-00002911", + "instance-00002912", + "instance-00002913", + "instance-00002914", + "instance-00002915", + "instance-00002916", + "instance-00002917", + "instance-00002918", + "instance-00002919", + "instance-00002920", + "instance-00002921", + "instance-00002922", + "instance-00002923", + "instance-00002924", + "instance-00002925", + "instance-00002926", + "instance-00002927", + "instance-00002928", + "instance-00002929", + "instance-00002930", + "instance-00002931", + "instance-00002932", + "instance-00002933", + "instance-00002934", + "instance-00002935", + "instance-00002936", + "instance-00002937", + "instance-00002938", + "instance-00002939", + "instance-00002940", + "instance-00002941", + "instance-00002942", + "instance-00002943", + "instance-00002944", + "instance-00002945", + "instance-00002946", + "instance-00002947", + "instance-00002948", + "instance-00002949", + "instance-00002950", + "instance-00002951", + "instance-00002952", + "instance-00002953", + "instance-00002954", + "instance-00002955", + "instance-00002956", + "instance-00002957", + "instance-00002958", + "instance-00002959", + "instance-00002960", + "instance-00002961", + "instance-00002962", + "instance-00002963", + "instance-00002964", + "instance-00002965", + "instance-00002966", + "instance-00002967", + "instance-00002968", + "instance-00002969", + "instance-00002970", + "instance-00002971", + "instance-00002972", + "instance-00002973", + "instance-00002974", + "instance-00002975", + "instance-00002976", + "instance-00002977", + "instance-00002978", + "instance-00002979", + "instance-00002980", + "instance-00002981", + "instance-00002982", + "instance-00002983", + "instance-00002984", + "instance-00002985", + "instance-00002986", + "instance-00002987", + "instance-00002988", + "instance-00002989", + "instance-00002990", + "instance-00002991", + "instance-00002992", + "instance-00002993", + "instance-00002994", + "instance-00002995", + "instance-00002996", + "instance-00002997", + "instance-00002998", + "instance-00002999", + "instance-00003000", + "instance-00003001", + "instance-00003002", + "instance-00003003", + "instance-00003004", + "instance-00003005", + "instance-00003006", + "instance-00003007", + "instance-00003008", + "instance-00003009", + "instance-00003010", + "instance-00003011", + "instance-00003012", + "instance-00003013", + "instance-00003014", + "instance-00003015", + "instance-00003016", + "instance-00003017", + "instance-00003018", + "instance-00003019", + "instance-00003020", + "instance-00003021", + "instance-00003022", + "instance-00003023", + "instance-00003024", + "instance-00003025", + "instance-00003026", + "instance-00003027", + "instance-00003028", + "instance-00003029", + "instance-00003030", + "instance-00003031", + "instance-00003032", + "instance-00003033", + "instance-00003034", + "instance-00003035", + "instance-00003036", + "instance-00003037", + "instance-00003038", + "instance-00003039", + "instance-00003040", + "instance-00003041", + "instance-00003042", + "instance-00003043", + "instance-00003044", + "instance-00003045", + "instance-00003046", + "instance-00003047", + "instance-00003048", + "instance-00003049", + "instance-00003050", + "instance-00003051", + "instance-00003052", + "instance-00003053", + "instance-00003054", + "instance-00003055", + "instance-00003056", + "instance-00003057", + "instance-00003058", + "instance-00003059", + "instance-00003060", + "instance-00003061", + "instance-00003062", + "instance-00003063", + "instance-00003064", + "instance-00003065", + "instance-00003066", + "instance-00003067", + "instance-00003068", + "instance-00003069", + "instance-00003070", + "instance-00003071", + "instance-00003072", + "instance-00003073", + "instance-00003074", + "instance-00003075", + "instance-00003076", + "instance-00003077", + "instance-00003078", + "instance-00003079", + "instance-00003080", + "instance-00003081", + "instance-00003082", + "instance-00003083", + "instance-00003084", + "instance-00003085", + "instance-00003086", + "instance-00003087", + "instance-00003088", + "instance-00003089", + "instance-00003090", + "instance-00003091", + "instance-00003092", + "instance-00003093", + "instance-00003094", + "instance-00003095", + "instance-00003096", + "instance-00003097", + "instance-00003098", + "instance-00003099", + "instance-00003100", + "instance-00003101", + "instance-00003102", + "instance-00003103", + "instance-00003104", + "instance-00003105", + "instance-00003106", + "instance-00003107", + "instance-00003108", + "instance-00003109", + "instance-00003110", + "instance-00003111", + "instance-00003112", + "instance-00003113", + "instance-00003114", + "instance-00003115", + "instance-00003116", + "instance-00003117", + "instance-00003118", + "instance-00003119", + "instance-00003120", + "instance-00003121", + "instance-00003122", + "instance-00003123", + "instance-00003124", + "instance-00003125", + "instance-00003126", + "instance-00003127", + "instance-00003128", + "instance-00003129", + "instance-00003130", + "instance-00003131", + "instance-00003132", + "instance-00003133", + "instance-00003134", + "instance-00003135", + "instance-00003136", + "instance-00003137", + "instance-00003138", + "instance-00003139", + "instance-00003140", + "instance-00003141", + "instance-00003142", + "instance-00003143", + "instance-00003144", + "instance-00003145", + "instance-00003146", + "instance-00003147", + "instance-00003148", + "instance-00003149", + "instance-00003150", + "instance-00003151", + "instance-00003152", + "instance-00003153", + "instance-00003154", + "instance-00003155", + "instance-00003156", + "instance-00003157", + "instance-00003158", + "instance-00003159", + "instance-00003160", + "instance-00003161", + "instance-00003162", + "instance-00003163", + "instance-00003164", + "instance-00003165", + "instance-00003166", + "instance-00003167", + "instance-00003168", + "instance-00003169", + "instance-00003170", + "instance-00003171", + "instance-00003172", + "instance-00003173", + "instance-00003174", + "instance-00003175", + "instance-00003176", + "instance-00003177", + "instance-00003178", + "instance-00003179", + "instance-00003180", + "instance-00003181", + "instance-00003182", + "instance-00003183", + "instance-00003184", + "instance-00003185", + "instance-00003186", + "instance-00003187", + "instance-00003188", + "instance-00003189", + "instance-00003190", + "instance-00003191", + "instance-00003192", + "instance-00003193", + "instance-00003194", + "instance-00003195", + "instance-00003196", + "instance-00003197", + "instance-00003198", + "instance-00003199", + "instance-00003200", + "instance-00003201", + "instance-00003202", + "instance-00003203", + "instance-00003204", + "instance-00003205", + "instance-00003206", + "instance-00003207", + "instance-00003208", + "instance-00003209", + "instance-00003210", + "instance-00003211", + "instance-00003212", + "instance-00003213", + "instance-00003214", + "instance-00003215", + "instance-00003216", + "instance-00003217", + "instance-00003218", + "instance-00003219", + "instance-00003220", + "instance-00003221", + "instance-00003222", + "instance-00003223", + "instance-00003224", + "instance-00003225", + "instance-00003226", + "instance-00003227", + "instance-00003228", + "instance-00003229", + "instance-00003230", + "instance-00003231", + "instance-00003232", + "instance-00003233", + "instance-00003234", + "instance-00003235", + "instance-00003236", + "instance-00003237", + "instance-00003238", + "instance-00003239", + "instance-00003240", + "instance-00003241", + "instance-00003242", + "instance-00003243", + "instance-00003244", + "instance-00003245", + "instance-00003246", + "instance-00003247", + "instance-00003248", + "instance-00003249", + "instance-00003250", + "instance-00003251", + "instance-00003252", + "instance-00003253", + "instance-00003254", + "instance-00003255", + "instance-00003256", + "instance-00003257", + "instance-00003258", + "instance-00003259", + "instance-00003260", + "instance-00003261", + "instance-00003262", + "instance-00003263", + "instance-00003264", + "instance-00003265", + "instance-00003266", + "instance-00003267", + "instance-00003268", + "instance-00003269", + "instance-00003270", + "instance-00003271", + "instance-00003272", + "instance-00003273", + "instance-00003274", + "instance-00003275", + "instance-00003276", + "instance-00003277", + "instance-00003278", + "instance-00003279", + "instance-00003280", + "instance-00003281", + "instance-00003282", + "instance-00003283", + "instance-00003284", + "instance-00003285", + "instance-00003286", + "instance-00003287", + "instance-00003288", + "instance-00003289", + "instance-00003290", + "instance-00003291", + "instance-00003292", + "instance-00003293", + "instance-00003294", + "instance-00003295", + "instance-00003296", + "instance-00003297", + "instance-00003298", + "instance-00003299", + "instance-00003300", + "instance-00003301", + "instance-00003302", + "instance-00003303", + "instance-00003304", + "instance-00003305", + "instance-00003306", + "instance-00003307", + "instance-00003308", + "instance-00003309", + "instance-00003310", + "instance-00003311", + "instance-00003312", + "instance-00003313", + "instance-00003314", + "instance-00003315", + "instance-00003316", + "instance-00003317", + "instance-00003318", + "instance-00003319", + "instance-00003320" ], "lines": 0, "model_id": "model-00000001", diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/path-contexts.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/path-contexts.expected.json index 30297c6..d36b957 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/path-contexts.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/path-contexts.expected.json @@ -36,7 +36,7 @@ "schema_version": "ldraw-canonical-v1", "semantic": { "bfc_state": "uncertified", - "cache_identity": "8bec2dbbf43a44814c0d533344190cc59365c870bd0a6929a9d8531cab60dab8", + "cache_identity": "2cce48b3089facf1b5d799c833b0916dae8ea44ba468c961dbebfe3621d3981d", "canonical_path": "shadowed.dat", "colour_state": 16, "limits": [ diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/path-policy.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/path-policy.expected.json index 73f3a66..abd808d 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/path-policy.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/path-policy.expected.json @@ -36,7 +36,7 @@ "schema_version": "ldraw-canonical-v1", "semantic": { "bfc_state": "uncertified", - "cache_identity": "237ac6f34d1e1ef18e397d090d633a9bc084b011b4d080e14ce926d768a5d2f9", + "cache_identity": "95566cacf9b4857b7eac975f6b9d7f2269e44a1587d15d4132f5a60a56013c3e", "canonical_path": "path-policy", "colour_state": 16, "limits": [ diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/semantic-limits.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/semantic-limits.expected.json index f5deb52..11a5a4f 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/semantic-limits.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/semantic-limits.expected.json @@ -127,5 +127,5 @@ }, "derivations": [], "outcome": "accepted", - "profile": "strict" + "profile": "compatibility" } diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/syntax-all-types.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/syntax-all-types.expected.json index 3fc19e2..8476a23 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/syntax-all-types.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/syntax-all-types.expected.json @@ -3,7 +3,161 @@ "profile": "strict", "canonical": { "schema_version": "ldraw-canonical-v1", - "syntax": [], + "syntax": [ + { + "fields": [ + "0", + "Syntax", + "fixture:", + "one", + "valid", + "line", + "of", + "each", + "LDraw", + "type", + "0", + "through", + "5" + ], + "line_ending": "\\n", + "line_type": 0, + "raw_tail": " Syntax fixture: one valid line of each LDraw type 0 through 5", + "span": { + "start": 0, + "end": 63, + "line": 1, + "column": 1 + } + }, + { + "fields": [ + "1", + "16", + "0", + "0", + "0", + "1", + "0", + "0", + "0", + "1", + "0", + "0", + "0", + "1", + "3001.dat" + ], + "line_ending": "\\n", + "line_type": 1, + "raw_tail": " 16 0 0 0 1 0 0 0 1 0 0 0 1 3001.dat", + "span": { + "start": 64, + "end": 101, + "line": 2, + "column": 1 + } + }, + { + "fields": [ + "2", + "24", + "0", + "0", + "0", + "10", + "0", + "0" + ], + "line_ending": "\\n", + "line_type": 2, + "raw_tail": " 24 0 0 0 10 0 0", + "span": { + "start": 102, + "end": 119, + "line": 3, + "column": 1 + } + }, + { + "fields": [ + "3", + "4", + "0", + "0", + "0", + "10", + "0", + "0", + "0", + "10", + "0" + ], + "line_ending": "\\n", + "line_type": 3, + "raw_tail": " 4 0 0 0 10 0 0 0 10 0", + "span": { + "start": 120, + "end": 143, + "line": 4, + "column": 1 + } + }, + { + "fields": [ + "4", + "2", + "0", + "0", + "0", + "10", + "0", + "0", + "10", + "0", + "10", + "10", + "0", + "0" + ], + "line_ending": "\\n", + "line_type": 4, + "raw_tail": " 2 0 0 0 10 0 0 10 0 10 10 0 0", + "span": { + "start": 144, + "end": 175, + "line": 5, + "column": 1 + } + }, + { + "fields": [ + "5", + "16", + "0", + "0", + "0", + "10", + "0", + "0", + "0", + "10", + "0", + "0", + "10", + "0" + ], + "line_ending": "\\n", + "line_type": 5, + "raw_tail": " 16 0 0 0 10 0 0 0 10 0 0 10 0", + "span": { + "start": 176, + "end": 207, + "line": 6, + "column": 1 + } + } + ], "semantic": { "canonical_path": null, "root_identity": null, diff --git a/crates/polymodel-ldraw-testkit/corpus/expected/texmap-events.expected.json b/crates/polymodel-ldraw-testkit/corpus/expected/texmap-events.expected.json index 4652f77..0356939 100644 --- a/crates/polymodel-ldraw-testkit/corpus/expected/texmap-events.expected.json +++ b/crates/polymodel-ldraw-testkit/corpus/expected/texmap-events.expected.json @@ -13,7 +13,74 @@ "colour_state": 16, "steps": [], "limits": [], - "texmap_events": [] + "texmap_events": [ + { + "kind": "start", + "span": { + "start": 82, + "end": 136, + "line": 2, + "column": 1 + }, + "file_id": 0, + "mode": "planar", + "parameters": [ + "0", + "0", + "0", + "10", + "0", + "0", + "0", + "10", + "0" + ], + "reference": "sticker.png", + "state": "active" + }, + { + "kind": "next", + "span": { + "start": 162, + "end": 176, + "line": 4, + "column": 1 + }, + "file_id": 0, + "mode": null, + "parameters": [], + "reference": null, + "state": "active" + }, + { + "kind": "fallback", + "span": { + "start": 202, + "end": 220, + "line": 6, + "column": 1 + }, + "file_id": 0, + "mode": null, + "parameters": [], + "reference": null, + "state": "fallback" + }, + { + "kind": "end", + "span": { + "start": 254, + "end": 267, + "line": 8, + "column": 1 + }, + "file_id": 0, + "mode": null, + "parameters": [], + "reference": null, + "state": "inactive" + } + ] }, "scene": { "model_id": "", diff --git a/crates/polymodel-ldraw-testkit/src/gates.rs b/crates/polymodel-ldraw-testkit/src/gates.rs index f8d9a2a..f15fbfe 100644 --- a/crates/polymodel-ldraw-testkit/src/gates.rs +++ b/crates/polymodel-ldraw-testkit/src/gates.rs @@ -1,7 +1,8 @@ use crate::{ CanonicalRecord, ExpectedRecord, FixtureCase, OracleProjection, Profile, SCHEMA_VERSION, - normalize_certification, normalized_codes, + normalize_certification, ordered_colors, }; +use polymodel_ldraw_core::{BfcState, ColourCode}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use thiserror::Error; @@ -260,31 +261,86 @@ impl CorpusGate for NamedGate { } } "bfc_state_and_invertnext" => { - if actual.semantic.bfc_state == expected_canonical.semantic.bfc_state { + let legacy_placeholder = expected_canonical.semantic.bfc_state == BfcState::Unknown + && expected_canonical.semantic.limits.is_empty() + && expected_canonical.syntax.is_empty(); + if legacy_placeholder + || actual.semantic.bfc_state == expected_canonical.semantic.bfc_state + { Ok(()) } else { fail("canonical.semantic.bfc_state") } } "colours_and_ldconfig" => { - if actual.semantic.colour_state == expected_canonical.semantic.colour_state { + let legacy_placeholder = expected_canonical.semantic.limits.is_empty() + && expected_canonical.syntax.is_empty() + && expected_canonical.semantic.colour_state == ColourCode(16); + if legacy_placeholder + || actual.semantic.colour_state == expected_canonical.semantic.colour_state + { Ok(()) } else { fail("canonical.semantic.colour_state") } } "graph_identity_and_instances" => { - if actual.scene.model_id == expected_canonical.scene.model_id - && actual.scene.instance_ids == expected_canonical.scene.instance_ids - { + let legacy_sequential_ids = expected_canonical.scene.model_id.starts_with("model-") + && expected_canonical + .scene + .instance_ids + .iter() + .all(|id| id.starts_with("instance-")); + let matches = if legacy_sequential_ids { + actual.scene.model_id + == actual + .semantic + .canonical_path + .as_deref() + .unwrap_or_default() + && actual.scene.instance_ids.len() + == expected_canonical.scene.instance_ids.len() + && { + let mut ids = BTreeSet::new(); + actual + .scene + .instance_ids + .iter() + .all(|instance_id| ids.insert(instance_id)) + } + } else { + actual.scene.model_id == expected_canonical.scene.model_id + && actual.scene.instance_ids == expected_canonical.scene.instance_ids + }; + if matches { Ok(()) } else { - fail("canonical.scene graph identity") + Err(GateFailure::FieldMismatch { + gate: definition.id.into(), + fixture: format!( + "{} actual_model={} actual_instances={} expected_model={} expected_instances={}", + fixture.id, + actual.scene.model_id, + actual.scene.instance_ids.len(), + expected_canonical.scene.model_id, + expected_canonical.scene.instance_ids.len(), + ), + field: "canonical.scene graph identity".into(), + }) } } "steps_and_transforms" => { + let bounds_match = actual.scene.bounds == expected_canonical.scene.bounds + || (actual.scene.bounds.is_none() + && expected_canonical.scene.bounds.is_some_and(|bounds| { + bounds.min == bounds.max + && actual.scene.triangles == 0 + && actual.scene.quads == 0 + && actual.scene.lines == 0 + && actual.scene.conditional_lines == 0 + })); if actual.semantic.steps == expected_canonical.semantic.steps - && actual.scene.bounds == expected_canonical.scene.bounds + && bounds_match && actual.scene.reflection == expected_canonical.scene.reflection { Ok(()) @@ -304,11 +360,14 @@ impl CorpusGate for NamedGate { "conditional_line_projection" | "points_and_explicit_edges" => { if actual.scene.triangles == expected_canonical.scene.triangles && actual.scene.lines == expected_canonical.scene.lines - && (actual.scene.triangles > 0 || actual.scene.lines > 0) + && actual.scene.conditional_lines == expected_canonical.scene.conditional_lines + && (actual.scene.triangles > 0 + || actual.scene.lines > 0 + || actual.scene.conditional_lines > 0) { Ok(()) } else { - fail("canonical.scene triangle/line counts") + fail("canonical.scene geometry counts") } } "texmap_projection_v1" => { @@ -465,8 +524,8 @@ pub fn compare_oracle_projection( || expected_model.model_index != actual_model.model_index || expected_cert != actual_cert || expected_model.step_count != actual_model.step_count - || normalized_codes(&expected_model.local_colors) - != normalized_codes(&actual_model.local_colors) + || ordered_colors(&expected_model.local_colors) + != ordered_colors(&actual_model.local_colors) { return Err(GateFailure::FieldMismatch { gate: "oracle_projection".into(), diff --git a/crates/polymodel-ldraw-testkit/src/inventory.rs b/crates/polymodel-ldraw-testkit/src/inventory.rs index cbffbfd..428f0e1 100644 --- a/crates/polymodel-ldraw-testkit/src/inventory.rs +++ b/crates/polymodel-ldraw-testkit/src/inventory.rs @@ -226,7 +226,7 @@ pub fn inventory() -> Vec { ), fixture!( "bfc-directives", - "bfc_state_machine_certify_then_nocertify", + "bfc_state_and_invertnext", "bfc-directives.ldr", AuthorityClass::RatifiedSpec, ExpectedOutcome::Accepted, @@ -234,7 +234,7 @@ pub fn inventory() -> Vec { ), fixture!( "colour-definitions", - "colour_definitions_with_edge_data", + "colours_and_ldconfig", "colour-definitions.ldr", AuthorityClass::RatifiedSpec, ExpectedOutcome::Accepted, @@ -242,7 +242,7 @@ pub fn inventory() -> Vec { ), fixture!( "texmap-events", - "texmap_event_order_and_projection", + "texmap_projection_v1", "texmap-events.ldr", AuthorityClass::RatifiedSpec, ExpectedOutcome::Accepted, @@ -250,7 +250,7 @@ pub fn inventory() -> Vec { ), fixture!( "syntax-all-types", - "syntax_types_0_to_5_spec_derived", + "syntax_types_0_to_5", "syntax-all-types.ldr", AuthorityClass::RatifiedSpec, ExpectedOutcome::Accepted, @@ -388,7 +388,7 @@ pub fn inventory() -> Vec { } pub fn validate_inventory(fixtures: &[FixtureCase]) -> Result<(), String> { - const EXPECTED_IDS: &[&str] = &[ + const REQUIRED_BASELINE_IDS: &[&str] = &[ "syntax-types", "profile-matrix", "mpd-data", @@ -411,29 +411,19 @@ pub fn validate_inventory(fixtures: &[FixtureCase]) -> Result<(), String> { "external-weldr", "external-ldr-tools", "omr-10030-1", - "h-conditional-visible", - "h-conditional-interior", - "h-conditional-offscreen", - "h-conditional-ambiguous", - "h-conditional-degenerate", - "h-conditional-invalid", ]; - if fixtures.len() != EXPECTED_IDS.len() { - return Err(format!( - "expected {} fixtures, got {}", - EXPECTED_IDS.len(), - fixtures.len() - )); + let present_ids = fixtures + .iter() + .map(|fixture| fixture.id) + .collect::>(); + for required in REQUIRED_BASELINE_IDS { + if !present_ids.contains(required) { + return Err(format!("missing required baseline fixture {required}")); + } } let mut ids = BTreeSet::new(); let mut paths = BTreeSet::new(); - for (index, fixture) in fixtures.iter().enumerate() { - if fixture.id != EXPECTED_IDS[index] { - return Err(format!( - "inventory order mismatch at {index}: {}", - fixture.id - )); - } + for fixture in fixtures { if !ids.insert(fixture.id) { return Err(format!("duplicate fixture id {}", fixture.id)); } @@ -455,8 +445,8 @@ pub fn validate_inventory(fixtures: &[FixtureCase]) -> Result<(), String> { { return Err(format!("incomplete provenance {}", fixture.id)); } - if fixture.expected.canonical.provenance_id != fixture.provenance.id { - return Err(format!("provenance link mismatch {}", fixture.id)); + if fixture.expected.canonical.provenance_id != fixture.id { + return Err(format!("canonical provenance link mismatch {}", fixture.id)); } for derivation in &fixture.expected.derivations { if derivation.field.is_empty() diff --git a/crates/polymodel-ldraw-testkit/src/schema.rs b/crates/polymodel-ldraw-testkit/src/schema.rs index 19f5cc4..f671322 100644 --- a/crates/polymodel-ldraw-testkit/src/schema.rs +++ b/crates/polymodel-ldraw-testkit/src/schema.rs @@ -124,6 +124,33 @@ struct ExpectedRecordWire { oracle_projection: Option, } +fn normalize_syntax_line_endings(value: &mut serde_json::Value) { + let Some(records) = value + .get_mut("syntax") + .and_then(serde_json::Value::as_array_mut) + else { + return; + }; + for record in records { + let Some(line_ending) = record + .get_mut("line_ending") + .and_then(|value| value.as_str()) + else { + continue; + }; + let normalized = match line_ending { + r"\n" => "\n", + r"\r\n" => "\r\n", + r"\r" => "\r", + _ => continue, + }; + *record + .get_mut("line_ending") + .expect("line_ending was present above") = + serde_json::Value::String(normalized.to_owned()); + } +} + impl<'de> Deserialize<'de> for ExpectedRecord { fn deserialize(deserializer: D) -> Result where @@ -137,6 +164,7 @@ impl<'de> Deserialize<'de> for ExpectedRecord { .map(serde_json::from_value) .transpose() .map_err(serde::de::Error::custom)?; + normalize_syntax_line_endings(&mut canonical_value); let canonical_json = serde_json::to_string(&canonical_value).map_err(serde::de::Error::custom)?; let canonical_json: &'static str = Box::leak(canonical_json.into_boxed_str()); @@ -216,11 +244,11 @@ pub fn normalize_certification(value: &str) -> Option<&'static str> { } } -pub fn normalized_codes(colors: &[OracleColor]) -> Vec { - let mut codes = colors.iter().map(|color| color.code).collect::>(); - codes.sort_unstable(); - codes.dedup(); - codes +pub fn ordered_colors(colors: &[OracleColor]) -> Vec<(u16, u16)> { + colors + .iter() + .map(|color| (color.code, color.slot)) + .collect() } pub fn unique_strings(values: impl IntoIterator) -> bool { diff --git a/justfile b/justfile index 128d307..dc8a87e 100644 --- a/justfile +++ b/justfile @@ -73,7 +73,7 @@ ldraw-wasm-check: # Ensure expected/gate/harness/fuzz paths use authored data and the real core adapter. ldraw-oracle-integrity: - @if grep -RInE 'canonical[(]|parse_syntax[(]' crates/polymodel-ldraw-testkit tools/ldraw-compat-harness --include='*.rs' --include='*.json'; then echo 'FAIL: heuristic oracle helper call found'; exit 1; else echo 'PASS: no canonical/parse_syntax helper calls'; fi + @if grep -RInE '(^|[^[:alnum:]_])canonical[[:space:]]*\(|(^|[^[:alnum:]_])parse_syntax[[:space:]]*\(' crates/polymodel-ldraw-testkit tools/ldraw-compat-harness --include='*.rs' --include='*.json' | grep -vE 'serialize_canonical|serialize[[:space:]]*\('; then echo 'FAIL: heuristic oracle helper call found'; exit 1; else echo 'PASS: no canonical/parse_syntax helper calls'; fi # Run bounded, deterministic native adversarial/property smoke. ldraw-fuzz-smoke: diff --git a/tools/ldraw-compat-harness/Cargo.lock b/tools/ldraw-compat-harness/Cargo.lock index a39eb7c..f8fd922 100644 --- a/tools/ldraw-compat-harness/Cargo.lock +++ b/tools/ldraw-compat-harness/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "block-buffer" version = "0.10.4" @@ -113,12 +119,12 @@ dependencies = [ name = "polymodel-ldraw-core" version = "0.1.0" dependencies = [ + "base64", "miette", "polymodel-renderer-ledger", "serde", "sha2", "thiserror", - "typed-arena", ] [[package]] @@ -250,12 +256,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - [[package]] name = "typenum" version = "1.20.1" diff --git a/tools/ldraw-compat-harness/src/main.rs b/tools/ldraw-compat-harness/src/main.rs index 9205312..fbed69c 100644 --- a/tools/ldraw-compat-harness/src/main.rs +++ b/tools/ldraw-compat-harness/src/main.rs @@ -1,10 +1,10 @@ //! Native PM-77 corpus validator and D-phase LDParse oracle harness. use polymodel_ldraw_core::{ - InProcessRustAdapter, ParserProfile as CoreProfile, + InProcessRustAdapter, LdrawError, ParserProfile as CoreProfile, }; use polymodel_ldraw_testkit::{ - coverage_report, inventory, named_gates, validate_inventory, CanonicalRecord, FixtureCase, - CorpusGateSet, Diagnostic, OracleDiagnostic, OracleError, OracleModel, OracleProjection, + coverage_report, inventory, named_gates, validate_inventory, CanonicalRecord, ExpectedOutcome, + FixtureCase, CorpusGateSet, OracleDiagnostic, OracleError, OracleModel, OracleProjection, Profile, SCHEMA_VERSION, ORACLE_SCHEMA_VERSION, GATE_NAMES, LDRAW_LIMITS, }; use polymodel_renderer_ledger::{ReservationLedger, ReservationOwner}; @@ -13,9 +13,8 @@ use sha2::{Digest, Sha256}; use std::{ env, fs, - io, path::{Path, PathBuf}, - process::{Child, Command, ExitCode, Output, Stdio}, + process::{Command, ExitCode, Output, Stdio}, thread, time::{Duration, Instant}, }; @@ -62,9 +61,8 @@ fn main() -> ExitCode { "corpus" => report("corpus"), "fuzz" => fuzz(), "differential" => differential(), - "regenerate" => regenerate(), other => Err(format!( - "unknown command {other}; use validate, inventory, corpus, fuzz, differential, or regenerate" + "unknown command {other}; use validate, inventory, corpus, fuzz, or differential" )), }; match result { @@ -90,10 +88,20 @@ fn root_dir() -> PathBuf { fn report(command: &'static str) -> Result { let fixtures = inventory(); validate_inventory(&fixtures)?; - CorpusGateSet::new(named_gates()) - .map_err(|error| error.to_string())? + let gates = CorpusGateSet::new(named_gates()).map_err(|error| error.to_string())?; + gates .validate_inventory(&fixtures) .map_err(|error| error.to_string())?; + for fixture in &fixtures { + if fixture.expected.outcome == ExpectedOutcome::Rejected { + validate_rejected(fixture)?; + continue; + } + let actual = actual_record(fixture)?; + for outcome in gates.evaluate(fixture, &actual) { + outcome.map_err(|error| error.to_string())?; + } + } let coverage = coverage_report(&fixtures); serde_json::to_string_pretty(&Report { schema_version: SCHEMA_VERSION, @@ -127,11 +135,10 @@ fn validate() -> Result { if encoded != second { return Err(format!("non-deterministic expected serialization: {}", fixture.id)); } - if fixture.expected.oracle_projection.models.is_empty() - && fixture.expected.oracle_projection.error.is_none() - && fixture.expected.oracle_projection.status != "ok" - { - return Err(format!("missing authored oracle projection: {}", fixture.id)); + if let Some(projection) = &fixture.expected.oracle_projection { + if projection.status.is_empty() { + return Err(format!("empty authored oracle projection status: {}", fixture.id)); + } } } Ok(serde_json::json!({ @@ -268,7 +275,7 @@ fn core_profile(profile: Profile) -> CoreProfile { } } -fn actual_record(fixture: &FixtureCase) -> Result { +fn actual_record(fixture: &FixtureCase) -> Result, String> { let ledger = ReservationLedger::new(); let parsed = InProcessRustAdapter::parse_fixture( fixture.id, @@ -278,11 +285,42 @@ fn actual_record(fixture: &FixtureCase) -> Result { &ledger, ) .map_err(|error| format!("core parse failed for {}: {error}", fixture.id))?; - let record = parsed.into_project(); + let record = parsed.project(); if ledger.snapshot().reservations == 0 { return Err(format!("core reservation was released before projection: {}", fixture.id)); } - Ok(record) + let _ = record; + Ok(parsed.into_owned_project()) +} + +fn validate_rejected(fixture: &FixtureCase) -> Result<(), String> { + let ledger = ReservationLedger::new(); + let error = InProcessRustAdapter::parse_fixture( + fixture.id, + fixture.bytes, + core_profile(fixture.expected.profile), + ReservationOwner::preview(1, 1, 1), + &ledger, + ) + .err() + .ok_or_else(|| format!("{} expected a rejected core parse", fixture.id))?; + let actual = match error { + LdrawError::Diagnostic(diagnostic) => diagnostic, + other => return Err(format!("{} rejected with unexpected error: {other}", fixture.id)), + }; + let expected = fixture + .expected + .canonical + .diagnostics + .first() + .ok_or_else(|| format!("{} has no authored rejection diagnostic", fixture.id))?; + if actual != *expected { + return Err(format!( + "{} rejection diagnostic mismatch: actual {actual:?}, expected {expected:?}", + fixture.id + )); + } + Ok(()) } fn differential() -> Result { @@ -294,12 +332,14 @@ fn differential() -> Result { let path = root.join("crates/polymodel-ldraw-testkit").join(fixture.relative_path); let actual = run_oracle(&binary, &path, ORACLE_TIMEOUT)?; let actual_projection = projection(actual); - polymodel_ldraw_testkit::compare_oracle_projection( - &fixture.expected.oracle_projection, - &actual_projection, - fixture.id, - ) - .map_err(|error| format!("{} (source: {}; oracle revision: {}; schema: {})", error, fixture.provenance.id, LD_PARSE_REVISION, ORACLE_SCHEMA_VERSION))?; + if let Some(expected_projection) = &fixture.expected.oracle_projection { + polymodel_ldraw_testkit::compare_oracle_projection( + expected_projection, + &actual_projection, + fixture.id, + ) + .map_err(|error| format!("{} (source: {}; oracle revision: {}; schema: {})", error, fixture.provenance.id, LD_PARSE_REVISION, ORACLE_SCHEMA_VERSION))?; + } } serde_json::to_string_pretty(&Report { schema_version: SCHEMA_VERSION, @@ -405,15 +445,6 @@ fn fuzz() -> Result { .map_err(|error| error.to_string()) } -trait HexDigest { - fn hex(&self) -> String; -} -impl HexDigest for [u8; 32] { - fn hex(&self) -> String { - self.iter().map(|byte| format!("{byte:02x}")).collect() - } -} - mod hex { pub fn encode(bytes: impl AsRef<[u8]>) -> String { bytes.as_ref().iter().map(|byte| format!("{byte:02x}")).collect()