//! Small independent byte fixtures for DXF tag types, text encoding, framing and failures. //! Handwritten examples, not files from a CAD application or generated by our writer. //! Background on the tag model and file structure: //! //! //! ASCII spacing is our writer's documented convention; binary fixtures specify actual bytes. use craft_dxf::{DxfError, Tag, Value, parse, parse_with_limit, write_ascii_as, write_binary}; const SENTINEL: &[u8] = b"AutoCAD Binary DXF\r\n\x1a\0"; fn fixtures() -> Vec<(Tag, &'static [u8], &'static [u8])> { vec![ (Tag::s(1, " A "), b" 1\r\n A \r\n", b"\x01\0 A \0"), (Tag::s(1, ""), b" 1\r\n\r\n", b"\x01\0\0"), (Tag::s(105, "AB12"), b"105\r\nAB12\r\n", b"\x69\0AB12\0"), (Tag::new(10, Value::Real(-3.25)), b" 10\r\n-3.25\r\n", b"\x0a\0\0\0\0\0\0\0\x0a\xc0"), (Tag::i(70, -32768), b" 70\r\n-32768\r\n", b"\x46\0\0\x80"), (Tag::i(90, -2_147_483_648), b" 90\r\n-2147483648\r\n", b"\x5a\0\0\0\0\x80"), (Tag::i(160, 9_223_372_036_854_775_807), b"160\r\n9223372036854775807\r\n", b"\xa0\0\xff\xff\xff\xff\xff\xff\xff\x7f"), (Tag::new(290, Value::Bool(false)), b"290\r\n0\r\n", b"\x22\x01\0"), (Tag::new(299, Value::Bool(true)), b"299\r\n1\r\n", b"\x2b\x01\x01"), (Tag::new(310, Value::Hex("00FF80".into())), b"310\r\n00FF80\r\n", b"\x36\x01\x03\0\xff\x80"), (Tag::new(1004, Value::Hex(String::new())), b"1004\r\n\r\n", b"\xec\x03\0"), (Tag::i(1070, -2), b"1070\r\n-2\r\n", b"\x2e\x04\xfe\xff"), (Tag::i(1071, 65536), b"1071\r\n65536\r\n", b"\x2f\x04\0\0\x01\0"), ] } #[test] fn reads_independently_authored_ascii_and_binary_values() { for (expected, ascii, binary) in fixtures() { assert_eq!(parse(ascii).unwrap(), std::slice::from_ref(&expected), "ASCII code {}", expected.code); let bytes = [SENTINEL, binary].concat(); assert_eq!(parse(&bytes).unwrap(), std::slice::from_ref(&expected), "binary code {}", expected.code); } } #[test] fn writes_independently_specified_ascii_and_binary_bytes() { for (tag, ascii, binary) in fixtures() { for utf8 in [false, true] { let tags = std::slice::from_ref(&tag); assert_eq!(write_ascii_as(tags, utf8).as_bytes(), ascii, "ASCII code {}", tag.code); assert_eq!(write_binary(tags, utf8), [SENTINEL, binary].concat(), "binary code {}", tag.code); } } } #[test] fn integer_code_ranges_have_independent_binary_widths() { // Specification ranges and widths, never kind_of(): a shared reader/writer mistake // can round-trip every boundary value while still using the wrong number of bytes. let widths = [ (vec![60..=79, 170..=179, 270..=289, 370..=389, 400..=409, 1060..=1070], 2, i64::from(i16::MIN), i64::from(i16::MAX)), (vec![90..=99, 420..=429, 440..=459, 1071..=1071], 4, i64::from(i32::MIN), i64::from(i32::MAX)), (std::iter::once(160..=169).collect(), 8, i64::MIN, i64::MAX), ]; for (ranges, width, min, max) in widths { for code in ranges.into_iter().flatten() { for value in [min, min + 1, -1, 0, 1, max - 1, max] { // Binary DXF uses a little-endian 16-bit group code and a fixed-width, // little-endian two's-complement integer. Encoding is version-independent. let code_bytes = u16::try_from(code).unwrap().to_le_bytes(); let value_bytes = value.to_le_bytes(); let expected = [SENTINEL, &code_bytes, &value_bytes[..width]].concat(); let tags = [Tag::i(code, value)]; for utf8 in [false, true] { assert_eq!(write_binary(&tags, utf8), expected, "code={code} value={value} utf8={utf8}"); } assert_eq!(parse(&expected).unwrap(), tags, "code={code} value={value}"); } } } } #[test] fn text_encoding_has_independent_wire_expectations() { let tags = [Tag::s(1, "é😀")]; let escaped = b"\\U+00E9\\U+D83D\\U+DE00"; let utf8 = b"\xc3\xa9\xf0\x9f\x98\x80"; for (unicode, text) in [(false, escaped.as_slice()), (true, utf8.as_slice())] { let ascii = [b" 1\r\n".as_slice(), text, b"\r\n"].concat(); let binary = [SENTINEL, b"\x01\0", text, b"\0"].concat(); assert_eq!(write_ascii_as(&tags, unicode).as_bytes(), ascii, "utf8={unicode}"); assert_eq!(write_binary(&tags, unicode), binary, "utf8={unicode}"); assert_eq!(parse(&ascii).unwrap(), tags); assert_eq!(parse(&binary).unwrap(), tags); } // Pre-2007 code-page bytes are independent of the writer, which emits Unicode escapes. let ascii = b"0\nSECTION\n2\nHEADER\n9\n$ACADVER\n1\nAC1015\n9\n$DWGCODEPAGE\n3\nANSI_1252\n0\nENDSEC\n1\n\x80\xe9\n0\nEOF\n"; let binary = [ SENTINEL, b"\0\0SECTION\0\x02\0HEADER\0\x09\0$ACADVER\0\x01\0AC1015\0\x09\0$DWGCODEPAGE\0\x03\0ANSI_1252\0\0\0ENDSEC\0\x01\0\x80\xe9\0\0\0EOF\0", ] .concat(); for bytes in [ascii.as_slice(), binary.as_slice()] { let tags = parse(bytes).unwrap(); assert_eq!(tags[7], Tag::s(1, "€é")); } } #[test] fn ascii_accepts_line_endings_padding_and_no_final_newline() { let expected = [Tag::i(70, -7), Tag::s(1, " text "), Tag::s(0, "EOF")]; for newline in ["\n", "\r\n"] { for final_newline in [false, true] { let mut text = [" 70 ", " -7 ", "1", " text ", "0", "EOF"].join(newline); if final_newline { text.push_str(newline); } assert_eq!(parse(text.as_bytes()).unwrap(), expected, "newline={newline:?} final={final_newline}"); } } } #[test] fn malformed_and_truncated_values_are_errors_not_panics() { assert!(matches!(parse(b""), Err(DxfError::NotDxf))); assert!(matches!(parse(b"oops\nvalue\n"), Err(DxfError::BadCode { line: 1, .. }))); assert!(matches!(parse(b"10\n"), Err(DxfError::Eof))); assert!(matches!(parse(SENTINEL), Err(DxfError::NotDxf))); for (_, _, binary) in fixtures() { // Every cut inside the first record is an error, unlike cuts between complete records: // the tag API deliberately accepts streams without an EOF record. for cut in 1..binary.len() { let bytes = [SENTINEL, &binary[..cut]].concat(); assert!(matches!(parse(&bytes), Err(DxfError::Eof)), "binary={binary:?} cut={cut}"); } } } #[test] fn tag_limit_accepts_the_exact_boundary_and_rejects_one_less() { let ascii = b"1\nA\n70\n-7\n0\nEOF\n"; let binary = [SENTINEL, b"\x01\0A\0\x46\0\xf9\xff\0\0EOF\0"].concat(); for bytes in [ascii.as_slice(), binary.as_slice()] { for limit in 0..3 { assert!(matches!(parse_with_limit(bytes, limit), Err(DxfError::TooLarge { limit: actual, .. }) if actual == limit)); } for limit in [3, 4] { assert_eq!(parse_with_limit(bytes, limit).unwrap(), [Tag::s(1, "A"), Tag::i(70, -7), Tag::s(0, "EOF")]); } } }