diff --git a/src/parser.rs b/src/parser.rs index cb4e9bb..32ae058 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -263,6 +263,30 @@ fn parse_line( _ => Err(ContentError::UnknownCommand {}), }, ]), + '5' => { + // G54 is the deprecated aperture-select prefix (gerber spec 8.1.1). + // The combined `G54Dnn*` form is still emitted by gerbv on save. + match linechars.next().ok_or(ContentError::UnknownCommand {})? { + '4' => { + let select = Ok(FunctionCode::GCode(GCode::SelectAperture).into()); + match linechars.next() { + None | Some('*') => Ok(vec![select]), + Some('D') => { + linechars.next_back(); + Ok(vec![ + select, + parse_aperture_selection_or_command( + line, + linechars.clone(), + ), + ]) + } + Some(_) => Ok(vec![Err(ContentError::UnknownCommand {})]), + } + } + _ => Ok(vec![Err(ContentError::UnknownCommand {})]), + } + } _ => Ok(vec![Err(ContentError::UnknownCommand {})]), } } @@ -1817,6 +1841,8 @@ fn parse_file_attribute(line: Chars) -> Result { ("Soldermask", args, len) if len <= 2 => { with_side_and_optional_index!(SolderMask, args) } + // `Keepout` is the serialized name per gerber-types; spec 11.15 (Rev 2017.11) prefers `Profile` now. + ("Keepout", args, 1) => with_side!(KeepOut, args), ("Legend", args, len) if len <= 2 => with_side_and_optional_index!(Legend, args), ("Component", args, 2) => with_layer_and_side!(Component, args), ("Paste", args, 1) => with_side!(Paste, args), diff --git a/tests/component_tests.rs b/tests/component_tests.rs index 78de3c4..78b877f 100644 --- a/tests/component_tests.rs +++ b/tests/component_tests.rs @@ -17,7 +17,7 @@ use std::collections::HashMap; use strum::VariantArray; mod util; use gerber_parser::util::{ - coordinates_from_gerber, gerber_to_reader, partial_coordinates_from_gerber, + coordinates_from_gerber, gerber_doc_as_str, gerber_to_reader, partial_coordinates_from_gerber, partial_coordinates_offset_from_gerber, }; use util::testing::logging_init; @@ -319,6 +319,65 @@ fn aperture_selection() { ) } +/// Test deprecated `G54` aperture selection (gerber spec 8.1.1). +/// Accepts both `G54*` (standalone) and the combined `G54Dnn*` form (emitted by gerbv on save). +#[test] +fn deprecated_g54_aperture_selection() { + // given + logging_init(); + + let reader = gerber_to_reader( + " + %FSLAX23Y23*% + %MOMM*% + + %ADD10C, 0.01*% + %ADD11R, 0.01X0.15*% + + G04 Deprecated combined G54Dnn form* + G54D10* + G04 Deprecated standalone G54 form* + G54* + G04 Deprecated combined G54Dnn form again* + G54D11* + + M02* + ", + ); + + // when + parse_and_filter!(reader, commands, filtered_commands, |cmd| matches!( + cmd, + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::SelectAperture(_) + ))) | Ok(Command::FunctionCode(FunctionCode::GCode( + GCode::SelectAperture + ))) + )); + + // then + assert_eq!( + filtered_commands, + vec![ + Ok(Command::FunctionCode(FunctionCode::GCode( + GCode::SelectAperture + ))), + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::SelectAperture(10) + ))), + Ok(Command::FunctionCode(FunctionCode::GCode( + GCode::SelectAperture + ))), + Ok(Command::FunctionCode(FunctionCode::GCode( + GCode::SelectAperture + ))), + Ok(Command::FunctionCode(FunctionCode::DCode( + DCode::SelectAperture(11) + ))), + ] + ) +} + /// Test the D01* statements (linear) #[test] #[allow(non_snake_case)] @@ -1966,6 +2025,84 @@ fn TF_file_attributes() { assert_eq!(filtered_commands, expected_commands,) } +/// Guards against silent drift between the parser and the gerber-types serializer +/// for every `FileAttribute` variant. Each TF line is written in the exact form the +/// serializer produces, so any divergence surfaces as a textual diff. +#[test] +#[allow(non_snake_case)] +fn TF_file_attributes_serialize_roundtrip() { + logging_init(); + + const GERBER: &str = "%FSLAX23Y23*% +%MOMM*% +%TF.Part,Single*% +%TF.Part,Array*% +%TF.Part,FabricationPanel*% +%TF.Part,Coupon*% +%TF.Part,Other,Value 1*% +%TF.FileFunction,Copper,L1,Top*% +%TF.FileFunction,Copper,L2,Inr,Plane*% +%TF.FileFunction,Copper,L3,Inr,Signal*% +%TF.FileFunction,Copper,L4,Bot,Mixed*% +%TF.FileFunction,Copper,L5,Bot,Hatched*% +%TF.FileFunction,Plated,1,2,PTH*% +%TF.FileFunction,Plated,1,6,Blind,Drill*% +%TF.FileFunction,Plated,3,4,Buried,Rout*% +%TF.FileFunction,NonPlated,1,2,NPTH*% +%TF.FileFunction,NonPlated,1,6,Blind,Mixed*% +%TF.FileFunction,NonPlated,3,4,Buried,Drill*% +%TF.FileFunction,Profile*% +%TF.FileFunction,Profile,P*% +%TF.FileFunction,Profile,NP*% +%TF.FileFunction,Keepout,Top*% +%TF.FileFunction,Keepout,Bot*% +%TF.FileFunction,Soldermask,Top*% +%TF.FileFunction,Soldermask,Bot,1*% +%TF.FileFunction,Legend,Top*% +%TF.FileFunction,Legend,Bot,2*% +%TF.FileFunction,Component,L1,Top*% +%TF.FileFunction,Paste,Top*% +%TF.FileFunction,Paste,Bot*% +%TF.FileFunction,Glue,Top*% +%TF.FileFunction,Carbonmask,Top*% +%TF.FileFunction,Goldmask,Top,1*% +%TF.FileFunction,Heatsinkmask,Bot*% +%TF.FileFunction,Peelablemask,Top*% +%TF.FileFunction,Silvermask,Bot,2*% +%TF.FileFunction,Tinmask,Top*% +%TF.FileFunction,Depthrout,Top*% +%TF.FileFunction,Vcut*% +%TF.FileFunction,Vcut,Top*% +%TF.FileFunction,Viafill*% +%TF.FileFunction,Pads,Top*% +%TF.FileFunction,Other,Value 1*% +%TF.FileFunction,Drillmap*% +%TF.FileFunction,FabricationDrawing*% +%TF.FileFunction,Vcutmap*% +%TF.FileFunction,AssemblyDrawing,Top*% +%TF.FileFunction,AssemblyDrawing,Bot*% +%TF.FileFunction,ArrayDrawing*% +%TF.FileFunction,OtherDrawing,Value 1*% +%TF.FilePolarity,Positive*% +%TF.FilePolarity,Negative*% +%TF.SameCoordinates*% +%TF.SameCoordinates,ident*% +%TF.SameCoordinates,ffffffff-ffff-ffff-ffff-ffffffffffff*% +%TF.CreationDate,2015-02-23T15:59:51+01:00*% +%TF.GenerationSoftware,MakerPnP,gerber-types*% +%TF.GenerationSoftware,MakerPnP,gerber-types,0.4.0*% +%TF.ProjectId,My PCB,ffffffff-ffff-ffff-ffff-ffffffffffff,2.0*% +%TF.MD5,6ab9e892830469cdff7e3e346331d404*% +%TFNonStandardAttribute,Value 1,Value 2*% +M02* +"; + + let doc = parse(gerber_to_reader(GERBER)).unwrap(); + let errors = doc.errors(); + assert!(errors.is_empty(), "parse produced errors: {:?}", errors); + assert_eq!(gerber_doc_as_str(&doc), GERBER); +} + #[test] #[should_panic] fn conflicting_aperture_codes() {