From 0d82c985958d21a8ce5a1c8c3444b479c22b442a Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:52:28 -0400 Subject: [PATCH 1/2] test(objects): delete the object decoders' inline mirror suites The inline #[cfg(test)] modules of 17 files under src/objects/ build synthetic bit streams in each decoder's own field layout and assert the fields they decode, or round-trip them through a hand-written encoder. They are unit tests, which this repository bans, and no job runs them since the Test job moved to tools/ci/run_test_targets.sh. Git history is the archive. acad_material's doc no longer says AcadMaterial is kept with its tests. The five object files an open pull request touches keep theirs until those land. --- src/objects/acad_detail_view_style.rs | 114 ------- src/objects/acad_group.rs | 91 ------ src/objects/acad_layout.rs | 163 ---------- src/objects/acad_material.rs | 49 +-- src/objects/acad_mleader_style.rs | 138 -------- src/objects/acad_mlinestyle.rs | 82 ----- src/objects/acad_plot_settings.rs | 92 ------ src/objects/acad_property_set_data.rs | 91 ------ src/objects/acad_scale.rs | 89 ------ src/objects/acad_section_view_style.rs | 133 -------- src/objects/acad_visual_style.rs | 426 ------------------------- src/objects/custom_dict_entry.rs | 61 ---- src/objects/dictionary_var.rs | 42 --- src/objects/placeholder.rs | 36 --- src/objects/proxy_entity.rs | 66 ---- src/objects/proxy_object.rs | 64 ---- src/objects/xdata.rs | 96 ------ 17 files changed, 1 insertion(+), 1832 deletions(-) diff --git a/src/objects/acad_detail_view_style.rs b/src/objects/acad_detail_view_style.rs index 8df5401d..88510309 100644 --- a/src/objects/acad_detail_view_style.rs +++ b/src/objects/acad_detail_view_style.rs @@ -242,117 +242,3 @@ pub fn decode_object( trailing_flags, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - const LABEL: &str = "%<\\AcVar ViewDetailId>%"; - - fn cmc(w: &mut BitWriter) { - w.write_bs_u(0); - w.write_bl_u(0xC000_0000); - w.write_rc(0); - } - - /// Write the `Metric50` field list; `inline_strings` selects the - /// pre-R2007 layout, where the three `TV` slots consume data bits. - fn write_body(w: &mut BitWriter, version: Version, inline_strings: bool) { - let r2018 = matches!(version, Version::R2018); - w.write_bs(0); - if inline_strings { - modern::tests::write_inline_tv(w, "Metric50"); - } - if r2018 { - w.write_b(false); - w.write_b(true); - } - w.write_bs(32); - w.write_b(false); - w.write_b(true); - w.write_b(true); - cmc(w); - w.write_bd(5.0); - if inline_strings { - modern::tests::write_inline_tv(w, ""); - } - w.write_bd(0.36); - w.write_rc(1); - cmc(w); - w.write_bd(5.0); - w.write_bs(25); - cmc(w); - cmc(w); - w.write_bd(5.0); - w.write_bs(0); - w.write_bd(15.0); - w.write_bs(1); - if inline_strings { - modern::tests::write_inline_tv(w, LABEL); - } - w.write_bs(25); - cmc(w); - w.write_bs(25); - cmc(w); - w.write_rc(0); - } - - fn build(version: Version) -> Vec { - let mut w = modern::tests::r2018_object_prefix(1); - write_body(&mut w, version, false); - let bits = crate::string_stream::tests::bits_of(&w); - let strings: Vec<&str> = if matches!(version, Version::R2018) { - vec!["Metric50", "Metric50", "", LABEL] - } else { - vec!["Metric50", "", LABEL] - }; - crate::string_stream::tests::build_payload(&bits, &strings) - } - - #[test] - fn r2018_detail_view_style_closes_on_its_string_stream() { - let payload = build(Version::R2018); - let s = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(s.name, "Metric50"); - assert_eq!(s.name_alias, "Metric50"); - assert_eq!(s.flags, 32); - assert_eq!(s.identifier_height, 5.0); - assert_eq!(s.arrow_symbol_size, 5.0); - assert_eq!(s.view_label_text_height, 5.0); - assert_eq!(s.view_label_offset, 15.0); - assert_eq!(s.boundary_line_weight, 25); - assert_eq!(s.connection_line_weight, 25); - assert_eq!(s.model_edge_line_weight, 25); - assert_eq!(s.identifier_color.method(), 0xC0); - assert_eq!(s.model_edge_color.method(), 0xC0); - assert_eq!(s.view_label_pattern, LABEL); - assert_eq!(s.trailing_flags, 0); - } - - #[test] - fn r2013_detail_view_style_has_no_r2018_head() { - let payload = build(Version::R2013); - let s = decode_object(&payload, 8, None, Version::R2013).unwrap(); - assert_eq!(s.name, "Metric50"); - assert_eq!(s.name_alias, ""); - assert!(!s.unknown_flag_a && !s.unknown_flag_b); - assert_eq!(s.flags, 32); - // The two extra R2018 head bits are not optional. - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } - - /// The R2004 layout: three inline `TV` fields, closing on the `RL` - /// object-data-size boundary. - #[test] - fn r2004_detail_view_style_reads_its_strings_inline() { - let mut w = modern::tests::r2004_object_prefix(1); - write_body(&mut w, Version::R2004, true); - let end = w.position_bits(); - let bytes = w.into_bytes(); - let s = decode_object(&bytes, 0, Some(end), Version::R2004).unwrap(); - assert_eq!(s.name, "Metric50"); - assert_eq!(s.view_label_pattern, LABEL); - assert_eq!(s.identifier_height, 5.0); - } -} diff --git a/src/objects/acad_group.rs b/src/objects/acad_group.rs index 2c351b7b..a7b60ba7 100644 --- a/src/objects/acad_group.rs +++ b/src/objects/acad_group.rs @@ -116,94 +116,3 @@ pub(crate) fn decode_object( split.finish("ACAD_GROUP")?; Ok(out) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - fn encode_tv_r2000(w: &mut BitWriter, s: &[u8]) { - w.write_bs_u(s.len() as u16); - for b in s { - w.write_rc(*b); - } - } - - #[test] - fn roundtrip_empty_group() { - let mut w = BitWriter::new(); - encode_tv_r2000(&mut w, b"MyGroup"); - w.write_bs(0); // named - w.write_bs(1); // selectable - w.write_bl(0); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let g = decode(&mut c, Version::R2000).unwrap(); - assert_eq!(g.name, "MyGroup"); - assert!(!g.is_unnamed()); - assert!(g.is_selectable()); - assert_eq!(g.num_members, 0); - } - - #[test] - fn roundtrip_populated_group() { - let mut w = BitWriter::new(); - encode_tv_r2000(&mut w, b"*A1"); - w.write_bs(2); // unnamed (AutoCAD-generated) - w.write_bs(0); // not selectable as a whole - w.write_bl(2); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let g = decode(&mut c, Version::R2000).unwrap(); - assert_eq!(g.name, "*A1"); - assert!(g.is_unnamed()); - assert!(!g.is_selectable()); - assert_eq!(g.num_members, 2); - } - - /// The exact field widths of `sample_AC1032.dwg`'s two GROUP - /// records: three 10-bit fields reading 2, 1 and 6. - #[test] - fn r2018_split_stream_group_matches_the_measured_widths() { - let mut body = modern::tests::r2018_object_prefix(1); - body.write_bs(2); // unnamed - body.write_bs(1); // selectable - body.write_bl(6); // member count - let bits = crate::string_stream::tests::bits_of(&body); - assert_eq!(bits.len(), 14 + 30, "measured budget is 30 bits of body"); - let payload = crate::string_stream::tests::build_payload(&bits, &[""]); - let g = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(g.name, ""); - assert_eq!(g.unnamed, 2); - assert_eq!(g.selectable, 1); - assert_eq!(g.num_members, 6); - } - - /// Two single-bit flags cannot fill the measured budget — the - /// pre-fix field list must now be rejected, not silently accepted. - #[test] - fn r2018_split_stream_group_rejects_the_old_boolean_field_list() { - let mut body = modern::tests::r2018_object_prefix(1); - body.write_b(true); - body.write_b(true); - body.write_bl(6); - let bits = crate::string_stream::tests::bits_of(&body); - let payload = crate::string_stream::tests::build_payload(&bits, &[""]); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } - - #[test] - fn rejects_excessive_handle_count() { - // Synthesize a stream where num_handles claims a count past the cap. - let mut w = BitWriter::new(); - encode_tv_r2000(&mut w, b"G"); - w.write_bs(0); - w.write_bs(1); - // BL 0b00 tag then 32-bit LE = 200_001 - w.write_bl((MAX_GROUP_HANDLES + 1) as i32); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let err = decode(&mut c, Version::R2000).unwrap_err(); - assert!(matches!(&err, Error::SectionMap(msg) if msg.contains("ACAD_GROUP"))); - } -} diff --git a/src/objects/acad_layout.rs b/src/objects/acad_layout.rs index 7fa90ed9..fcb5892e 100644 --- a/src/objects/acad_layout.rs +++ b/src/objects/acad_layout.rs @@ -304,166 +304,3 @@ pub fn decode_object( trailing_strings, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - use crate::objects::acad_plot_settings::tests::write_split_stream_block; - - fn write_bd3(w: &mut BitWriter, p: Point3D) { - w.write_bd(p.x); - w.write_bd(p.y); - w.write_bd(p.z); - } - - fn write_rd2(w: &mut BitWriter, p: Point2D) { - w.write_rd(p.x); - w.write_rd(p.y); - } - - const X_AXIS: Point3D = Point3D { - x: 1.0, - y: 0.0, - z: 0.0, - }; - const Y_AXIS: Point3D = Point3D { - x: 0.0, - y: 1.0, - z: 0.0, - }; - - /// Build the LAYOUT half of the body, with the `TV` layout name - /// left to the string stream. - fn write_layout_block(w: &mut BitWriter, tab_order: i32, limmax: Point2D, viewports: i32) { - w.write_bl(tab_order); - w.write_bs(1); // flags - write_bd3(w, Point3D::default()); // ucs_origin - write_rd2(w, Point2D::default()); // limmin - write_rd2(w, limmax); - write_bd3(w, Point3D::default()); // insertion_point - write_bd3(w, X_AXIS); - write_bd3(w, Y_AXIS); - w.write_bd(0.0); // elevation - w.write_bs(0); // ucs_ortho_view_type - write_bd3(w, Point3D::default()); // extent_min - write_bd3( - w, - Point3D { - x: 3.0, - y: 4.0, - z: 0.0, - }, - ); // extent_max - w.write_bl(viewports); - } - - fn build_r2018(name: &str, tab_order: i32, limmax: Point2D, viewports: i32) -> Vec { - let mut body = modern::tests::r2018_object_prefix(1); - write_split_stream_block(&mut body); - write_layout_block(&mut body, tab_order, limmax, viewports); - let bits = crate::string_stream::tests::bits_of(&body); - crate::string_stream::tests::build_payload( - &bits, - &[ - "", - "none_device", - "ANSI_A_(8.50_x_11.00_Inches)", - "acad.ctb", - name, - ], - ) - } - - #[test] - fn r2018_split_stream_layout_closes_on_its_string_stream() { - let payload = build_r2018("Layout1", 1, Point2D { x: 10.75, y: 8.25 }, 2); - let l = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(l.layout_name, "Layout1"); - assert_eq!(l.tab_order, 1); - assert_eq!(l.flags, 1); - assert_eq!(l.viewport_count, 2); - assert!(!l.is_model_space()); - assert_eq!(l.ucs_x_axis, X_AXIS); - assert_eq!(l.ucs_y_axis, Y_AXIS); - assert!((l.limits_width() - 10.75).abs() < 1e-12); - assert!((l.limits_height() - 8.25).abs() < 1e-12); - assert!((l.extents_diagonal() - 5.0).abs() < 1e-12); - assert!(l.trailing_strings.is_empty()); - // The embedded plot-settings block round-trips too. - assert_eq!(l.plot_settings.printer_config_name, "none_device"); - assert_eq!(l.plot_settings.paper_size, "ANSI_A_(8.50_x_11.00_Inches)"); - assert!((l.plot_settings.paper_width - 215.9).abs() < 1e-12); - assert!((l.plot_settings.paper_height - 279.4).abs() < 1e-12); - assert!((l.plot_settings.margin_left - 6.35).abs() < 1e-12); - assert_eq!(l.plot_settings.shade_plot_custom_dpi, 300); - } - - #[test] - fn model_layout_is_recognised() { - let payload = build_r2018("Model", 0, Point2D { x: 12.0, y: 9.0 }, 0); - let l = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert!(l.is_model_space()); - assert_eq!(l.viewport_count, 0); - assert!((l.limits_width() - 12.0).abs() < 1e-12); - } - - /// `viewport_count` is a `BL`: reading the same body with a 32-bit - /// `RL` there would overrun the string-stream start, so the - /// boundary check must reject a body that carries an `RL`. - #[test] - fn an_rl_sized_viewport_count_does_not_close() { - let mut body = modern::tests::r2018_object_prefix(1); - write_split_stream_block(&mut body); - body.write_bl(1); // tab_order - body.write_bs(1); // flags - write_bd3(&mut body, Point3D::default()); - write_rd2(&mut body, Point2D::default()); - write_rd2(&mut body, Point2D { x: 1.0, y: 1.0 }); - write_bd3(&mut body, Point3D::default()); - write_bd3(&mut body, X_AXIS); - write_bd3(&mut body, Y_AXIS); - body.write_bd(0.0); - body.write_bs(0); - write_bd3(&mut body, Point3D::default()); - write_bd3(&mut body, Point3D::default()); - body.write_rl(2); // an RL viewport count instead of a BL - let bits = crate::string_stream::tests::bits_of(&body); - let payload = crate::string_stream::tests::build_payload( - &bits, - &["", "none_device", "A4", "", "Layout1"], - ); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } - - /// The plot-settings block is not optional: a body that omits it - /// leaves the field list short and the boundary check has to - /// reject it. - #[test] - fn a_body_without_the_plot_settings_block_is_rejected() { - let mut body = modern::tests::r2018_object_prefix(1); - write_layout_block(&mut body, 0, Point2D::default(), 0); - let bits = crate::string_stream::tests::bits_of(&body); - let payload = crate::string_stream::tests::build_payload(&bits, &["", "", "", "", "Model"]); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } - - #[test] - fn uninitialised_extents_report_a_zero_diagonal() { - let l = AcadLayout { - extent_min: Point3D { - x: 1e20, - y: 1e20, - z: 1e20, - }, - extent_max: Point3D { - x: -1e20, - y: -1e20, - z: -1e20, - }, - ..AcadLayout::default() - }; - assert!(l.has_uninitialised_extents()); - assert_eq!(l.extents_diagonal(), 0.0); - } -} diff --git a/src/objects/acad_material.rs b/src/objects/acad_material.rs index 5d33b04d..88195701 100644 --- a/src/objects/acad_material.rs +++ b/src/objects/acad_material.rs @@ -287,7 +287,7 @@ pub(crate) fn decode_object( /// The strings and data-field budget of an ACAD_MATERIAL record, as /// [`read_strings`] measured them before the field list was derived. -/// [`Material`] supersedes it; it is kept with its tests. +/// [`Material`] supersedes it; it stays as public API. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AcadMaterial { /// Material name — `ByLayer`, `ByBlock` and `Global` in every @@ -336,50 +336,3 @@ pub fn read_strings( data_field_bits, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - #[test] - fn r2018_split_stream_material_reads_its_names_from_the_string_stream() { - let mut body = modern::tests::r2018_object_prefix(1); - // Stand-in for the undecoded data fields. - for _ in 0..40 { - body.write_b(false); - } - let bits = crate::string_stream::tests::bits_of(&body); - let payload = - crate::string_stream::tests::build_payload(&bits, &["Global", "", "", "", "", "", ""]); - let material = read_strings(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(material.name, "Global"); - assert_eq!(material.description, ""); - assert_eq!(material.extra_strings.len(), 5); - assert_eq!(material.data_field_bits, Some(40)); - } - - #[test] - fn inline_layout_reads_its_names_from_the_data_cursor() { - let mut w = BitWriter::new(); - w.write_bs_u(0); // EED terminator - w.write_bl(1); // num_reactors - w.write_b(true); // no xdictionary (R2004+) - w.write_bs_u(8); - for b in b"ByLayer\0" { - w.write_rc(*b); - } - w.write_bs_u(0); // empty description - let after_names = w.position_bits(); - for _ in 0..24 { - w.write_b(false); - } - let end = w.position_bits(); - let bytes = w.into_bytes(); - let material = read_strings(&bytes, 0, Some(end), Version::R2004).unwrap(); - assert_eq!(material.name, "ByLayer"); - assert_eq!(material.description, ""); - assert!(material.extra_strings.is_empty()); - assert_eq!(material.data_field_bits, Some(end - after_names)); - } -} diff --git a/src/objects/acad_mleader_style.rs b/src/objects/acad_mleader_style.rs index edeb347f..6f0381c9 100644 --- a/src/objects/acad_mleader_style.rs +++ b/src/objects/acad_mleader_style.rs @@ -315,141 +315,3 @@ pub fn decode_object( extended_flag, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - fn cmc(w: &mut BitWriter, rgb: u32) { - w.write_bs_u(0); - w.write_bl_u(rgb); - w.write_rc(0); - } - - /// The `Standard` multileader style, with the values every corpus - /// record decodes. - fn build(version: Version) -> Vec { - // R2010 predates the R2013+ AcDs binary-data bit of the common - // object prefix, so it takes the shorter prefix. - let mut w = if matches!(version, Version::R2013 | Version::R2018) { - modern::tests::r2018_object_prefix(1) - } else { - modern::tests::r2004_object_prefix(1) - }; - write_common_body(&mut w, version, false); - let bits = crate::string_stream::tests::bits_of(&w); - crate::string_stream::tests::build_payload(&bits, &["Standard", ""]) - } - - /// Write the field list itself. `inline_strings` selects the - /// pre-R2007 layout, where the two `TV` slots consume data bits. - fn write_common_body(w: &mut BitWriter, version: Version, inline_strings: bool) { - if version.is_r2010_plus() { - w.write_bs(2); // version - } - w.write_bs(2); // content_type - w.write_bs(1); // draw_mleader_order_type - w.write_bs(0); // draw_leader_order_type - w.write_bl(2); // max_leader_segment_points - w.write_bd(0.0); - w.write_bd(0.0); - w.write_bs(1); // leader_line_type - cmc(w, 0xC100_0000); - w.write_bl(-2); // leader_lineweight - w.write_b(true); // enable_landing - w.write_bd(0.09); - w.write_b(true); // enable_dogleg - w.write_bd(0.36); - if inline_strings { - modern::tests::write_inline_tv(w, "Standard"); - } - w.write_bd(0.18); // arrowhead_size - if inline_strings { - modern::tests::write_inline_tv(w, ""); - } - w.write_bs(1); - w.write_bs(6); - w.write_bs(1); - w.write_bs(0); - cmc(w, 0xC100_0000); - w.write_bd(0.18); // text_height - w.write_b(false); - w.write_b(false); - w.write_bd(0.18); // align_space - cmc(w, 0xC100_0000); - w.write_bd(1.0); - w.write_bd(1.0); - w.write_bd(1.0); - w.write_b(true); - w.write_bd(0.0); - w.write_b(true); - w.write_bs(0); - w.write_bd(1.0); // scale - w.write_b(false); - w.write_b(true); - w.write_bd(0.125); // break_gap_size - if version.is_r2010_plus() { - w.write_bs(0); - w.write_bs(9); - w.write_bs(9); - } - if matches!(version, Version::R2013 | Version::R2018) { - w.write_b(true); - } - } - - #[test] - fn r2018_mleader_style_closes_on_its_string_stream() { - let payload = build(Version::R2018); - let s = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(s.version, 2); - assert_eq!(s.content_type, 2); - assert_eq!(s.max_leader_segment_points, 2); - assert_eq!(s.leader_line_color.method(), 0xC1); - assert_eq!(s.leader_lineweight, -2); - assert!(s.enable_landing && s.enable_dogleg); - assert!((s.landing_gap - 0.09).abs() < 1e-12); - assert!((s.dogleg_length - 0.36).abs() < 1e-12); - assert!((s.arrowhead_size - 0.18).abs() < 1e-12); - assert!((s.text_height - 0.18).abs() < 1e-12); - assert!((s.break_gap_size - 0.125).abs() < 1e-12); - assert_eq!(s.description, "Standard"); - assert_eq!(s.default_mtext_contents, ""); - assert_eq!(s.block_content_scale_x, 1.0); - assert_eq!(s.top_text_attachment_direction, 9); - assert!(s.extended_flag); - } - - #[test] - fn r2010_mleader_style_has_no_r2013_tail_flag() { - let payload = build(Version::R2010); - let s = decode_object(&payload, 8, None, Version::R2010).unwrap(); - assert_eq!(s.version, 2); - assert!(!s.extended_flag); - assert_eq!(s.bottom_text_attachment_direction, 9); - } - - /// The R2004 layout: `TV` fields inline, no leading `version` word - /// and no attachment-direction tail, closing on the `RL` - /// object-data-size boundary. - #[test] - fn r2004_mleader_style_omits_the_r2010_fields() { - let mut w = modern::tests::r2004_object_prefix(1); - write_common_body(&mut w, Version::R2004, true); - let end = w.position_bits(); - let bytes = w.into_bytes(); - let s = decode_object(&bytes, 0, Some(end), Version::R2004).unwrap(); - assert_eq!(s.version, 0); - assert_eq!(s.text_attachment_direction, 0); - assert_eq!(s.description, "Standard"); - assert_eq!(s.leader_lineweight, -2); - assert!((s.break_gap_size - 0.125).abs() < 1e-12); - } - - #[test] - fn r2013_body_rejected_by_the_r2010_field_list() { - let payload = build(Version::R2018); - assert!(decode_object(&payload, 8, None, Version::R2010).is_err()); - } -} diff --git a/src/objects/acad_mlinestyle.rs b/src/objects/acad_mlinestyle.rs index e0cd1280..33e7b44e 100644 --- a/src/objects/acad_mlinestyle.rs +++ b/src/objects/acad_mlinestyle.rs @@ -153,85 +153,3 @@ fn read_fields(split: &mut modern::ObjectStream<'_>, version: Version) -> Result lines, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - fn cmc(w: &mut BitWriter, index: u16, rgb: u32, color_byte: u8) { - w.write_bs_u(index); - w.write_bl_u(rgb); - w.write_rc(color_byte); - } - - /// The `STANDARD` style every corpus file carries: two elements at - /// ±0.5, both ByLayer, both with the `32767` linetype sentinel. - fn build(version: Version) -> Vec { - let mut body = modern::tests::r2018_object_prefix(1); - body.write_bs(0); // flags - cmc(&mut body, 0, 0xC000_0000, 0); // fill colour, ByLayer - body.write_bd(std::f64::consts::FRAC_PI_2); - body.write_bd(std::f64::consts::FRAC_PI_2); - body.write_rc(2); // linesinstyle - for offset in [0.5, -0.5] { - body.write_bd(offset); - cmc(&mut body, 0, 0xC000_0000, 0); - if !matches!(version, Version::R2018) { - body.write_bs(32767); - } - } - let bits = crate::string_stream::tests::bits_of(&body); - crate::string_stream::tests::build_payload(&bits, &["STANDARD", ""]) - } - - #[test] - fn r2018_mlinestyle_closes_on_its_string_stream() { - let payload = build(Version::R2018); - let style = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(style.name, "STANDARD"); - assert_eq!(style.description, ""); - assert_eq!(style.flags, 0); - assert_eq!(style.fill_color.method(), 0xC0); - assert!((style.start_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12); - assert!((style.end_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12); - assert_eq!(style.lines.len(), 2); - assert_eq!(style.lines[0].offset, 0.5); - assert_eq!(style.lines[1].offset, -0.5); - assert_eq!(style.lines[0].color.method(), 0xC0); - // R2018 takes the per-line linetype from the handle stream. - assert_eq!(style.lines[0].linetype_index, 0); - } - - /// The pre-R2018 `BS ltindex` is not optional: an R2013 body read - /// with the R2018 field list leaves both index words unread, and the - /// boundary check has to reject that. - #[test] - fn r2013_body_rejected_by_the_r2018_field_list() { - let payload = build(Version::R2013); - let style = decode_object(&payload, 8, None, Version::R2013).unwrap(); - assert_eq!(style.lines[0].linetype_index, 32767); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } - - /// …and the converse. - #[test] - fn r2018_body_rejected_by_the_r2013_field_list() { - let payload = build(Version::R2018); - assert!(decode_object(&payload, 8, None, Version::R2013).is_err()); - } - - #[test] - fn rejects_too_many_lines() { - let mut body = modern::tests::r2018_object_prefix(0); - body.write_bs(0); - cmc(&mut body, 0, 0xC000_0000, 0); - body.write_bd(0.0); - body.write_bd(0.0); - body.write_rc(17); // > 16 - let bits = crate::string_stream::tests::bits_of(&body); - let payload = crate::string_stream::tests::build_payload(&bits, &["BIG", ""]); - let err = decode_object(&payload, 8, None, Version::R2018).unwrap_err(); - assert!(matches!(&err, Error::SectionMap(msg) if msg.contains("MLINESTYLE"))); - } -} diff --git a/src/objects/acad_plot_settings.rs b/src/objects/acad_plot_settings.rs index d8a9e194..bf96079d 100644 --- a/src/objects/acad_plot_settings.rs +++ b/src/objects/acad_plot_settings.rs @@ -278,95 +278,3 @@ pub fn decode_object( split.finish("PLOTSETTINGS")?; Ok(settings) } - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - /// Write the §20.4.84 plot-settings block with `TV` slots left to - /// the string stream, as an R2007+ record does. - pub(crate) fn write_split_stream_block(w: &mut BitWriter) { - w.write_bs(1712); // plot_layout_flags - w.write_bd(6.35); // margin_left - w.write_bd(19.05); // margin_bottom - w.write_bd(6.35); // margin_right - w.write_bd(19.05); // margin_top - w.write_bd(215.9); // paper_width - w.write_bd(279.4); // paper_height - w.write_bd(0.0); // plot_origin.x - w.write_bd(0.0); // plot_origin.y - w.write_bs(0); // paper_units - w.write_bs(1); // plot_rotation - w.write_bs(5); // plot_type - w.write_bd(0.0); // window_min.x - w.write_bd(0.0); // window_min.y - w.write_bd(0.0); // window_max.x - w.write_bd(0.0); // window_max.y - w.write_bd(1.0); // real_world_units - w.write_bd(1.0); // drawing_units - w.write_bs(16); // standard_scale_type - w.write_bd(1.0); // scale_factor - w.write_bd(0.0); // paper_image_origin.x - w.write_bd(0.0); // paper_image_origin.y - w.write_bs(0); // shade_plot_mode - w.write_bs(2); // shade_plot_resolution_level - w.write_bs(300); // shade_plot_custom_dpi - } - - fn build_r2018() -> Vec { - let mut body = modern::tests::r2018_object_prefix(1); - write_split_stream_block(&mut body); - let bits = crate::string_stream::tests::bits_of(&body); - crate::string_stream::tests::build_payload( - &bits, - &[ - "", - "none_device", - "ANSI_A_(8.50_x_11.00_Inches)", - "acad.ctb", - ], - ) - } - - #[test] - fn r2018_split_stream_plot_settings_closes_on_its_string_stream() { - let payload = build_r2018(); - let s = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(s.page_setup_name, ""); - assert_eq!(s.printer_config_name, "none_device"); - assert_eq!(s.paper_size, "ANSI_A_(8.50_x_11.00_Inches)"); - assert_eq!(s.current_style_sheet, "acad.ctb"); - assert_eq!(s.plot_layout_flags, 1712); - assert!((s.margin_left - 6.35).abs() < 1e-12); - assert!((s.margin_top - 19.05).abs() < 1e-12); - assert!((s.paper_width - 215.9).abs() < 1e-12); - assert!((s.paper_height - 279.4).abs() < 1e-12); - assert_eq!(s.plot_rotation, 1); - assert_eq!(s.plot_type, 5); - assert_eq!(s.standard_scale_type, 16); - assert_eq!(s.shade_plot_custom_dpi, 300); - assert!((s.printable_width() - (215.9 - 12.7)).abs() < 1e-12); - assert!((s.printable_height() - (279.4 - 38.1)).abs() < 1e-12); - assert_eq!(s.custom_scale(), Some(1.0)); - } - - /// A body one field short of the list must not satisfy the - /// boundary check — the decoder has to error rather than return a - /// plausible-looking struct. - #[test] - fn a_short_body_is_rejected_by_the_boundary_check() { - let mut body = modern::tests::r2018_object_prefix(1); - write_split_stream_block(&mut body); - body.write_bs(0); // one field too many - let bits = crate::string_stream::tests::bits_of(&body); - let payload = crate::string_stream::tests::build_payload(&bits, &["", "x", "", ""]); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } - - #[test] - fn custom_scale_is_none_when_drawing_units_is_zero() { - let s = AcadPlotSettings::default(); - assert_eq!(s.custom_scale(), None); - } -} diff --git a/src/objects/acad_property_set_data.rs b/src/objects/acad_property_set_data.rs index 857abda8..9ef48a4e 100644 --- a/src/objects/acad_property_set_data.rs +++ b/src/objects/acad_property_set_data.rs @@ -119,94 +119,3 @@ pub fn decode(c: &mut BitCursor<'_>, version: Version) -> Result Vec { - let mut w = modern::tests::r2018_object_prefix(1); - write_body(&mut w, version, false); - let bits = crate::string_stream::tests::bits_of(&w); - let strings: Vec<&str> = if matches!(version, Version::R2018) { - vec!["Metric50", "Metric50", EXCLUDE, LABEL, "ANSI31"] - } else { - vec!["Metric50", EXCLUDE, LABEL, "ANSI31"] - }; - crate::string_stream::tests::build_payload(&bits, &strings) - } - - #[test] - fn r2018_section_view_style_closes_on_its_string_stream() { - let payload = build(Version::R2018); - let s = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(s.name, "Metric50"); - assert_eq!(s.name_alias, "Metric50"); - assert_eq!(s.flags, 44); - assert_eq!(s.identifier_exclude_characters, EXCLUDE); - assert_eq!(s.hatch_pattern_name, "ANSI31"); - assert_eq!(s.view_label_pattern, LABEL); - assert_eq!(s.cutting_plane_line_weight, 25); - assert_eq!(s.end_line_weight, 50); - assert_eq!(s.hatch_background_color.method(), 0xC8); - assert_eq!(s.hatch_color.method(), 0xC0); - assert_eq!(s.hatch_scale, 2.5); - assert_eq!(s.hatch_spacing, 2.5); - assert_eq!(s.hatch_angle_count, 6); - assert!((s.hatch_angles[0] - FRAC_PI_2).abs() < 1e-12); - assert!((s.hatch_angles[4] - 1.8325957145940461).abs() < 1e-12); - } - - #[test] - fn r2013_section_view_style_has_no_r2018_head() { - let payload = build(Version::R2013); - let s = decode_object(&payload, 8, None, Version::R2013).unwrap(); - assert_eq!(s.name, "Metric50"); - assert_eq!(s.name_alias, ""); - assert_eq!(s.identifier_exclude_characters, EXCLUDE); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } - - /// The R2004 layout: four inline `TV` fields, closing on the `RL` - /// object-data-size boundary. - #[test] - fn r2004_section_view_style_reads_its_strings_inline() { - let mut w = modern::tests::r2004_object_prefix(1); - write_body(&mut w, Version::R2004, true); - let end = w.position_bits(); - let bytes = w.into_bytes(); - let s = decode_object(&bytes, 0, Some(end), Version::R2004).unwrap(); - assert_eq!(s.name, "Metric50"); - assert_eq!(s.identifier_exclude_characters, EXCLUDE); - assert_eq!(s.hatch_pattern_name, "ANSI31"); - assert_eq!(s.hatch_scale, 2.5); - } -} diff --git a/src/objects/acad_visual_style.rs b/src/objects/acad_visual_style.rs index acc3db2a..1c186f1f 100644 --- a/src/objects/acad_visual_style.rs +++ b/src/objects/acad_visual_style.rs @@ -865,429 +865,3 @@ fn decode_paired( trailing_strings, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - /// Write one `(value, flag)` pair's flag. - fn flag(w: &mut BitWriter, v: i16) { - w.write_bs(v); - } - - fn cmc(w: &mut BitWriter, index: u16, rgb: u32, color_byte: u8) { - w.write_bs_u(index); - w.write_bl_u(rgb); - w.write_rc(color_byte); - } - - /// Build the 28-property R2010 body every release shares. - fn write_common_properties(w: &mut BitWriter) { - w.write_bs(2); // face_lighting_model - flag(w, 1); - w.write_bs(1); // face_lighting_quality - flag(w, 1); - w.write_bs(1); // face_color_mode - flag(w, 1); - w.write_bs(2); // face_modifier - flag(w, 1); - w.write_bd(0.6); // face_opacity - flag(w, 1); - w.write_bd(30.0); // face_specular - flag(w, 1); - cmc(w, 0, 0xC2FF_FFFF, 0); // face_mono_color - flag(w, 1); - w.write_bs(0); // edge_model - flag(w, 1); - w.write_bs(0); // edge_style - flag(w, 1); - cmc(w, 0, 0xC300_0007, 0); // edge_intersection_color - flag(w, 1); - cmc(w, 0, 0xC800_0000, 0); // edge_obscured_color - flag(w, 1); - w.write_bs(1); // edge_obscured_linetype - flag(w, 1); - w.write_bs(1); // edge_intersection_linetype - flag(w, 1); - w.write_bd(1.0); // edge_crease_angle - flag(w, 1); - w.write_bs(8); // edge_modifier - flag(w, 1); - cmc(w, 0, 0xC300_0007, 0); // edge_color - flag(w, 1); - w.write_bd(1.0); // edge_opacity - flag(w, 1); - w.write_bs(1); // edge_width - flag(w, 1); - w.write_bs(6); // edge_overhang - flag(w, 1); - w.write_bs(2); // edge_jitter - flag(w, 1); - cmc(w, 0, 0xC300_0007, 0); // edge_silhouette_color - flag(w, 1); - w.write_bs(5); // edge_silhouette_width - flag(w, 1); - w.write_bs(0); // edge_halo_gap - flag(w, 1); - w.write_bs(0); // edge_isoline_count - flag(w, 1); - w.write_b(false); // edge_hide_precision - flag(w, 1); - w.write_bs(13); // edge_style_apply - flag(w, 1); - w.write_bd(0.0); // display_brightness - flag(w, 1); - w.write_bs(0); // display_shadow_type - flag(w, 1); - } - - fn write_r2013_tail(w: &mut BitWriter) { - for kind in R2013_TAIL { - match kind { - TailKind::Short => w.write_bs(50), - TailKind::Double => w.write_bd(1.0), - TailKind::Bool => w.write_b(true), - TailKind::Color => cmc(w, 0, 0xC200_0000, 0), - // A `TV`'s text is in the string stream; no data bits. - TailKind::Text => {} - } - flag(w, 1); - } - } - - /// The common object data an R2010 non-entity object leads with: - /// empty EED chain, `BL` reactor count, no xdictionary. R2010 - /// predates the R2013+ AcDs binary-data bit. - fn r2010_object_prefix(num_reactors: i32) -> BitWriter { - let mut w = BitWriter::new(); - w.write_bs_u(0); - w.write_bl(num_reactors); - w.write_b(true); - w - } - - fn build(version: Version) -> Vec { - let mut body = if matches!(version, Version::R2013 | Version::R2018) { - modern::tests::r2018_object_prefix(1) - } else { - r2010_object_prefix(1) - }; - body.write_bs(0); // internal_style_type - body.write_bs(2); // format_version - body.write_b(true); // is_internal_use_only - write_common_properties(&mut body); - if matches!(version, Version::R2013 | Version::R2018) { - write_r2013_tail(&mut body); - } - let bits = crate::string_stream::tests::bits_of(&body); - crate::string_stream::tests::build_payload(&bits, &["Flat", "strokes_ogs.tif"]) - } - - /// Write one colour in the form `version` uses: the `CMC` triple - /// from R2004, the bare `BS` index before it (§2.11). - fn color(w: &mut BitWriter, version: Version, index: u16, rgb: u32, color_byte: u8) { - w.write_bs_u(index); - if version.is_r2004_plus() { - w.write_bl_u(rgb); - w.write_rc(color_byte); - } - } - - /// The flag-less field list: 30 fields, no flags, R2007's extra - /// 2-bit slot, `is_internal_use_only` last. Mirrors the module's - /// measured table for every release from R14 to R2007. - /// - /// The colour arguments carry both encodings of the same colour, so - /// one table serves both bands: `ColorChange`'s grey is - /// `0xC2808080` from R2004 and ACI index `8` before it, and the - /// "none" method `0xC8000000` is index `257`. - fn write_legacy_properties(w: &mut BitWriter, version: Version) { - w.write_bs(0); // internal_style_type - w.write_bs(2); // face_lighting_model - w.write_bs(1); // face_lighting_quality - w.write_bs(1); // face_color_mode - w.write_bd(-0.6); // face_opacity — signed on the flag-less list - w.write_bd(30.0); // face_specular - color(w, version, 7, 0xC2FF_FFFF, 0); // face_mono_color - w.write_bs(2); // face_modifier - w.write_bs(0); // edge_model - w.write_bs(0); // edge_style - color(w, version, 7, 0xC300_0007, 0); // edge_intersection_color - color(w, version, 257, 0xC800_0000, 0); // edge_obscured_color - w.write_bs(1); // edge_obscured_linetype - w.write_bd(1.0); // edge_crease_angle - w.write_bs(8); // edge_modifier - color(w, version, 7, 0xC300_0007, 0); // edge_color - w.write_bd(1.0); // edge_opacity - w.write_bs(1); // edge_width - w.write_bs(6); // edge_overhang - w.write_bs(2); // edge_jitter - color(w, version, 7, 0xC300_0007, 0); // edge_silhouette_color - w.write_bs(5); // edge_silhouette_width - w.write_rc(0); // the constant run's leading byte - w.write_bs(0); // edge_halo_gap - w.write_b(false); // edge_hide_precision - w.write_bs(0); // edge_isoline_count - w.write_bs(1); // edge_intersection_linetype - w.write_bs(13); // edge_style_apply - w.write_bl(-50); // display_brightness — a BL on the flag-less list - w.write_bs(0); // display_shadow_type - if matches!(version, Version::R2007) { - w.write_bs(0); // display_unknown_short — R2007 only - } - w.write_b(true); // is_internal_use_only - } - - /// An R2000 or R2004 record: inline `TV`, inline layout, and the - /// object prologue's `RL` object-data-size as the boundary. R2000 - /// predates the xdictionary-missing flag R2004 added to the common - /// object data. - /// `fields` selects which release's field list the body carries, so - /// a test can hand one release's decoder another release's bytes. - fn build_inline_with(version: Version, fields: Version) -> (Vec, usize) { - let mut body = BitWriter::new(); - body.write_bs_u(0); // EED terminator - body.write_bl(1); // num_reactors - if version.is_r2004_plus() { - body.write_b(true); // no xdictionary - } - modern::tests::write_inline_tv(&mut body, "Flat"); - write_legacy_properties(&mut body, fields); - let end = body.position_bits(); - (body.into_bytes(), end) - } - - fn build_inline(version: Version) -> (Vec, usize) { - build_inline_with(version, version) - } - - fn build_r2004() -> (Vec, usize) { - build_inline(Version::R2004) - } - - /// An R14 record: the object-data size in bits lives *inside* the - /// common object data, between the EED chain and the reactor count, - /// so the record carries its own boundary and the caller passes - /// none. The body is written twice because the `RL` has to hold the - /// length of what follows it. - fn build_r14() -> Vec { - let write = |size: u32| { - let mut body = BitWriter::new(); - body.write_bs_u(0); // EED terminator - body.write_rl(size); // R13/R14 object-data size in bits - body.write_bl(1); // num_reactors - modern::tests::write_inline_tv(&mut body, "Flat"); - write_legacy_properties(&mut body, Version::R14); - body - }; - let end = write(0).position_bits() as u32; - write(end).into_bytes() - } - - /// An R2007 record: §19.1's object prologue writes a `BS` object - /// type then the `RL` end of the data + string area, the `TV` - /// fields live in the string stream, and the area closes with the - /// string-stream size and the *strings present* flag. - fn build_r2007(fields: Version, strings: &[&str]) -> (Vec, usize) { - let mut sw = BitWriter::new(); - for s in strings { - sw.write_bs_u(s.encode_utf16().count() as u16); - for unit in s.encode_utf16() { - sw.write_rc((unit & 0xFF) as u8); - sw.write_rc((unit >> 8) as u8); - } - } - let string_bits = sw.position_bits(); - let string_bytes = sw.into_bytes(); - - let mut body = BitWriter::new(); - body.write_bs_u(0); // EED terminator - body.write_bl(1); // num_reactors - body.write_b(true); // no xdictionary - write_legacy_properties(&mut body, fields); - let body_bits = crate::string_stream::tests::bits_of(&body); - - // `BS` object type (2 bits for the 0 form) + `RL` = 34 bits. - let body_start = 34; - let section_end = body_start + body_bits.len() + string_bits + 17; - - let mut w = BitWriter::new(); - w.write_bs_u(0); // object type - w.write_rl(section_end as u32); - for bit in &body_bits { - w.write_b(*bit); - } - for i in 0..string_bits { - let byte = string_bytes[i / 8]; - w.write_b((byte >> (7 - (i % 8))) & 1 != 0); - } - w.write_rs(string_bits as i16); - w.write_b(true); // strings present - while w.position_bits() % 8 != 0 { - w.write_b(false); - } - (w.into_bytes(), body_start) - } - - /// R14 carries its own boundary and reads colours as bare indices; - /// the same field list closes on it. - #[test] - fn r14_visual_style_closes_on_the_rl_in_its_common_object_data() { - let payload = build_r14(); - let style = decode_object(&payload, 0, None, Version::R14).unwrap(); - assert_eq!(style.description, "Flat"); - assert_eq!(style.internal_style_type, 0); - assert!(style.is_internal_use_only); - // Pre-R2004 colours are the bare index; no true-colour word. - assert_eq!(style.face_mono_color.index, 7); - assert_eq!(style.face_mono_color.rgb, 0); - assert_eq!(style.edge_obscured_color.index, 257); - assert!((style.display_brightness + 50.0).abs() < 1e-12); - assert_eq!(style.display_unknown_short, 0); - assert!(style.property_flags.is_empty()); - } - - /// R2000 uses the same field list as R14 but takes its boundary - /// from the object prologue, and has no xdictionary-missing flag. - #[test] - fn r2000_visual_style_closes_with_bare_index_colours() { - let (payload, end) = build_inline(Version::R2000); - let style = decode_object(&payload, 0, Some(end), Version::R2000).unwrap(); - assert_eq!(style.description, "Flat"); - assert_eq!(style.face_mono_color.index, 7); - assert_eq!(style.edge_obscured_color.index, 257); - assert_eq!(style.edge_silhouette_color.index, 7); - assert_eq!(style.edge_style_apply, 13); - assert!(style.is_internal_use_only); - } - - /// The R2004 colour form is 34 bits wider per colour than the R14 / - /// R2000 one, so five colour slots put an R2000 body 170 bits short - /// of the R2004 reading and the boundary check has to reject it. - #[test] - fn r2000_body_rejected_by_the_r2004_colour_form() { - let (payload, end) = build_inline(Version::R2000); - assert!(decode_object(&payload, 0, Some(end), Version::R2004).is_err()); - } - - #[test] - fn r2004_inline_visual_style_closes_on_its_object_data_size() { - let (payload, end) = build_r2004(); - let style = decode_object(&payload, 0, Some(end), Version::R2004).unwrap(); - assert_eq!(style.description, "Flat"); - assert_eq!(style.internal_style_type, 0); - // R2004 has no format_version slot at all. - assert_eq!(style.format_version, 0); - assert!(style.is_internal_use_only); - assert_eq!(style.face_lighting_model, 2); - assert!((style.face_opacity + 0.6).abs() < 1e-12); - assert!((style.face_specular - 30.0).abs() < 1e-12); - assert_eq!(style.face_modifier, 2); - assert_eq!(style.face_mono_color.method(), 0xC2); - assert_eq!(style.edge_obscured_color.method(), 0xC8); - assert_eq!(style.edge_silhouette_width, 5); - assert_eq!(style.edge_unknown_byte, 0); - assert_eq!(style.edge_intersection_linetype, 1); - assert_eq!(style.edge_style_apply, 13); - assert!((style.display_brightness + 50.0).abs() < 1e-12); - // No per-property flags exist on this release. - assert!(style.property_flags.is_empty()); - assert!(style.extended.is_empty()); - } - - /// The flag-less body is 28 `BS` flags shorter than the paired one, - /// so the boundary check has to reject it under the R2010 list. - #[test] - fn r2004_body_rejected_by_the_r2010_field_list() { - let (payload, end) = build_r2004(); - assert!(decode_object(&payload, 0, Some(end), Version::R2010).is_err()); - } - - /// R2007 takes its `TV` from the string stream and writes one more - /// 2-bit slot than R2004 before the record's final `B`. - #[test] - fn r2007_split_stream_visual_style_closes_on_its_string_stream() { - let (payload, body_start) = build_r2007(Version::R2007, &["Flat"]); - let style = decode_object(&payload, body_start, None, Version::R2007).unwrap(); - assert_eq!(style.description, "Flat"); - assert_eq!(style.internal_style_type, 0); - assert_eq!(style.face_mono_color.method(), 0xC2); - assert_eq!(style.edge_obscured_color.method(), 0xC8); - assert!((style.display_brightness + 50.0).abs() < 1e-12); - assert_eq!(style.display_shadow_type, 0); - assert_eq!(style.display_unknown_short, 0); - assert!(style.is_internal_use_only); - assert!(style.property_flags.is_empty()); - } - - /// The extra slot is not optional: an R2007-shaped record whose - /// body carries the R2004 field list ends two bits short of its - /// string stream, and the boundary check has to reject that rather - /// than return a plausible struct. - #[test] - fn r2007_body_without_the_extra_slot_is_rejected() { - let (payload, body_start) = build_r2007(Version::R2004, &["Flat"]); - assert!(decode_object(&payload, body_start, None, Version::R2007).is_err()); - } - - /// …and the converse: a body that *does* carry the extra slot must - /// not satisfy the R2004 field list, which is two bits shorter. - #[test] - fn r2004_body_rejected_when_it_carries_the_r2007_slot() { - let (payload, end) = build_inline_with(Version::R2004, Version::R2007); - assert!(decode_object(&payload, 0, Some(end), Version::R2004).is_err()); - } - - #[test] - fn r2018_split_stream_visual_style_closes_on_its_string_stream() { - let payload = build(Version::R2018); - let style = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(style.description, "Flat"); - assert_eq!(style.internal_style_type, 0); - assert_eq!(style.format_version, 2); - assert!(style.is_internal_use_only); - assert_eq!(style.face_lighting_model, 2); - assert!((style.face_opacity - 0.6).abs() < 1e-12); - assert!((style.face_specular - 30.0).abs() < 1e-12); - assert_eq!(style.face_mono_color.method(), 0xC2); - assert_eq!(style.face_mono_color.payload(), 0x00FF_FFFF); - assert_eq!(style.edge_intersection_color.method(), 0xC3); - assert_eq!(style.edge_intersection_color.payload(), 7); - assert!(!style.edge_hide_precision); - assert_eq!(style.display_shadow_type, 0); - assert_eq!(style.property_flags.len(), 58); - assert_eq!(style.extended.len(), 30); - assert_eq!( - style.extended[26].value, - VisualStyleValue::Text("strokes_ogs.tif".to_string()) - ); - assert!(style.trailing_strings.is_empty()); - } - - #[test] - fn r2010_visual_style_has_no_r2013_tail() { - let payload = build(Version::R2010); - let style = decode_object(&payload, 8, None, Version::R2010).unwrap(); - assert_eq!(style.description, "Flat"); - assert_eq!(style.property_flags.len(), 28); - assert!(style.extended.is_empty()); - } - - /// The R2013 tail is not optional: reading an R2013 record with the - /// R2010 field list leaves 30 properties unread, and the boundary - /// check has to reject that rather than return a plausible struct. - #[test] - fn r2013_body_rejected_by_the_r2010_field_list() { - let payload = build(Version::R2018); - assert!(decode_object(&payload, 8, None, Version::R2010).is_err()); - } - - /// …and the converse: an R2010-length body must not satisfy the - /// R2013 field list. - #[test] - fn r2010_body_rejected_by_the_r2013_field_list() { - let payload = build(Version::R2010); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } -} diff --git a/src/objects/custom_dict_entry.rs b/src/objects/custom_dict_entry.rs index 1e34b757..e36db603 100644 --- a/src/objects/custom_dict_entry.rs +++ b/src/objects/custom_dict_entry.rs @@ -82,64 +82,3 @@ impl CustomDictEntries { .map(|e| &e.value_handle) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - fn encode_tv_r2000(w: &mut BitWriter, s: &[u8]) { - w.write_bs_u(s.len() as u16); - for b in s { - w.write_rc(*b); - } - } - - #[test] - fn roundtrip_empty() { - let mut w = BitWriter::new(); - w.write_bs_u(0); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let e = decode(&mut c, Version::R2000).unwrap(); - assert!(e.is_empty()); - } - - #[test] - fn roundtrip_populated() { - let mut w = BitWriter::new(); - w.write_bs_u(2); - encode_tv_r2000(&mut w, b"SPATIAL_INDEX"); - w.write_handle(2, 0x40); - encode_tv_r2000(&mut w, b"AcDbVariableDictionary"); - w.write_handle(2, 0x41); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let e = decode(&mut c, Version::R2000).unwrap(); - assert_eq!(e.len(), 2); - assert_eq!(e.get("SPATIAL_INDEX").unwrap().value, 0x40); - assert_eq!(e.get("AcDbVariableDictionary").unwrap().value, 0x41); - assert!(e.get("MISSING").is_none()); - } - - #[test] - fn lookup_is_case_sensitive() { - let mut w = BitWriter::new(); - w.write_bs_u(1); - encode_tv_r2000(&mut w, b"CaseSensitiveKey"); - w.write_handle(2, 0x99); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let e = decode(&mut c, Version::R2000).unwrap(); - assert!(e.get("CaseSensitiveKey").is_some()); - assert!(e.get("casesensitivekey").is_none()); - } - - /// The cap sits above the natural BS upper bound (65535). The - /// check still exists as a defensive guard if the reader is - /// ever refactored to use a wider count field. - #[test] - fn cap_constant_is_set_conservatively() { - assert!(MAX_CUSTOM_DICT_ENTRIES >= u16::MAX as usize); - } -} diff --git a/src/objects/dictionary_var.rs b/src/objects/dictionary_var.rs index 4685228f..74a054b8 100644 --- a/src/objects/dictionary_var.rs +++ b/src/objects/dictionary_var.rs @@ -69,45 +69,3 @@ pub(crate) fn decode_object( split.finish("DICTIONARYVAR")?; Ok(out) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - #[test] - fn roundtrip_inline_value() { - let mut w = BitWriter::new(); - w.write_rc(0); - w.write_bs_u(8); - for b in b"Metric50" { - w.write_rc(*b); - } - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let v = decode(&mut c, Version::R2000).unwrap(); - assert_eq!(v.schema, 0); - assert_eq!(v.value, "Metric50"); - } - - #[test] - fn r2018_split_stream_var_reads_its_value_from_the_string_stream() { - let mut body = modern::tests::r2018_object_prefix(1); - body.write_rc(0); - let bits = crate::string_stream::tests::bits_of(&body); - let payload = crate::string_stream::tests::build_payload(&bits, &["STANDARD"]); - let v = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(v.schema, 0); - assert_eq!(v.value, "STANDARD"); - } - - #[test] - fn r2018_split_stream_var_rejects_a_misaligned_body() { - let mut body = modern::tests::r2018_object_prefix(1); - body.write_rc(0); - body.write_rc(0); - let bits = crate::string_stream::tests::bits_of(&body); - let payload = crate::string_stream::tests::build_payload(&bits, &["STANDARD"]); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } -} diff --git a/src/objects/placeholder.rs b/src/objects/placeholder.rs index debada89..9fe41752 100644 --- a/src/objects/placeholder.rs +++ b/src/objects/placeholder.rs @@ -52,39 +52,3 @@ pub(crate) fn decode_object( num_reactors: split.num_reactors, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - - #[test] - fn r2018_placeholder_is_exactly_the_common_object_data() { - let body = modern::tests::r2018_object_prefix(1); - let bits = crate::string_stream::tests::bits_of(&body); - let payload = modern::tests::build_payload_without_strings(&bits); - let p = decode_object(&payload, 8, None, Version::R2018).unwrap(); - assert_eq!(p.num_reactors, 1); - } - - #[test] - fn r2018_placeholder_rejects_a_record_with_a_body() { - let mut body = modern::tests::r2018_object_prefix(1); - body.write_bl(7); // a field a placeholder must not have - let bits = crate::string_stream::tests::bits_of(&body); - let payload = modern::tests::build_payload_without_strings(&bits); - assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); - } - - #[test] - fn r2004_placeholder_closes_on_the_inline_object_size() { - let mut w = BitWriter::new(); - w.write_bs_u(0); - w.write_bl(1); - w.write_b(true); - let end = w.position_bits(); - let bytes = w.into_bytes(); - let p = decode_object(&bytes, 0, Some(end), Version::R2004).unwrap(); - assert_eq!(p.num_reactors, 1); - } -} diff --git a/src/objects/proxy_entity.rs b/src/objects/proxy_entity.rs index 30a680ca..63c04f68 100644 --- a/src/objects/proxy_entity.rs +++ b/src/objects/proxy_entity.rs @@ -68,69 +68,3 @@ pub fn decode(c: &mut BitCursor<'_>, _version: crate::version::Version) -> Resul handles, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - use crate::version::Version; - - #[test] - fn roundtrip_empty_proxy() { - let mut w = BitWriter::new(); - w.write_bl(500); - w.write_bl(0); - w.write_bl(0); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let p = decode(&mut c, Version::R2000).unwrap(); - assert_eq!(p.proxy_class_id, 500); - assert!(p.raw_proxy_data.is_empty()); - assert!(p.handles.is_empty()); - } - - #[test] - fn roundtrip_populated_proxy() { - let mut w = BitWriter::new(); - w.write_bl(501); - let payload: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF]; - w.write_bl(payload.len() as i32); - for b in &payload { - w.write_rc(*b); - } - w.write_bl(2); - w.write_handle(5, 0x10); - w.write_handle(5, 0x20); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let p = decode(&mut c, Version::R2000).unwrap(); - assert_eq!(p.proxy_class_id, 501); - assert_eq!(p.raw_proxy_data, &payload); - assert_eq!(p.handles.len(), 2); - assert_eq!(p.handles[0].value, 0x10); - assert_eq!(p.handles[1].value, 0x20); - } - - #[test] - fn rejects_oversized_data() { - let mut w = BitWriter::new(); - w.write_bl(500); - w.write_bl((MAX_PROXY_DATA_BYTES + 1) as i32); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let err = decode(&mut c, Version::R2000).unwrap_err(); - assert!(matches!(&err, Error::SectionMap(msg) if msg.contains("ProxyEntity"))); - } - - #[test] - fn rejects_excessive_handle_count() { - let mut w = BitWriter::new(); - w.write_bl(500); - w.write_bl(0); - w.write_bl((MAX_PROXY_HANDLES + 1) as i32); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let err = decode(&mut c, Version::R2000).unwrap_err(); - assert!(matches!(&err, Error::SectionMap(msg) if msg.contains("handles"))); - } -} diff --git a/src/objects/proxy_object.rs b/src/objects/proxy_object.rs index c2358b73..4daa8d1f 100644 --- a/src/objects/proxy_object.rs +++ b/src/objects/proxy_object.rs @@ -66,67 +66,3 @@ pub fn decode(c: &mut BitCursor<'_>, _version: crate::version::Version) -> Resul handles, }) } - -#[cfg(test)] -mod tests { - use super::*; - use crate::bitwriter::BitWriter; - use crate::version::Version; - - #[test] - fn roundtrip_empty_proxy() { - let mut w = BitWriter::new(); - w.write_bl(600); - w.write_bl(0); - w.write_bl(0); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let p = decode(&mut c, Version::R2000).unwrap(); - assert_eq!(p.proxy_class_id, 600); - assert!(p.raw_proxy_data.is_empty()); - assert!(p.handles.is_empty()); - } - - #[test] - fn roundtrip_populated_proxy_object() { - let mut w = BitWriter::new(); - w.write_bl(601); - let payload: [u8; 3] = [0xC0, 0xDE, 0x42]; - w.write_bl(payload.len() as i32); - for b in &payload { - w.write_rc(*b); - } - w.write_bl(1); - w.write_handle(3, 0x55); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let p = decode(&mut c, Version::R2000).unwrap(); - assert_eq!(p.proxy_class_id, 601); - assert_eq!(p.raw_proxy_data, &payload); - assert_eq!(p.handles.len(), 1); - assert_eq!(p.handles[0].value, 0x55); - } - - #[test] - fn rejects_oversized_data() { - let mut w = BitWriter::new(); - w.write_bl(600); - w.write_bl((MAX_PROXY_DATA_BYTES + 1) as i32); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let err = decode(&mut c, Version::R2000).unwrap_err(); - assert!(matches!(&err, Error::SectionMap(msg) if msg.contains("ProxyObject"))); - } - - #[test] - fn rejects_excessive_handle_count() { - let mut w = BitWriter::new(); - w.write_bl(600); - w.write_bl(0); - w.write_bl((MAX_PROXY_HANDLES + 1) as i32); - let bytes = w.into_bytes(); - let mut c = BitCursor::new(&bytes); - let err = decode(&mut c, Version::R2000).unwrap_err(); - assert!(matches!(&err, Error::SectionMap(msg) if msg.contains("handles"))); - } -} diff --git a/src/objects/xdata.rs b/src/objects/xdata.rs index 730f9117..22c38f45 100644 --- a/src/objects/xdata.rs +++ b/src/objects/xdata.rs @@ -146,99 +146,3 @@ fn decode_item(c: &mut BitCursor<'_>, version: Version, code: u16) -> Result Date: Wed, 28 Oct 2026 06:57:51 -0400 Subject: [PATCH 2/2] test(objects): keep the four suites that use objects/modern.rs's helpers acad_detail_view_style, acad_mleader_style, acad_section_view_style and acad_visual_style are the last users of modern::tests::r2004_object_prefix and write_inline_tv. modern.rs is touched by the open R13 pull requests, so the helpers stay, and with them these four suites, until those land: with their users gone the helpers are dead code in the lib test build, which -D warnings rejects (Test, windows-latest, stable). --- src/objects/acad_detail_view_style.rs | 114 +++++++ src/objects/acad_mleader_style.rs | 138 ++++++++ src/objects/acad_section_view_style.rs | 133 ++++++++ src/objects/acad_visual_style.rs | 426 +++++++++++++++++++++++++ 4 files changed, 811 insertions(+) diff --git a/src/objects/acad_detail_view_style.rs b/src/objects/acad_detail_view_style.rs index 88510309..8df5401d 100644 --- a/src/objects/acad_detail_view_style.rs +++ b/src/objects/acad_detail_view_style.rs @@ -242,3 +242,117 @@ pub fn decode_object( trailing_flags, }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitwriter::BitWriter; + + const LABEL: &str = "%<\\AcVar ViewDetailId>%"; + + fn cmc(w: &mut BitWriter) { + w.write_bs_u(0); + w.write_bl_u(0xC000_0000); + w.write_rc(0); + } + + /// Write the `Metric50` field list; `inline_strings` selects the + /// pre-R2007 layout, where the three `TV` slots consume data bits. + fn write_body(w: &mut BitWriter, version: Version, inline_strings: bool) { + let r2018 = matches!(version, Version::R2018); + w.write_bs(0); + if inline_strings { + modern::tests::write_inline_tv(w, "Metric50"); + } + if r2018 { + w.write_b(false); + w.write_b(true); + } + w.write_bs(32); + w.write_b(false); + w.write_b(true); + w.write_b(true); + cmc(w); + w.write_bd(5.0); + if inline_strings { + modern::tests::write_inline_tv(w, ""); + } + w.write_bd(0.36); + w.write_rc(1); + cmc(w); + w.write_bd(5.0); + w.write_bs(25); + cmc(w); + cmc(w); + w.write_bd(5.0); + w.write_bs(0); + w.write_bd(15.0); + w.write_bs(1); + if inline_strings { + modern::tests::write_inline_tv(w, LABEL); + } + w.write_bs(25); + cmc(w); + w.write_bs(25); + cmc(w); + w.write_rc(0); + } + + fn build(version: Version) -> Vec { + let mut w = modern::tests::r2018_object_prefix(1); + write_body(&mut w, version, false); + let bits = crate::string_stream::tests::bits_of(&w); + let strings: Vec<&str> = if matches!(version, Version::R2018) { + vec!["Metric50", "Metric50", "", LABEL] + } else { + vec!["Metric50", "", LABEL] + }; + crate::string_stream::tests::build_payload(&bits, &strings) + } + + #[test] + fn r2018_detail_view_style_closes_on_its_string_stream() { + let payload = build(Version::R2018); + let s = decode_object(&payload, 8, None, Version::R2018).unwrap(); + assert_eq!(s.name, "Metric50"); + assert_eq!(s.name_alias, "Metric50"); + assert_eq!(s.flags, 32); + assert_eq!(s.identifier_height, 5.0); + assert_eq!(s.arrow_symbol_size, 5.0); + assert_eq!(s.view_label_text_height, 5.0); + assert_eq!(s.view_label_offset, 15.0); + assert_eq!(s.boundary_line_weight, 25); + assert_eq!(s.connection_line_weight, 25); + assert_eq!(s.model_edge_line_weight, 25); + assert_eq!(s.identifier_color.method(), 0xC0); + assert_eq!(s.model_edge_color.method(), 0xC0); + assert_eq!(s.view_label_pattern, LABEL); + assert_eq!(s.trailing_flags, 0); + } + + #[test] + fn r2013_detail_view_style_has_no_r2018_head() { + let payload = build(Version::R2013); + let s = decode_object(&payload, 8, None, Version::R2013).unwrap(); + assert_eq!(s.name, "Metric50"); + assert_eq!(s.name_alias, ""); + assert!(!s.unknown_flag_a && !s.unknown_flag_b); + assert_eq!(s.flags, 32); + // The two extra R2018 head bits are not optional. + assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); + } + + /// The R2004 layout: three inline `TV` fields, closing on the `RL` + /// object-data-size boundary. + #[test] + fn r2004_detail_view_style_reads_its_strings_inline() { + let mut w = modern::tests::r2004_object_prefix(1); + write_body(&mut w, Version::R2004, true); + let end = w.position_bits(); + let bytes = w.into_bytes(); + let s = decode_object(&bytes, 0, Some(end), Version::R2004).unwrap(); + assert_eq!(s.name, "Metric50"); + assert_eq!(s.view_label_pattern, LABEL); + assert_eq!(s.identifier_height, 5.0); + } +} diff --git a/src/objects/acad_mleader_style.rs b/src/objects/acad_mleader_style.rs index 6f0381c9..edeb347f 100644 --- a/src/objects/acad_mleader_style.rs +++ b/src/objects/acad_mleader_style.rs @@ -315,3 +315,141 @@ pub fn decode_object( extended_flag, }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitwriter::BitWriter; + + fn cmc(w: &mut BitWriter, rgb: u32) { + w.write_bs_u(0); + w.write_bl_u(rgb); + w.write_rc(0); + } + + /// The `Standard` multileader style, with the values every corpus + /// record decodes. + fn build(version: Version) -> Vec { + // R2010 predates the R2013+ AcDs binary-data bit of the common + // object prefix, so it takes the shorter prefix. + let mut w = if matches!(version, Version::R2013 | Version::R2018) { + modern::tests::r2018_object_prefix(1) + } else { + modern::tests::r2004_object_prefix(1) + }; + write_common_body(&mut w, version, false); + let bits = crate::string_stream::tests::bits_of(&w); + crate::string_stream::tests::build_payload(&bits, &["Standard", ""]) + } + + /// Write the field list itself. `inline_strings` selects the + /// pre-R2007 layout, where the two `TV` slots consume data bits. + fn write_common_body(w: &mut BitWriter, version: Version, inline_strings: bool) { + if version.is_r2010_plus() { + w.write_bs(2); // version + } + w.write_bs(2); // content_type + w.write_bs(1); // draw_mleader_order_type + w.write_bs(0); // draw_leader_order_type + w.write_bl(2); // max_leader_segment_points + w.write_bd(0.0); + w.write_bd(0.0); + w.write_bs(1); // leader_line_type + cmc(w, 0xC100_0000); + w.write_bl(-2); // leader_lineweight + w.write_b(true); // enable_landing + w.write_bd(0.09); + w.write_b(true); // enable_dogleg + w.write_bd(0.36); + if inline_strings { + modern::tests::write_inline_tv(w, "Standard"); + } + w.write_bd(0.18); // arrowhead_size + if inline_strings { + modern::tests::write_inline_tv(w, ""); + } + w.write_bs(1); + w.write_bs(6); + w.write_bs(1); + w.write_bs(0); + cmc(w, 0xC100_0000); + w.write_bd(0.18); // text_height + w.write_b(false); + w.write_b(false); + w.write_bd(0.18); // align_space + cmc(w, 0xC100_0000); + w.write_bd(1.0); + w.write_bd(1.0); + w.write_bd(1.0); + w.write_b(true); + w.write_bd(0.0); + w.write_b(true); + w.write_bs(0); + w.write_bd(1.0); // scale + w.write_b(false); + w.write_b(true); + w.write_bd(0.125); // break_gap_size + if version.is_r2010_plus() { + w.write_bs(0); + w.write_bs(9); + w.write_bs(9); + } + if matches!(version, Version::R2013 | Version::R2018) { + w.write_b(true); + } + } + + #[test] + fn r2018_mleader_style_closes_on_its_string_stream() { + let payload = build(Version::R2018); + let s = decode_object(&payload, 8, None, Version::R2018).unwrap(); + assert_eq!(s.version, 2); + assert_eq!(s.content_type, 2); + assert_eq!(s.max_leader_segment_points, 2); + assert_eq!(s.leader_line_color.method(), 0xC1); + assert_eq!(s.leader_lineweight, -2); + assert!(s.enable_landing && s.enable_dogleg); + assert!((s.landing_gap - 0.09).abs() < 1e-12); + assert!((s.dogleg_length - 0.36).abs() < 1e-12); + assert!((s.arrowhead_size - 0.18).abs() < 1e-12); + assert!((s.text_height - 0.18).abs() < 1e-12); + assert!((s.break_gap_size - 0.125).abs() < 1e-12); + assert_eq!(s.description, "Standard"); + assert_eq!(s.default_mtext_contents, ""); + assert_eq!(s.block_content_scale_x, 1.0); + assert_eq!(s.top_text_attachment_direction, 9); + assert!(s.extended_flag); + } + + #[test] + fn r2010_mleader_style_has_no_r2013_tail_flag() { + let payload = build(Version::R2010); + let s = decode_object(&payload, 8, None, Version::R2010).unwrap(); + assert_eq!(s.version, 2); + assert!(!s.extended_flag); + assert_eq!(s.bottom_text_attachment_direction, 9); + } + + /// The R2004 layout: `TV` fields inline, no leading `version` word + /// and no attachment-direction tail, closing on the `RL` + /// object-data-size boundary. + #[test] + fn r2004_mleader_style_omits_the_r2010_fields() { + let mut w = modern::tests::r2004_object_prefix(1); + write_common_body(&mut w, Version::R2004, true); + let end = w.position_bits(); + let bytes = w.into_bytes(); + let s = decode_object(&bytes, 0, Some(end), Version::R2004).unwrap(); + assert_eq!(s.version, 0); + assert_eq!(s.text_attachment_direction, 0); + assert_eq!(s.description, "Standard"); + assert_eq!(s.leader_lineweight, -2); + assert!((s.break_gap_size - 0.125).abs() < 1e-12); + } + + #[test] + fn r2013_body_rejected_by_the_r2010_field_list() { + let payload = build(Version::R2018); + assert!(decode_object(&payload, 8, None, Version::R2010).is_err()); + } +} diff --git a/src/objects/acad_section_view_style.rs b/src/objects/acad_section_view_style.rs index 9b75602e..9540f030 100644 --- a/src/objects/acad_section_view_style.rs +++ b/src/objects/acad_section_view_style.rs @@ -285,3 +285,136 @@ pub fn decode_object( hatch_angles, }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitwriter::BitWriter; + use std::f64::consts::FRAC_PI_2; + + const EXCLUDE: &str = "I, O, Q, S, X, Z"; + const LABEL: &str = "%<\\AcVar ViewSectionStartId>%"; + const ANGLES: [f64; 5] = [ + FRAC_PI_2, + 0.2617993877991494, + 1.3089969389957472, + -0.2617993877991494, + 1.8325957145940461, + ]; + + fn cmc(w: &mut BitWriter, rgb: u32) { + w.write_bs_u(0); + w.write_bl_u(rgb); + w.write_rc(0); + } + + fn write_body(w: &mut BitWriter, version: Version, inline_strings: bool) { + let r2018 = matches!(version, Version::R2018); + w.write_bs(0); + if inline_strings { + modern::tests::write_inline_tv(w, "Metric50"); + } + if r2018 { + w.write_b(false); + w.write_b(true); + } + w.write_bs(44); + w.write_b(true); + w.write_b(true); + w.write_b(false); + cmc(w, 0xC000_0000); + w.write_bd(5.0); + cmc(w, 0xC000_0000); + w.write_bd(5.0); + if inline_strings { + modern::tests::write_inline_tv(w, EXCLUDE); + } + w.write_bd(10.0); + w.write_bs(25); + cmc(w, 0xC000_0000); + w.write_bs(50); + cmc(w, 0xC000_0000); + w.write_bd(5.0); + w.write_bd(5.0); + cmc(w, 0xC000_0000); + w.write_bd(5.0); + w.write_bs(0); + w.write_bd(15.0); + w.write_bs(1); + if inline_strings { + modern::tests::write_inline_tv(w, LABEL); + } + cmc(w, 0xC000_0000); + cmc(w, 0xC800_0000); + if inline_strings { + modern::tests::write_inline_tv(w, "ANSI31"); + } + w.write_rc(98); + w.write_bd(2.5); + w.write_bs(0); + w.write_bd(2.5); + w.write_bs(6); + w.write_bs(0); + for angle in ANGLES { + w.write_bd(angle); + } + } + + fn build(version: Version) -> Vec { + let mut w = modern::tests::r2018_object_prefix(1); + write_body(&mut w, version, false); + let bits = crate::string_stream::tests::bits_of(&w); + let strings: Vec<&str> = if matches!(version, Version::R2018) { + vec!["Metric50", "Metric50", EXCLUDE, LABEL, "ANSI31"] + } else { + vec!["Metric50", EXCLUDE, LABEL, "ANSI31"] + }; + crate::string_stream::tests::build_payload(&bits, &strings) + } + + #[test] + fn r2018_section_view_style_closes_on_its_string_stream() { + let payload = build(Version::R2018); + let s = decode_object(&payload, 8, None, Version::R2018).unwrap(); + assert_eq!(s.name, "Metric50"); + assert_eq!(s.name_alias, "Metric50"); + assert_eq!(s.flags, 44); + assert_eq!(s.identifier_exclude_characters, EXCLUDE); + assert_eq!(s.hatch_pattern_name, "ANSI31"); + assert_eq!(s.view_label_pattern, LABEL); + assert_eq!(s.cutting_plane_line_weight, 25); + assert_eq!(s.end_line_weight, 50); + assert_eq!(s.hatch_background_color.method(), 0xC8); + assert_eq!(s.hatch_color.method(), 0xC0); + assert_eq!(s.hatch_scale, 2.5); + assert_eq!(s.hatch_spacing, 2.5); + assert_eq!(s.hatch_angle_count, 6); + assert!((s.hatch_angles[0] - FRAC_PI_2).abs() < 1e-12); + assert!((s.hatch_angles[4] - 1.8325957145940461).abs() < 1e-12); + } + + #[test] + fn r2013_section_view_style_has_no_r2018_head() { + let payload = build(Version::R2013); + let s = decode_object(&payload, 8, None, Version::R2013).unwrap(); + assert_eq!(s.name, "Metric50"); + assert_eq!(s.name_alias, ""); + assert_eq!(s.identifier_exclude_characters, EXCLUDE); + assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); + } + + /// The R2004 layout: four inline `TV` fields, closing on the `RL` + /// object-data-size boundary. + #[test] + fn r2004_section_view_style_reads_its_strings_inline() { + let mut w = modern::tests::r2004_object_prefix(1); + write_body(&mut w, Version::R2004, true); + let end = w.position_bits(); + let bytes = w.into_bytes(); + let s = decode_object(&bytes, 0, Some(end), Version::R2004).unwrap(); + assert_eq!(s.name, "Metric50"); + assert_eq!(s.identifier_exclude_characters, EXCLUDE); + assert_eq!(s.hatch_pattern_name, "ANSI31"); + assert_eq!(s.hatch_scale, 2.5); + } +} diff --git a/src/objects/acad_visual_style.rs b/src/objects/acad_visual_style.rs index 1c186f1f..acc3db2a 100644 --- a/src/objects/acad_visual_style.rs +++ b/src/objects/acad_visual_style.rs @@ -865,3 +865,429 @@ fn decode_paired( trailing_strings, }) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::bitwriter::BitWriter; + + /// Write one `(value, flag)` pair's flag. + fn flag(w: &mut BitWriter, v: i16) { + w.write_bs(v); + } + + fn cmc(w: &mut BitWriter, index: u16, rgb: u32, color_byte: u8) { + w.write_bs_u(index); + w.write_bl_u(rgb); + w.write_rc(color_byte); + } + + /// Build the 28-property R2010 body every release shares. + fn write_common_properties(w: &mut BitWriter) { + w.write_bs(2); // face_lighting_model + flag(w, 1); + w.write_bs(1); // face_lighting_quality + flag(w, 1); + w.write_bs(1); // face_color_mode + flag(w, 1); + w.write_bs(2); // face_modifier + flag(w, 1); + w.write_bd(0.6); // face_opacity + flag(w, 1); + w.write_bd(30.0); // face_specular + flag(w, 1); + cmc(w, 0, 0xC2FF_FFFF, 0); // face_mono_color + flag(w, 1); + w.write_bs(0); // edge_model + flag(w, 1); + w.write_bs(0); // edge_style + flag(w, 1); + cmc(w, 0, 0xC300_0007, 0); // edge_intersection_color + flag(w, 1); + cmc(w, 0, 0xC800_0000, 0); // edge_obscured_color + flag(w, 1); + w.write_bs(1); // edge_obscured_linetype + flag(w, 1); + w.write_bs(1); // edge_intersection_linetype + flag(w, 1); + w.write_bd(1.0); // edge_crease_angle + flag(w, 1); + w.write_bs(8); // edge_modifier + flag(w, 1); + cmc(w, 0, 0xC300_0007, 0); // edge_color + flag(w, 1); + w.write_bd(1.0); // edge_opacity + flag(w, 1); + w.write_bs(1); // edge_width + flag(w, 1); + w.write_bs(6); // edge_overhang + flag(w, 1); + w.write_bs(2); // edge_jitter + flag(w, 1); + cmc(w, 0, 0xC300_0007, 0); // edge_silhouette_color + flag(w, 1); + w.write_bs(5); // edge_silhouette_width + flag(w, 1); + w.write_bs(0); // edge_halo_gap + flag(w, 1); + w.write_bs(0); // edge_isoline_count + flag(w, 1); + w.write_b(false); // edge_hide_precision + flag(w, 1); + w.write_bs(13); // edge_style_apply + flag(w, 1); + w.write_bd(0.0); // display_brightness + flag(w, 1); + w.write_bs(0); // display_shadow_type + flag(w, 1); + } + + fn write_r2013_tail(w: &mut BitWriter) { + for kind in R2013_TAIL { + match kind { + TailKind::Short => w.write_bs(50), + TailKind::Double => w.write_bd(1.0), + TailKind::Bool => w.write_b(true), + TailKind::Color => cmc(w, 0, 0xC200_0000, 0), + // A `TV`'s text is in the string stream; no data bits. + TailKind::Text => {} + } + flag(w, 1); + } + } + + /// The common object data an R2010 non-entity object leads with: + /// empty EED chain, `BL` reactor count, no xdictionary. R2010 + /// predates the R2013+ AcDs binary-data bit. + fn r2010_object_prefix(num_reactors: i32) -> BitWriter { + let mut w = BitWriter::new(); + w.write_bs_u(0); + w.write_bl(num_reactors); + w.write_b(true); + w + } + + fn build(version: Version) -> Vec { + let mut body = if matches!(version, Version::R2013 | Version::R2018) { + modern::tests::r2018_object_prefix(1) + } else { + r2010_object_prefix(1) + }; + body.write_bs(0); // internal_style_type + body.write_bs(2); // format_version + body.write_b(true); // is_internal_use_only + write_common_properties(&mut body); + if matches!(version, Version::R2013 | Version::R2018) { + write_r2013_tail(&mut body); + } + let bits = crate::string_stream::tests::bits_of(&body); + crate::string_stream::tests::build_payload(&bits, &["Flat", "strokes_ogs.tif"]) + } + + /// Write one colour in the form `version` uses: the `CMC` triple + /// from R2004, the bare `BS` index before it (§2.11). + fn color(w: &mut BitWriter, version: Version, index: u16, rgb: u32, color_byte: u8) { + w.write_bs_u(index); + if version.is_r2004_plus() { + w.write_bl_u(rgb); + w.write_rc(color_byte); + } + } + + /// The flag-less field list: 30 fields, no flags, R2007's extra + /// 2-bit slot, `is_internal_use_only` last. Mirrors the module's + /// measured table for every release from R14 to R2007. + /// + /// The colour arguments carry both encodings of the same colour, so + /// one table serves both bands: `ColorChange`'s grey is + /// `0xC2808080` from R2004 and ACI index `8` before it, and the + /// "none" method `0xC8000000` is index `257`. + fn write_legacy_properties(w: &mut BitWriter, version: Version) { + w.write_bs(0); // internal_style_type + w.write_bs(2); // face_lighting_model + w.write_bs(1); // face_lighting_quality + w.write_bs(1); // face_color_mode + w.write_bd(-0.6); // face_opacity — signed on the flag-less list + w.write_bd(30.0); // face_specular + color(w, version, 7, 0xC2FF_FFFF, 0); // face_mono_color + w.write_bs(2); // face_modifier + w.write_bs(0); // edge_model + w.write_bs(0); // edge_style + color(w, version, 7, 0xC300_0007, 0); // edge_intersection_color + color(w, version, 257, 0xC800_0000, 0); // edge_obscured_color + w.write_bs(1); // edge_obscured_linetype + w.write_bd(1.0); // edge_crease_angle + w.write_bs(8); // edge_modifier + color(w, version, 7, 0xC300_0007, 0); // edge_color + w.write_bd(1.0); // edge_opacity + w.write_bs(1); // edge_width + w.write_bs(6); // edge_overhang + w.write_bs(2); // edge_jitter + color(w, version, 7, 0xC300_0007, 0); // edge_silhouette_color + w.write_bs(5); // edge_silhouette_width + w.write_rc(0); // the constant run's leading byte + w.write_bs(0); // edge_halo_gap + w.write_b(false); // edge_hide_precision + w.write_bs(0); // edge_isoline_count + w.write_bs(1); // edge_intersection_linetype + w.write_bs(13); // edge_style_apply + w.write_bl(-50); // display_brightness — a BL on the flag-less list + w.write_bs(0); // display_shadow_type + if matches!(version, Version::R2007) { + w.write_bs(0); // display_unknown_short — R2007 only + } + w.write_b(true); // is_internal_use_only + } + + /// An R2000 or R2004 record: inline `TV`, inline layout, and the + /// object prologue's `RL` object-data-size as the boundary. R2000 + /// predates the xdictionary-missing flag R2004 added to the common + /// object data. + /// `fields` selects which release's field list the body carries, so + /// a test can hand one release's decoder another release's bytes. + fn build_inline_with(version: Version, fields: Version) -> (Vec, usize) { + let mut body = BitWriter::new(); + body.write_bs_u(0); // EED terminator + body.write_bl(1); // num_reactors + if version.is_r2004_plus() { + body.write_b(true); // no xdictionary + } + modern::tests::write_inline_tv(&mut body, "Flat"); + write_legacy_properties(&mut body, fields); + let end = body.position_bits(); + (body.into_bytes(), end) + } + + fn build_inline(version: Version) -> (Vec, usize) { + build_inline_with(version, version) + } + + fn build_r2004() -> (Vec, usize) { + build_inline(Version::R2004) + } + + /// An R14 record: the object-data size in bits lives *inside* the + /// common object data, between the EED chain and the reactor count, + /// so the record carries its own boundary and the caller passes + /// none. The body is written twice because the `RL` has to hold the + /// length of what follows it. + fn build_r14() -> Vec { + let write = |size: u32| { + let mut body = BitWriter::new(); + body.write_bs_u(0); // EED terminator + body.write_rl(size); // R13/R14 object-data size in bits + body.write_bl(1); // num_reactors + modern::tests::write_inline_tv(&mut body, "Flat"); + write_legacy_properties(&mut body, Version::R14); + body + }; + let end = write(0).position_bits() as u32; + write(end).into_bytes() + } + + /// An R2007 record: §19.1's object prologue writes a `BS` object + /// type then the `RL` end of the data + string area, the `TV` + /// fields live in the string stream, and the area closes with the + /// string-stream size and the *strings present* flag. + fn build_r2007(fields: Version, strings: &[&str]) -> (Vec, usize) { + let mut sw = BitWriter::new(); + for s in strings { + sw.write_bs_u(s.encode_utf16().count() as u16); + for unit in s.encode_utf16() { + sw.write_rc((unit & 0xFF) as u8); + sw.write_rc((unit >> 8) as u8); + } + } + let string_bits = sw.position_bits(); + let string_bytes = sw.into_bytes(); + + let mut body = BitWriter::new(); + body.write_bs_u(0); // EED terminator + body.write_bl(1); // num_reactors + body.write_b(true); // no xdictionary + write_legacy_properties(&mut body, fields); + let body_bits = crate::string_stream::tests::bits_of(&body); + + // `BS` object type (2 bits for the 0 form) + `RL` = 34 bits. + let body_start = 34; + let section_end = body_start + body_bits.len() + string_bits + 17; + + let mut w = BitWriter::new(); + w.write_bs_u(0); // object type + w.write_rl(section_end as u32); + for bit in &body_bits { + w.write_b(*bit); + } + for i in 0..string_bits { + let byte = string_bytes[i / 8]; + w.write_b((byte >> (7 - (i % 8))) & 1 != 0); + } + w.write_rs(string_bits as i16); + w.write_b(true); // strings present + while w.position_bits() % 8 != 0 { + w.write_b(false); + } + (w.into_bytes(), body_start) + } + + /// R14 carries its own boundary and reads colours as bare indices; + /// the same field list closes on it. + #[test] + fn r14_visual_style_closes_on_the_rl_in_its_common_object_data() { + let payload = build_r14(); + let style = decode_object(&payload, 0, None, Version::R14).unwrap(); + assert_eq!(style.description, "Flat"); + assert_eq!(style.internal_style_type, 0); + assert!(style.is_internal_use_only); + // Pre-R2004 colours are the bare index; no true-colour word. + assert_eq!(style.face_mono_color.index, 7); + assert_eq!(style.face_mono_color.rgb, 0); + assert_eq!(style.edge_obscured_color.index, 257); + assert!((style.display_brightness + 50.0).abs() < 1e-12); + assert_eq!(style.display_unknown_short, 0); + assert!(style.property_flags.is_empty()); + } + + /// R2000 uses the same field list as R14 but takes its boundary + /// from the object prologue, and has no xdictionary-missing flag. + #[test] + fn r2000_visual_style_closes_with_bare_index_colours() { + let (payload, end) = build_inline(Version::R2000); + let style = decode_object(&payload, 0, Some(end), Version::R2000).unwrap(); + assert_eq!(style.description, "Flat"); + assert_eq!(style.face_mono_color.index, 7); + assert_eq!(style.edge_obscured_color.index, 257); + assert_eq!(style.edge_silhouette_color.index, 7); + assert_eq!(style.edge_style_apply, 13); + assert!(style.is_internal_use_only); + } + + /// The R2004 colour form is 34 bits wider per colour than the R14 / + /// R2000 one, so five colour slots put an R2000 body 170 bits short + /// of the R2004 reading and the boundary check has to reject it. + #[test] + fn r2000_body_rejected_by_the_r2004_colour_form() { + let (payload, end) = build_inline(Version::R2000); + assert!(decode_object(&payload, 0, Some(end), Version::R2004).is_err()); + } + + #[test] + fn r2004_inline_visual_style_closes_on_its_object_data_size() { + let (payload, end) = build_r2004(); + let style = decode_object(&payload, 0, Some(end), Version::R2004).unwrap(); + assert_eq!(style.description, "Flat"); + assert_eq!(style.internal_style_type, 0); + // R2004 has no format_version slot at all. + assert_eq!(style.format_version, 0); + assert!(style.is_internal_use_only); + assert_eq!(style.face_lighting_model, 2); + assert!((style.face_opacity + 0.6).abs() < 1e-12); + assert!((style.face_specular - 30.0).abs() < 1e-12); + assert_eq!(style.face_modifier, 2); + assert_eq!(style.face_mono_color.method(), 0xC2); + assert_eq!(style.edge_obscured_color.method(), 0xC8); + assert_eq!(style.edge_silhouette_width, 5); + assert_eq!(style.edge_unknown_byte, 0); + assert_eq!(style.edge_intersection_linetype, 1); + assert_eq!(style.edge_style_apply, 13); + assert!((style.display_brightness + 50.0).abs() < 1e-12); + // No per-property flags exist on this release. + assert!(style.property_flags.is_empty()); + assert!(style.extended.is_empty()); + } + + /// The flag-less body is 28 `BS` flags shorter than the paired one, + /// so the boundary check has to reject it under the R2010 list. + #[test] + fn r2004_body_rejected_by_the_r2010_field_list() { + let (payload, end) = build_r2004(); + assert!(decode_object(&payload, 0, Some(end), Version::R2010).is_err()); + } + + /// R2007 takes its `TV` from the string stream and writes one more + /// 2-bit slot than R2004 before the record's final `B`. + #[test] + fn r2007_split_stream_visual_style_closes_on_its_string_stream() { + let (payload, body_start) = build_r2007(Version::R2007, &["Flat"]); + let style = decode_object(&payload, body_start, None, Version::R2007).unwrap(); + assert_eq!(style.description, "Flat"); + assert_eq!(style.internal_style_type, 0); + assert_eq!(style.face_mono_color.method(), 0xC2); + assert_eq!(style.edge_obscured_color.method(), 0xC8); + assert!((style.display_brightness + 50.0).abs() < 1e-12); + assert_eq!(style.display_shadow_type, 0); + assert_eq!(style.display_unknown_short, 0); + assert!(style.is_internal_use_only); + assert!(style.property_flags.is_empty()); + } + + /// The extra slot is not optional: an R2007-shaped record whose + /// body carries the R2004 field list ends two bits short of its + /// string stream, and the boundary check has to reject that rather + /// than return a plausible struct. + #[test] + fn r2007_body_without_the_extra_slot_is_rejected() { + let (payload, body_start) = build_r2007(Version::R2004, &["Flat"]); + assert!(decode_object(&payload, body_start, None, Version::R2007).is_err()); + } + + /// …and the converse: a body that *does* carry the extra slot must + /// not satisfy the R2004 field list, which is two bits shorter. + #[test] + fn r2004_body_rejected_when_it_carries_the_r2007_slot() { + let (payload, end) = build_inline_with(Version::R2004, Version::R2007); + assert!(decode_object(&payload, 0, Some(end), Version::R2004).is_err()); + } + + #[test] + fn r2018_split_stream_visual_style_closes_on_its_string_stream() { + let payload = build(Version::R2018); + let style = decode_object(&payload, 8, None, Version::R2018).unwrap(); + assert_eq!(style.description, "Flat"); + assert_eq!(style.internal_style_type, 0); + assert_eq!(style.format_version, 2); + assert!(style.is_internal_use_only); + assert_eq!(style.face_lighting_model, 2); + assert!((style.face_opacity - 0.6).abs() < 1e-12); + assert!((style.face_specular - 30.0).abs() < 1e-12); + assert_eq!(style.face_mono_color.method(), 0xC2); + assert_eq!(style.face_mono_color.payload(), 0x00FF_FFFF); + assert_eq!(style.edge_intersection_color.method(), 0xC3); + assert_eq!(style.edge_intersection_color.payload(), 7); + assert!(!style.edge_hide_precision); + assert_eq!(style.display_shadow_type, 0); + assert_eq!(style.property_flags.len(), 58); + assert_eq!(style.extended.len(), 30); + assert_eq!( + style.extended[26].value, + VisualStyleValue::Text("strokes_ogs.tif".to_string()) + ); + assert!(style.trailing_strings.is_empty()); + } + + #[test] + fn r2010_visual_style_has_no_r2013_tail() { + let payload = build(Version::R2010); + let style = decode_object(&payload, 8, None, Version::R2010).unwrap(); + assert_eq!(style.description, "Flat"); + assert_eq!(style.property_flags.len(), 28); + assert!(style.extended.is_empty()); + } + + /// The R2013 tail is not optional: reading an R2013 record with the + /// R2010 field list leaves 30 properties unread, and the boundary + /// check has to reject that rather than return a plausible struct. + #[test] + fn r2013_body_rejected_by_the_r2010_field_list() { + let payload = build(Version::R2018); + assert!(decode_object(&payload, 8, None, Version::R2010).is_err()); + } + + /// …and the converse: an R2010-length body must not satisfy the + /// R2013 field list. + #[test] + fn r2010_body_rejected_by_the_r2013_field_list() { + let payload = build(Version::R2010); + assert!(decode_object(&payload, 8, None, Version::R2018).is_err()); + } +}