Files
craft-libs/crates/dxf/tests/conformance.rs
ebb40fa0c9 craft-dxf: R12 binary, more code pages, borrowing sections, exponent reals (ports of open CadCraft PRs) (#847)
* craft-dxf: the ASCII reader unescapes only text that holds a backslash

Only a string with a backslash can hold a \U+ or \M+ escape; every other
string moves into the tag as it is instead of being copied. The EOF check
reads the value before it moves.

Ported from storytold/cadcraft#1833.

* craft-dxf: write_ascii_in_page writes the code page without the UTF-8 fallback

write_ascii_in falls back to pure ASCII when the code page bytes happen to
be valid UTF-8, for readers that guess UTF-8. A reader that decodes a
pre-2007 DXF strictly by its $DWGCODEPAGE needs the code page bytes.

Ported from storytold/cadcraft#2851.

* craft-dxf: behaviour-preserving cleanups

Exact group-code mappings in one place, documented errors on the fallible
public parsers, and simpler option, result and formatting idioms. No change
to parsing, writing, numeric conversion, encoding or the public API.

Ported from storytold/cadcraft#2571.

* craft-dxf: read R12 and earlier binary DXF (1-byte group codes)

Binary DXF written before R13 stores each group code in one byte, with 255
escaping a 2-byte code. The reader only knew the R13+ layout, where codes
are two bytes, and also treated 255 as an escape there. The layout is now
told from the first tag; 255 is an escape only in the 1-byte layout and an
ordinary code in R13+.

Ported from storytold/cadcraft#1499.

* craft-dxf: section_slices, record_slices and Tag::str_ref borrow instead of copying

sections() and records() copy every tag out of the parsed list, so a reader
that keeps the list holds a large file's tags two or three times. The new
functions return slices of the list. sections() and records() now build
their copies from them, so there is one implementation of each split.

Ported from storytold/cadcraft#1708. storytold/cadcraft#1588 solves the same
problem by changing Section itself; this keeps the existing API.

* craft-dxf: reals beyond 1e16 or below 1e-10 are written in exponent form; string_groups

format_real wrote f64::MAX as 309 digits. Values outside 1e-10..1e16 are now
written as AutoCAD writes them (1.0E+20) and read back exactly.
string_groups splits text into the 250-character groups long strings are
stored in (MAX_STRING_CHARS).

Ported from storytold/cadcraft#1790.

* craft-dxf: DOS 720/737/775/855/857/860/861/863/864/865/869 and ISO 8859-15 code pages; cheaper unescape and hex

$DWGCODEPAGE values DOS720, 737, 775, 855, 857, 860, 861, 863, 864, 865, 869
and ISO8859_15 had no decoder and fell back to Windows-1252. ISO 8859-15
decodes through encoding_rs, the DOS pages through tables checked against
the Unicode Consortium mapping files (dos_tables.rs). DOS 864's byte 0x25 is
U+066A, so a writer escapes '%' there. U+FFFD is never written as a table
byte. A string without an escape moves into its tag without a copy, cstr
finds its terminator with one scan, and binary chunks are hex-encoded from a
table.

Ported from storytold/cadcraft#1271. Its group-code-255 change is carried by
the R12 binary port: the 1-byte layout is detected only when the escaped
code is one that needs the escape, so an R13+ file starting with code 255
still reads.

* craft-dxf: Korean Johab (ANSI_1361) text and \M+4 escapes decode

Johab had no decoder: files declaring ANSI_1361 were read as Windows-1252
and \M+4 escapes stayed literal. Hangul syllables are composed from the
three 5-bit jamo indices (KS X 1001 annex 3); symbols and Hanja map onto
KS X 1001 rows decoded as EUC-KR. Read only: a writer escapes instead.

Ported from storytold/cadcraft#1911 (its DOS code pages are covered by the
tables from storytold/cadcraft#1271).

---------

Co-authored-by: mo <131036780+mbolling@users.noreply.github.com>
Co-authored-by: ffishman <20211677+ffishman@users.noreply.github.com>
Co-authored-by: Cédric Legrand <cedric@legrand-tech.fr>
Co-authored-by: valentinevg812-eng <valentinevg812@gmail.com>
Co-authored-by: Mads Holten Rasmussen <mads.h.rasmussen@gmail.com>
Co-authored-by: LastSkywalker <maksim@sky-tehnol.uk>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: Francesco BV <francesco.esposito@bvitalia.net>
2026-11-11 14:58:05 -05:00

146 lines
7.0 KiB
Rust

//! 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:
//! <https://ezdxf.readthedocs.io/en/stable/dxfinternals/dxftags.html>
//! <https://ezdxf.readthedocs.io/en/stable/dxfinternals/filestructure.html>
//! 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")]);
}
}
}