diff --git a/crates/polymodel-ldraw-core/src/adapter.rs b/crates/polymodel-ldraw-core/src/adapter.rs index 73f9ca6..66a542c 100644 --- a/crates/polymodel-ldraw-core/src/adapter.rs +++ b/crates/polymodel-ldraw-core/src/adapter.rs @@ -20,7 +20,7 @@ impl InProcessRustAdapter { options.profile = request.profile; options.semantic_budget = request.semantic_budget; options.root_name = request.root_name.into(); - let mut result = LdrawParser.parse_bytes(request.bytes, options)?; + let mut result = LdrawParser.parse_bytes(request.bytes, options, None)?; result.provenance_id = Cow::Owned(request.provenance_id.to_owned()); let _ = request.fixture_id; Ok(result) diff --git a/crates/polymodel-ldraw-core/src/lib.rs b/crates/polymodel-ldraw-core/src/lib.rs index cb0669b..46513b0 100644 --- a/crates/polymodel-ldraw-core/src/lib.rs +++ b/crates/polymodel-ldraw-core/src/lib.rs @@ -64,6 +64,7 @@ mod tests { .parse_bytes( b"3 16 0 0 0 1 0 0 0 1 0\n", ParseOptions::new(owner, &ledger), + None, ) .unwrap(); assert!(ledger.snapshot().reservations > 0); @@ -82,12 +83,12 @@ mod tests { .expect("BFC fixture NOCERTIFY line"); let certified = LdrawParser - .parse_bytes(&bytes[..nocertify_start], options(&ledger)) + .parse_bytes(&bytes[..nocertify_start], options(&ledger), None) .expect("CERTIFY should certify the model before NOCERTIFY"); assert_eq!(certified.semantic.bfc_state, BfcState::Certified); let result = LdrawParser - .parse_bytes(bytes, options(&ledger)) + .parse_bytes(bytes, options(&ledger), None) .expect("complete BFC fixture should parse"); assert_eq!(result.semantic.bfc_state, BfcState::Uncertified); assert_eq!(result.scene.triangles, 2); @@ -100,7 +101,7 @@ mod tests { let bytes = b"0 FILE root.ldr\n1 16 0 0 0 1 0 0 0 1 0 0 0 0 child.dat\n0 NOFILE\n0 FILE child.dat\n3 16 0 0 0 1 0 0 0 1 0\n0 NOFILE\n"; let first_ledger = ReservationLedger::new(); let first = LdrawParser - .parse_bytes(bytes, options(&first_ledger)) + .parse_bytes(bytes, options(&first_ledger), None) .unwrap(); assert_eq!(first.scene.model_id, "root.ldr"); assert_eq!(first.scene.instance_ids, vec!["root.ldr→child.dat#1"]); @@ -115,7 +116,7 @@ mod tests { let second_ledger = ReservationLedger::new(); let second = LdrawParser - .parse_bytes(bytes, options(&second_ledger)) + .parse_bytes(bytes, options(&second_ledger), None) .unwrap(); assert_eq!(second.scene.model_id, first.scene.model_id); assert_eq!(second.scene.instance_ids, first.scene.instance_ids); @@ -125,7 +126,7 @@ mod tests { let bytes = b"0 FILE root.ldr\n1 16 0 0 0 1 0 0 0 1 0 0 0 0 child.dat\n1 16 0 0 0 1 0 0 0 1 0 0 0 0 child.dat\n0 NOFILE\n0 FILE child.dat\n1 16 0 0 0 1 0 0 0 1 0 0 0 0 grand.dat\n0 NOFILE\n0 FILE grand.dat\n3 16 0 0 0 1 0 0 0 1 0\n0 NOFILE\n"; let ledger = ReservationLedger::new(); let result = LdrawParser - .parse_bytes(bytes, options(&ledger)) + .parse_bytes(bytes, options(&ledger), None) .expect("repeated child instances should parse"); assert_eq!(result.scene.triangles, 2); assert_eq!(result.scene.instance_ids.len(), 4); @@ -136,7 +137,7 @@ mod tests { let ledger = ReservationLedger::new(); let mut o = options(&ledger); o.limits.triangles = 0; - let error = match LdrawParser.parse_bytes(b"3 16 0 0 0 1 0 0 0 1 0\n", o) { + let error = match LdrawParser.parse_bytes(b"3 16 0 0 0 1 0 0 0 1 0\n", o, None) { Ok(_) => panic!("triangle limit should reject before publication"), Err(error) => error, }; @@ -314,7 +315,7 @@ mod tests { let fixture = corpus_fixture("bfc-directives"); let ledger = ReservationLedger::new(); let result = LdrawParser - .parse_bytes(fixture.bytes, options(&ledger)) + .parse_bytes(fixture.bytes, options(&ledger), None) .expect("BFC directives fixture should parse"); assert_scene_counts(&result, 2, 0, 0, 0); @@ -374,7 +375,7 @@ mod tests { let bytes = fixture.bytes; let ledger = ReservationLedger::new(); let result = LdrawParser - .parse_bytes(bytes, options(&ledger)) + .parse_bytes(bytes, options(&ledger), None) .expect("TEXMAP fixture should parse"); assert_eq!(result.semantic.texmap_events.len(), 4); assert_eq!( @@ -436,7 +437,7 @@ mod tests { let bytes = fixture.bytes; let ledger = ReservationLedger::new(); let result = LdrawParser - .parse_bytes(bytes, options(&ledger)) + .parse_bytes(bytes, options(&ledger), None) .expect("all-types fixture should parse"); assert_eq!(result.syntax.len(), 6); assert_eq!( @@ -509,7 +510,7 @@ mod tests { let bytes = fixture.bytes; let ledger = ReservationLedger::new(); let result = LdrawParser - .parse_bytes(bytes, options(&ledger)) + .parse_bytes(bytes, options(&ledger), None) .expect("colour definitions fixture should parse"); let bright_red = result.colours.get(300).expect("colour code 300"); diff --git a/crates/polymodel-ldraw-core/src/model.rs b/crates/polymodel-ldraw-core/src/model.rs index 750bd01..7c3d11a 100644 --- a/crates/polymodel-ldraw-core/src/model.rs +++ b/crates/polymodel-ldraw-core/src/model.rs @@ -101,6 +101,32 @@ impl<'src> ColourTable<'src> { self.local_slots.insert(Cow::Borrowed(name), slot); Some(slot) } + + pub(crate) fn into_owned(self) -> ColourTable<'static> { + ColourTable { + entries: self + .entries + .into_iter() + .map(|(code, colour)| { + ( + code, + Colour { + code: colour.code, + name: colour.name.map(|name| Cow::Owned(name.into_owned())), + data: colour.data, + edge: colour.edge, + }, + ) + }) + .collect(), + local_slots: self + .local_slots + .into_iter() + .map(|(name, slot)| (Cow::Owned(name.into_owned()), slot)) + .collect(), + next_local: self.next_local, + } + } } pub(crate) fn parse_colour<'src>(tokens: &[Token<'src>]) -> Option> { diff --git a/crates/polymodel-ldraw-core/src/parser.rs b/crates/polymodel-ldraw-core/src/parser.rs index d3582cb..19757db 100644 --- a/crates/polymodel-ldraw-core/src/parser.rs +++ b/crates/polymodel-ldraw-core/src/parser.rs @@ -106,13 +106,11 @@ impl LdrawParser { .map(|include| include.name.to_string()) .collect::>(); for include_name in include_names { - let normalized_name = include_name.replace('\\\\', "/"); - if models.iter().any(|model| { - model - .path - .as_str() - .eq_ignore_ascii_case(&normalized_name) - }) || !visited_names.insert(normalized_name.to_ascii_lowercase()) + let normalized_name = include_name.replace('\\', "/"); + if models + .iter() + .any(|model| model.path.as_str().eq_ignore_ascii_case(&normalized_name)) + || !visited_names.insert(normalized_name.to_ascii_lowercase()) { continue; } @@ -172,14 +170,7 @@ impl LdrawParser { model_index += 1; } } - let (scene, summaries) = traverse( - &mut models, - &options, - &mut counters, - &mut diagnostics, - resolver, - &arena, - )?; + let (scene, summaries) = traverse(&models, &options, &mut counters, &mut diagnostics)?; let root = models.first(); let semantic = SemanticRecord { canonical_path: root.map(|m| Cow::Owned(m.path.to_string())), @@ -197,7 +188,15 @@ impl LdrawParser { .iter() .map(|name| Cow::Borrowed(*name)) .collect(), - texmap_events: root.map(|m| m.texmap_events.clone()).unwrap_or_default(), + texmap_events: root + .map(|m| { + m.texmap_events + .clone() + .into_iter() + .map(|event| event.into_owned()) + .collect() + }) + .unwrap_or_default(), }; Ok(ParseResult { syntax, @@ -207,13 +206,145 @@ impl LdrawParser { counters, files: files.into_iter().map(OwnedVirtualFile::from).collect(), models: summaries, - colours, + colours: colours.into_owned(), arenas, provenance_id: Cow::Borrowed(DEFAULT_PROVENANCE), }) } } +fn parse_model<'src>( + file: &VirtualFile<'src>, + options: &ParseOptions<'_>, + counters: &mut LimitCounters, + diagnostics: &mut Vec, + arenas: &mut SemanticArenas, + colours: &mut ColourTable<'src>, + texture_ids: &mut BTreeSet>, +) -> Result, ParseError> { + let colours_before = colours.local_len(); + if options.cancellation.cancelled { + return Err(ParseError::Cancelled); + } + let path = NormalizedPath::new(&file.name)?; + let file_bytes = file + .lines + .iter() + .flat_map(|line| { + line.raw + .iter() + .copied() + .chain(line.ending.as_str().as_bytes().iter().copied()) + }) + .collect::>(); + let key = CacheKey::new(path.clone(), options.resolved_root, &file_bytes); + let mut model = ModelData { + file_id: file.id, + path, + key, + bfc: BfcFrame::default(), + colour: DEFAULT_COLOUR, + colours: colours.clone(), + steps: Vec::new(), + texmap_state: TexmapState::Inactive, + texmap_descriptor: None, + texmap_stack: Vec::new(), + texmap_next_span: None, + texmap_fallback_seen: false, + texmap_events: Vec::new(), + data_payload: false, + includes: Vec::new(), + triangles: 0, + quads: 0, + lines: 0, + conditional_lines: 0, + geometry: Vec::new(), + bounds: Bounds3::default(), + reflection: false, + }; + for line in &file.lines { + if line.blank { + continue; + } + if options.cancellation.cancelled { + return Err(ParseError::Cancelled); + } + if counters + .add(LimitKind::Commands, 1, &options.limits) + .is_err() + { + return Err(limit_error(LimitKind::Commands, Some(line.span))); + } + let before = diagnostics.len(); + let result = process_line( + &mut model, + line, + options.profile, + diagnostics, + counters, + &options.limits, + colours, + texture_ids, + ); + if let Err(error) = result { + let continue_diagnostic = match &error { + ParseError::Diagnostic(diagnostic) => { + options.profile != ParserProfile::Strict + && !matches!( + diagnostic.code, + DiagnosticCode::LineBytesLimit + | DiagnosticCode::FilesLimit + | DiagnosticCode::SemanticArenaReservationLimit + | DiagnosticCode::IncludeDepthLimit + | DiagnosticCode::CommandsLimit + | DiagnosticCode::InstancesLimit + | DiagnosticCode::TrianglesLimit + | DiagnosticCode::LinesLimit + | DiagnosticCode::TexturesLimit + | DiagnosticCode::FetchesLimit + | DiagnosticCode::DiagnosticsLimit + | DiagnosticCode::ResourceBytesLimit + ) + } + _ => false, + }; + if !continue_diagnostic { + return Err(error); + } + } + let diagnostic_count = u64::try_from(diagnostics.len()) + .map_err(|_| ParseError::Overflow("diagnostic count conversion"))?; + if diagnostics.len() > before && diagnostic_count > options.limits.diagnostics { + return Err(limit_error(LimitKind::Diagnostics, Some(line.span))); + } + } + let include_count = u64::try_from(model.includes.len()) + .map_err(|_| ParseError::Overflow("include count conversion"))?; + let colour_entries = colours + .local_len() + .checked_sub(colours_before) + .ok_or(ParseError::Overflow("colour entry delta"))?; + let colour_entries = u64::try_from(colour_entries) + .map_err(|_| ParseError::Overflow("colour entry conversion"))?; + let graph_entries = include_count + .checked_add(1) + .ok_or(ParseError::Overflow("graph entry estimate"))?; + let texture_events = u64::try_from(model.texmap_events.len()) + .map_err(|_| ParseError::Overflow("TEXMAP event conversion"))?; + let child_charge = 256u64 + .checked_add( + include_count + .checked_mul(128) + .ok_or(ParseError::Overflow("instance arena estimate"))?, + ) + .and_then(|charge| charge.checked_add(graph_entries.checked_mul(64).unwrap_or(u64::MAX))) + .and_then(|charge| charge.checked_add(colour_entries.checked_mul(32).unwrap_or(u64::MAX))) + .and_then(|charge| charge.checked_add(texture_events.checked_mul(48).unwrap_or(u64::MAX))) + .ok_or(ParseError::Overflow("model arena estimate"))?; + arenas.child(options, child_charge)?; + Ok(model) +} + 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"); diff --git a/crates/polymodel-ldraw-core/src/texmap.rs b/crates/polymodel-ldraw-core/src/texmap.rs index 01b5b0c..8bab450 100644 --- a/crates/polymodel-ldraw-core/src/texmap.rs +++ b/crates/polymodel-ldraw-core/src/texmap.rs @@ -57,9 +57,7 @@ impl<'src> TextureDescriptor<'src> { .map(|value| Cow::Owned(value.into_owned())) .collect(), pngfile: Cow::Owned(self.pngfile.into_owned()), - glossmap: self - .glossmap - .map(|value| Cow::Owned(value.into_owned())), + glossmap: self.glossmap.map(|value| Cow::Owned(value.into_owned())), } } } @@ -83,6 +81,23 @@ pub struct TexmapEvent<'src> { pub reference: Option>, pub state: TexmapState, } +impl<'src> TexmapEvent<'src> { + pub(crate) fn into_owned(self) -> TexmapEvent<'static> { + TexmapEvent { + kind: self.kind, + span: self.span, + file_id: self.file_id, + mode: self.mode, + parameters: self + .parameters + .into_iter() + .map(|value| Cow::Owned(value.into_owned())) + .collect(), + reference: self.reference.map(|value| Cow::Owned(value.into_owned())), + state: self.state, + } + } +} use crate::model::ModelData; use crate::scanner::{ScannedLine, TokenKind};